fix: Anthropic OAuth compatibility — Claude Code identity fingerprinting (#1597)

Anthropic routes OAuth/subscription requests based on Claude Code's
identity markers. Without them, requests get intermittent 500 errors
(~25% failure rate observed). This matches what pi-ai (clawdbot) and
OpenCode both implement for OAuth compatibility.

Changes (OAuth tokens only — API key users unaffected):

1. Headers: user-agent 'claude-cli/2.1.2 (external, cli)' + x-app 'cli'
2. System prompt: prepend 'You are Claude Code, Anthropic's official CLI'
3. System prompt sanitization: replace Hermes/Nous references
4. Tool names: prefix with 'mcp_' (Claude Code convention for non-native tools)
5. Tool name stripping: remove 'mcp_' prefix from response tool calls

Before: 9/12 OK, 1 hard fail, 4 needed retries (~25% error rate)
After: 16/16 OK, 0 failures, 0 retries (0% error rate)
This commit is contained in:
Teknium 2026-03-16 17:08:22 -07:00 committed by GitHub
parent e6cf1c94a8
commit 2158c44efd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 12 deletions

View file

@ -546,6 +546,8 @@ class AIAgent:
effective_key = api_key or resolve_anthropic_token() or ""
self._anthropic_api_key = effective_key
self._anthropic_base_url = base_url
from agent.anthropic_adapter import _is_oauth_token as _is_oat
self._is_anthropic_oauth = _is_oat(effective_key)
self._anthropic_client = build_anthropic_client(effective_key, base_url)
# No OpenAI client needed for Anthropic mode
self.client = None
@ -3372,6 +3374,7 @@ class AIAgent:
tools=self.tools,
max_tokens=self.max_tokens,
reasoning_config=self.reasoning_config,
is_oauth=getattr(self, "_is_anthropic_oauth", False),
)
if self.api_mode == "codex_responses":
@ -3789,7 +3792,7 @@ class AIAgent:
tool_calls = assistant_msg.tool_calls
elif self.api_mode == "anthropic_messages" and not _aux_available:
from agent.anthropic_adapter import normalize_anthropic_response as _nar_flush
_flush_msg, _ = _nar_flush(response)
_flush_msg, _ = _nar_flush(response, strip_tool_prefix=getattr(self, '_is_anthropic_oauth', False))
if _flush_msg and _flush_msg.tool_calls:
tool_calls = _flush_msg.tool_calls
elif hasattr(response, "choices") and response.choices:
@ -4550,9 +4553,10 @@ class AIAgent:
if self.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_kwargs as _bak, normalize_anthropic_response as _nar
_ant_kw = _bak(model=self.model, messages=api_messages, tools=None,
max_tokens=self.max_tokens, reasoning_config=self.reasoning_config)
max_tokens=self.max_tokens, reasoning_config=self.reasoning_config,
is_oauth=getattr(self, '_is_anthropic_oauth', False))
summary_response = self._anthropic_messages_create(_ant_kw)
_msg, _ = _nar(summary_response)
_msg, _ = _nar(summary_response, strip_tool_prefix=getattr(self, '_is_anthropic_oauth', False))
final_response = (_msg.content or "").strip()
else:
summary_response = self._ensure_primary_openai_client(reason="iteration_limit_summary").chat.completions.create(**summary_kwargs)
@ -4580,9 +4584,10 @@ class AIAgent:
elif self.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_kwargs as _bak2, normalize_anthropic_response as _nar2
_ant_kw2 = _bak2(model=self.model, messages=api_messages, tools=None,
is_oauth=getattr(self, '_is_anthropic_oauth', False),
max_tokens=self.max_tokens, reasoning_config=self.reasoning_config)
retry_response = self._anthropic_messages_create(_ant_kw2)
_retry_msg, _ = _nar2(retry_response)
_retry_msg, _ = _nar2(retry_response, strip_tool_prefix=getattr(self, '_is_anthropic_oauth', False))
final_response = (_retry_msg.content or "").strip()
else:
summary_kwargs = {
@ -5644,7 +5649,9 @@ class AIAgent:
assistant_message, finish_reason = self._normalize_codex_response(response)
elif self.api_mode == "anthropic_messages":
from agent.anthropic_adapter import normalize_anthropic_response
assistant_message, finish_reason = normalize_anthropic_response(response)
assistant_message, finish_reason = normalize_anthropic_response(
response, strip_tool_prefix=getattr(self, "_is_anthropic_oauth", False)
)
else:
assistant_message = response.choices[0].message