From f69234ab7e30c9c4ebd382722a4a8323c0e7c98c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 6 Apr 2026 16:56:05 +0900 Subject: [PATCH] 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. --- src/shared/model-error-classifier.test.ts | 44 +++++++++++++++++++++++ src/shared/model-error-classifier.ts | 27 ++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/shared/model-error-classifier.test.ts b/src/shared/model-error-classifier.test.ts index 28fbf4047..fdaf14ba2 100644 --- a/src/shared/model-error-classifier.test.ts +++ b/src/shared/model-error-classifier.test.ts @@ -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" } diff --git a/src/shared/model-error-classifier.ts b/src/shared/model-error-classifier.ts index 29a0cb801..cbd0b8c12 100644 --- a/src/shared/model-error-classifier.ts +++ b/src/shared/model-error-classifier.ts @@ -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 }