mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 12:27:53 +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);
|
logPerfSummary(phase, conversationId, callStartMs, llmCallCount, retryCount, failoverCount);
|
||||||
return lastResult;
|
return lastResult;
|
||||||
}
|
}
|
||||||
// Any other non-null errored result with a classified type that doStreamCall
|
// RATE_LIMIT / SERVER_ERROR past their retry budget are provider-level
|
||||||
// chose NOT to retry (i.e. UNKNOWN, or RATE_LIMIT/SERVER_ERROR past MAX_RETRIES)
|
// failures: the same model will not recover within this turn, but a
|
||||||
// must exit — otherwise we silently spin through attempts and waste seconds
|
// different provider can. Break to the fallback chain instead of
|
||||||
// per turn on unrecoverable errors like DashScope's "url error" / unknown model.
|
// 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);
|
recordPrimary(false);
|
||||||
logPerfSummary(phase, conversationId, callStartMs, llmCallCount, retryCount, failoverCount);
|
logPerfSummary(phase, conversationId, callStartMs, llmCallCount, retryCount, failoverCount);
|
||||||
return lastResult;
|
return lastResult;
|
||||||
|
|||||||
@ -166,6 +166,36 @@ class NodeStreamingChatHelperFailoverTest {
|
|||||||
verify(fallback, times(1)).stream(any(Prompt.class));
|
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
|
// Bonus: confirm no infinite loop / regression on success path
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user