diff --git a/src/hooks/runtime-fallback/error-classifier.ts b/src/hooks/runtime-fallback/error-classifier.ts index 33b17ccf3..1f18b93ad 100644 --- a/src/hooks/runtime-fallback/error-classifier.ts +++ b/src/hooks/runtime-fallback/error-classifier.ts @@ -106,10 +106,13 @@ function isLocalizedQuotaExhaustionMessage(message: string): boolean { export function classifyErrorType(error: unknown): string | undefined { const message = getErrorMessage(error) - const errorName = extractErrorName(error)?.toLowerCase() + // Normalize by stripping underscores and dashes so snake_case / kebab-case + // provider error names (e.g. "insufficient_quota", "RESOURCE_EXHAUSTED") + // match the existing alphanumeric .includes() checks below. + const errorName = extractErrorName(error)?.toLowerCase().replace(/[_-]/g, "") if ( - errorName?.includes("ai_loadapikeyerror") || + errorName?.includes("ailoadapikeyerror") || errorName?.includes("loadapi") || (/api.?key.?is.?missing/i.test(message) && /environment variable/i.test(message)) ) { @@ -132,6 +135,7 @@ export function classifyErrorType(error: unknown): string | undefined { errorName?.includes("quotaexceeded") || errorName?.includes("insufficientquota") || errorName?.includes("billingerror") || + errorName?.includes("resourceexhausted") || /quota.?exceeded/i.test(message) || /exceeded.*quota/i.test(message) || /usage\s*quota/i.test(message) || @@ -139,6 +143,7 @@ export function classifyErrorType(error: unknown): string | undefined { /insufficient.?(?:quota|balance|funds?)/i.test(message) || /billing.?(?:hard.?)?limit/i.test(message) || /exhausted\s+your\s+capacity/i.test(message) || + /resource.?exhausted/i.test(message) || /out\s+of\s+credits?/i.test(message) || /payment.?required/i.test(message) || /usage\s+limit/i.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 f55c632cf..1a26bfb89 100644 --- a/src/hooks/runtime-fallback/quota-error-classifier.regression.test.ts +++ b/src/hooks/runtime-fallback/quota-error-classifier.regression.test.ts @@ -89,4 +89,52 @@ describe("runtime-fallback quota error regressions", () => { expect(errorType).toBe("quota_exceeded") expect(retryable).toBe(true) }) + + test("classifies Google RESOURCE_EXHAUSTED (gRPC code 8) as quota_exceeded", () => { + //#given + const error = { + name: "RESOURCE_EXHAUSTED", + message: "Quota exceeded for quota metric 'Generate Content' and limit 'Generate Content quota per minute'.", + } + + //#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 Google ResourceExhausted message without HTTP status as quota_exceeded", () => { + //#given + const error = { + name: "GoogleGenerativeAIError", + message: "Resource exhausted: Please try again later.", + } + + //#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 snake_case OpenAI insufficient_quota error name as quota_exceeded", () => { + //#given + const error = { + name: "insufficient_quota", + message: "You exceeded your current quota, please check your plan and billing details.", + } + + //#when + const errorType = classifyErrorType(error) + const retryable = isRetryableError(error, [429, 500, 502, 503, 504]) + + //#then + expect(errorType).toBe("quota_exceeded") + expect(retryable).toBe(true) + }) })