fix(runtime-fallback): match ZAI 'Limit Exhausted' quota errors (fixes #4207)

The ZAI (Zhipu) provider emits 'Weekly/Monthly Limit Exhausted. Your limit will reset at YYYY-MM-DD HH:MM:SS' when the coding-plan subscription quota is hit. None of the existing quota regex patterns (/quota.?exceeded/, /usage\s+limit/, /exhausted\s+your\s+capacity/, /credit\s+balance.*too\s+low/, etc.) match the 'Limit Exhausted' phrasing, so the runtime-fallback never fires and the user is stuck on the dead model.

Add /limit\s+exhausted/i to both pattern lists that gate fallback dispatch:
- RETRYABLE_ERROR_PATTERNS in constants.ts (text-pattern path used by extractStatusCode + retryable scan)
- classifyErrorType quota_exceeded branch in error-classifier.ts (typed classification path used by isRetryableError)

The pattern is intentionally narrow: it requires the literal token 'Limit' followed by whitespace then 'Exhausted'. It matches the ZAI weekly, monthly, and combined Weekly/Monthly variants but does not collide with unrelated phrases such as 'context limit' or 'rate limit' that already have their own dedicated patterns.

Regression coverage added to quota-error-classifier.regression.test.ts:
- 'Weekly/Monthly Limit Exhausted. Your limit will reset at 2026-05-20 15:43:27' -> quota_exceeded + retryable=true
- 'Weekly Limit Exhausted. Your limit will reset at 2026-05-28 10:30:00' -> quota_exceeded + retryable=true

Verification: 11/11 quota-error-classifier.regression.test.ts pass (was 9 pass + 2 fail pre-fix). Broader runtime-fallback suite goes from 135/196 pass to 137/198 pass (the 61 pre-existing failures are unrelated to this change and reproduce on a clean upstream/dev checkout). bun run typecheck clean.
This commit is contained in:
MoerAI
2026-05-21 18:39:51 +09:00
parent f1bf61efb9
commit 3513c45a02
3 changed files with 41 additions and 0 deletions
+1
View File
@@ -30,6 +30,7 @@ export const RETRYABLE_ERROR_PATTERNS = [
/exceeded.*quota/i,
/usage\s*quota/i,
/exhausted\s+your\s+capacity/i,
/limit\s+exhausted/i,
/all\s+credentials\s+for\s+model/i,
/cool(?:ing)?\s+down/i,
/model.{0,20}?not.{0,10}?supported/i,
@@ -148,6 +148,7 @@ export function classifyErrorType(error: unknown): string | undefined {
/payment.?required/i.test(message) ||
/usage\s+limit/i.test(message) ||
/credit\s+balance.*too\s+low/i.test(message) ||
/limit\s+exhausted/i.test(message) ||
/使用上限/.test(message) ||
/达到.*限制/.test(message) ||
/额度.*不足/.test(message) ||
@@ -143,4 +143,43 @@ describe("runtime-fallback quota error regressions", () => {
expect(errorType).toBe("quota_exceeded")
expect(retryable).toBe(true)
})
test("classifies ZAI weekly/monthly Limit Exhausted as quota_exceeded and triggers fallback", () => {
//#given
// ZAI (Zhipu) emits this exact message for both weekly and monthly quota
// exhaustion on the coding-plan subscription. The capitalization, the
// 'Limit Exhausted' phrasing, and the 'limit will reset at' suffix do not
// match any of the existing quota regex patterns, so the runtime-fallback
// never fires and the user is stuck on the dead model.
const error = {
message:
"Weekly/Monthly Limit Exhausted. Your limit will reset at 2026-05-20 15:43:27",
}
//#when
const errorType = classifyErrorType(error)
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
//#then
expect(errorType).toBe("quota_exceeded")
expect(retryable).toBe(true)
})
test("classifies ZAI weekly-only Limit Exhausted variant as quota_exceeded", () => {
//#given
// Weekly-only variant uses the same phrasing minus the slash; the regex
// must not require the literal `Weekly/Monthly` prefix.
const error = {
message:
"Weekly Limit Exhausted. Your limit will reset at 2026-05-28 10:30:00",
}
//#when
const errorType = classifyErrorType(error)
const retryable = isRetryableError(error, [429, 500, 502, 503, 504])
//#then
expect(errorType).toBe("quota_exceeded")
expect(retryable).toBe(true)
})
})