From ea9e8d6e8c80fbdd0b0b4864b833d2bfd3b429bb Mon Sep 17 00:00:00 2001 From: charleneleong-ai Date: Fri, 17 Apr 2026 18:34:06 +0000 Subject: [PATCH] fix(classifier): treat Anthropic "out of extra usage" 400 as billing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anthropic returns HTTP 400 with "You're out of extra usage. Add more at claude.ai/settings/usage and keep going." when the account's extra-usage allowance is depleted. The existing _BILLING_PATTERNS list did not include this wording, so classify_api_error fell through to generic format_error — non-retryable and should_fallback=False — causing the agent to abort instead of engaging the configured fallback chain. Add the pattern and a regression test covering the exact Anthropic body. --- agent/error_classifier.py | 1 + tests/agent/test_error_classifier.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/agent/error_classifier.py b/agent/error_classifier.py index 8111880a7..6e8a85aa2 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -110,6 +110,7 @@ _BILLING_PATTERNS = [ "exceeded your current quota", "account is deactivated", "plan does not include", + "out of extra usage", # Anthropic OAuth Pro/Max overage bucket depleted (HTTP 400) "out of funds", "run out of funds", "balance_depleted", diff --git a/tests/agent/test_error_classifier.py b/tests/agent/test_error_classifier.py index 16b881861..22e0c7999 100644 --- a/tests/agent/test_error_classifier.py +++ b/tests/agent/test_error_classifier.py @@ -1295,6 +1295,25 @@ class TestAdversarialEdgeCases: result = classify_api_error(e) assert result.reason == FailoverReason.billing + def test_400_anthropic_extra_usage_exhausted(self): + """Anthropic returns 400 with 'out of extra usage' when the user's + extra-usage allowance is depleted. Must classify as billing so the + fallback chain engages (with credential rotation) instead of the + generic format_error path, which never rotates. (#11736, #13170)""" + e = MockAPIError( + "You're out of extra usage. Add more at claude.ai/settings/usage and keep going.", + status_code=400, + body={"error": { + "type": "invalid_request_error", + "message": "You're out of extra usage. Add more at claude.ai/settings/usage and keep going.", + }}, + ) + result = classify_api_error(e, provider="anthropic") + assert result.reason == FailoverReason.billing + assert result.should_fallback is True + assert result.retryable is False + assert result.should_rotate_credential is True + def test_200_with_error_body(self): """200 status with error in body — should be unknown, not crash.""" class WeirdSuccess(Exception):