mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
Retry fallback after network errors and HTTP failures
This commit is contained in:
parent
15f67ab8d8
commit
4cf0c2468c
4 changed files with 47 additions and 12 deletions
|
|
@ -162,7 +162,7 @@ Network errors move to the next attempt. Status-code fallback depends on the mod
|
|||
| Retry | `408`, `409`, `429`, `5xx` |
|
||||
| Fallback targets | Any `4xx` or `5xx` |
|
||||
|
||||
For `429` rate-limit responses, CCR waits before the next attempt. It honors `Retry-After` when the upstream provides it; otherwise it uses exponential backoff starting at 1 second and capped at 30 seconds per attempt.
|
||||
Before moving to the next attempt, CCR waits for every fallback-triggering failure, including network errors. It honors a positive `Retry-After` header when the upstream provides one; otherwise it uses exponential backoff starting at 1 second and capped at 30 seconds per attempt.
|
||||
|
||||
**Fallback targets** also switches on `4xx` because model-not-found, auth, or provider-side rejection errors may only affect the current target. If the fallback model works, the request can still succeed.
|
||||
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ Fallback 处理请求失败后的降级。第一次选模型由路由完成;
|
|||
| 继续重试 | `408`、`409`、`429`、`5xx` |
|
||||
| 失败降级目标 | 任意 `4xx` 或 `5xx` |
|
||||
|
||||
对于 `429` 限流响应,CCR 会在下一次尝试前等待。上游提供 `Retry-After` 时会优先遵守;否则使用从 1 秒开始、单次最多 30 秒的指数退避。
|
||||
进入下一次尝试前,CCR 会对每个触发 Fallback 的失败进行等待,包括网络错误。上游提供正数 `Retry-After` 时会优先遵守;否则使用从 1 秒开始、单次最多 30 秒的指数退避。
|
||||
|
||||
**失败降级目标** 对 `4xx` 也会切换,是因为模型不存在、鉴权或供应商侧拒绝等错误可能只影响当前目标。切换后如果备用模型可用,请求仍然可以成功。
|
||||
|
||||
|
|
|
|||
|
|
@ -5681,7 +5681,7 @@ async function fetchUpstreamWithFallback(input: {
|
|||
});
|
||||
|
||||
if (hasNextAttempt && shouldFallbackAfterStatus(response.status, fallbackMode)) {
|
||||
const delayMs = retryDelayAfterStatus(response.status, response, failedAttempts.length);
|
||||
const delayMs = retryDelayAfterStatus(response.status, response.headers, failedAttempts.length);
|
||||
failedAttempts.push({
|
||||
credentialChain: attempt.credentialChain,
|
||||
credentialIds: attempt.credentialIds,
|
||||
|
|
@ -5704,10 +5704,13 @@ async function fetchUpstreamWithFallback(input: {
|
|||
};
|
||||
} catch (error) {
|
||||
const message = formatError(error);
|
||||
const delayMs = hasNextAttempt && !input.signal?.aborted
|
||||
? retryDelayAfterNetworkError(failedAttempts.length)
|
||||
: 0;
|
||||
failedAttempts.push({
|
||||
credentialChain: attempt.credentialChain,
|
||||
credentialIds: attempt.credentialIds,
|
||||
delayMs: 0,
|
||||
delayMs,
|
||||
error: message,
|
||||
model: attempt.model
|
||||
});
|
||||
|
|
@ -5719,6 +5722,9 @@ async function fetchUpstreamWithFallback(input: {
|
|||
});
|
||||
}
|
||||
if (hasNextAttempt) {
|
||||
if (delayMs > 0) {
|
||||
await delay(delayMs);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
throw new UpstreamRequestError(message, {
|
||||
|
|
@ -6170,17 +6176,30 @@ function shouldFallbackAfterStatus(statusCode: number, mode: RouterFallbackMode)
|
|||
return false;
|
||||
}
|
||||
|
||||
function retryDelayAfterStatus(statusCode: number, response: Response, failedAttemptIndex: number): number {
|
||||
if (statusCode !== 429) {
|
||||
return 0;
|
||||
}
|
||||
const retryAfterMs = parseRetryAfterHeaderMs(response.headers.get("retry-after"));
|
||||
if (retryAfterMs !== undefined) {
|
||||
return clampNumber(retryAfterMs, 0, upstreamRetryAfterMaxMs);
|
||||
function retryDelayAfterStatus(_statusCode: number, headers: Headers, failedAttemptIndex: number): number {
|
||||
const retryAfterMs = parseRetryAfterHeaderMs(headers.get("retry-after"));
|
||||
if (retryAfterMs !== undefined && retryAfterMs > 0) {
|
||||
return clampNumber(retryAfterMs, 1, upstreamRetryAfterMaxMs);
|
||||
}
|
||||
return exponentialRetryBackoffMs(failedAttemptIndex);
|
||||
}
|
||||
|
||||
function retryDelayAfterNetworkError(failedAttemptIndex: number): number {
|
||||
return exponentialRetryBackoffMs(failedAttemptIndex);
|
||||
}
|
||||
|
||||
export function fallbackRetryDelayAfterStatusForTest(input: { failedAttemptIndex?: number; retryAfter?: string | null; statusCode: number }): number {
|
||||
const headers = new Headers();
|
||||
if (input.retryAfter !== undefined && input.retryAfter !== null) {
|
||||
headers.set("retry-after", input.retryAfter);
|
||||
}
|
||||
return retryDelayAfterStatus(input.statusCode, headers, input.failedAttemptIndex ?? 0);
|
||||
}
|
||||
|
||||
export function fallbackRetryDelayAfterNetworkErrorForTest(failedAttemptIndex = 0): number {
|
||||
return retryDelayAfterNetworkError(failedAttemptIndex);
|
||||
}
|
||||
|
||||
function parseRetryAfterHeaderMs(value: string | null): number | undefined {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { ClaudeCodeRouterPlugin } from "../../packages/core/src/gateway/claude-code-router-plugin.ts";
|
||||
import { prepareGatewayUpstreamAttemptForTest } from "../../packages/core/src/gateway/service.ts";
|
||||
import {
|
||||
fallbackRetryDelayAfterNetworkErrorForTest,
|
||||
fallbackRetryDelayAfterStatusForTest,
|
||||
prepareGatewayUpstreamAttemptForTest
|
||||
} from "../../packages/core/src/gateway/service.ts";
|
||||
|
||||
function createRouterPlugin(options = {}) {
|
||||
const agent = options.agent ?? "claude-code";
|
||||
|
|
@ -42,6 +46,18 @@ function createRouterPlugin(options = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
test("fallback retry delay backs off retryable HTTP statuses", () => {
|
||||
assert.equal(fallbackRetryDelayAfterStatusForTest({ statusCode: 503 }), 1000);
|
||||
assert.equal(fallbackRetryDelayAfterStatusForTest({ failedAttemptIndex: 1, statusCode: 408 }), 2000);
|
||||
assert.equal(fallbackRetryDelayAfterStatusForTest({ retryAfter: "3", statusCode: 429 }), 3000);
|
||||
assert.equal(fallbackRetryDelayAfterStatusForTest({ retryAfter: "0", statusCode: 429 }), 1000);
|
||||
});
|
||||
|
||||
test("fallback retry delay backs off network errors", () => {
|
||||
assert.equal(fallbackRetryDelayAfterNetworkErrorForTest(), 1000);
|
||||
assert.equal(fallbackRetryDelayAfterNetworkErrorForTest(2), 4000);
|
||||
});
|
||||
|
||||
function createIssue1480UserConfig() {
|
||||
return {
|
||||
APIKEY: "gateway-key",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue