diff --git a/packages/teamcode/src/session/llm-headers.ts b/packages/teamcode/src/session/llm-headers.ts new file mode 100644 index 00000000..e07051d5 --- /dev/null +++ b/packages/teamcode/src/session/llm-headers.ts @@ -0,0 +1,35 @@ +export namespace LLMHeaders { + export type Input = { + providerID: string + sessionID: string + requestID: string + client: string + version: string + projectID?: string + parentSessionID?: string + } + + const ZEN_PROVIDER_PREFIX = "opencode" + + export function isZenProvider(providerID: string) { + return providerID.startsWith(ZEN_PROVIDER_PREFIX) + } + + export function build(input: Input): Record { + const headers: Record = isZenProvider(input.providerID) + ? { + "x-opencode-session": input.sessionID, + "x-opencode-request": input.requestID, + "x-opencode-client": input.client, + "User-Agent": `opencode/${input.version}`, + } + : { + "x-session-affinity": input.sessionID, + "X-Session-Id": input.sessionID, + "User-Agent": `teamcode/${input.version}`, + } + if (isZenProvider(input.providerID) && input.projectID) headers["x-opencode-project"] = input.projectID + if (input.parentSessionID) headers["x-parent-session-id"] = input.parentSessionID + return headers + } +} diff --git a/packages/teamcode/src/session/llm.ts b/packages/teamcode/src/session/llm.ts index cdb05f67..2d5ff85b 100644 --- a/packages/teamcode/src/session/llm.ts +++ b/packages/teamcode/src/session/llm.ts @@ -18,6 +18,7 @@ import { Wildcard } from "@/util/wildcard" import { SessionID } from "@/session/schema" import { Auth } from "@/auth" import { InstallationVersion } from "@teamcode-ai/core/installation/version" +import { LLMHeaders } from "./llm-headers" import { EffectBridge } from "@/effect/bridge" import { RuntimeFlags } from "@/effect/runtime-flags" const log = Log.create({ service: "llm" }) @@ -28,6 +29,13 @@ type Result = Awaited> const mergeOptions = (target: Record, source: Record | undefined): Record => mergeDeep(target, source ?? {}) as Record +const approvalTitle = (parsed: unknown): string => { + if (typeof parsed !== "object" || parsed === null) return "" + const record: Record = Object.fromEntries(Object.entries(parsed)) + const candidate = record.title ?? record.name ?? "" + return typeof candidate === "string" ? candidate : "" +} + export type StreamInput = { user: MessageV2.User sessionID: string @@ -83,7 +91,7 @@ const live: Layer.Layer< providerID: input.model.providerID, }) - const [language, cfg, item, info] = yield* Effect.all( + const [language, , item, info] = yield* Effect.all( [ provider.getLanguage(input.model), config.get(), @@ -225,7 +233,7 @@ const live: Layer.Layer< return { result: "", error: `Unknown tool: ${toolName}` } } try { - const result = await t.execute!(JSON.parse(argsJson), { + const result = await t.execute(JSON.parse(argsJson), { toolCallId: _requestID, messages: input.messages, abortSignal: input.abort, @@ -265,8 +273,8 @@ const live: Layer.Layer< }) const toolPatterns = approvalTools.map((t: { name: string; args: string }) => { try { - const parsed = JSON.parse(t.args) as Record - const title = (parsed?.title ?? parsed?.name ?? "") as string + const parsed: unknown = JSON.parse(t.args) + const title = approvalTitle(parsed) return title ? `${t.name}: ${title}` : t.name } catch { return t.name @@ -295,7 +303,7 @@ const live: Layer.Layer< }) } - const opencodeProjectID = input.model.providerID.startsWith("teamcode") + const opencodeProjectID = LLMHeaders.isZenProvider(input.model.providerID) ? (yield* InstanceState.context).project.id : undefined @@ -373,19 +381,15 @@ const live: Layer.Layer< maxOutputTokens: params.maxOutputTokens, abortSignal: input.abort, headers: { - ...(input.model.providerID.startsWith("teamcode") - ? { - "x-teamcode-project": opencodeProjectID, - "x-teamcode-session": input.sessionID, - "x-teamcode-request": input.user.id, - "x-teamcode-client": flags.client, - "User-Agent": `teamcode/${InstallationVersion}`, - } - : { - "x-session-affinity": input.sessionID, - ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}), - "User-Agent": `teamcode/${InstallationVersion}`, - }), + ...LLMHeaders.build({ + providerID: input.model.providerID, + sessionID: input.sessionID, + parentSessionID: input.parentSessionID, + requestID: input.user.id, + client: flags.client, + version: InstallationVersion, + projectID: opencodeProjectID, + }), ...input.model.headers, ...headers, }, diff --git a/packages/teamcode/test/session/llm-headers.test.ts b/packages/teamcode/test/session/llm-headers.test.ts new file mode 100644 index 00000000..77ab740c --- /dev/null +++ b/packages/teamcode/test/session/llm-headers.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test } from "bun:test" +import { LLMHeaders } from "../../src/session/llm-headers" + +const base = { + sessionID: "ses_123", + requestID: "msg_456", + client: "cli", + version: "9.9.9", +} + +describe("session.llm-headers", () => { + test("opencode zen providers send x-opencode-* headers and opencode user agent", () => { + const headers = LLMHeaders.build({ ...base, providerID: "opencode", projectID: "prj_1" }) + expect(headers).toEqual({ + "x-opencode-project": "prj_1", + "x-opencode-session": "ses_123", + "x-opencode-request": "msg_456", + "x-opencode-client": "cli", + "User-Agent": "opencode/9.9.9", + }) + }) + + test("opencode-go is treated as an opencode provider", () => { + const headers = LLMHeaders.build({ ...base, providerID: "opencode-go", projectID: "prj_1" }) + expect(headers["x-opencode-session"]).toBe("ses_123") + expect(headers["User-Agent"]).toBe("opencode/9.9.9") + }) + + test("opencode providers omit x-opencode-project when project id is missing", () => { + const headers = LLMHeaders.build({ ...base, providerID: "opencode" }) + expect(headers).not.toHaveProperty("x-opencode-project") + expect(headers["x-opencode-session"]).toBe("ses_123") + }) + + test("other providers send session affinity and teamcode user agent", () => { + const headers = LLMHeaders.build({ ...base, providerID: "anthropic" }) + expect(headers).toEqual({ + "x-session-affinity": "ses_123", + "X-Session-Id": "ses_123", + "User-Agent": "teamcode/9.9.9", + }) + expect(headers).not.toHaveProperty("x-opencode-session") + }) + + test("parent session id is forwarded for every provider", () => { + const zen = LLMHeaders.build({ ...base, providerID: "opencode", parentSessionID: "ses_parent" }) + const other = LLMHeaders.build({ ...base, providerID: "anthropic", parentSessionID: "ses_parent" }) + expect(zen["x-parent-session-id"]).toBe("ses_parent") + expect(other["x-parent-session-id"]).toBe("ses_parent") + }) +})