Skip to content

Commit f69234a

Browse files
committed
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.
1 parent 9438c89 commit f69234a

2 files changed

Lines changed: 69 additions & 2 deletions

File tree

src/shared/model-error-classifier.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,50 @@ describe("model-error-classifier", () => {
150150
expect(result).toBe(false)
151151
})
152152

153+
test("treats quota reset message as non-retryable STOP error (no error name)", () => {
154+
//#given
155+
const error = { message: "quota will reset after 1 hour" }
156+
157+
//#when
158+
const result = shouldRetryError(error)
159+
160+
//#then
161+
expect(result).toBe(false)
162+
})
163+
164+
test("treats quota exceeded message as non-retryable STOP error (no error name)", () => {
165+
//#given
166+
const error = { message: "quota exceeded for this billing period" }
167+
168+
//#when
169+
const result = shouldRetryError(error)
170+
171+
//#then
172+
expect(result).toBe(false)
173+
})
174+
175+
test("treats usage limit reached message as non-retryable STOP error (no error name)", () => {
176+
//#given
177+
const error = { message: "usage limit has been reached for your account" }
178+
179+
//#when
180+
const result = shouldRetryError(error)
181+
182+
//#then
183+
expect(result).toBe(false)
184+
})
185+
186+
test("treats insufficient credits message as non-retryable STOP error (no error name)", () => {
187+
//#given
188+
const error = { message: "insufficient credits to complete this request" }
189+
190+
//#when
191+
const result = shouldRetryError(error)
192+
193+
//#then
194+
expect(result).toBe(false)
195+
})
196+
153197
test("treats 'bad request' message as retryable (GitHub Copilot rolling update)", () => {
154198
//#given
155199
const error = { message: "400 Bad Request" }

src/shared/model-error-classifier.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ const RETRYABLE_MESSAGE_PATTERNS = [
4040
"rate_limit",
4141
"rate limit",
4242
"quota",
43-
"quota will reset after",
44-
"usage limit has been reached",
4543
"all credentials for model",
4644
"cooling down",
4745
"exhausted your capacity",
@@ -76,6 +74,25 @@ const RETRYABLE_MESSAGE_PATTERNS = [
7674
"529",
7775
]
7876

77+
/**
78+
* Message patterns that indicate a non-retryable STOP error (quota/billing exhaustion).
79+
* These take precedence over RETRYABLE_MESSAGE_PATTERNS.
80+
*/
81+
const STOP_MESSAGE_PATTERNS = [
82+
"quota will reset after",
83+
"quota exceeded",
84+
"usage limit has been reached",
85+
"free usage limit",
86+
"billing limit",
87+
"monthly limit",
88+
"plan limit",
89+
"subscription limit",
90+
"out of credits",
91+
"credits exhausted",
92+
"insufficient credits",
93+
"insufficient balance",
94+
]
95+
7996
const AUTO_RETRY_GATE_PATTERNS = [
8097
"rate limit",
8198
"quota",
@@ -121,6 +138,12 @@ export function isRetryableModelError(error: ErrorInfo): boolean {
121138

122139
// Check message patterns for unknown errors
123140
const msg = error.message?.toLowerCase() ?? ""
141+
142+
// STOP patterns take precedence over retryable patterns
143+
if (STOP_MESSAGE_PATTERNS.some((pattern) => msg.includes(pattern))) {
144+
return false
145+
}
146+
124147
if (hasProviderAutoRetrySignal(msg)) {
125148
return true
126149
}

0 commit comments

Comments
 (0)