diff --git a/packages/ai/src/api/openai-codex-responses.ts b/packages/ai/src/api/openai-codex-responses.ts index 9c1831a02..a6bbc2ce7 100644 --- a/packages/ai/src/api/openai-codex-responses.ts +++ b/packages/ai/src/api/openai-codex-responses.ts @@ -1643,7 +1643,11 @@ function buildBaseCodexHeaders( accountId: string, token: string, ): Headers { - const headers = new Headers(initHeaders); + // Defaults first so model and caller headers can override them, matching the other providers. + const headers = new Headers({ originator: "pi", "User-Agent": getPiUserAgent() }); + for (const [key, value] of Object.entries(initHeaders || {})) { + headers.set(key, value); + } for (const [key, value] of Object.entries(additionalHeaders || {})) { if (value === null) { headers.delete(key); @@ -1653,8 +1657,6 @@ function buildBaseCodexHeaders( } headers.set("Authorization", `Bearer ${token}`); headers.set("chatgpt-account-id", accountId); - headers.set("originator", "pi"); - headers.set("User-Agent", getPiUserAgent()); return headers; } diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index 9aabc09c3..9283a95d6 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -797,6 +797,55 @@ describe("openai-codex streaming", () => { expect(capturedHeaders?.get("x-client-request-id")).toBe("x".repeat(64)); }); + it("lets model and caller headers override originator and User-Agent", async () => { + const token = mockToken(); + let capturedHeaders: Headers | undefined; + const encoder = new TextEncoder(); + vi.stubGlobal( + "fetch", + vi.fn(async (_input: string | URL, init?: RequestInit) => { + capturedHeaders = init?.headers instanceof Headers ? init.headers : undefined; + return new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(buildSSEPayload({ status: "completed" }))); + controller.close(); + }, + }), + { status: 200, headers: { "content-type": "text/event-stream" } }, + ); + }), + ); + + const model: Model<"openai-codex-responses"> = { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + headers: { originator: "my-app" }, + }; + const context = normalizeContext({ + systemPrompt: "You are a helpful assistant.", + messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], + }); + + await streamOpenAICodexResponses(model, context, { + apiKey: token, + transport: "sse", + headers: { "user-agent": "my-app/1.0", Authorization: "Bearer ignored" }, + }).result(); + + expect(capturedHeaders?.get("originator")).toBe("my-app"); + expect(capturedHeaders?.get("User-Agent")).toBe("my-app/1.0"); + expect(capturedHeaders?.get("Authorization")).toBe(`Bearer ${token}`); + }); + it("preserves gpt-5.5 xhigh reasoning effort from simple options", async () => { const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-")); process.env.PI_CODING_AGENT_DIR = tempDir;