From 26f1e00f713ddea52a6d9981906b30326a0c0dba Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 27 May 2026 01:11:24 +0200 Subject: [PATCH] fix(ai): use hyphenated Codex session header closes #4967 --- packages/ai/CHANGELOG.md | 1 + .../ai/src/providers/openai-codex-responses.ts | 4 ++-- packages/ai/test/openai-codex-stream.test.ts | 17 +++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index ef945ff3..00fe90e7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed OpenAI Codex Responses cache-affinity headers to send `session-id` instead of proxy-incompatible `session_id` ([#4967](https://github.com/earendil-works/pi/issues/4967)). - Fixed `openai-codex/gpt-5.3-codex-spark` generated metadata to use its 128k context window ([#4969](https://github.com/earendil-works/pi/issues/4969)). - Fixed OpenRouter/Poolside context overflow detection for `maximum allowed input length` errors ([#4943](https://github.com/earendil-works/pi/issues/4943)). diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index b44de436..2cf5b920 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -1376,7 +1376,7 @@ function buildSSEHeaders( headers.set("content-type", "application/json"); if (sessionId) { - headers.set("session_id", sessionId); + headers.set("session-id", sessionId); headers.set("x-client-request-id", sessionId); } @@ -1397,6 +1397,6 @@ function buildWebSocketHeaders( headers.delete("openai-beta"); headers.set("OpenAI-Beta", OPENAI_BETA_RESPONSES_WEBSOCKETS); headers.set("x-client-request-id", requestId); - headers.set("session_id", requestId); + headers.set("session-id", requestId); return headers; } diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index 0c404e58..540f7658 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -311,7 +311,7 @@ describe("openai-codex streaming", () => { expect(result.stopReason).toBe("length"); }); - it("sets session_id/x-client-request-id headers and prompt_cache_key when sessionId is provided", async () => { + it("sets session-id/x-client-request-id headers and prompt_cache_key when sessionId is provided", async () => { const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-")); process.env.PI_CODING_AGENT_DIR = tempDir; @@ -372,7 +372,8 @@ describe("openai-codex streaming", () => { if (url === "https://chatgpt.com/backend-api/codex/responses") { const headers = init?.headers instanceof Headers ? init.headers : undefined; // Verify sessionId is set in headers - expect(headers?.get("session_id")).toBe(sessionId); + expect(headers?.get("session-id")).toBe(sessionId); + expect(headers?.has("session_id")).toBe(false); expect(headers?.get("x-client-request-id")).toBe(sessionId); // Verify sessionId is set in request body as prompt_cache_key @@ -721,7 +722,7 @@ describe("openai-codex streaming", () => { }, ); - it("does not set session_id/x-client-request-id headers when sessionId is not provided", async () => { + it("does not set session-id/x-client-request-id headers when sessionId is not provided", async () => { const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-")); process.env.PI_CODING_AGENT_DIR = tempDir; @@ -781,6 +782,7 @@ describe("openai-codex streaming", () => { if (url === "https://chatgpt.com/backend-api/codex/responses") { const headers = init?.headers instanceof Headers ? init.headers : undefined; // Verify headers are not set when sessionId is not provided + expect(headers?.has("session-id")).toBe(false); expect(headers?.has("session_id")).toBe(false); expect(headers?.has("x-client-request-id")).toBe(false); @@ -819,6 +821,7 @@ describe("openai-codex streaming", () => { it("forwards auto transport from streamSimple options and uses cached websocket context", async () => { const token = mockToken(); const sentBodies: unknown[] = []; + let capturedWebSocketHeaders: Record | undefined; const fetchMock = vi.fn(async () => new Response("unexpected fetch", { status: 500 })); vi.stubGlobal("fetch", fetchMock); @@ -826,7 +829,10 @@ describe("openai-codex streaming", () => { class MockWebSocket { private listeners = new Map void>>(); - constructor(_url: string, _protocols?: string | string[] | { headers?: Record }) { + constructor(_url: string, protocols?: string | string[] | { headers?: Record }) { + if (protocols && typeof protocols === "object" && !Array.isArray(protocols)) { + capturedWebSocketHeaders = protocols.headers; + } queueMicrotask(() => this.dispatch("open", {})); } @@ -917,6 +923,9 @@ describe("openai-codex streaming", () => { }).result(); expect(sentBodies).toHaveLength(1); + expect(capturedWebSocketHeaders?.["session-id"]).toBe("session-auto"); + expect(capturedWebSocketHeaders?.session_id).toBeUndefined(); + expect(capturedWebSocketHeaders?.["x-client-request-id"]).toBe("session-auto"); expect(global.fetch).not.toHaveBeenCalled(); expect(getOpenAICodexWebSocketDebugStats("session-auto")).toMatchObject({ cachedContextRequests: 1,