mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +08:00
fix(agent): fail over to backup providers when the primary is rate-limited
This commit is contained in:
parent
95ce1bae24
commit
c1b878b7e1
@ -588,10 +588,22 @@ public class NodeStreamingChatHelper {
|
||||
logPerfSummary(phase, conversationId, callStartMs, llmCallCount, retryCount, failoverCount);
|
||||
return lastResult;
|
||||
}
|
||||
// Any other non-null errored result with a classified type that doStreamCall
|
||||
// chose NOT to retry (i.e. UNKNOWN, or RATE_LIMIT/SERVER_ERROR past MAX_RETRIES)
|
||||
// must exit — otherwise we silently spin through attempts and waste seconds
|
||||
// per turn on unrecoverable errors like DashScope's "url error" / unknown model.
|
||||
// RATE_LIMIT / SERVER_ERROR past their retry budget are provider-level
|
||||
// failures: the same model will not recover within this turn, but a
|
||||
// different provider can. Break to the fallback chain instead of
|
||||
// returning — recordPrimary(false) runs once at the post-loop provider
|
||||
// health check below, and if every fallback also fails the chain
|
||||
// walker re-surfaces this same error to the caller.
|
||||
if (lastResult.errorType() == ErrorType.RATE_LIMIT
|
||||
|| lastResult.errorType() == ErrorType.SERVER_ERROR) {
|
||||
log.warn("[{}] Primary exhausted retries (type={}) — handing off to fallback chain",
|
||||
phase, lastResult.errorType());
|
||||
break;
|
||||
}
|
||||
// Any other non-null errored result (e.g. UNKNOWN) that doStreamCall
|
||||
// chose NOT to retry must exit — otherwise we silently spin through
|
||||
// attempts and waste seconds per turn on unrecoverable errors like
|
||||
// DashScope's "url error" / unknown model.
|
||||
recordPrimary(false);
|
||||
logPerfSummary(phase, conversationId, callStartMs, llmCallCount, retryCount, failoverCount);
|
||||
return lastResult;
|
||||
|
||||
@ -166,6 +166,36 @@ class NodeStreamingChatHelperFailoverTest {
|
||||
verify(fallback, times(1)).stream(any(Prompt.class));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// C5 regression: RATE_LIMIT (429) must fall back, not surface
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Prior to this fix a rate-limited primary exhausted its 2 same-model
|
||||
* retries and then {@code return}ed the 429 error result directly,
|
||||
* skipping the fallback chain entirely — the 429 surfaced as the
|
||||
* conversation's answer even though other providers were healthy.
|
||||
* After the fix RATE_LIMIT breaks out to the chain walker, mirroring
|
||||
* AUTH_ERROR / BILLING.
|
||||
*
|
||||
* <p>Note: this test waits out two real retry backoffs (~3s + ~6s) on
|
||||
* the primary before the hand-off, so it runs for ~10s by design.</p>
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("C5 (regression): primary RATE_LIMIT (429) hands off to the fallback chain")
|
||||
void rateLimitFallsBack() {
|
||||
ChatModel primary = errorModel(new RuntimeException("429 Too Many Requests"));
|
||||
ChatModel fallback = successModel("recovered after rate limit");
|
||||
var helper = helper(primary, List.of(new FallbackEntry("dashscope", fallback)), "zhipu-cn");
|
||||
|
||||
var result = helper.streamCall(primary, smallPrompt(), "conv-c5", "reasoning");
|
||||
|
||||
assertEquals("recovered after rate limit", result.text(),
|
||||
"a rate-limited primary must fail over instead of surfacing the 429");
|
||||
verify(primary, atLeast(2)).stream(any(Prompt.class));
|
||||
verify(fallback, times(1)).stream(any(Prompt.class));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Bonus: confirm no infinite loop / regression on success path
|
||||
// ============================================================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user