fix(anthropic): use claude-code/ UA prefix for OAuth to avoid 404 (#48534)
Anthropic's OAuth endpoints 404 for the claude-cli/ User-Agent prefix. Switch all three OAuth UA sites (build_anthropic_client, refresh_anthropic_oauth_pure, run_hermes_oauth_login_pure) to the claude-code/ prefix Anthropic expects. Salvaged from #51948. Co-authored-by: DhivinX <20087092+DhivinX@users.noreply.github.com>
This commit is contained in:
parent
5881791adc
commit
49e129e495
2 changed files with 87 additions and 3 deletions
|
|
@ -817,7 +817,7 @@ def build_anthropic_client(
|
|||
kwargs["auth_token"] = api_key
|
||||
kwargs["default_headers"] = {
|
||||
"anthropic-beta": ",".join(all_betas),
|
||||
"user-agent": f"claude-cli/{_get_claude_code_version()} (external, cli)",
|
||||
"user-agent": f"claude-code/{_get_claude_code_version()} (external, cli)",
|
||||
"x-app": "cli",
|
||||
}
|
||||
else:
|
||||
|
|
@ -1045,7 +1045,7 @@ def refresh_anthropic_oauth_pure(refresh_token: str, *, use_json: bool = False)
|
|||
data=data,
|
||||
headers={
|
||||
"Content-Type": content_type,
|
||||
"User-Agent": f"claude-cli/{_get_claude_code_version()} (external, cli)",
|
||||
"User-Agent": f"claude-code/{_get_claude_code_version()} (external, cli)",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
|
@ -1478,6 +1478,8 @@ def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]:
|
|||
# Anthropic migrated the OAuth token endpoint to platform.claude.com;
|
||||
# console.anthropic.com now 404s. Try the new host first, then fall
|
||||
# back to console for older deployments (mirrors the refresh path).
|
||||
# Use the claude-code/ UA prefix: Anthropic blocks claude-cli/ on the
|
||||
# OAuth token endpoint (returns 404 for all versions).
|
||||
result = None
|
||||
last_error = None
|
||||
for endpoint in _OAUTH_TOKEN_URLS:
|
||||
|
|
@ -1486,7 +1488,7 @@ def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]:
|
|||
data=exchange_data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": f"claude-cli/{_get_claude_code_version()} (external, cli)",
|
||||
"User-Agent": f"claude-code/{_get_claude_code_version()} (external, cli)",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
|
|
|||
82
tests/agent/test_anthropic_oauth_ua_prefix.py
Normal file
82
tests/agent/test_anthropic_oauth_ua_prefix.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"""Regression tests for the OAuth User-Agent header in anthropic_adapter.py.
|
||||
|
||||
Anthropic now 404s the OAuth token endpoint for any ``claude-cli/`` UA prefix
|
||||
(issue #48534). The adapter must use ``claude-code/`` instead.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestOAuthUserAgentPrefix:
|
||||
"""All OAuth-related HTTP requests must use ``claude-code/`` UA, not ``claude-cli/``."""
|
||||
|
||||
def test_build_anthropic_client_oauth_ua(self):
|
||||
"""build_anthropic_client with OAuth token must use claude-code UA."""
|
||||
from agent.anthropic_adapter import build_anthropic_client
|
||||
|
||||
mock_sdk = MagicMock()
|
||||
with patch("agent.anthropic_adapter._get_anthropic_sdk", return_value=mock_sdk):
|
||||
build_anthropic_client("sk-ant-oauth-abc123", "https://api.anthropic.com")
|
||||
|
||||
# Inspect the kwargs passed to Anthropic()
|
||||
call_kwargs = mock_sdk.Anthropic.call_args[1]
|
||||
headers = call_kwargs.get("default_headers", {})
|
||||
ua = headers.get("user-agent", "") or headers.get("User-Agent", "")
|
||||
|
||||
assert "claude-code/" in ua, f"Expected claude-code/ in UA, got: {ua}"
|
||||
assert "claude-cli/" not in ua, f"Must not use claude-cli/ prefix: {ua}"
|
||||
|
||||
def test_no_claude_cli_in_source(self):
|
||||
"""Source file must not contain claude-cli/ UA pattern (blocks OAuth)."""
|
||||
import inspect
|
||||
import agent.anthropic_adapter as mod
|
||||
|
||||
source = inspect.getsource(mod)
|
||||
# Allow claude-cli in comments/strings that reference the old behavior
|
||||
# but not in actual header assignments
|
||||
lines = source.split("\n")
|
||||
for i, line in enumerate(lines, 1):
|
||||
stripped = line.strip()
|
||||
if "claude-cli/" in stripped and ("User-Agent" in stripped or "user-agent" in stripped):
|
||||
pytest.fail(
|
||||
f"Line {i}: claude-cli/ still used in User-Agent header: {stripped}"
|
||||
)
|
||||
|
||||
def test_token_exchange_ua_prefix(self):
|
||||
"""run_hermes_oauth_login_pure must not send claude-cli/ UA."""
|
||||
import inspect
|
||||
import agent.anthropic_adapter as mod
|
||||
|
||||
# Get the source of the exchange function
|
||||
try:
|
||||
source = inspect.getsource(mod.run_hermes_oauth_login_pure)
|
||||
except AttributeError:
|
||||
pytest.skip("run_hermes_oauth_login_pure not found")
|
||||
|
||||
assert "claude-cli/" not in source, (
|
||||
"run_hermes_oauth_login_pure still uses claude-cli/ UA"
|
||||
)
|
||||
assert "claude-code/" in source, (
|
||||
"run_hermes_oauth_login_pure should use claude-code/ UA"
|
||||
)
|
||||
|
||||
def test_token_refresh_ua_prefix(self):
|
||||
"""_refresh_oauth_token_raw must not send claude-cli/ UA."""
|
||||
import inspect
|
||||
import agent.anthropic_adapter as mod
|
||||
|
||||
# Find the function that does the actual refresh HTTP call
|
||||
for name in ("_refresh_oauth_token_raw", "_do_token_refresh", "_refresh_oauth_token"):
|
||||
func = getattr(mod, name, None)
|
||||
if func and callable(func):
|
||||
source = inspect.getsource(func)
|
||||
if "urllib.request.Request" in source:
|
||||
assert "claude-cli/" not in source, (
|
||||
f"{name} still uses claude-cli/ UA"
|
||||
)
|
||||
break
|
||||
Loading…
Add table
Add a link
Reference in a new issue