mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
Merge remote-tracking branch 'origin/dev/3.1' into dev/3.1
This commit is contained in:
commit
1da1723530
4 changed files with 50 additions and 14 deletions
|
|
@ -5,6 +5,7 @@
|
|||
<a href="https://discord.gg/rdftVMaUcS"><img alt="Discord" src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" /></a>
|
||||
<a href="https://x.com/musistudio2026"><img alt="X" src="https://img.shields.io/badge/X-@musistudio2026-000000?logo=x&logoColor=white" /></a>
|
||||
<a href="https://github.com/musistudio/claude-code-router/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/musistudio/claude-code-router" /></a>
|
||||
<a href="https://github.com/musistudio/claude-code-router/releases"><img alt="Desktop downloads" src="https://img.shields.io/github/downloads/musistudio/claude-code-router/total?label=Desktop%20downloads&logo=github" /></a>
|
||||
<a href="https://ccrdesk.top/"><img alt="Documentation" src="https://img.shields.io/badge/Docs-ccrdesk.top-0ea5e9?style=flat" /></a>
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<a href="https://discord.gg/rdftVMaUcS"><img alt="Discord" src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" /></a>
|
||||
<a href="https://x.com/musistudio2026"><img alt="X" src="https://img.shields.io/badge/X-@musistudio2026-000000?logo=x&logoColor=white" /></a>
|
||||
<a href="https://github.com/musistudio/claude-code-router/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/musistudio/claude-code-router" /></a>
|
||||
<a href="https://github.com/musistudio/claude-code-router/releases"><img alt="桌面端下载次数" src="https://img.shields.io/github/downloads/musistudio/claude-code-router/total?label=%E6%A1%8C%E9%9D%A2%E7%AB%AF%E4%B8%8B%E8%BD%BD&logo=github" /></a>
|
||||
<a href="https://ccrdesk.top/"><img alt="文档" src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-ccrdesk.top-0ea5e9?style=flat" /></a>
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -2380,8 +2380,12 @@ function configRead(params, values) {
|
|||
model: agentEnv(runtimeAgent, "MODEL") || DEFAULT_MODEL,
|
||||
model_catalog_json: JSON.stringify(modelCatalogConfigValue()),
|
||||
model_provider: agentEnv(runtimeAgent, "MODEL_PROVIDER") || "claude-code",
|
||||
approval_policy: "default",
|
||||
sandbox_mode: "workspace-write"
|
||||
approval_policy: "default"
|
||||
// sandbox_mode intentionally omitted: let Codex read it from its own
|
||||
// config.toml (e.g. [windows] sandbox) instead of forcing workspace-write.
|
||||
// Forcing workspace-write triggers codex-windows-sandbox-setup.exe on every
|
||||
// command, which fails on systems where the COM+ catalog is unavailable
|
||||
// (see openai/codex#29332), surfacing as repeated error dialogs.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -637,7 +637,7 @@ class GatewayService {
|
|||
const client = inferGatewayClient(apiKey, request.headers);
|
||||
const cursorCompatPreparation = prepareCursorOpenAICompatChatBody(this.config, client, method, path, requestBody);
|
||||
if (cursorCompatPreparation) {
|
||||
headers["x-ccr-cursor-openai-compat"] = cursorCompatPreparation.diagnostic;
|
||||
headers["x-ccr-cursor-openai-compat"] = sanitizeHeaderValue(cursorCompatPreparation.diagnostic);
|
||||
}
|
||||
let bodyToForward: Buffer | undefined = cursorCompatPreparation?.body ?? requestBody;
|
||||
let routeFallback = this.config.Router.fallback;
|
||||
|
|
@ -645,12 +645,12 @@ class GatewayService {
|
|||
let codexApplyPatchBridgeActive = false;
|
||||
const claudeModelRewrite = prepareClaudeCodeDiscoveredModelRequest(this.config, request.headers, method, path, bodyToForward);
|
||||
if (claudeModelRewrite) {
|
||||
headers["x-ccr-claude-model-discovery"] = claudeModelRewrite.diagnostic;
|
||||
headers["x-ccr-claude-model-discovery"] = sanitizeHeaderValue(claudeModelRewrite.diagnostic);
|
||||
bodyToForward = claudeModelRewrite.body;
|
||||
}
|
||||
const claudeAppModelRewrite = prepareClaudeAppFallbackModelRequest(this.config, method, path, bodyToForward);
|
||||
if (claudeAppModelRewrite) {
|
||||
headers["x-ccr-claude-app-model-rewrite"] = claudeAppModelRewrite.diagnostic;
|
||||
headers["x-ccr-claude-app-model-rewrite"] = sanitizeHeaderValue(claudeAppModelRewrite.diagnostic);
|
||||
bodyToForward = claudeAppModelRewrite.body;
|
||||
routedModel = claudeAppModelRewrite.routedModel;
|
||||
}
|
||||
|
|
@ -680,6 +680,16 @@ class GatewayService {
|
|||
upstreamAbortController.abort(new Error(clientDisconnectMessage));
|
||||
onClientDisconnect?.();
|
||||
});
|
||||
response.on("error", (error) => {
|
||||
// Client-side write failures (EPIPE / ECONNRESET when the client closes
|
||||
// mid-stream, common during tool execution) must not crash the main
|
||||
// process as an Uncaught Exception. Swallow them here; the close handler
|
||||
// above already records the disconnect via writeStreamLog.
|
||||
if (!clientDisconnected) {
|
||||
clientDisconnected = true;
|
||||
upstreamAbortController.abort(new Error(clientDisconnectMessage));
|
||||
}
|
||||
});
|
||||
|
||||
const writeRequestLog = (
|
||||
statusCode: number,
|
||||
|
|
@ -745,10 +755,10 @@ class GatewayService {
|
|||
});
|
||||
const serialized = Buffer.from(`${JSON.stringify(routed.body)}\n`, "utf8");
|
||||
headers["content-type"] = "application/json";
|
||||
headers["x-ccr-route-reason"] = routed.decision.reason;
|
||||
headers["x-ccr-route-reason"] = sanitizeHeaderValue(routed.decision.reason);
|
||||
routeFallback = routed.decision.fallback ?? routeFallback;
|
||||
if (routed.decision.model) {
|
||||
headers["x-ccr-routed-model"] = routed.decision.model;
|
||||
headers["x-ccr-routed-model"] = sanitizeHeaderValue(routed.decision.model);
|
||||
routedModel = routed.decision.model;
|
||||
}
|
||||
bodyToForward = serialized;
|
||||
|
|
@ -763,10 +773,10 @@ class GatewayService {
|
|||
});
|
||||
const serialized = Buffer.from(`${JSON.stringify(routed.body)}\n`, "utf8");
|
||||
headers["content-type"] = "application/json";
|
||||
headers["x-ccr-route-reason"] = routed.decision.reason;
|
||||
headers["x-ccr-route-reason"] = sanitizeHeaderValue(routed.decision.reason);
|
||||
routeFallback = routed.decision.fallback ?? routeFallback;
|
||||
if (routed.decision.model) {
|
||||
headers["x-ccr-routed-model"] = routed.decision.model;
|
||||
headers["x-ccr-routed-model"] = sanitizeHeaderValue(routed.decision.model);
|
||||
routedModel = routed.decision.model;
|
||||
}
|
||||
bodyToForward = serialized;
|
||||
|
|
@ -783,7 +793,7 @@ class GatewayService {
|
|||
if (codexApplyPatchBridgeRequest) {
|
||||
bodyToForward = codexApplyPatchBridgeRequest.body;
|
||||
codexApplyPatchBridgeActive = true;
|
||||
headers["x-ccr-codex-patch-bridge"] = codexApplyPatchBridgeRequest.diagnostic;
|
||||
headers["x-ccr-codex-patch-bridge"] = sanitizeHeaderValue(codexApplyPatchBridgeRequest.diagnostic);
|
||||
headers["content-type"] = "application/json";
|
||||
}
|
||||
|
||||
|
|
@ -908,7 +918,13 @@ class GatewayService {
|
|||
bodyToForward = upstreamResult.attempt.body ?? bodyToForward;
|
||||
routedModel = upstreamResult.attempt.model ?? routedModel;
|
||||
const responseHeaders = rewriteCapabilityResponseHeaders(
|
||||
mergeFallbackResponseHeaders(upstreamResponseHeaders(upstreamResult), upstreamResult),
|
||||
// Copy into a mutable Headers instance: upstream fetch Response.headers
|
||||
// can be immutable (TypeError: immutable on .delete/.set), and
|
||||
// mergeFallbackResponseHeaders returns the original object as-is when
|
||||
// no fallback occurred. Codex apply_patch / web-search paths call
|
||||
// .delete("content-length") below, which would otherwise throw and
|
||||
// surface as a 502.
|
||||
new Headers(mergeFallbackResponseHeaders(upstreamResponseHeaders(upstreamResult), upstreamResult)),
|
||||
this.config
|
||||
);
|
||||
const upstreamResponse = upstreamResult.response;
|
||||
|
|
@ -997,7 +1013,7 @@ class GatewayService {
|
|||
writeStreamLog();
|
||||
}
|
||||
});
|
||||
responseBody.once("error", (error) => {
|
||||
responseBody.on("error", (error) => {
|
||||
streamDetectedError ??= sseErrorDetector.finish();
|
||||
writeStreamLog(clientDisconnected ? clientDisconnectMessage : formatError(error));
|
||||
});
|
||||
|
|
@ -5627,7 +5643,7 @@ function prepareUpstreamCredentialAttempt(input: {
|
|||
const headers: Record<string, string> = {
|
||||
...input.headers,
|
||||
"x-target-providers": selection.credentials.map((candidate) => candidate.internalName).join(","),
|
||||
"x-ccr-logical-provider": target.provider.name,
|
||||
"x-ccr-logical-provider": providerRuntimeId(target.provider),
|
||||
"x-ccr-provider-credential-chain": selection.credentials.map((candidate) => candidate.credentialId).join(",")
|
||||
};
|
||||
delete headers["x-target-provider"];
|
||||
|
|
@ -6084,7 +6100,7 @@ function mergeFallbackResponseHeaders(headers: Headers, result: UpstreamFetchRes
|
|||
merged.set("x-ccr-fallback-delays-ms", formatFallbackDelays(result.failedAttempts));
|
||||
}
|
||||
if (result.attempt.model) {
|
||||
merged.set("x-ccr-fallback-model", result.attempt.model);
|
||||
merged.set("x-ccr-fallback-model", sanitizeHeaderValue(result.attempt.model));
|
||||
}
|
||||
}
|
||||
if (credentialIds.length) {
|
||||
|
|
@ -6505,6 +6521,20 @@ function sanitizeProviderHeaderId(value: string | undefined): string | undefined
|
|||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function sanitizeHeaderValue(value: unknown): string {
|
||||
// HTTP header values must be ByteString (code point <= 255). Values derived
|
||||
// from user-facing names — model selectors like "小米mimo/...", provider
|
||||
// names, route reasons — can contain non-ASCII characters that crash Node's
|
||||
// fetch/undici with "Cannot convert argument to a ByteString" (surfaced as
|
||||
// 502). Normalize to ASCII while preserving case and printable punctuation.
|
||||
const text = typeof value === "string" && value.trim() ? value : "unknown";
|
||||
const sanitized = text
|
||||
.replace(/[^\x20-\x7E]+/g, "-")
|
||||
.replace(/-{2,}/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return sanitized || "unknown";
|
||||
}
|
||||
|
||||
function providerCredentialInternalName(
|
||||
provider: GatewayProviderConfig,
|
||||
protocol: GatewayProviderProtocol,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue