mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-29 03:12:10 +08:00
Merge branch 'main' into dev/3.1
# Conflicts: # README.md # README_zh.md
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
@@ -7,6 +7,9 @@ type UpstreamRequest = {
|
||||
};
|
||||
|
||||
type ProviderPluginRequestInput = {
|
||||
request?: {
|
||||
headers?: Record<string, string | string[] | undefined>;
|
||||
};
|
||||
upstreamRequest: UpstreamRequest;
|
||||
};
|
||||
|
||||
@@ -15,6 +18,35 @@ const ccrAuthHeaderNames = new Set([
|
||||
"x-auth-sub"
|
||||
]);
|
||||
|
||||
const ccrRoutingHeaderNames = new Set([
|
||||
"x-gateway-target-provider",
|
||||
"x-gateway-target-provider-name",
|
||||
"x-target-model",
|
||||
"x-target-provider",
|
||||
"x-target-providers"
|
||||
]);
|
||||
|
||||
const clientAuthHeaderNames = new Set([
|
||||
"api-key",
|
||||
"authorization",
|
||||
"x-api-key"
|
||||
]);
|
||||
|
||||
const transportHeaderNames = new Set([
|
||||
"connection",
|
||||
"content-encoding",
|
||||
"content-length",
|
||||
"expect",
|
||||
"host",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailer",
|
||||
"transfer-encoding",
|
||||
"upgrade"
|
||||
]);
|
||||
|
||||
/**
|
||||
* Removes CCR-owned routing, authentication and observability metadata at the
|
||||
* final provider boundary. Provider credentials and non-CCR custom X-Auth
|
||||
@@ -30,6 +62,53 @@ export function sanitizeUpstreamProviderHeaders(headers: Record<string, string>)
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores client headers after the core protocol adapter has rebuilt the
|
||||
* provider request. Provider-generated auth and content headers win on name
|
||||
* collisions, while transport and CCR-owned headers never cross the boundary.
|
||||
*/
|
||||
export function mergeUpstreamProviderHeaders(
|
||||
requestHeaders: Record<string, string | string[] | undefined> | undefined,
|
||||
upstreamHeaders: Record<string, string>
|
||||
): Record<string, string> {
|
||||
const connectionHeaders = new Set(transportHeaderNames);
|
||||
for (const value of headerValues(requestHeaders?.connection)) {
|
||||
for (const name of value.split(",")) {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
if (normalized) connectionHeaders.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
const merged: Record<string, string> = {};
|
||||
for (const [name, value] of Object.entries(requestHeaders ?? {})) {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
if (
|
||||
!normalized ||
|
||||
value === undefined ||
|
||||
normalized.startsWith("x-ccr-") ||
|
||||
ccrAuthHeaderNames.has(normalized) ||
|
||||
ccrRoutingHeaderNames.has(normalized) ||
|
||||
clientAuthHeaderNames.has(normalized) ||
|
||||
connectionHeaders.has(normalized)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
merged[normalized] = Array.isArray(value) ? value.join(",") : value;
|
||||
}
|
||||
|
||||
for (const [name, value] of Object.entries(sanitizeUpstreamProviderHeaders(upstreamHeaders))) {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
if (!normalized || connectionHeaders.has(normalized)) continue;
|
||||
merged[normalized] = value;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function headerValues(value: string | string[] | undefined): string[] {
|
||||
if (value === undefined) return [];
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
export function createGatewayPlugin() {
|
||||
return {
|
||||
providerHooks: [{
|
||||
@@ -39,7 +118,7 @@ export function createGatewayPlugin() {
|
||||
ok: true as const,
|
||||
value: {
|
||||
...input.upstreamRequest,
|
||||
headers: sanitizeUpstreamProviderHeaders(input.upstreamRequest.headers)
|
||||
headers: mergeUpstreamProviderHeaders(input.request?.headers, input.upstreamRequest.headers)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,3 +43,54 @@ test("gateway sanitizer hook runs on the final upstream request shape", async ()
|
||||
});
|
||||
assert.equal(upstreamRequest.headers["x-ccr-provider-credential-id"], "credential-id");
|
||||
});
|
||||
|
||||
test("gateway sanitizer hook forwards client headers without overriding provider headers", async () => {
|
||||
const [hook] = createGatewayPlugin().providerHooks;
|
||||
const upstreamRequest = {
|
||||
body: { model: "provider-model" },
|
||||
headers: {
|
||||
authorization: "Bearer provider-token",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
url: "https://provider.example/v1/responses"
|
||||
};
|
||||
|
||||
const result = await hook.transformRequest({
|
||||
request: {
|
||||
headers: {
|
||||
authorization: "Bearer ccr-client-token",
|
||||
connection: "keep-alive, x-hop-only",
|
||||
"content-length": "123",
|
||||
"content-type": "text/plain",
|
||||
cookie: "client-cookie=value",
|
||||
host: "127.0.0.1:3457",
|
||||
"http-referer": "https://cherry-ai.com",
|
||||
"user-agent": "Codex Desktop",
|
||||
"x-auth-api-key-id": "profile:codex",
|
||||
"x-auth-provider-extension": "provider-extension",
|
||||
"x-ccr-core-auth": "core-secret",
|
||||
"x-custom-provider-header": "custom-value",
|
||||
"x-custom-list": ["one", "two"],
|
||||
"x-hop-only": "remove-me",
|
||||
"x-target-model": "internal-provider/internal-model",
|
||||
"x-target-provider": "internal-provider",
|
||||
"x-title": "Claude Code Router"
|
||||
}
|
||||
},
|
||||
upstreamRequest
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(result.value.headers, {
|
||||
authorization: "Bearer provider-token",
|
||||
"content-type": "application/json",
|
||||
cookie: "client-cookie=value",
|
||||
"http-referer": "https://cherry-ai.com",
|
||||
"user-agent": "Codex Desktop",
|
||||
"x-auth-provider-extension": "provider-extension",
|
||||
"x-custom-provider-header": "custom-value",
|
||||
"x-custom-list": "one,two",
|
||||
"x-title": "Claude Code Router"
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user