fix(core): share affinity in provider session headers (#51931)

This commit is contained in:
Aiden Cline
2026-09-28 19:30:24 -05:00
committed by GitHub
parent bda798b167
commit 1fc05ca590
6 changed files with 38 additions and 11 deletions
@@ -11,6 +11,7 @@ import { Model } from "../../model.js"
import { Agent } from "../../agent.js"
import { define } from "@opencode/plugin/effect/plugin"
import { Provider } from "../../provider.js"
import { SessionAffinity } from "../../session/affinity.js"
import type { PluginInternal } from "../internal.js"
const clientID = "Ov23li8tweQw6odWQebz"
@@ -272,7 +273,7 @@ export const GithubCopilotPlugin = define({
.pipe(Effect.orElseSucceed(() => undefined))
const interaction = interactionType(evt.kind, session?.parentID !== undefined)
evt.headers["X-Interaction-Type"] = interaction
evt.headers["X-Interaction-Id"] = evt.sessionID
evt.headers["X-Interaction-Id"] = session ? SessionAffinity.get(session) : evt.sessionID
if (interaction !== "conversation-agent") evt.headers["x-initiator"] = "agent"
}),
{ providerID: Provider.ID.githubCopilot },
+7 -2
View File
@@ -9,6 +9,7 @@ import { Bus } from "../../bus.js"
import { Integration } from "../../integration.js"
import { OauthCallbackPage } from "../../oauth/page.js"
import { Provider } from "../../provider.js"
import { SessionAffinity } from "../../session/affinity.js"
import type { PluginInternal } from "../internal.js"
const clientID = "app_EMoamEEZ73f0CkXaXp7hrann"
@@ -299,12 +300,16 @@ export const OpenAIPlugin = define({
yield* ctx.session.hook(
"model.request",
(evt) =>
Effect.sync(() => {
Effect.gen(function* () {
if (!chatgpt) return
if (evt.baseURL && URL.canParse(evt.baseURL) && new URL(evt.baseURL).origin === "https://api.openai.com")
evt.baseURL = codexBaseURL
const session = yield* ctx.session
.get({ sessionID: evt.sessionID })
.pipe(Effect.orElseSucceed(() => undefined))
evt.headers.originator = "opencode"
evt.headers["session-id"] = evt.sessionID
// ChatGPT routes its prompt cache on this header, so children share the parent's.
evt.headers["session-id"] = session ? SessionAffinity.get(session) : evt.sessionID
}),
{ providerID: Provider.ID.openai },
)
+8
View File
@@ -0,0 +1,8 @@
export * as SessionAffinity from "./affinity.js"
import type { SessionSchema } from "./schema.js"
// TODO: Should the `model.request` hook expose affinity so plugins stop deriving it from `ctx.session.get`?
/** The Session ID that groups model requests for provider cache routing: children share the parent's, forks the fork source's. */
export const get = (session: Pick<SessionSchema.Info, "id" | "parentID" | "fork">) =>
session.parentID ?? session.fork?.sessionID ?? session.id
+2 -1
View File
@@ -31,6 +31,7 @@ import { Permission } from "../permission.js"
import { PluginHooks } from "../plugin/hooks.js"
import { QuestionTool } from "../tool/plugin/question.js"
import { Tool } from "../tool.js"
import { SessionAffinity } from "./affinity.js"
import { SessionModelTransport } from "./model-transport.js"
import { SessionProviderContext } from "./provider-context.js"
import { SessionRunnerModel } from "./runner/model.js"
@@ -270,7 +271,7 @@ export const layer = Layer.effect(
const entries = Object.entries(shaped.options)
const generation = Object.fromEntries(entries.filter(([k]) => GENERATION_KEYS.has(k))) as GenerationOptionsFields
const providerOptions = Object.fromEntries(entries.filter(([k]) => !GENERATION_KEYS.has(k)))
const affinity = session.parentID ?? session.fork?.sessionID ?? session.id
const affinity = SessionAffinity.get(session)
const base = LLM.request({
model: model.model,
http: {
@@ -183,10 +183,11 @@ describe("GithubCopilotPlugin", () => {
it.effect("classifies child-session steps as subagent interactions", () =>
Effect.gen(function* () {
yield* addPlugin()
const event = yield* modelRequest((yield* sessions()).child, "primary")
const ids = yield* sessions()
const event = yield* modelRequest(ids.child, "primary")
expect(event.headers).toEqual({
"X-Interaction-Type": "conversation-subagent",
"X-Interaction-Id": event.sessionID,
"X-Interaction-Id": ids.parent,
"x-initiator": "agent",
})
}),
@@ -207,10 +208,11 @@ describe("GithubCopilotPlugin", () => {
it.effect("classifies compaction requests by kind rather than agent", () =>
Effect.gen(function* () {
yield* addPlugin()
const event = yield* modelRequest((yield* sessions()).child, "compaction", "build")
const ids = yield* sessions()
const event = yield* modelRequest(ids.child, "compaction", "build")
expect(event.headers).toEqual({
"X-Interaction-Type": "conversation-compaction",
"X-Interaction-Id": event.sessionID,
"X-Interaction-Id": ids.parent,
"x-initiator": "agent",
})
}),
@@ -1,6 +1,6 @@
import { Money } from "@opencode/schema/money"
import { Agent } from "@opencode/schema/agent"
import { Session } from "@opencode/schema/session"
import { Session } from "@opencode/core/session"
import { OpenAIResponses } from "@opencode/ai/protocols/openai-responses"
import { describe, expect } from "bun:test"
import { ConfigProvider, DateTime, Effect } from "effect"
@@ -41,10 +41,14 @@ function required<T>(value: T | undefined): T {
return value
}
const request = Effect.fn(function* (providerID: Provider.ID, baseURL: string) {
const request = Effect.fn(function* (
providerID: Provider.ID,
baseURL: string,
sessionID = Session.ID.make("ses_test"),
) {
const hooks = yield* PluginHooks.Service
const event = yield* hooks.trigger("session", "model.request", {
sessionID: Session.ID.make("ses_test"),
sessionID,
agent: Agent.ID.make("build"),
model: Model.Ref.make({ providerID, id: Model.ID.make("gpt-5.5") }),
kind: "primary",
@@ -156,6 +160,12 @@ describe("OpenAIPlugin", () => {
expect(custom.headers).not.toHaveProperty("originator")
expect(proxy.baseURL).toBe("https://proxy.example/v1?region=us")
expect(proxy.headers).toMatchObject({ originator: "opencode", "session-id": "ses_test" })
const sessions = yield* Session.Service
const location = yield* Location.Service
const parent = yield* sessions.create({ location: { directory: location.directory } })
const child = yield* sessions.create({ parentID: parent.id })
const childRequest = yield* request(Provider.ID.openai, "https://api.openai.com/v1", child.id)
expect(childRequest.headers).toMatchObject({ "session-id": parent.id })
const eligible = required(yield* models.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
expect(eligible.package).toBe("@opencode/ai/providers/openai")
expect(eligible.headers).toMatchObject({ originator: "opencode", "chatgpt-account-id": "acct_123" })