fix(error-classifier): quota/billing errors are non-retryable STOP (fixes #3126)

Add STOP_MESSAGE_PATTERNS that take precedence over RETRYABLE_MESSAGE_PATTERNS.
Message-only quota errors (no error name) now correctly return false from
isRetryableModelError, preventing unnecessary fallback retries on exhausted quotas.

- quota will reset after...
- quota exceeded
- usage limit has been reached
- free usage limit / billing limit / plan limit / subscription limit
- out of credits / credits exhausted / insufficient credits / insufficient balance

Also add 4 regression tests covering message-only quota STOP cases.
This commit is contained in:
YeonGyu-Kim
2026-04-06 16:56:05 +09:00
parent 9438c89c1c
commit f69234ab7e
2 changed files with 69 additions and 2 deletions
+44
View File
@@ -150,6 +150,50 @@ describe("model-error-classifier", () => {
expect(result).toBe(false)
})
test("treats quota reset message as non-retryable STOP error (no error name)", () => {
//#given
const error = { message: "quota will reset after 1 hour" }
//#when
const result = shouldRetryError(error)
//#then
expect(result).toBe(false)
})
test("treats quota exceeded message as non-retryable STOP error (no error name)", () => {
//#given
const error = { message: "quota exceeded for this billing period" }
//#when
const result = shouldRetryError(error)
//#then
expect(result).toBe(false)
})
test("treats usage limit reached message as non-retryable STOP error (no error name)", () => {
//#given
const error = { message: "usage limit has been reached for your account" }
//#when
const result = shouldRetryError(error)
//#then
expect(result).toBe(false)
})
test("treats insufficient credits message as non-retryable STOP error (no error name)", () => {
//#given
const error = { message: "insufficient credits to complete this request" }
//#when
const result = shouldRetryError(error)
//#then
expect(result).toBe(false)
})
test("treats 'bad request' message as retryable (GitHub Copilot rolling update)", () => {
//#given
const error = { message: "400 Bad Request" }
+25 -2
View File
@@ -40,8 +40,6 @@ const RETRYABLE_MESSAGE_PATTERNS = [
"rate_limit",
"rate limit",
"quota",
"quota will reset after",
"usage limit has been reached",
"all credentials for model",
"cooling down",
"exhausted your capacity",
@@ -76,6 +74,25 @@ const RETRYABLE_MESSAGE_PATTERNS = [
"529",
]
/**
* Message patterns that indicate a non-retryable STOP error (quota/billing exhaustion).
* These take precedence over RETRYABLE_MESSAGE_PATTERNS.
*/
const STOP_MESSAGE_PATTERNS = [
"quota will reset after",
"quota exceeded",
"usage limit has been reached",
"free usage limit",
"billing limit",
"monthly limit",
"plan limit",
"subscription limit",
"out of credits",
"credits exhausted",
"insufficient credits",
"insufficient balance",
]
const AUTO_RETRY_GATE_PATTERNS = [
"rate limit",
"quota",
@@ -121,6 +138,12 @@ export function isRetryableModelError(error: ErrorInfo): boolean {
// Check message patterns for unknown errors
const msg = error.message?.toLowerCase() ?? ""
// STOP patterns take precedence over retryable patterns
if (STOP_MESSAGE_PATTERNS.some((pattern) => msg.includes(pattern))) {
return false
}
if (hasProviderAutoRetrySignal(msg)) {
return true
}