From b5bc45a4671b72894b89ac15b8a23c859bda07ee Mon Sep 17 00:00:00 2001 From: pazyork Date: Sat, 25 Apr 2026 21:48:49 +0800 Subject: [PATCH] fix(agent): trigger eager fallback on 503/529 provider overload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a provider returns 503 (Service Unavailable) or 529 (Overloaded), the agent should fall back to an alternate provider immediately. Credential-pool rotation cannot fix provider-side overload — rotating keys against the same overloaded servers is useless. Two minimal changes: 1. error_classifier: set should_fallback=True for 503/529 (consistent with rate_limit and billing classifications) 2. run_agent: add independent eager-fallback block for overloaded, placed after the rate-limit pool-rotation deferral block. Overloaded bypasses the _pool_may_recover_from_rate_limit check because credential rotation cannot resolve provider-side capacity issues. More focused than adding overloaded to the is_rate_limited tuple and complementary to #14055 (message-pattern classification path). --- agent/error_classifier.py | 2 +- run_agent.py | 15 +++++++++++++++ tests/agent/test_error_classifier.py | 4 ++++ tests/run_agent/test_provider_fallback.py | 7 +++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/agent/error_classifier.py b/agent/error_classifier.py index 87324d67677e..ae893f432e82 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -611,7 +611,7 @@ def _classify_by_status( return result_fn(FailoverReason.server_error, retryable=True) if status_code in (503, 529): - return result_fn(FailoverReason.overloaded, retryable=True) + return result_fn(FailoverReason.overloaded, retryable=True, should_fallback=True) # Other 4xx — non-retryable if 400 <= status_code < 500: diff --git a/run_agent.py b/run_agent.py index 7187499bb1c4..90a66de9af4e 100644 --- a/run_agent.py +++ b/run_agent.py @@ -11144,6 +11144,21 @@ def _stop_spinner(): primary_recovery_attempted = False continue + # Eager fallback for provider overload (503/529). + # Provider-side overload cannot be fixed by credential rotation, + # so bypass the pool check and switch to a fallback immediately. + # Fixes #11314 / #10210. + if ( + classified.reason == FailoverReason.overloaded + and self._fallback_index < len(self._fallback_chain) + ): + self._emit_status("⚠️ Provider overloaded — switching to fallback provider...") + if self._try_activate_fallback(reason=classified.reason): + retry_count = 0 + compression_attempts = 0 + primary_recovery_attempted = False + continue + # ── Nous Portal: record rate limit & skip retries ───── # When Nous returns a 429, record the reset time to a # shared file so ALL sessions (cron, gateway, auxiliary) diff --git a/tests/agent/test_error_classifier.py b/tests/agent/test_error_classifier.py index e8a92774b47a..e339e5e0e21d 100644 --- a/tests/agent/test_error_classifier.py +++ b/tests/agent/test_error_classifier.py @@ -283,11 +283,15 @@ def test_503_overloaded(self): e = MockAPIError("Service Unavailable", status_code=503) result = classify_api_error(e) assert result.reason == FailoverReason.overloaded + assert result.should_fallback is True + assert result.retryable is True def test_529_anthropic_overloaded(self): e = MockAPIError("Overloaded", status_code=529) result = classify_api_error(e) assert result.reason == FailoverReason.overloaded + assert result.should_fallback is True + assert result.retryable is True # ── Model not found ── diff --git a/tests/run_agent/test_provider_fallback.py b/tests/run_agent/test_provider_fallback.py index 44de0846f4df..7064f4a0559d 100644 --- a/tests/run_agent/test_provider_fallback.py +++ b/tests/run_agent/test_provider_fallback.py @@ -5,6 +5,13 @@ advancement through multiple providers. """ +import sys +import types + +# Prevent heavy imports from failing in lightweight test environments. +sys.modules.setdefault("fire", types.SimpleNamespace(Fire=lambda *a, **k: None)) +sys.modules.setdefault("firecrawl", types.SimpleNamespace(Firecrawl=object)) + from unittest.mock import MagicMock, patch from run_agent import AIAgent, _pool_may_recover_from_rate_limit