fix: preserve provider request headers in logs (#5257)

Integrated into release/v3.8.40 — provider request headers preserved in logs; verified combo reads native Response.headers (no regression).
This commit is contained in:
Randi
2026-06-28 17:43:51 -04:00
committed by GitHub
parent b87b9abe75
commit c779be6532
3 changed files with 183 additions and 17 deletions

View File

@@ -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,

View File

@@ -49,7 +49,6 @@ test.afterEach(() => {
async function withEnv<T>(entries: Record<string, string | undefined>, fn: () => T | Promise<T>) {
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<T>(entries: Record<string, string | undefined>, 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);
});

View File

@@ -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");
});