fix(kiro): avoid treating high-traffic 429s as quota exhaustion (#2153)

Integrated into release/v3.8.0 — fixes transient Kiro 429s being incorrectly classified as quota exhaustion
This commit is contained in:
Gioxa
2026-05-11 20:05:48 +07:00
committed by GitHub
parent 19c15a5139
commit fdb4c63244
4 changed files with 37 additions and 6 deletions

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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", () => {