Runtime identity resolved through hermes_cli.__version__ (a static 0.0.0 on source installs, rewritten by release stamping) leaked v0.0.0 into About, /api/health, User-Agents, and plugin compat, and source updates showed "couldn't reach update server" because identity and channel authority disagreed with the checkout. Now: get_version_info() resolves install stamp -> live git -> unknown, never pyproject metadata, never a package constant. Source checkouts derive identity from their reachable release tag; the completion tail of every successful install/update/historical takeover atomically rewrites install-stamp.json with that identity; a stale source stamp whose commit no longer matches HEAD defers to live git. ACP/TUI use derived_version for display and base_version for protocol fields; all ~44 runtime __version__ consumers migrated; hermes_cli.__version__ and generated _version.py are gone; release stamping only touches the native manifests external builders consume (nix/tauri/cargo) and passes release identity straight into write_install_stamp.py; pyproject.toml stays inert 0.0.0. Desktop no longer synthesizes a competing install-stamp.json: the checkout owns its stamp, and desktop-bootstrap classification keys on the bootstrap-complete marker. verify-bootstrap-version-stamp.py now cross-checks the checkout's stamp (baseVersion + commit == HEAD). Validation: 31-file focused suite green (version identity, stamping, adoption, providers, gateway, acp/tui runtime identity, api server via extras env, release graph); desktop tsc + 25 vitest green; real-repo probe: base=unknown derived=git.0635606.dirty source=git on this checkout; clean-env imports resolve entirely from this tree; windows footgun + compat-pointer scans clean.
185 lines
6.8 KiB
Python
185 lines
6.8 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 hermes_cli.version_info import get_version_info
|
|
from run_agent import AIAgent
|
|
|
|
|
|
@patch("agent.process_bootstrap.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"] == f"HermesAgent/{get_version_info().base_version}"
|
|
|
|
|
|
@patch("agent.process_bootstrap.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("agent.process_bootstrap.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"] == f"HermesAgent/{get_version_info().base_version}"
|
|
|
|
|
|
@patch("agent.process_bootstrap.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("agent.process_bootstrap.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("agent.process_bootstrap.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("agent.process_bootstrap.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
|