fix(opencode): add namespaced session identity headers (#52370)

This commit is contained in:
Aiden Cline
2026-09-30 15:39:43 -05:00
committed by GitHub
parent 9b4882db54
commit e9f8a210b9
4 changed files with 95 additions and 65 deletions
+2
View File
@@ -206,6 +206,8 @@ const layer = Layer.effect(
model, model,
http: { http: {
headers: { headers: {
"x-opencode-session-id": session.id,
...(session.parentID ? { "x-opencode-parent-session-id": session.parentID } : {}),
"x-session-affinity": session.id, "x-session-affinity": session.id,
"X-Session-Id": session.id, "X-Session-Id": session.id,
...(session.parentID ? { "x-parent-session-id": session.parentID } : {}), ...(session.parentID ? { "x-parent-session-id": session.parentID } : {}),
+10 -1
View File
@@ -1103,10 +1103,12 @@ describe("SessionRunnerLLM", () => {
expect(requests).toHaveLength(2) expect(requests).toHaveLength(2)
expect(requests.map((request) => request.http?.headers)).toEqual([ expect(requests.map((request) => request.http?.headers)).toEqual([
{ {
"x-opencode-session-id": sessionID,
"x-session-affinity": sessionID, "x-session-affinity": sessionID,
"X-Session-Id": sessionID, "X-Session-Id": sessionID,
}, },
{ {
"x-opencode-session-id": sessionID,
"x-session-affinity": sessionID, "x-session-affinity": sessionID,
"X-Session-Id": sessionID, "X-Session-Id": sessionID,
}, },
@@ -2528,6 +2530,7 @@ describe("SessionRunnerLLM", () => {
yield* session.resume(sessionID) yield* session.resume(sessionID)
expect(requests[0]?.http?.headers).toEqual({ expect(requests[0]?.http?.headers).toEqual({
"x-opencode-session-id": sessionID,
"x-session-affinity": sessionID, "x-session-affinity": sessionID,
"X-Session-Id": sessionID, "X-Session-Id": sessionID,
}) })
@@ -2551,7 +2554,13 @@ describe("SessionRunnerLLM", () => {
requests.length = 0 requests.length = 0
yield* session.resume(sessionID) 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,
})
}), }),
) )
@@ -185,6 +185,8 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
params, params,
messageTransformOptions: options, messageTransformOptions: options,
headers: { headers: {
"x-opencode-session-id": input.sessionID,
...(input.parentSessionID ? { "x-opencode-parent-session-id": input.parentSessionID } : {}),
...(input.model.providerID.startsWith("opencode") ...(input.model.providerID.startsWith("opencode")
? { ? {
...(opencodeProjectID ? { "x-opencode-project": opencodeProjectID } : {}), ...(opencodeProjectID ? { "x-opencode-project": opencodeProjectID } : {}),
+81 -64
View File
@@ -756,72 +756,89 @@ describe("session.llm.stream", () => {
const vivgridFixture = { providerID: "vivgrid", modelID: "gemini-3.1-pro-preview" } const vivgridFixture = { providerID: "vivgrid", modelID: "gemini-3.1-pro-preview" }
const opencodeFixture = { providerID: "opencode-test", modelID: vivgridFixture.modelID } const opencodeFixture = { providerID: "opencode-test", modelID: vivgridFixture.modelID }
it.instance( const headerCases = [
"sends the parent session header for opencode providers", { providerID: opencodeFixture.providerID, child: false },
() => { providerID: opencodeFixture.providerID, child: true },
Effect.gen(function* () { { providerID: "custom-test", child: false },
const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID) { providerID: "custom-test", child: true },
const request = waitRequest( ]
"/chat/completions", headerCases.forEach((input) => {
new Response(createChatStream("Hello"), { it.instance(
status: 200, `sends session identity headers for ${input.child ? "child" : "root"} sessions on ${input.providerID}`,
headers: { "Content-Type": "text/event-stream" }, () =>
}), Effect.gen(function* () {
) const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID)
const resolved = yield* Provider.use.getModel( const request = waitRequest(
ProviderV2.ID.make(opencodeFixture.providerID), "/chat/completions",
ModelV2.ID.make(opencodeFixture.modelID), new Response(createChatStream("Hello"), {
) status: 200,
const sessionID = SessionID.make("session-child") headers: { "Content-Type": "text/event-stream" },
const parentSessionID = SessionID.make("session-parent") }),
const agent = { )
name: "test", const resolved = yield* Provider.use.getModel(
mode: "primary", ProviderV2.ID.make(input.providerID),
options: {}, ModelV2.ID.make(opencodeFixture.modelID),
permission: [{ permission: "*", pattern: "*", action: "allow" }], )
} satisfies Agent.Info const sessionID = SessionID.make("session-child")
const user = { const parentSessionID = input.child ? SessionID.make("session-parent") : undefined
id: MessageID.make("msg_user-parent-header"), const agent = {
sessionID, name: "test",
role: "user", mode: "primary",
time: { created: Date.now() }, options: {},
agent: agent.name, permission: [{ permission: "*", pattern: "*", action: "allow" }],
model: { } satisfies Agent.Info
providerID: ProviderV2.ID.make(opencodeFixture.providerID), const user = {
modelID: resolved.id, id: MessageID.make("msg_user-parent-header"),
}, sessionID,
} satisfies SessionV1.User role: "user",
time: { created: Date.now() },
yield* drain({ agent: agent.name,
user, model: {
sessionID, providerID: ProviderV2.ID.make(input.providerID),
parentSessionID, modelID: resolved.id,
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` },
}, },
}, } 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( it.instance(
"sends temperature, tokens, and reasoning options for openai-compatible models", "sends temperature, tokens, and reasoning options for openai-compatible models",