diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 874086a06bd..b509f3cb745 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -206,6 +206,8 @@ const layer = Layer.effect( model, http: { headers: { + "x-opencode-session-id": session.id, + ...(session.parentID ? { "x-opencode-parent-session-id": session.parentID } : {}), "x-session-affinity": session.id, "X-Session-Id": session.id, ...(session.parentID ? { "x-parent-session-id": session.parentID } : {}), diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index cc58b43b295..d2a34bf65ce 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -1103,10 +1103,12 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(2) expect(requests.map((request) => request.http?.headers)).toEqual([ { + "x-opencode-session-id": sessionID, "x-session-affinity": sessionID, "X-Session-Id": sessionID, }, { + "x-opencode-session-id": sessionID, "x-session-affinity": sessionID, "X-Session-Id": sessionID, }, @@ -2528,6 +2530,7 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests[0]?.http?.headers).toEqual({ + "x-opencode-session-id": sessionID, "x-session-affinity": sessionID, "X-Session-Id": sessionID, }) @@ -2551,7 +2554,13 @@ describe("SessionRunnerLLM", () => { requests.length = 0 yield* session.resume(sessionID) - expect(requests[0]?.http?.headers?.["x-parent-session-id"]).toBe(parentID) + expect(requests[0]?.http?.headers).toEqual({ + "x-opencode-session-id": sessionID, + "x-opencode-parent-session-id": parentID, + "x-session-affinity": sessionID, + "X-Session-Id": sessionID, + "x-parent-session-id": parentID, + }) }), ) diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index e000d6ca49b..a945b66f7e8 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -185,6 +185,8 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre params, messageTransformOptions: options, headers: { + "x-opencode-session-id": input.sessionID, + ...(input.parentSessionID ? { "x-opencode-parent-session-id": input.parentSessionID } : {}), ...(input.model.providerID.startsWith("opencode") ? { ...(opencodeProjectID ? { "x-opencode-project": opencodeProjectID } : {}), diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index fcb536f46d9..ed0ad2022e2 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -756,72 +756,89 @@ describe("session.llm.stream", () => { const vivgridFixture = { providerID: "vivgrid", modelID: "gemini-3.1-pro-preview" } const opencodeFixture = { providerID: "opencode-test", modelID: vivgridFixture.modelID } - it.instance( - "sends the parent session header for opencode providers", - () => - Effect.gen(function* () { - const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID) - const request = waitRequest( - "/chat/completions", - new Response(createChatStream("Hello"), { - status: 200, - headers: { "Content-Type": "text/event-stream" }, - }), - ) - const resolved = yield* Provider.use.getModel( - ProviderV2.ID.make(opencodeFixture.providerID), - ModelV2.ID.make(opencodeFixture.modelID), - ) - const sessionID = SessionID.make("session-child") - const parentSessionID = SessionID.make("session-parent") - const agent = { - name: "test", - mode: "primary", - options: {}, - permission: [{ permission: "*", pattern: "*", action: "allow" }], - } satisfies Agent.Info - const user = { - id: MessageID.make("msg_user-parent-header"), - sessionID, - role: "user", - time: { created: Date.now() }, - agent: agent.name, - model: { - providerID: ProviderV2.ID.make(opencodeFixture.providerID), - modelID: resolved.id, - }, - } satisfies SessionV1.User - - yield* drain({ - user, - sessionID, - parentSessionID, - model: resolved, - agent, - system: ["You are a helpful assistant."], - messages: [{ role: "user", content: "Hello" }], - tools: {}, - }) - - expect((yield* Effect.promise(() => request)).headers.get("x-parent-session-id")).toBe(parentSessionID) - }), - { - config: () => { - const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID) - return { - enabled_providers: [opencodeFixture.providerID], - provider: { - [opencodeFixture.providerID]: { - name: "OpenCode Test", - npm: "@ai-sdk/openai-compatible", - models: { [fixture.model.id]: configModel(fixture.model) as ConfigModel }, - options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` }, + const headerCases = [ + { providerID: opencodeFixture.providerID, child: false }, + { providerID: opencodeFixture.providerID, child: true }, + { providerID: "custom-test", child: false }, + { providerID: "custom-test", child: true }, + ] + headerCases.forEach((input) => { + it.instance( + `sends session identity headers for ${input.child ? "child" : "root"} sessions on ${input.providerID}`, + () => + Effect.gen(function* () { + const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID) + const request = waitRequest( + "/chat/completions", + new Response(createChatStream("Hello"), { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + }), + ) + const resolved = yield* Provider.use.getModel( + ProviderV2.ID.make(input.providerID), + ModelV2.ID.make(opencodeFixture.modelID), + ) + const sessionID = SessionID.make("session-child") + const parentSessionID = input.child ? SessionID.make("session-parent") : undefined + const agent = { + name: "test", + mode: "primary", + options: {}, + permission: [{ permission: "*", pattern: "*", action: "allow" }], + } satisfies Agent.Info + const user = { + id: MessageID.make("msg_user-parent-header"), + sessionID, + role: "user", + time: { created: Date.now() }, + agent: agent.name, + model: { + providerID: ProviderV2.ID.make(input.providerID), + modelID: resolved.id, }, - }, - } + } satisfies SessionV1.User + + yield* drain({ + user, + sessionID, + parentSessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + messages: [{ role: "user", content: "Hello" }], + tools: {}, + }) + + const headers = (yield* Effect.promise(() => request)).headers + expect(headers.get("x-opencode-session-id")).toBe(sessionID) + expect(headers.get("x-opencode-parent-session-id")).toBe(parentSessionID ?? null) + expect(headers.get("x-parent-session-id")).toBe(parentSessionID ?? null) + if (input.providerID.startsWith("opencode")) { + expect(headers.get("x-opencode-session")).toBe(sessionID) + return + } + expect(headers.get("x-session-affinity")).toBe(sessionID) + expect(headers.get("X-Session-Id")).toBe(sessionID) + }), + { + config: () => { + const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID) + return { + enabled_providers: [input.providerID], + provider: { + [input.providerID]: { + name: "OpenCode Test", + npm: "@ai-sdk/openai-compatible", + models: { [fixture.model.id]: configModel(fixture.model) as ConfigModel }, + options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` }, + }, + }, + } + }, }, - }, - ) + ) + }) it.instance( "sends temperature, tokens, and reasoning options for openai-compatible models",