Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2098,7 +2098,9 @@ def _is_payment_error(exc: Exception) -> bool:
if status in {402, 429, None}:
if any(kw in err_lower for kw in ("credits", "insufficient funds",
"can only afford", "billing",
"payment required")):
"payment required", "quota",
"too many tokens", "daily limit",
"tokens per day")):
return True
return False

Expand Down Expand Up @@ -4522,8 +4524,7 @@ def call_llm(
# Only try alternative providers when the user didn't explicitly
# configure this task's provider. Explicit provider = hard constraint;
# auto (the default) = best-effort fallback chain. (#7559)
is_auto = resolved_provider in {"auto", "", None}
if should_fallback and is_auto:
if should_fallback:
if _is_payment_error(first_err):
reason = "payment error"
# Resolve the actual provider label (resolved_provider may be
Expand Down Expand Up @@ -4851,8 +4852,7 @@ async def async_call_llm(
or _is_connection_error(first_err)
or _is_rate_limit_error(first_err)
)
is_auto = resolved_provider in {"auto", "", None}
if should_fallback and is_auto:
if should_fallback:
if _is_payment_error(first_err):
reason = "payment error"
_mark_provider_unhealthy(
Expand Down
46 changes: 46 additions & 0 deletions tests/agent/test_auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,25 @@ def test_no_status_code_no_message(self):
exc = Exception("connection reset")
assert _is_payment_error(exc) is False

def test_429_with_quota_message(self):
exc = Exception("Too many tokens per day: quota exceeded")
exc.status_code = 429
assert _is_payment_error(exc) is True

def test_429_with_daily_limit_message(self):
exc = Exception("daily limit reached, try again tomorrow")
exc.status_code = 429
assert _is_payment_error(exc) is True

def test_429_with_too_many_tokens_message(self):
exc = Exception("Too many tokens requested today")
exc.status_code = 429
assert _is_payment_error(exc) is True

def test_no_status_quota_exceeded(self):
exc = Exception("quota exceeded for this model")
assert _is_payment_error(exc) is True


class TestIsRateLimitError:
"""_is_rate_limit_error detects 429 rate-limit errors warranting fallback."""
Expand Down Expand Up @@ -1111,6 +1130,33 @@ def test_429_rate_limit_triggers_fallback(self, monkeypatch):
# Fallback client should have been used
assert fallback_client.chat.completions.create.called

def test_explicit_provider_gets_fallback_on_payment_error(self, monkeypatch):
"""Non-auto (explicit) providers should still get fallback on payment/quota errors (#26803)."""
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")

primary_client = MagicMock()
quota_err = Exception("Too many tokens per day: quota exceeded")
quota_err.status_code = 429
primary_client.chat.completions.create.side_effect = quota_err

fallback_client = MagicMock()
fallback_client.chat.completions.create.return_value = MagicMock(choices=[
MagicMock(message=MagicMock(content="fallback response"))
])

with patch("agent.auxiliary_client._get_cached_client",
return_value=(primary_client, "google/gemini-3-flash-preview")), \
patch("agent.auxiliary_client._resolve_task_provider_model",
return_value=("openrouter", "google/gemini-3-flash-preview", None, None, None)), \
patch("agent.auxiliary_client._try_payment_fallback",
return_value=(fallback_client, "fallback-model", "nous")):
result = call_llm(
task="compression",
messages=[{"role": "user", "content": "hello"}],
)
# Even with explicit provider "openrouter", fallback should trigger
assert fallback_client.chat.completions.create.called

# ---------------------------------------------------------------------------
# Gate: _resolve_api_key_provider must skip anthropic when not configured
# ---------------------------------------------------------------------------
Expand Down
Loading