diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 718b778f0..ea8702cb8 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -160,6 +160,16 @@ _OR_HEADERS = { "X-OpenRouter-Categories": "productivity,cli-agent", } +# Vercel AI Gateway app attribution headers. HTTP-Referer maps to +# referrerUrl and X-Title maps to appName in the gateway's analytics. +from hermes_cli import __version__ as _HERMES_VERSION + +_AI_GATEWAY_HEADERS = { + "HTTP-Referer": "https://hermes-agent.nousresearch.com", + "X-Title": "Hermes Agent", + "User-Agent": f"HermesAgent/{_HERMES_VERSION}", +} + # Nous Portal extra_body for product attribution. # Callers should pass this as extra_body in chat.completions.create() # when the auxiliary client is backed by Nous Portal. diff --git a/run_agent.py b/run_agent.py index 999b99629..6dd28d11f 100644 --- a/run_agent.py +++ b/run_agent.py @@ -4994,11 +4994,13 @@ class AIAgent: return True def _apply_client_headers_for_base_url(self, base_url: str) -> None: - from agent.auxiliary_client import _OR_HEADERS + from agent.auxiliary_client import _AI_GATEWAY_HEADERS, _OR_HEADERS normalized = (base_url or "").lower() if "openrouter" in normalized: self._client_kwargs["default_headers"] = dict(_OR_HEADERS) + elif "ai-gateway.vercel.sh" in normalized: + self._client_kwargs["default_headers"] = dict(_AI_GATEWAY_HEADERS) elif "api.githubcopilot.com" in normalized: from hermes_cli.models import copilot_default_headers diff --git a/tests/run_agent/test_provider_attribution_headers.py b/tests/run_agent/test_provider_attribution_headers.py new file mode 100644 index 000000000..a2c543ee7 --- /dev/null +++ b/tests/run_agent/test_provider_attribution_headers.py @@ -0,0 +1,65 @@ +"""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 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-OpenRouter-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_unknown_base_url_clears_default_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._client_kwargs["default_headers"] = {"X-Stale": "yes"} + + agent._apply_client_headers_for_base_url("https://api.example.com/v1") + + assert "default_headers" not in agent._client_kwargs