diff --git a/src/resources/extensions/sf/error-classifier.ts b/src/resources/extensions/sf/error-classifier.ts index bfbadde35..ee0ddf3ab 100644 --- a/src/resources/extensions/sf/error-classifier.ts +++ b/src/resources/extensions/sf/error-classifier.ts @@ -51,7 +51,8 @@ const PERMANENT_RE = /auth|unauthorized|forbidden|invalid.*key|invalid.*api|billing|quota exceeded|account/i; // Include provider-specific quota-window phrasing like "hit your limit", "usage limit", "quota reached" const RATE_LIMIT_RE = - /rate.?limit|too many requests|429|hit your limit|usage limit|quota (?:reached|hit)|limit.*resets?/i; + /rate.?limit|too many requests|429|hit your limit|usage limit|quota (?:reached|hit|will reset)|limit.*resets?|exhausted (?:your|the) (?:quota|capacity|usage)/i; +const RESET_QUOTA_DELAY_RE = /reset(?:s)?(?:\s+(?:in|after))?\s+(\d+)s/i; // Unsupported-model: provider rejected the model for the current account/plan (#4513). // Checked before `permanent` because PERMANENT_RE also matches /account/i. const UNSUPPORTED_MODEL_MODEL_RE = /\b(?:model|deployment)\b/i; @@ -118,7 +119,12 @@ export function classifyError( if (retryAfterMs != null && retryAfterMs > 0) { return { kind: "rate-limit", retryAfterMs }; } - const resetMatch = errorMsg.match(RESET_DELAY_RE); + // Try the existing "reset in Ns" first, then the broader + // "reset(s)? (in|after) Ns" form that catches "Your quota will reset + // after 51s" — common across providers (Anthropic capacity exhaustion, + // OpenAI usage caps, etc.). + const resetMatch = + errorMsg.match(RESET_DELAY_RE) ?? errorMsg.match(RESET_QUOTA_DELAY_RE); const delayMs = resetMatch ? Number(resetMatch[1]) * 1000 : 60_000; return { kind: "rate-limit", retryAfterMs: delayMs }; }