Files
hermes-agent/tests/run_agent/test_provider_attribution_headers.py
T
Adolanium 7fa084f58e fix: send Hermes Agent attribution headers to OpenCode Zen and Go
OpenCode identifies clients by request headers, the same way OpenRouter
does. Our opencode-zen and opencode-go profiles never set any, so every
request went out with the OpenAI SDK default "OpenAI/Python x.y.z"
User-Agent and OpenCode had no way to tell the traffic was Hermes Agent.

Two changes:

- Add HTTP-Referer, X-Title, and a HermesAgent User-Agent to both
  OpenCode profiles through profile.default_headers, the same path
  Fireworks uses. This covers chat_completions, codex_responses,
  auxiliary clients, model switches, and the models catalog fetch.
- Merge the same headers in build_anthropic_client for opencode.ai
  base URLs. The Anthropic Messages route (Claude on Zen, MiniMax and
  Qwen on Go) builds its client there and never sees profile headers.

Verified against the live Go relay with a real key. Both wire formats
return HTTP 200 and the requests now carry X-Title "Hermes Agent",
HTTP-Referer, and User-Agent HermesAgent/0.20.0.
2026-08-13 02:03:40 -07:00

284 lines
9.4 KiB
Python

"""Attribution default_headers applied per provider via base-URL detection.
Mirrors the OpenRouter pattern for the Vercel AI Gateway so that
referrerUrl / appName / User-Agent flow into gateway analytics.
"""
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from run_agent import AIAgent
@patch("run_agent.OpenAI")
def test_openrouter_base_url_applies_or_headers(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
agent._apply_client_headers_for_base_url("https://openrouter.ai/api/v1")
headers = agent._client_kwargs["default_headers"]
assert headers["HTTP-Referer"] == "https://hermes-agent.nousresearch.com"
assert headers["X-Title"] == "Hermes Agent"
@patch("run_agent.OpenAI")
def test_ai_gateway_base_url_applies_attribution_headers(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
agent._apply_client_headers_for_base_url("https://ai-gateway.vercel.sh/v1")
headers = agent._client_kwargs["default_headers"]
assert headers["HTTP-Referer"] == "https://hermes-agent.nousresearch.com"
assert headers["X-Title"] == "Hermes Agent"
assert headers["User-Agent"].startswith("HermesAgent/")
@patch("run_agent.OpenAI")
def test_routermint_base_url_applies_user_agent_header(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="test-key",
base_url="https://api.routermint.com/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
agent._apply_client_headers_for_base_url("https://api.routermint.com/v1")
headers = agent._client_kwargs["default_headers"]
assert headers["User-Agent"].startswith("HermesAgent/")
@patch("run_agent.OpenAI")
def test_nvidia_cloud_base_url_applies_billing_origin_header(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="test-key",
base_url="https://integrate.api.nvidia.com/v1",
model="nvidia/test-model",
provider="nvidia",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
assert agent._client_kwargs["default_headers"]["X-BILLING-INVOKE-ORIGIN"] == "HermesAgent"
agent._apply_client_headers_for_base_url("https://integrate.api.nvidia.com/v1")
headers = agent._client_kwargs["default_headers"]
assert headers["X-BILLING-INVOKE-ORIGIN"] == "HermesAgent"
@patch("run_agent.OpenAI")
def test_fireworks_applies_attribution_via_profile_fallback(mock_openai):
"""Fireworks has no host-specific branch — its attribution headers come
from the profile.default_headers fallback, the path a model switch
re-runs."""
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="test-key",
base_url="https://api.fireworks.ai/inference/v1",
model="accounts/fireworks/models/kimi-k2p6",
provider="fireworks",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
agent._apply_client_headers_for_base_url("https://api.fireworks.ai/inference/v1")
headers = agent._client_kwargs["default_headers"]
assert headers["HTTP-Referer"] == "https://hermes-agent.nousresearch.com"
assert headers["X-Title"] == "Hermes Agent"
assert headers["User-Agent"].startswith("HermesAgent/")
@patch("run_agent.OpenAI")
def test_opencode_go_applies_attribution_via_profile_fallback(mock_openai):
"""OpenCode (Zen/Go) attributes traffic by header like OpenRouter does.
Without profile.default_headers the relay only sees the OpenAI SDK's
generic User-Agent and Hermes Agent traffic shows up unattributed."""
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="test-key",
base_url="https://opencode.ai/zen/go/v1",
model="glm-5",
provider="opencode-go",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
agent._apply_client_headers_for_base_url("https://opencode.ai/zen/go/v1")
headers = agent._client_kwargs["default_headers"]
assert headers["HTTP-Referer"] == "https://hermes-agent.nousresearch.com"
assert headers["X-Title"] == "Hermes Agent"
assert headers["User-Agent"].startswith("HermesAgent/")
@patch("run_agent.OpenAI")
def test_opencode_zen_applies_attribution_via_profile_fallback(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="test-key",
base_url="https://opencode.ai/zen/v1",
model="claude-sonnet-4-5",
provider="opencode-zen",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
agent._apply_client_headers_for_base_url("https://opencode.ai/zen/v1")
headers = agent._client_kwargs["default_headers"]
assert headers["HTTP-Referer"] == "https://hermes-agent.nousresearch.com"
assert headers["X-Title"] == "Hermes Agent"
assert headers["User-Agent"].startswith("HermesAgent/")
@patch("run_agent.OpenAI")
def test_routed_client_preserves_openai_sdk_custom_headers(mock_openai):
mock_openai.return_value = MagicMock()
routed_client = SimpleNamespace(
api_key="test-key",
base_url="https://integrate.api.nvidia.com/v1",
_custom_headers={"X-BILLING-INVOKE-ORIGIN": "HermesAgent"},
)
with patch("agent.auxiliary_client.resolve_provider_client", return_value=(
routed_client,
"nvidia/test-model",
)):
agent = AIAgent(
provider="nvidia",
model="nvidia/test-model",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
headers = agent._client_kwargs["default_headers"]
assert headers["X-BILLING-INVOKE-ORIGIN"] == "HermesAgent"
@patch("run_agent.OpenAI")
def test_openrouter_headers_include_response_cache_when_enabled(mock_openai):
"""When openrouter.response_cache is True, the cache header is injected."""
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
with patch("hermes_cli.config.load_config", return_value={
"openrouter": {"response_cache": True, "response_cache_ttl": 600},
}), patch("hermes_cli.config.load_config_readonly", return_value={
"openrouter": {"response_cache": True, "response_cache_ttl": 600},
}):
agent._apply_client_headers_for_base_url("https://openrouter.ai/api/v1")
headers = agent._client_kwargs["default_headers"]
assert headers["HTTP-Referer"] == "https://hermes-agent.nousresearch.com"
assert headers["X-OpenRouter-Cache"] == "true"
assert headers["X-OpenRouter-Cache-TTL"] == "600"
# ---------------------------------------------------------------------------
# model.default_headers — user-configured overrides (#40033)
# ---------------------------------------------------------------------------
@patch("run_agent.OpenAI")
def test_user_default_headers_override_sdk_user_agent(mock_openai):
"""``model.default_headers`` lets a custom endpoint swap the OpenAI SDK
User-Agent that some gateways/WAFs reject (the #40033 reproduction)."""
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="test-key",
base_url="http://localhost:8080/v1",
model="my-custom-model",
provider="custom",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
with patch("hermes_cli.config.load_config", return_value={
"model": {"default_headers": {"User-Agent": "curl/8.7.1", "X-Extra": "1"}},
}), patch("hermes_cli.config.load_config_readonly", return_value={
"model": {"default_headers": {"User-Agent": "curl/8.7.1", "X-Extra": "1"}},
}):
agent._apply_client_headers_for_base_url("http://localhost:8080/v1")
headers = agent._client_kwargs["default_headers"]
assert headers["User-Agent"] == "curl/8.7.1"
assert headers["X-Extra"] == "1"
@patch("run_agent.OpenAI")
def test_openrouter_headers_no_cache_when_disabled(mock_openai):
"""When openrouter.response_cache is False, no cache headers are sent."""
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
with patch("hermes_cli.config.load_config", return_value={
"openrouter": {"response_cache": False},
}), patch("hermes_cli.config.load_config_readonly", return_value={
"openrouter": {"response_cache": False},
}):
agent._apply_client_headers_for_base_url("https://openrouter.ai/api/v1")
headers = agent._client_kwargs["default_headers"]
assert headers["HTTP-Referer"] == "https://hermes-agent.nousresearch.com"
assert "X-OpenRouter-Cache" not in headers
assert "X-OpenRouter-Cache-TTL" not in headers