Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 21 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1561,32 +1561,37 @@ export class ClaudeCodeProxy {
* Build request headers for a specific provider
*/
private buildProviderHeaders(provider: 'anthropic' | 'zai' | 'openrouter', reqHeaders: Record<string, string>): Record<string, string> {
if (provider === 'zai') {
const headers: Record<string, string> = {
host: new URL(this.config.zai.baseUrl).host,
authorization: `Bearer ${this.config.zai.apiKey}`,
};
// Copy allowed headers
// Headers that must NOT be forwarded to a non-Anthropic upstream. Beyond the
// hop-by-hop/auth headers, Anthropic-specific routing headers — especially
// `x-api-key` — make OpenRouter route the request into a restricted data-policy
// path and return `404: No endpoints available matching your guardrail
// restrictions`. `content-length` is recomputed after cleanBody, so drop the
// client's value too.
const blockedForwardHeaders = [
"authorization", "transfer-encoding", "connection", "host", "content-length",
"x-api-key", "anthropic-version", "anthropic-beta", "anthropic-dangerous-direct-browser-access",
];
const copyAllowed = (headers: Record<string, string>) => {
for (const [key, value] of Object.entries(reqHeaders)) {
if (!["authorization", "transfer-encoding", "connection", "host"].includes(key)) {
if (!blockedForwardHeaders.includes(key.toLowerCase())) {
headers[key] = value;
}
}
return headers;
};

if (provider === 'zai') {
return copyAllowed({
host: new URL(this.config.zai.baseUrl).host,
authorization: `Bearer ${this.config.zai.apiKey}`,
});
} else if (provider === 'openrouter') {
const headers: Record<string, string> = {
return copyAllowed({
host: new URL(this.config.openrouter.baseUrl).host,
authorization: `Bearer ${this.config.openrouter.apiKey}`,
"HTTP-Referer": "https://claude.ai/code",
"X-Title": "Claude Code",
};
// Copy allowed headers
for (const [key, value] of Object.entries(reqHeaders)) {
if (!["authorization", "transfer-encoding", "connection", "host"].includes(key)) {
headers[key] = value;
}
}
return headers;
});
} else {
// Anthropic headers
return this.cleanHeaders(reqHeaders);
Expand Down
19 changes: 18 additions & 1 deletion tests/proxy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -557,10 +557,12 @@ describe("Claude Code Proxy provider request normalization", () => {
const proxy = createProxy();
let capturedUrl = "";
let capturedBody = "";
let capturedHeaders = {};

proxy.httpRequest = async (url, options) => {
capturedUrl = url;
capturedBody = String(options.body);
capturedHeaders = options.headers;
return {
status: 200,
headers: options.headers,
Expand All @@ -577,7 +579,14 @@ describe("Claude Code Proxy provider request normalization", () => {
metadata: { source: "test" },
extra_field: "should-be-stripped",
}),
{ "content-type": "application/json" },
{
"content-type": "application/json",
// Claude Code always sends these Anthropic-specific headers; forwarding
// x-api-key to OpenRouter triggers a guardrail 404.
"x-api-key": "sk-ant-should-not-leak",
"anthropic-version": "2023-06-01",
"anthropic-beta": "claude-code-20250219",
},
"/v1/messages?beta=true",
"POST",
);
Expand All @@ -588,6 +597,14 @@ describe("Claude Code Proxy provider request normalization", () => {
assert.equal(parsedBody.model, "~anthropic/claude-sonnet-latest");
assert.equal(parsedBody.extra_field, undefined);
assert.deepEqual(parsedBody.metadata, { source: "test" });

// Anthropic-specific headers must NOT reach OpenRouter (cause guardrail 404).
assert.equal(capturedHeaders["x-api-key"], undefined);
assert.equal(capturedHeaders["anthropic-version"], undefined);
assert.equal(capturedHeaders["anthropic-beta"], undefined);
// OpenRouter's own auth + attribution headers are present.
assert.equal(capturedHeaders.authorization, "Bearer openrouter-test-key");
assert.equal(capturedHeaders["HTTP-Referer"], "https://claude.ai/code");
});

it("normalizes OpenRouter message responses to strict Anthropic shape", async () => {
Expand Down
Loading