fix(classifier): treat Anthropic "out of extra usage" 400 as billing

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.
This commit is contained in:
charleneleong-ai 2026-04-17 18:34:06 +00:00 committed by Teknium
parent 12556a9a77
commit ea9e8d6e8c
2 changed files with 20 additions and 0 deletions

View file

@ -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",

View file

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