From d83f0ffe62b37843bed10fadc9994f36b516561d Mon Sep 17 00:00:00 2001 From: AlexFucuson9 Date: Thu, 16 Jul 2026 19:30:39 +0700 Subject: [PATCH] fix: classify 429 quota-exhaustion errors as non-retryable for fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Ollama Cloud returns HTTP 429 with "session usage limit" / "upgrade for higher limits", the error was classified as a transient rate_limit (retryable=True). This caused the system to retry the same provider 3 times then give up — never triggering fallback_providers. Fix: check _USAGE_LIMIT_PATTERNS in the 429 handler (same logic already present in the 402 and no-status-code paths). Transient signals ("try again", "resets at") → rate_limit; otherwise → billing (non-retryable, should_fallback=True). Fixes #65563 --- agent/error_classifier.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/agent/error_classifier.py b/agent/error_classifier.py index 6afdf4e076e0a..bfba535930c05 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -987,6 +987,34 @@ def _classify_by_status( FailoverReason.overloaded, retryable=True, ) + # Quota / usage-limit exhaustion disguised as 429. Some providers + # (Ollama Cloud, etc.) return HTTP 429 with body text like + # "you have reached your session usage limit, upgrade for higher + # limits" — this is NOT a transient rate limit but quota exhaustion. + # Without this check, the error falls through to the default + # rate_limit path (retryable=True), which retries the same provider + # 3 times then gives up — never triggering fallback_providers. + # Disambiguate: transient signals ("try again", "resets at") mean + # it's a periodic quota → rate_limit; otherwise it's billing-like + # exhaustion → non-retryable + should_fallback. (#65563) + has_usage_limit = any(p in error_msg for p in _USAGE_LIMIT_PATTERNS) + if has_usage_limit: + has_transient_signal = any( + p in error_msg for p in _USAGE_LIMIT_TRANSIENT_SIGNALS + ) + if has_transient_signal: + return result_fn( + FailoverReason.rate_limit, + retryable=True, + should_rotate_credential=True, + should_fallback=True, + ) + return result_fn( + FailoverReason.billing, + retryable=False, + should_rotate_credential=True, + should_fallback=True, + ) # Distinguish an OpenRouter-aggregator upstream 429 (an upstream model # like DeepSeek rate-limited OpenRouter's aggregate traffic) from an # account-level 429 (the user's key is actually throttled). OpenRouter