Files
hermes-agent/tests/agent/test_anthropic_adapter_extra_headers.py
teknium1 6f6ed01355 fix: WAF 403s stop reading as key rejections; anthropic_messages routes send custom_providers extra_headers
Two gaps for custom providers behind a WAF/CDN:

- `build_anthropic_client` never consulted `custom_providers[].extra_headers`,
  so a relay in `anthropic_messages` mode that rejects the SDK User-Agent kept
  403ing even with `extra_headers: {User-Agent: ...}` configured, while the
  OpenAI-wire clients already applied it. The lookup now lives in
  `_new_sdk_client`, the one constructor every builder path goes through
  (init, /model switch, rebuild, auxiliary), keyed by the caller's raw route
  because entries are keyed by the `/v1` form the normalizer strips.
  Salvaged direction of #46002 (@wait4xx). Fixes #24293, #9721.

- `_status_403` classified every non-billing 403 as `auth`, so a WAF's plain
  "Your request was blocked." or a Cloudflare browser challenge printed "Your
  API key was rejected" and could rotate a healthy credential. A 403 carrying
  established block/challenge markers is now `upstream_blocked`: no rotation,
  no retry, fallback allowed, WAF/User-Agent guidance on every surface (CLI
  loop, chat copy, cli chat error copy, TUI gateway + Ink TUI copy). Generic
  403 and all 401 keep the auth verdict. Salvaged direction of #70567
  (@ooiuuii) and #53114 (@AgenticSpark). Fixes #53099, #70566.
2026-09-19 09:57:21 -07:00

33 lines
1.3 KiB
Python

"""Anthropic-messages clients honour ``custom_providers[].extra_headers`` (#24293, #9721).
The OpenAI-wire clients apply the per-provider headers; ``build_anthropic_client`` used to skip
them, so a relay behind a WAF that rejects the SDK User-Agent kept 403ing in anthropic_messages mode.
"""
from unittest.mock import patch
from agent.anthropic_adapter import build_anthropic_client
_ROUTE = "https://proxy.example.com/v1"
_CONFIG = {"custom_providers": [{
"name": "wafproxy", "base_url": _ROUTE, "api_mode": "anthropic_messages",
"extra_headers": {"User-Agent": "HermesAgent/1.0", "X-Privacy-Tier": "enterprise"},
}]}
def _build(route):
with patch("agent.anthropic_adapter._require_sdk") as sdk, patch("hermes_cli.config.load_config", return_value=_CONFIG):
build_anthropic_client("sk-test", route)
return sdk.return_value.Anthropic.call_args.kwargs["default_headers"]
def test_matching_route_merges_extra_headers_after_betas():
headers = _build(_ROUTE)
assert headers["User-Agent"] == "HermesAgent/1.0"
assert headers["X-Privacy-Tier"] == "enterprise"
assert "anthropic-beta" in headers # provider headers add to, not replace, the beta set
def test_other_route_does_not_inherit_extra_headers():
headers = _build("https://other.example.com/v1")
assert "User-Agent" not in headers and "X-Privacy-Tier" not in headers