From c779be6532bcda2da37c143e47da3323b120e611 Mon Sep 17 00:00:00 2001 From: Randi <55005611+rdself@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:43:51 -0400 Subject: [PATCH] fix: preserve provider request headers in logs (#5257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.40 — provider request headers preserved in logs; verified combo reads native Response.headers (no regression). --- open-sse/handlers/chatCore.ts | 14 +- tests/unit/executor-codex.test.ts | 9 +- .../provider-request-failure-pipeline.test.ts | 177 +++++++++++++++++- 3 files changed, 183 insertions(+), 17 deletions(-) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 15d34b6b60..e79f865f6b 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -2501,9 +2501,8 @@ export async function handleChatCore({ response: new Response(clientBody, { status: res.response.status, statusText: res.response.statusText, - headers: res.response.headers, + headers: new Headers(normalizeHeaders(res.response.headers)), }), - headers: res.response.headers, }; } @@ -2540,9 +2539,9 @@ export async function handleChatCore({ const statusText = rawResult.response.statusText; const headersObj = normalizeHeaders(rawResult.response.headers); - const headers = new Headers(headersObj); - stripStaleForwardingHeaders(headers); - const contentType = (headers.get("content-type") || "").toLowerCase(); + const responseHeaders = new Headers(headersObj); + stripStaleForwardingHeaders(responseHeaders); + const contentType = (responseHeaders.get("content-type") || "").toLowerCase(); const payload = await readNonStreamingResponseBody( rawResult.response, contentType, @@ -2553,14 +2552,13 @@ export async function handleChatCore({ return { ...rawResult, - response: new Response(payload, { status, statusText, headers }), - headers, + response: new Response(payload, { status, statusText, headers: responseHeaders }), _dedupSnapshot: { status, statusText, headers: (() => { const arr: [string, string][] = []; - headers.forEach((v, k) => arr.push([k, v])); + responseHeaders.forEach((v, k) => arr.push([k, v])); return arr; })(), payload, diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index e2ab6cb7f4..9b4ecaa7da 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -49,7 +49,6 @@ test.afterEach(() => { async function withEnv(entries: Record, fn: () => T | Promise) { const previous = new Map(); - for (const [key, value] of Object.entries(entries)) { previous.set(key, process.env[key]); if (value === undefined) { @@ -58,7 +57,6 @@ async function withEnv(entries: Record, fn: () => process.env[key] = value; } } - try { return await fn(); } finally { @@ -81,7 +79,6 @@ test("Codex helper functions isolate rate-limit scopes and parse quota headers", "x-codex-7d-limit": "5000", "x-codex-7d-reset-at": new Date(Date.now() + 120_000).toISOString(), }); - assert.equal(getCodexModelScope("codex-spark-mini"), "spark"); assert.equal(getCodexModelScope("gpt-5.3-codex-spark"), "spark"); assert.equal(getCodexModelScope("codex-bengalfox"), "spark"); @@ -154,7 +151,6 @@ test("isCodexResponsesWebSocketRequired: OMNIROUTE_CODEX_WS_ENABLED=false forces test("CodexExecutor.buildUrl honors /responses subpaths and compact mode", () => { const executor = new CodexExecutor(); - assert.equal( executor.buildUrl("gpt-5.3-codex", true, 0, {}), "https://chatgpt.com/backend-api/codex/responses" @@ -1106,10 +1102,7 @@ test("CodexExecutor.transformRequest preserves namespace MCP tools and hosted to assert.deepEqual(result.tool_choice, { type: "function", name: "jira_get_issue" }); - const body = { - tools: [{ type: "function", name: "exec_command", parameters: { type: "object" } }], - tool_choice: { type: "local_shell" }, - }; + const body = { tools: [], tool_choice: { type: "local_shell" } }; normalizeCodexTools(body); assert.equal(body.tool_choice, undefined); }); diff --git a/tests/unit/provider-request-failure-pipeline.test.ts b/tests/unit/provider-request-failure-pipeline.test.ts index 3d7df17efa..cedb7e3f16 100644 --- a/tests/unit/provider-request-failure-pipeline.test.ts +++ b/tests/unit/provider-request-failure-pipeline.test.ts @@ -24,6 +24,8 @@ const { clearModelLock } = await import("../../open-sse/services/accountFallback const { getCallLogs, getCallLogById } = await import("../../src/lib/usage/callLogs.ts"); const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts"); const { resetPayloadRulesConfigForTests } = await import("../../open-sse/services/payloadRules.ts"); +const { CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA, CONTEXT_1M_BETA_HEADER } = + await import("../../open-sse/services/claudeCodeCompatible.ts"); const originalFetch = globalThis.fetch; @@ -286,7 +288,11 @@ test("successful response includes both providerRequest and providerResponse in }), { status: 200, - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "cf-ray": "response-ray", + server: "cloudflare", + }, } ); }; @@ -317,4 +323,173 @@ test("successful response includes both providerRequest and providerResponse in detail.pipelinePayloads.providerResponse, "providerResponse must be present on success" ); + assert.equal( + detail.pipelinePayloads.providerRequest.headers["cf-ray"], + undefined, + "providerRequest headers must not be overwritten with upstream response headers" + ); + assert.equal( + detail.pipelinePayloads.providerRequest.headers.server, + undefined, + "providerRequest headers must not include upstream response server header" + ); + assert.equal(detail.pipelinePayloads.providerRequest.headers.Accept, "application/json"); + assert.equal(detail.pipelinePayloads.providerRequest.headers["Content-Type"], "application/json"); + assert.equal(detail.pipelinePayloads.providerRequest.headers.Authorization, "[REDACTED]"); +}); + +test("streaming response preserves request headers in providerRequest pipeline payload", async () => { + const body = { + model: "gpt-4o-mini", + stream: true, + messages: [{ role: "user", content: "hello" }], + }; + + const upstreamPayload = [ + `data: ${JSON.stringify({ + id: "chatcmpl-stream", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + choices: [{ index: 0, delta: { role: "assistant", content: "world" } }], + })}`, + "", + `data: ${JSON.stringify({ + id: "chatcmpl-stream", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + })}`, + "", + "data: [DONE]", + "", + ].join("\n"); + + globalThis.fetch = async () => { + return new Response(upstreamPayload, { + status: 200, + headers: { + "Content-Type": "text/event-stream", + "cf-ray": "stream-response-ray", + server: "cloudflare", + }, + }); + }; + + const result = await handleChatCore({ + body: structuredClone(body), + modelInfo: { provider: "openai", model: "gpt-4o-mini", extendedContext: false }, + credentials: { apiKey: "sk-test", providerSpecificData: {} }, + log: noopLog(), + clientRawRequest: { + endpoint: "/v1/chat/completions", + body: structuredClone(body), + headers: new Headers({ accept: "text/event-stream" }), + }, + userAgent: "unit-test", + } as any); + + assert.equal(result.success, true); + await result.response.text(); + await waitForAsyncSideEffects(); + + const detail = await waitFor(getLatestCallLog); + assert.ok(detail, "expected a call log to be persisted"); + assert.ok(detail.pipelinePayloads, "expected pipeline payloads"); + + const providerRequest = detail.pipelinePayloads.providerRequest; + assert.ok(providerRequest, "providerRequest must be present on streaming success"); + assert.equal( + providerRequest.headers["cf-ray"], + undefined, + "streaming providerRequest headers must not be response headers" + ); + assert.equal(providerRequest.headers.server, undefined); + assert.equal(providerRequest.headers.Accept, "text/event-stream"); + assert.equal(providerRequest.headers["Content-Type"], "application/json"); + assert.equal(providerRequest.headers.Authorization, "[REDACTED]"); +}); + +test("CC-compatible providerRequest log keeps request beta headers and summarized thinking body", async () => { + const body = { + model: "claude-opus-4-6", + messages: [{ role: "user", content: "hello" }], + stream: false, + }; + + const upstreamPayload = [ + "event: message_start", + 'data: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","model":"claude-opus-4-6","usage":{"input_tokens":7,"output_tokens":0}}}', + "", + "event: content_block_start", + 'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}', + "", + "event: content_block_delta", + 'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}', + "", + "event: message_delta", + 'data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":1}}', + "", + "event: message_stop", + 'data: {"type":"message_stop"}', + "", + ].join("\n"); + + globalThis.fetch = async () => { + return new Response(upstreamPayload, { + status: 200, + headers: { + "Content-Type": "text/event-stream", + "cf-ray": "cc-response-ray", + server: "cloudflare", + }, + }); + }; + + const result = await handleChatCore({ + body: structuredClone(body), + modelInfo: { + provider: "anthropic-compatible-cc-test", + model: "claude-opus-4-6", + extendedContext: false, + }, + credentials: { + apiKey: "sk-test", + providerSpecificData: { + baseUrl: "https://proxy.example.com", + requestDefaults: { + context1m: true, + redactThinking: true, + summarizeThinking: true, + }, + }, + }, + log: noopLog(), + clientRawRequest: { + endpoint: "/v1/chat/completions", + body: structuredClone(body), + headers: new Headers({ accept: "application/json" }), + }, + userAgent: "unit-test", + } as any); + + assert.equal(result.success, true); + await result.response.json(); + await waitForAsyncSideEffects(); + + const detail = await waitFor(getLatestCallLog); + assert.ok(detail, "expected a call log to be persisted"); + assert.ok(detail.pipelinePayloads, "expected pipeline payloads"); + + const providerRequest = detail.pipelinePayloads.providerRequest; + assert.ok(providerRequest, "providerRequest must be present on CC-compatible success"); + assert.equal(providerRequest.headers["cf-ray"], undefined); + assert.equal(providerRequest.headers.server, undefined); + assert.equal(providerRequest.headers.Accept, "application/json"); + assert.match(providerRequest.headers["anthropic-beta"], new RegExp(CONTEXT_1M_BETA_HEADER)); + assert.match( + providerRequest.headers["anthropic-beta"], + new RegExp(CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA) + ); + assert.equal(providerRequest.body.thinking.display, "summarized"); });