Skip to content
Closed
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
38 changes: 33 additions & 5 deletions agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,27 @@ def _get_claude_code_version() -> str:
return _claude_code_version_cache


_HERMES_AGENT_VERSION_FALLBACK = "0.0.0"
_hermes_agent_version_cache: Optional[str] = None


def _get_hermes_agent_version() -> str:
"""Lazily resolve the installed hermes-agent version for User-Agent headers.

Used when overriding the Anthropic SDK's default ``Anthropic/Python <ver>``
user-agent on third-party endpoints; self-hosted proxies behind Cloudflare
bot-detection 403 the SDK fingerprint, but accept any non-SDK UA.
"""
global _hermes_agent_version_cache
if _hermes_agent_version_cache is None:
try:
from hermes_cli import __version__ as _v
_hermes_agent_version_cache = str(_v) if _v else _HERMES_AGENT_VERSION_FALLBACK
except Exception:
_hermes_agent_version_cache = _HERMES_AGENT_VERSION_FALLBACK
return _hermes_agent_version_cache


def _is_oauth_token(key: str) -> bool:
"""Check if the key is an Anthropic OAuth/setup token.

Expand Down Expand Up @@ -734,13 +755,20 @@ def build_anthropic_client(
if common_betas:
kwargs["default_headers"] = {"anthropic-beta": ",".join(common_betas)}
elif _is_third_party_anthropic_endpoint(base_url):
# Third-party proxies (Microsoft Foundry, AWS Bedrock, etc.) use their
# own API keys with x-api-key auth. Skip OAuth detection — their keys
# don't follow Anthropic's sk-ant-* prefix convention and would be
# misclassified as OAuth tokens.
# Third-party proxies (Microsoft Foundry, AWS Bedrock, self-hosted
# relays, GLM proxies, etc.) use their own API keys with x-api-key
# auth. Skip OAuth detection — their keys don't follow Anthropic's
# sk-ant-* prefix convention and would be misclassified as OAuth
# tokens.
#
# Override the SDK's default ``Anthropic/Python <ver>`` user-agent
# because Cloudflare-style WAF bot detection on self-hosted proxies
# 403s the SDK fingerprint by default. (#24293)
kwargs["api_key"] = api_key
headers = {"User-Agent": f"hermes-agent/{_get_hermes_agent_version()}"}
if common_betas:
kwargs["default_headers"] = {"anthropic-beta": ",".join(common_betas)}
headers["anthropic-beta"] = ",".join(common_betas)
kwargs["default_headers"] = headers
elif _is_oauth_token(api_key):
# OAuth access token / setup-token → Bearer auth + Claude Code identity.
# Anthropic routes OAuth requests based on user-agent and headers;
Expand Down
37 changes: 34 additions & 3 deletions tests/agent/test_anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,40 @@ def test_custom_base_url(self):
build_anthropic_client("sk-ant-api03-x", base_url="https://custom.api.com")
kwargs = mock_sdk.Anthropic.call_args[1]
assert kwargs["base_url"] == "https://custom.api.com"
assert kwargs["default_headers"] == {
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
}
headers = kwargs["default_headers"]
assert headers["anthropic-beta"] == (
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
)
# Third-party endpoints override the SDK's default User-Agent so
# Cloudflare-style WAF bot detection doesn't 403 the SDK fingerprint.
assert headers["User-Agent"].startswith("hermes-agent/")

def test_third_party_endpoint_overrides_sdk_user_agent(self):
"""Custom-provider endpoints get a hermes-agent UA instead of the
Anthropic SDK's default ``Anthropic/Python <ver>``, which Cloudflare
WAF rules block on self-hosted proxies (#24293)."""
with patch("agent.anthropic_adapter._anthropic_sdk") as mock_sdk:
build_anthropic_client(
"proxy-key-xyz",
base_url="https://proxy.example.com/v1",
)
kwargs = mock_sdk.Anthropic.call_args[1]
assert kwargs["api_key"] == "proxy-key-xyz"
ua = kwargs["default_headers"]["User-Agent"]
assert ua.startswith("hermes-agent/"), ua
# The Anthropic SDK default fingerprint must NOT leak through.
assert "Anthropic/Python" not in ua

def test_native_anthropic_endpoint_does_not_override_user_agent(self):
"""Direct anthropic.com requests must NOT carry the third-party UA
override — the SDK's default ``Anthropic/Python`` UA is expected by
Anthropic's own infrastructure and routing."""
with patch("agent.anthropic_adapter._anthropic_sdk") as mock_sdk:
build_anthropic_client("sk-ant-api03-direct")
kwargs = mock_sdk.Anthropic.call_args[1]
headers = kwargs.get("default_headers", {}) or {}
assert "User-Agent" not in headers
assert "user-agent" not in headers

def test_azure_anthropic_endpoint_keeps_context_1m_beta(self):
with patch("agent.anthropic_adapter._anthropic_sdk") as mock_sdk:
Expand Down
Loading