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
9 changes: 8 additions & 1 deletion agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ def build_anthropic_client(
timeout: float = None,
*,
drop_context_1m_beta: bool = False,
user_agent: str | None = None,
):
"""Create an Anthropic client, auto-detecting setup-tokens vs API keys.

Expand Down Expand Up @@ -591,8 +592,14 @@ def build_anthropic_client(
# don't follow Anthropic's sk-ant-* prefix convention and would be
# misclassified as OAuth tokens.
kwargs["api_key"] = api_key
# Third-party endpoints often sit behind Cloudflare or similar WAFs
# that block SDK User-Agent strings (Anthropic/Python, etc.).
# Override with a neutral User-Agent to avoid 403 blocks.
_safe_ua = (user_agent or "hermes-agent")[:256].replace("\r", "").replace("\n", "")
_tp_headers = {"User-Agent": _safe_ua}
if common_betas:
kwargs["default_headers"] = {"anthropic-beta": ",".join(common_betas)}
_tp_headers["anthropic-beta"] = ",".join(common_betas)
kwargs["default_headers"] = _tp_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
3 changes: 3 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,7 @@ def _resolve_runtime_agent_kwargs() -> dict:
"command": runtime.get("command"),
"args": list(runtime.get("args") or []),
"credential_pool": runtime.get("credential_pool"),
"user_agent": runtime.get("user_agent"),
}


Expand Down Expand Up @@ -746,6 +747,7 @@ def _try_resolve_fallback_provider() -> dict | None:
"command": runtime.get("command"),
"args": list(runtime.get("args") or []),
"credential_pool": runtime.get("credential_pool"),
"user_agent": runtime.get("user_agent"),
"model": entry.get("model"),
}
except Exception as fb_exc:
Expand Down Expand Up @@ -1880,6 +1882,7 @@ def _resolve_turn_agent_config(self, user_message: str, model: str, runtime_kwar
"command": runtime_kwargs.get("command"),
"args": list(runtime_kwargs.get("args") or []),
"credential_pool": runtime_kwargs.get("credential_pool"),
"user_agent": runtime_kwargs.get("user_agent"),
}
route = {
"model": model,
Expand Down
10 changes: 10 additions & 0 deletions hermes_cli/runtime_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,9 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
api_mode = _parse_api_mode(entry.get("api_mode") or entry.get("transport"))
if api_mode:
result["api_mode"] = api_mode
_ua = str(entry.get("user_agent", "") or "").strip()
if _ua:
result["user_agent"] = _ua
return result
# Also check the 'name' field if present
display_name = entry.get("name", "")
Expand All @@ -429,6 +432,9 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
api_mode = _parse_api_mode(entry.get("api_mode") or entry.get("transport"))
if api_mode:
result["api_mode"] = api_mode
_ua2 = str(entry.get("user_agent", "") or "").strip()
if _ua2:
result["user_agent"] = _ua2
return result

# Fall back to custom_providers: list (legacy format)
Expand Down Expand Up @@ -560,6 +566,10 @@ def _resolve_named_custom_runtime(
# provider name differs from the actual model string the API expects.
if custom_provider.get("model"):
result["model"] = custom_provider["model"]
# Propagate custom User-Agent for third-party endpoints behind WAFs.
_user_agent = str(custom_provider.get("user_agent", "") or "").strip()
if _user_agent:
result["user_agent"] = _user_agent
return result


Expand Down
17 changes: 16 additions & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,7 @@ def __init__(
api_key: str = None,
provider: str = None,
api_mode: str = None,
user_agent: str | None = None,
acp_command: str = None,
acp_args: list[str] | None = None,
command: str = None,
Expand Down Expand Up @@ -1193,6 +1194,7 @@ def __init__(
self.load_soul_identity = load_soul_identity
self.pass_session_id = pass_session_id
self._credential_pool = credential_pool
self._user_agent = user_agent
self.log_prefix_chars = log_prefix_chars
self.log_prefix = f"{log_prefix} " if log_prefix else ""
# Store effective base URL for feature detection (prompt caching, reasoning, etc.)
Expand Down Expand Up @@ -1546,7 +1548,7 @@ def __init__(
# the third-party identity-injection bug.
from agent.anthropic_adapter import _is_oauth_token as _is_oat
self._is_anthropic_oauth = _is_oat(effective_key) if _is_native_anthropic else False
self._anthropic_client = build_anthropic_client(effective_key, base_url, timeout=_provider_timeout)
self._anthropic_client = build_anthropic_client(effective_key, base_url, timeout=_provider_timeout, user_agent=user_agent)
# No OpenAI client needed for Anthropic mode
self.client = None
self._client_kwargs = {}
Expand Down Expand Up @@ -1628,13 +1630,19 @@ def __init__(
# Fall back to profile.default_headers for providers that
# declare custom headers (e.g. Vercel AI Gateway attribution,
# Kimi User-Agent on non-kimi.com endpoints).
_got_profile_headers = False
try:
from providers import get_provider_profile as _gpf
_ph = _gpf(self.provider)
if _ph and _ph.default_headers:
client_kwargs["default_headers"] = dict(_ph.default_headers)
_got_profile_headers = True
except Exception:
pass
# Third-party endpoints behind Cloudflare block SDK User-Agents.
if not _got_profile_headers:
_safe_ua = (user_agent or "hermes-agent")[:256].replace("\r", "").replace("\n", "")
client_kwargs["default_headers"] = {"User-Agent": _safe_ua}
else:
# No explicit creds — use the centralized provider router
from agent.auxiliary_client import resolve_provider_client
Expand Down Expand Up @@ -2616,6 +2624,7 @@ def switch_model(self, new_model, new_provider, api_key='', base_url='', api_mod
self._anthropic_client = build_anthropic_client(
effective_key, self._anthropic_base_url,
timeout=get_provider_request_timeout(self.provider, self.model),
user_agent=self._user_agent,
)
self._is_anthropic_oauth = _is_oauth_token(effective_key) if _is_native_anthropic else False
self.client = None
Expand Down Expand Up @@ -7109,6 +7118,7 @@ def _try_refresh_anthropic_client_credentials(self) -> bool:
new_token,
getattr(self, "_anthropic_base_url", None),
timeout=get_provider_request_timeout(self.provider, self.model),
user_agent=self._user_agent,
)
except Exception as exc:
logger.warning("Failed to rebuild Anthropic client after credential refresh: %s", exc)
Expand Down Expand Up @@ -7177,6 +7187,7 @@ def _swap_credential(self, entry) -> None:
self._anthropic_client = build_anthropic_client(
runtime_key, runtime_base,
timeout=get_provider_request_timeout(self.provider, self.model),
user_agent=self._user_agent,
)
self._is_anthropic_oauth = _is_oauth_token(runtime_key) if self.provider == "anthropic" else False
self.api_key = runtime_key
Expand Down Expand Up @@ -7318,6 +7329,7 @@ def _rebuild_anthropic_client(self) -> None:
getattr(self, "_anthropic_base_url", None),
timeout=get_provider_request_timeout(self.provider, self.model),
drop_context_1m_beta=_drop_1m,
user_agent=self._user_agent,
)

def _interruptible_api_call(self, api_kwargs: dict):
Expand Down Expand Up @@ -8676,6 +8688,7 @@ def _try_activate_fallback(self, reason: "FailoverReason | None" = None) -> bool
self._anthropic_base_url = fb_base_url
self._anthropic_client = build_anthropic_client(
effective_key, self._anthropic_base_url, timeout=_fb_timeout,
user_agent=self._user_agent,
)
self._is_anthropic_oauth = _is_oauth_token(effective_key) if fb_provider == "anthropic" else False
self.client = None
Expand Down Expand Up @@ -8820,6 +8833,7 @@ def _restore_primary_runtime(self) -> bool:
self._anthropic_client = build_anthropic_client(
rt["anthropic_api_key"], rt["anthropic_base_url"],
timeout=get_provider_request_timeout(self.provider, self.model),
user_agent=self._user_agent,
)
self._is_anthropic_oauth = rt["is_anthropic_oauth"]
self.client = None
Expand Down Expand Up @@ -8919,6 +8933,7 @@ def _try_recover_primary_transport(
self._anthropic_client = build_anthropic_client(
rt["anthropic_api_key"], rt["anthropic_base_url"],
timeout=get_provider_request_timeout(self.provider, self.model),
user_agent=self._user_agent,
)
self._is_anthropic_oauth = rt["is_anthropic_oauth"]
self.client = None
Expand Down