feat(ab-corpus): exponential backoff + bump retries=2 on ark/deepseek

ab-v3 left 36 ark empty + 15 timeout + 4 429 + 6 deepseek empty + 4
minimax timeout AFTER the existing retries=1 fired 88 times. Linear
backoff 250ms*(attempt+1) was too tight when stepping up to
retries=2 (500ms then 750ms isn't a typical Ark recovery window).

Switches to exponential 250ms*4^attempt — 250 / 1000 / 4000ms
spacing — and bumps ark + deepseek to retries=2. minimax opts in to
retries=1 (its 4 errors were wall-clock timeouts, not the model's
<think> truncation that retry can't fix anyway).

10 existing retry tests still pass; the retries=2 case now sleeps
1.25s instead of 0.75s, still well under vitest's default timeout.
This commit is contained in:
Fini 2026-04-29 09:49:41 +08:00
parent c2252f2172
commit 7d419cffd1
4 changed files with 27 additions and 15 deletions

View file

@ -47,9 +47,11 @@ export async function callArk(args: CallArkArgs): Promise<ChatCallResult> {
// failure was Ark returning empty `choices[0].message.content` or
// exceeding the 120s wall clock, not a model-quality issue (the
// model itself routed to the right element tool 80% of the time
// when it did respond). One retry is the minimum that flips most
// of those into successes; bumping higher would mostly waste
// budget on the genuinely broken minority.
retries: 1,
// when it did respond). retries=1 left 36 empty + 15 timeout + 4
// 429 still leaking through ab-v3 (520 calls); retries=2 with
// exponential backoff gives the provider a 1000ms recovery window
// before the third attempt, which on Ark's coding tier is usually
// enough to clear the upstream brownout.
retries: 2,
});
}

View file

@ -40,9 +40,9 @@ export async function callDeepSeek(args: CallDeepSeekArgs): Promise<ChatCallResu
maxTokens: args.maxTokens,
label: 'deepseek',
// ab-v2 garbage attribution: 6 of 40 deepseek-v4-pro runs returned
// empty content (server-side flakiness, not model output). Same
// rationale as ark.ts — one retry recovers the transient ones,
// anything more would mostly burn budget on real failures.
retries: 1,
// empty content (server-side flakiness, not model output). retries=1
// recovered most but ab-v3 still leaked 6 empty-content errors.
// Bumped to 2 alongside ark to share the exponential backoff window.
retries: 2,
});
}

View file

@ -35,5 +35,10 @@ export async function callMinimax(args: CallMinimaxArgs): Promise<ChatCallResult
temperature: args.temperature,
maxTokens: args.maxTokens,
label: 'minimax',
// ab-v3 saw 4 minimax timeouts on 104 runs — all wall-clock aborts,
// none were content-quality issues. One retry picks up the
// recoverable ones without burning budget on the truncation /
// <think> chatter that characterizes minimax model-side failures.
retries: 1,
});
}

View file

@ -70,16 +70,20 @@ export interface CallOpenAICompatArgs {
*/
timeoutMs?: number;
/**
* Retry attempts on transient errors. Default 0 (no retry). Set to 1
* Retry attempts on transient errors. Default 0 (no retry). Set to 2
* for providers known to flake on empty content / timeout (Ark hosting
* GLM-5.1+Kimi-K2.6, DeepSeek api.deepseek.com). MiniMax / Bailian /
* Codex don't enable this — their ab-v2 failures were model-quality
* (truncation, malformed DSL), where retry burns budget for nothing.
* GLM-5.1+Kimi-K2.6, DeepSeek api.deepseek.com). 1 is enough for
* MiniMax (saw 4 timeouts in 104 ab-v3 runs); Codex / Bailian don't
* enable this — their ab-v2 failures were model-quality (truncation,
* malformed DSL), where retry burns budget for nothing.
*
* Retried: empty `choices[0].message.content`, abort/timeout, HTTP 5xx,
* HTTP 429. NOT retried: HTTP 4xx other than 429 (auth / bad request).
*
* Backoff: linear, 250ms × (attempt+1).
* Backoff: exponential, 250ms × 4^attempt — 250ms before retry 1,
* 1000ms before retry 2, 4000ms before retry 3. Linear backoff was
* too tight when retries=2 (500ms then 750ms is < a typical Ark
* recovery window); exponential gives the provider room to settle.
*/
retries?: number;
}
@ -105,13 +109,14 @@ export async function callOpenAICompat(args: CallOpenAICompatArgs): Promise<Chat
if (attempt === retries || !isTransientError(lastErr)) {
throw lastErr;
}
const delayMs = 250 * 4 ** attempt;
console.error(
`[${label}] transient error on attempt ${attempt + 1}/${retries + 1}, retrying: ${truncate(
`[${label}] transient error on attempt ${attempt + 1}/${retries + 1}, retrying in ${delayMs}ms: ${truncate(
lastErr.message,
200,
)}`,
);
await sleep(250 * (attempt + 1));
await sleep(delayMs);
}
}
throw lastErr ?? new Error(`${label}: callOpenAICompat exited loop without result`);