diff --git a/packages/core/src/plugin/provider/chatgpt.ts b/packages/core/src/plugin/provider/chatgpt.ts index 0c3c58fc064a..93525be369db 100644 --- a/packages/core/src/plugin/provider/chatgpt.ts +++ b/packages/core/src/plugin/provider/chatgpt.ts @@ -13,6 +13,7 @@ import { Integration } from "../../integration.js" import { Model } from "../../model.js" import { OauthCallbackPage } from "../../oauth/page.js" import { Provider } from "../../provider.js" +import { SessionAffinity } from "../../session/affinity.js" import type { PluginInternal } from "../internal.js" // First-time sign-in registers a user-owned client; OpenAI returns its issued client ID on the callback. @@ -264,6 +265,21 @@ export const ChatGPTPlugin = define({ }), { providerID }, ) + yield* ctx.session.hook( + "model.request", + (evt) => + Effect.gen(function* () { + if (!chatgpt) return + const session = yield* ctx.session + .get({ sessionID: evt.sessionID }) + .pipe(Effect.orElseSucceed(() => undefined)) + // Mirror the Codex client's session headers: ChatGPT derives prompt-cache affinity from session-id. + evt.headers["session-id"] = session ? SessionAffinity.get(session) : evt.sessionID + evt.headers["thread-id"] = evt.sessionID + evt.headers["x-client-request-id"] = evt.sessionID + }), + { providerID }, + ) yield* ctx.provider.transform((providers) => { const item = providers.get(providerID) if (!item) return diff --git a/packages/core/test/plugin/provider-chatgpt.test.ts b/packages/core/test/plugin/provider-chatgpt.test.ts index f74ac8c83f5e..2415bbe8d86a 100644 --- a/packages/core/test/plugin/provider-chatgpt.test.ts +++ b/packages/core/test/plugin/provider-chatgpt.test.ts @@ -73,10 +73,14 @@ const authorize = Effect.fn(function* () { return new URL(attempt.url) }) -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", @@ -767,8 +771,21 @@ describe("ChatGPTPlugin", () => { expect(provider.settings?.baseURL).toBe("https://api.openai.com/v1") expect(provider.headers).not.toHaveProperty("x-openai-chatpass-test") expect(direct.baseURL).toBe("https://api.openai.com/v1") - expect(direct.headers).toEqual({}) + expect(direct.headers).toEqual({ + "session-id": "ses_test", + "thread-id": "ses_test", + "x-client-request-id": "ses_test", + }) expect(direct.hasHttpHooks).toBe(false) + 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 }) + expect((yield* request(Provider.ID.openai, "https://api.openai.com/v1", child.id)).headers).toEqual({ + "session-id": parent.id, + "thread-id": child.id, + "x-client-request-id": child.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).not.toHaveProperty("x-openai-chatpass-test") @@ -845,6 +862,7 @@ describe("ChatGPTPlugin", () => { expect(provider.settings?.transport).toBe("websocket") expect(model.settings?.transport).toBeUndefined() expect(direct.baseURL).toBe("https://api.openai.com/v1") + expect(direct.headers).toEqual({}) expect(direct.hasHttpHooks).toBe(false) expect(provider.headers).not.toHaveProperty("x-openai-chatpass-test") expect(required(yield* models.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(true)