From fdb4c632449d2fef2c36546a526143b53a6bdad8 Mon Sep 17 00:00:00 2001 From: Gioxa Date: Mon, 11 May 2026 20:05:48 +0700 Subject: [PATCH] fix(kiro): avoid treating high-traffic 429s as quota exhaustion (#2153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.0 — fixes transient Kiro 429s being incorrectly classified as quota exhaustion --- open-sse/services/accountFallback.ts | 8 +++++++- src/sse/handlers/chat.ts | 13 ++++++++----- tests/unit/account-fallback-service.test.ts | 15 +++++++++++++++ tests/unit/classify429.test.ts | 7 +++++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/open-sse/services/accountFallback.ts b/open-sse/services/accountFallback.ts index dbc087252d..12271a22cf 100644 --- a/open-sse/services/accountFallback.ts +++ b/open-sse/services/accountFallback.ts @@ -474,8 +474,14 @@ export function lockModelIfPerModelQuota( export function shouldMarkAccountExhaustedFrom429( provider: string | null | undefined, model: string | null | undefined = null, - connectionPassthroughModels?: boolean + connectionPassthroughModels?: boolean, + failureKind?: FailureKind ): boolean { + // A plain 429 means transient rate limiting / high traffic for many OAuth providers. + // Only connection-poison the quota cache when the upstream body explicitly says + // the long-window quota is exhausted; otherwise fallback should try another account + // without making this one look quota-depleted for 5 minutes. + if (failureKind === "rate_limit" || failureKind === "transient") return false; return ( shouldPreserveQuotaSignalsFor429(provider) && !hasPerModelQuota(provider, model, connectionPassthroughModels) diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index e816960249..9a9bb63a90 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -1064,15 +1064,18 @@ async function handleSingleModelChat( dailyQuotaExhausted = true; } - // 7. Mark account as quota-exhausted on 429 response (non-daily-quota errors) - // For providers that route quota/cooldown at model scope, a 429 on one model - // does not mean the whole connection is exhausted. - // Daily quota errors are handled above; only process regular rate_limit here + // 7. Mark account as quota-exhausted only for explicit long-window quota signals. + // A plain 429/high-traffic response should trigger fallback/cooldown, not poison + // quotaCache as exhausted for 5 minutes while usage quota may still be available. if (!dailyQuotaExhausted) { const passthroughModels = credentials.providerSpecificData?.passthroughModels; + const failureKind = + result.status === 429 + ? classify429FromError({ status: result.status, message: errorStr }) + : undefined; if ( result.status === 429 && - shouldMarkAccountExhaustedFrom429(provider, model, passthroughModels) + shouldMarkAccountExhaustedFrom429(provider, model, passthroughModels, failureKind) ) { markAccountExhaustedFrom429(credentials.connectionId, provider); } diff --git a/tests/unit/account-fallback-service.test.ts b/tests/unit/account-fallback-service.test.ts index d3dac846d1..dc153c37d0 100644 --- a/tests/unit/account-fallback-service.test.ts +++ b/tests/unit/account-fallback-service.test.ts @@ -295,6 +295,21 @@ test("shouldMarkAccountExhaustedFrom429 skips connection poisoning for compatibl assert.equal(shouldMarkAccountExhaustedFrom429("claude", "claude-sonnet-4-6"), true); }); +test("shouldMarkAccountExhaustedFrom429 does not poison quota cache for transient 429s", () => { + assert.equal( + shouldMarkAccountExhaustedFrom429("kiro", "claude-opus-4.7", undefined, "rate_limit"), + false + ); + assert.equal( + shouldMarkAccountExhaustedFrom429("kiro", "claude-opus-4.7", undefined, "transient"), + false + ); + assert.equal( + shouldMarkAccountExhaustedFrom429("kiro", "claude-opus-4.7", undefined, "quota_exhausted"), + true + ); +}); + test("hasPerModelQuota returns true for GitHub Copilot provider (#1624)", () => { assert.equal(hasPerModelQuota("github"), true); assert.equal(hasPerModelQuota("github", "gpt-5.1-codex-max"), true); diff --git a/tests/unit/classify429.test.ts b/tests/unit/classify429.test.ts index 75d1a4fbd3..a57b04e134 100644 --- a/tests/unit/classify429.test.ts +++ b/tests/unit/classify429.test.ts @@ -66,6 +66,13 @@ test("classify429: 429 without quota keyword returns 'rate_limit'", () => { }), "rate_limit" ); + assert.equal( + classify429({ + status: 429, + body: "I am experiencing high traffic, please try again shortly.", + }), + "rate_limit" + ); }); test("looksLikeQuotaExhausted: detects all known keyword variants", () => {