diff --git a/src/hooks/runtime-fallback/constants.ts b/src/hooks/runtime-fallback/constants.ts index 43df0f936..50bea61bb 100644 --- a/src/hooks/runtime-fallback/constants.ts +++ b/src/hooks/runtime-fallback/constants.ts @@ -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, diff --git a/src/hooks/runtime-fallback/error-classifier.ts b/src/hooks/runtime-fallback/error-classifier.ts index 4595f020f..0c3d9b845 100644 --- a/src/hooks/runtime-fallback/error-classifier.ts +++ b/src/hooks/runtime-fallback/error-classifier.ts @@ -175,6 +175,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) || diff --git a/src/hooks/runtime-fallback/quota-error-classifier.regression.test.ts b/src/hooks/runtime-fallback/quota-error-classifier.regression.test.ts index 9657af523..386b452eb 100644 --- a/src/hooks/runtime-fallback/quota-error-classifier.regression.test.ts +++ b/src/hooks/runtime-fallback/quota-error-classifier.regression.test.ts @@ -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) + }) })