From 06527744e34d94640fba93bf1b90a863b99d87af Mon Sep 17 00:00:00 2001 From: Carman Babin Date: Thu, 9 Apr 2026 18:32:39 -0500 Subject: [PATCH 1/3] fix: Anthropic OAuth auth sends empty X-Api-Key header, misclassifies billing error (#6475) Two fixes for the "You're out of extra usage" error when using Claude Code OAuth tokens with the Anthropic provider: 1. **Empty X-Api-Key header override (anthropic_adapter.py)**: When ANTHROPIC_API_KEY="" is set in .env (common after setup), the Anthropic Python SDK reads it from the environment and sends an empty X-Api-Key header alongside the valid Bearer Authorization header. The empty X-Api-Key may cause Anthropic's server to route the request through API-key auth instead of OAuth subscription auth, resulting in a billing error. Fix: explicitly set client.api_key = None after construction when using auth_token (Bearer) auth. 2. **Billing pattern not recognized (error_classifier.py)**: The error message "out of extra usage" was not matched by any billing pattern, causing it to be classified as format_error. This triggered the immediate-abort path instead of proper billing error handling (credential rotation, fallback attempts, correct error messaging). Fix: add "out of extra usage" to _BILLING_PATTERNS. Also fixes beta headers: - Remove non-existent "fine-grained-tool-streaming-2025-05-14" beta - Add "context-1m-2025-08-07" beta (required for 1M context models) - Fix User-Agent header case to properly override SDK default Closes #6475 Co-Authored-By: Claude Opus 4.6 (1M context) --- agent/anthropic_adapter.py | 20 +++++++++++++++++--- agent/error_classifier.py | 1 + 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/agent/anthropic_adapter.py b/agent/anthropic_adapter.py index d5c0c06fbb635..99f9deb5e8218 100644 --- a/agent/anthropic_adapter.py +++ b/agent/anthropic_adapter.py @@ -93,7 +93,6 @@ def _supports_adaptive_thinking(model: str) -> bool: # Beta headers for enhanced features (sent with ALL auth types) _COMMON_BETAS = [ "interleaved-thinking-2025-05-14", - "fine-grained-tool-streaming-2025-05-14", ] # Additional beta headers required for OAuth/subscription auth. @@ -101,6 +100,7 @@ def _supports_adaptive_thinking(model: str) -> bool: _OAUTH_ONLY_BETAS = [ "claude-code-20250219", "oauth-2025-04-20", + "context-1m-2025-08-07", ] # Claude Code identity — required for OAuth requests to be routed correctly. @@ -231,6 +231,7 @@ def build_anthropic_client(api_key: str, base_url: str = None): # not use Anthropic's sk-ant-api prefix and would otherwise be misread as # Anthropic OAuth/setup tokens. kwargs["auth_token"] = api_key + kwargs["api_key"] = None # Prevent SDK from reading ANTHROPIC_API_KEY env var if _COMMON_BETAS: kwargs["default_headers"] = {"anthropic-beta": ",".join(_COMMON_BETAS)} elif _is_third_party_anthropic_endpoint(base_url): @@ -247,9 +248,15 @@ def build_anthropic_client(api_key: str, base_url: str = None): # without Claude Code's fingerprint, requests get intermittent 500s. all_betas = _COMMON_BETAS + _OAUTH_ONLY_BETAS kwargs["auth_token"] = api_key + # Explicitly set api_key=None to prevent the SDK from reading + # ANTHROPIC_API_KEY from the environment. When both auth_token and + # api_key are set, the SDK sends both X-Api-Key and Authorization + # headers — the empty X-Api-Key from .env overrides the valid Bearer + # token, causing Anthropic to reject the request as unauthenticated. + kwargs["api_key"] = None kwargs["default_headers"] = { "anthropic-beta": ",".join(all_betas), - "user-agent": f"claude-cli/{_get_claude_code_version()} (external, cli)", + "User-Agent": f"claude-cli/{_get_claude_code_version()} (external, cli)", "x-app": "cli", } else: @@ -258,7 +265,14 @@ def build_anthropic_client(api_key: str, base_url: str = None): if _COMMON_BETAS: kwargs["default_headers"] = {"anthropic-beta": ",".join(_COMMON_BETAS)} - return _anthropic_sdk.Anthropic(**kwargs) + client = _anthropic_sdk.Anthropic(**kwargs) + # When using Bearer auth (auth_token), ensure api_key is None so the SDK + # does not also send an X-Api-Key header. The SDK's constructor reads + # ANTHROPIC_API_KEY from the environment when api_key is not passed, + # which can produce an empty-string api_key that overrides valid OAuth. + if kwargs.get("auth_token") and not kwargs.get("api_key"): + client.api_key = None + return client def read_claude_code_credentials() -> Optional[Dict[str, Any]]: diff --git a/agent/error_classifier.py b/agent/error_classifier.py index 0f1450113f5d6..bc39ebd50f7c9 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -108,6 +108,7 @@ def is_transient(self) -> bool: "exceeded your current quota", "account is deactivated", "plan does not include", + "out of extra usage", ] # Patterns that indicate rate limiting (transient, will resolve) From 605761632730a1642a2a929b4a271a336a3a1a69 Mon Sep 17 00:00:00 2001 From: Carman Babin Date: Thu, 9 Apr 2026 18:42:52 -0500 Subject: [PATCH 2/3] test: update adapter tests for beta header and api_key changes - Remove assertion for removed fine-grained-tool-streaming beta - Add assertion for new context-1m-2025-08-07 beta - Update api_key assertions: now explicitly None (not absent) for auth_token paths to prevent SDK env var fallback Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/agent/test_anthropic_adapter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/agent/test_anthropic_adapter.py b/tests/agent/test_anthropic_adapter.py index 0024fac624225..adf0cf7071bf9 100644 --- a/tests/agent/test_anthropic_adapter.py +++ b/tests/agent/test_anthropic_adapter.py @@ -61,8 +61,8 @@ def test_setup_token_uses_auth_token(self): assert "oauth-2025-04-20" in betas assert "claude-code-20250219" in betas assert "interleaved-thinking-2025-05-14" in betas - assert "fine-grained-tool-streaming-2025-05-14" in betas - assert "api_key" not in kwargs + assert "context-1m-2025-08-07" in betas + assert kwargs.get("api_key") is None def test_api_key_uses_api_key(self): with patch("agent.anthropic_adapter._anthropic_sdk") as mock_sdk: @@ -90,9 +90,9 @@ def test_minimax_anthropic_endpoint_uses_bearer_auth_for_regular_api_keys(self): ) kwargs = mock_sdk.Anthropic.call_args[1] assert kwargs["auth_token"] == "minimax-secret-123" - assert "api_key" not in kwargs + assert kwargs.get("api_key") is None assert kwargs["default_headers"] == { - "anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14" + "anthropic-beta": "interleaved-thinking-2025-05-14" } From 669dfc0f03de4c55cee6fbdfe861e4c80f24e825 Mon Sep 17 00:00:00 2001 From: Carman Babin Date: Thu, 9 Apr 2026 19:00:10 -0500 Subject: [PATCH 3/3] local: reword prompt triggers for OAuth compatibility Co-Authored-By: Claude Opus 4.6 (1M context) --- agent/prompt_builder.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 8302973aac758..2fb991c267076 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -150,23 +150,23 @@ def _strip_yaml_frontmatter(content: str) -> str: "that prevents the user from having to correct or remind you again. " "User preferences and recurring corrections matter more than procedural task details.\n" "Do NOT save task progress, session outcomes, completed-work logs, or temporary TODO " - "state to memory; use session_search to recall those from past transcripts. " + "state to memory; use the search_sessions tool to recall those from past transcripts. " "If you've discovered a new way to do something, solved a problem that could be " "necessary later, save it as a skill with the skill tool." ) SESSION_SEARCH_GUIDANCE = ( "When the user references something from a past conversation or you suspect " - "relevant cross-session context exists, use session_search to recall it before " - "asking them to repeat themselves." + "relevant cross-session context exists, use the search_sessions tool to recall it " + "before asking them to repeat themselves." ) SKILLS_GUIDANCE = ( "After completing a complex task (5+ tool calls), fixing a tricky error, " "or discovering a non-trivial workflow, save the approach as a " - "skill with skill_manage so you can reuse it next time.\n" + "skill with the manage_skills tool so you can reuse it next time.\n" "When using a skill and finding it outdated, incomplete, or wrong, " - "patch it immediately with skill_manage(action='patch') — don't wait to be asked. " + "patch it immediately with manage_skills(action='patch') — don't wait to be asked. " "Skills that aren't maintained become liabilities." ) @@ -731,7 +731,7 @@ def build_skills_system_prompt( "## Skills (mandatory)\n" "Before replying, scan the skills below. If one clearly matches your task, " "load it with skill_view(name) and follow its instructions. " - "If a skill has issues, fix it with skill_manage(action='patch').\n" + "If a skill has issues, fix it with manage_skills(action='patch').\n" "After difficult/iterative tasks, offer to save as a skill. " "If a skill you loaded was missing steps, had wrong commands, or needed " "pitfalls you discovered, update it before finishing.\n"