Skip to content
Merged
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
20 changes: 20 additions & 0 deletions agent/conversation_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,26 @@ def _billing_or_entitlement_message(

provider_label = (provider or "").strip() or "the selected provider"
model_label = (model or "").strip() or "the selected model"

# Anthropic Claude Pro/Max OAuth subscriptions surface exhaustion of the
# metered "extra usage" bucket as a hard 400 ("You're out of extra
# usage"). Point at the exact settings page and note the cycle-reset
# option, since the generic "add credits with that provider" line doesn't
# apply to a subscription — the user waits for the reset or switches to an
# API key.
if (provider or "").strip().lower() == "anthropic":
lines = [
(
f"{provider_label} reported that your Claude subscription usage is "
f"exhausted for {model_label} (included quota + extra-usage credits)."
),
"Options: wait for the billing cycle to reset, or add extra usage at "
"https://claude.ai/settings/usage",
"You can also switch to an Anthropic API key or another provider with "
"/model <model> --provider <provider>.",
]
return "\n".join(lines)

lines = [
(
f"{provider_label} reported that billing, credits, or account "
Expand Down
1 change: 1 addition & 0 deletions agent/error_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def is_auth(self) -> bool:
"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",
Expand Down
2 changes: 2 additions & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"jnibarger01@gmail.com": "jnibarger01", # PR #35130 salvage (ReDoS-bound threat-pattern filler + FTS5 query cap + V4A Move-File approval/traversal targets)
"290868363+petrichor-op@users.noreply.github.com": "petrichor-op", # PR #41281 salvage (never persist ephemeral empty-response recovery scaffolding to the SQLite session store / JSON log; filter by flag not position)
"283494121+redactdeveloper@users.noreply.github.com": "redactdeveloper", # PR #36897 salvage (route /sessions & /history through prompt_toolkit-safe print; filter doctor missing-key summary to CLI-enabled toolsets)
"charleneleong84@gmail.com": "charleneleong-ai", # PR #11736 salvage (classify Anthropic "out of extra usage" 400 as billing)
"janrenz@Mac.fritz.box": "janrenz", # PR #35862 salvage (prompt_caching.enabled escape hatch for strict providers)
"syahidfrd@gmail.com": "syahidfrd", # PR #17059 salvage (tag unverified senders in Slack thread context to mitigate indirect prompt injection)
"22971845+H2KFORGIVEN@users.noreply.github.com": "H2KFORGIVEN", # PR #22523 salvage (turn-pair preservation: never orphan the last user ask at head_end during compaction)
"5823452+sgabel@users.noreply.github.com": "sgabel", # PR #13139 salvage (redact secrets in user-facing approval prompts)
Expand Down
46 changes: 46 additions & 0 deletions tests/agent/test_anthropic_billing_guidance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Tests for the Anthropic-subscription branch of
``agent.conversation_loop._billing_or_entitlement_message``.

Regression context: Anthropic Claude Pro/Max OAuth subscriptions surface
exhaustion of the metered "extra usage" bucket as a hard HTTP 400
("You're out of extra usage. Add more at claude.ai/settings/usage..."),
which classifies as ``FailoverReason.billing``. The generic billing
guidance ("add credits with that provider") is wrong for a subscription —
the user waits for the cycle reset or switches to an API key. This branch
gives Anthropic-specific, actionable guidance (folds in PR #40073's UX).
"""
from __future__ import annotations

from agent.conversation_loop import _billing_or_entitlement_message


def test_anthropic_subscription_exhausted_guidance():
"""Anthropic billing guidance points at the exact settings page and
the cycle-reset option, not the generic 'add credits' line."""
msg = _billing_or_entitlement_message(
capability="model access",
provider="anthropic",
base_url="https://api.anthropic.com",
model="claude-opus-4-7",
)
assert "claude.ai/settings/usage" in msg
# Must mention the subscription cycle reset (not generic 'add credits').
assert "reset" in msg.lower()
# Must still offer the provider-switch escape hatch.
assert "/model" in msg
# Model name should be interpolated.
assert "claude-opus-4-7" in msg


def test_non_anthropic_billing_guidance_unaffected():
"""A non-Anthropic provider keeps the generic billing guidance and does
NOT get the Anthropic-specific claude.ai settings link."""
msg = _billing_or_entitlement_message(
capability="model access",
provider="openrouter",
base_url="https://openrouter.ai/api/v1",
model="anthropic/claude-opus-4.7",
)
assert "claude.ai/settings/usage" not in msg
# Generic path still surfaces the OpenRouter credits link.
assert "openrouter.ai/settings/credits" in msg
19 changes: 19 additions & 0 deletions tests/agent/test_error_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,25 @@ def test_400_with_billing_text(self):
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):
Expand Down
Loading