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):