diff --git a/src/providers/proxies/claude-proxy.ts b/src/providers/proxies/claude-proxy.ts index d8302d1..e8a249b 100644 --- a/src/providers/proxies/claude-proxy.ts +++ b/src/providers/proxies/claude-proxy.ts @@ -653,8 +653,19 @@ export const prepareClaudeProxyRequest = ( // OAuth sessions require Claude Code identity headers. // https://github.com/badlogic/pi-mono/blob/5c0ec26c28c918c5301f218e8c13fcc540d8e3a4/packages/ai/src/providers/anthropic.ts#L525-L538 + // Claude Code's Messages client uses the 2023-06-01 API version and accepts + // JSON even when `stream: true` requests an SSE response. + input.headers.delete("x-api-key"); + input.headers.delete("cookie"); + input.headers.delete("proxy-authorization"); + input.headers.delete("content-encoding"); + input.headers.delete("content-length"); + input.headers.delete("host"); input.headers.set("authorization", `Bearer ${input.accessToken}`); + input.headers.set("anthropic-version", "2023-06-01"); input.headers.set("anthropic-beta", mergedBetas); + input.headers.set("accept", "application/json"); + input.headers.set("content-type", "application/json"); // Keep existing accounts on the minimum supported Claude Code version even // when their persisted metadata still contains an older user agent. input.headers.set("user-agent", CLAUDE_CLI_USER_AGENT); diff --git a/tests/providers/proxy-contract.test.ts b/tests/providers/proxy-contract.test.ts index dfadd0a..6e13636 100644 --- a/tests/providers/proxy-contract.test.ts +++ b/tests/providers/proxy-contract.test.ts @@ -3024,6 +3024,16 @@ describe("proxy contract: claude", () => { test("merges beta headers and rewrites payload tool names", () => { const headers = new Headers({ "anthropic-beta": `custom-beta,${CLAUDE_REQUIRED_BETA_HEADERS[0]}`, + "anthropic-version": "2020-01-01", + accept: "text/plain", + "content-type": "text/plain", + "content-encoding": "gzip", + "content-length": "1234", + authorization: "Bearer caller-token", + "x-api-key": "caller-key", + cookie: "session=caller-cookie", + "proxy-authorization": "Basic caller-secret", + host: "caller.invalid", }); const requestBody = { system: @@ -3065,6 +3075,19 @@ describe("proxy contract: claude", () => { }; expect(headers.get("authorization")).toBe("Bearer claude-token"); + expect(headers.get("anthropic-version")).toBe("2023-06-01"); + expect(headers.get("accept")).toBe("application/json"); + expect(headers.get("content-type")).toBe("application/json"); + for (const name of [ + "x-api-key", + "cookie", + "proxy-authorization", + "content-encoding", + "content-length", + "host", + ]) { + expect(headers.has(name)).toBe(false); + } expect(headers.get("user-agent")).toBe(CLAUDE_CLI_USER_AGENT); expect(headers.get("anthropic-beta")).toBe( [...CLAUDE_REQUIRED_BETA_HEADERS, "custom-beta"].join(",") @@ -3101,6 +3124,22 @@ describe("proxy contract: claude", () => { expect(transformed.messages[0]?.content[0]?.name).toBe("mcp_Shell"); }); + test("keeps Claude Code JSON Accept for streaming Messages requests", () => { + const headers = new Headers({ accept: "text/plain" }); + prepareClaudeProxyRequest({ + requestUrl: new URL("https://kleis.local/v1/messages"), + headers, + bodyText: '{"stream":true}', + bodyJson: { stream: true }, + accessToken: "claude-token", + metadata: null, + }); + + expect(headers.get("accept")).toBe("application/json"); + expect(headers.get("content-type")).toBe("application/json"); + expect(headers.get("anthropic-version")).toBe("2023-06-01"); + }); + test("rewrites repo path and directories in non-OpenCode Claude system prompts", () => { const requestBody = { system: