mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-23 05:31:23 +00:00
feat(nvidia): add NIM billing origin header
This commit is contained in:
parent
4e89c53082
commit
13c3d4b4ef
5 changed files with 162 additions and 6 deletions
|
|
@ -369,6 +369,21 @@ def build_or_headers(or_config: dict | None = None) -> dict:
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
# NVIDIA NIM cloud billing attribution. Keep this host-gated because the
|
||||||
|
# nvidia provider also supports local/on-prem NIM endpoints via NVIDIA_BASE_URL.
|
||||||
|
_NVIDIA_NIM_CLOUD_HEADERS = {
|
||||||
|
"X-BILLING-INVOKE-ORIGIN": "HermesAgent",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_nvidia_nim_headers(base_url: str | None) -> dict:
|
||||||
|
"""Return NVIDIA NIM cloud attribution headers for build.nvidia.com traffic."""
|
||||||
|
if base_url_host_matches(str(base_url or ""), "integrate.api.nvidia.com"):
|
||||||
|
return dict(_NVIDIA_NIM_CLOUD_HEADERS)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
# Vercel AI Gateway app attribution headers. HTTP-Referer maps to
|
# Vercel AI Gateway app attribution headers. HTTP-Referer maps to
|
||||||
# referrerUrl and X-Title maps to appName in the gateway's analytics.
|
# referrerUrl and X-Title maps to appName in the gateway's analytics.
|
||||||
from hermes_cli import __version__ as _HERMES_VERSION
|
from hermes_cli import __version__ as _HERMES_VERSION
|
||||||
|
|
@ -1372,6 +1387,8 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
|
||||||
from hermes_cli.models import copilot_default_headers
|
from hermes_cli.models import copilot_default_headers
|
||||||
|
|
||||||
extra["default_headers"] = copilot_default_headers()
|
extra["default_headers"] = copilot_default_headers()
|
||||||
|
elif base_url_host_matches(base_url, "integrate.api.nvidia.com"):
|
||||||
|
extra["default_headers"] = build_nvidia_nim_headers(base_url)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
from providers import get_provider_profile as _gpf_aux
|
from providers import get_provider_profile as _gpf_aux
|
||||||
|
|
@ -1407,6 +1424,8 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
|
||||||
from hermes_cli.models import copilot_default_headers
|
from hermes_cli.models import copilot_default_headers
|
||||||
|
|
||||||
extra["default_headers"] = copilot_default_headers()
|
extra["default_headers"] = copilot_default_headers()
|
||||||
|
elif base_url_host_matches(base_url, "integrate.api.nvidia.com"):
|
||||||
|
extra["default_headers"] = build_nvidia_nim_headers(base_url)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
from providers import get_provider_profile as _gpf_aux2
|
from providers import get_provider_profile as _gpf_aux2
|
||||||
|
|
@ -2690,6 +2709,8 @@ def _to_async_client(sync_client, model: str, is_vision: bool = False):
|
||||||
)
|
)
|
||||||
elif base_url_host_matches(sync_base_url, "api.kimi.com"):
|
elif base_url_host_matches(sync_base_url, "api.kimi.com"):
|
||||||
async_kwargs["default_headers"] = {"User-Agent": "claude-code/0.1.0"}
|
async_kwargs["default_headers"] = {"User-Agent": "claude-code/0.1.0"}
|
||||||
|
elif base_url_host_matches(sync_base_url, "integrate.api.nvidia.com"):
|
||||||
|
async_kwargs["default_headers"] = build_nvidia_nim_headers(sync_base_url)
|
||||||
else:
|
else:
|
||||||
# Fall back to profile.default_headers for providers that declare
|
# Fall back to profile.default_headers for providers that declare
|
||||||
# client-level headers on their ProviderProfile (e.g. attribution
|
# client-level headers on their ProviderProfile (e.g. attribution
|
||||||
|
|
@ -2951,6 +2972,8 @@ def resolve_provider_client(
|
||||||
extra["default_headers"] = copilot_request_headers(
|
extra["default_headers"] = copilot_request_headers(
|
||||||
is_agent_turn=True, is_vision=is_vision
|
is_agent_turn=True, is_vision=is_vision
|
||||||
)
|
)
|
||||||
|
elif base_url_host_matches(custom_base, "integrate.api.nvidia.com"):
|
||||||
|
extra["default_headers"] = build_nvidia_nim_headers(custom_base)
|
||||||
else:
|
else:
|
||||||
# Fall back to profile.default_headers for providers that
|
# Fall back to profile.default_headers for providers that
|
||||||
# declare client-level attribution headers on their profile.
|
# declare client-level attribution headers on their profile.
|
||||||
|
|
@ -3149,6 +3172,8 @@ def resolve_provider_client(
|
||||||
headers.update(copilot_request_headers(
|
headers.update(copilot_request_headers(
|
||||||
is_agent_turn=True, is_vision=is_vision
|
is_agent_turn=True, is_vision=is_vision
|
||||||
))
|
))
|
||||||
|
elif base_url_host_matches(base_url, "integrate.api.nvidia.com"):
|
||||||
|
headers.update(build_nvidia_nim_headers(base_url))
|
||||||
else:
|
else:
|
||||||
# Fall back to profile.default_headers for providers that declare
|
# Fall back to profile.default_headers for providers that declare
|
||||||
# client-level attribution headers on their profile (e.g. GMI
|
# client-level attribution headers on their profile (e.g. GMI
|
||||||
|
|
|
||||||
30
run_agent.py
30
run_agent.py
|
|
@ -1664,6 +1664,9 @@ class AIAgent:
|
||||||
if base_url_host_matches(effective_base, "openrouter.ai"):
|
if base_url_host_matches(effective_base, "openrouter.ai"):
|
||||||
from agent.auxiliary_client import build_or_headers
|
from agent.auxiliary_client import build_or_headers
|
||||||
client_kwargs["default_headers"] = build_or_headers()
|
client_kwargs["default_headers"] = build_or_headers()
|
||||||
|
elif base_url_host_matches(effective_base, "integrate.api.nvidia.com"):
|
||||||
|
from agent.auxiliary_client import build_nvidia_nim_headers
|
||||||
|
client_kwargs["default_headers"] = build_nvidia_nim_headers(effective_base)
|
||||||
elif base_url_host_matches(effective_base, "api.routermint.com"):
|
elif base_url_host_matches(effective_base, "api.routermint.com"):
|
||||||
client_kwargs["default_headers"] = _routermint_headers()
|
client_kwargs["default_headers"] = _routermint_headers()
|
||||||
elif base_url_host_matches(effective_base, "api.githubcopilot.com"):
|
elif base_url_host_matches(effective_base, "api.githubcopilot.com"):
|
||||||
|
|
@ -1702,9 +1705,15 @@ class AIAgent:
|
||||||
}
|
}
|
||||||
if _provider_timeout is not None:
|
if _provider_timeout is not None:
|
||||||
client_kwargs["timeout"] = _provider_timeout
|
client_kwargs["timeout"] = _provider_timeout
|
||||||
# Preserve any default_headers the router set
|
# Preserve provider-specific headers the router set. The
|
||||||
if hasattr(_routed_client, '_default_headers') and _routed_client._default_headers:
|
# OpenAI SDK stores caller-provided default_headers in
|
||||||
client_kwargs["default_headers"] = dict(_routed_client._default_headers)
|
# _custom_headers; older/mocked clients may expose
|
||||||
|
# _default_headers instead.
|
||||||
|
_routed_headers = getattr(_routed_client, "_custom_headers", None)
|
||||||
|
if not _routed_headers:
|
||||||
|
_routed_headers = getattr(_routed_client, "_default_headers", None)
|
||||||
|
if _routed_headers:
|
||||||
|
client_kwargs["default_headers"] = dict(_routed_headers)
|
||||||
else:
|
else:
|
||||||
# When the user explicitly chose a non-OpenRouter provider
|
# When the user explicitly chose a non-OpenRouter provider
|
||||||
# but no credentials were found, fail fast with a clear
|
# but no credentials were found, fail fast with a clear
|
||||||
|
|
@ -1753,8 +1762,11 @@ class AIAgent:
|
||||||
}
|
}
|
||||||
if _provider_timeout is not None:
|
if _provider_timeout is not None:
|
||||||
client_kwargs["timeout"] = _provider_timeout
|
client_kwargs["timeout"] = _provider_timeout
|
||||||
if hasattr(_fb_client, "_default_headers") and _fb_client._default_headers:
|
_fb_headers = getattr(_fb_client, "_custom_headers", None)
|
||||||
client_kwargs["default_headers"] = dict(_fb_client._default_headers)
|
if not _fb_headers:
|
||||||
|
_fb_headers = getattr(_fb_client, "_default_headers", None)
|
||||||
|
if _fb_headers:
|
||||||
|
client_kwargs["default_headers"] = dict(_fb_headers)
|
||||||
_fb_resolved = True
|
_fb_resolved = True
|
||||||
break
|
break
|
||||||
if not _fb_resolved:
|
if not _fb_resolved:
|
||||||
|
|
@ -7334,12 +7346,18 @@ class AIAgent:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _apply_client_headers_for_base_url(self, base_url: str) -> None:
|
def _apply_client_headers_for_base_url(self, base_url: str) -> None:
|
||||||
from agent.auxiliary_client import _AI_GATEWAY_HEADERS, build_or_headers
|
from agent.auxiliary_client import (
|
||||||
|
_AI_GATEWAY_HEADERS,
|
||||||
|
build_nvidia_nim_headers,
|
||||||
|
build_or_headers,
|
||||||
|
)
|
||||||
|
|
||||||
if base_url_host_matches(base_url, "openrouter.ai"):
|
if base_url_host_matches(base_url, "openrouter.ai"):
|
||||||
self._client_kwargs["default_headers"] = build_or_headers()
|
self._client_kwargs["default_headers"] = build_or_headers()
|
||||||
elif base_url_host_matches(base_url, "ai-gateway.vercel.sh"):
|
elif base_url_host_matches(base_url, "ai-gateway.vercel.sh"):
|
||||||
self._client_kwargs["default_headers"] = dict(_AI_GATEWAY_HEADERS)
|
self._client_kwargs["default_headers"] = dict(_AI_GATEWAY_HEADERS)
|
||||||
|
elif base_url_host_matches(base_url, "integrate.api.nvidia.com"):
|
||||||
|
self._client_kwargs["default_headers"] = build_nvidia_nim_headers(base_url)
|
||||||
elif base_url_host_matches(base_url, "api.routermint.com"):
|
elif base_url_host_matches(base_url, "api.routermint.com"):
|
||||||
self._client_kwargs["default_headers"] = _routermint_headers()
|
self._client_kwargs["default_headers"] = _routermint_headers()
|
||||||
elif base_url_host_matches(base_url, "api.githubcopilot.com"):
|
elif base_url_host_matches(base_url, "api.githubcopilot.com"):
|
||||||
|
|
|
||||||
|
|
@ -2415,10 +2415,51 @@ def _clean_env(monkeypatch):
|
||||||
"""Strip provider env vars so each test starts clean."""
|
"""Strip provider env vars so each test starts clean."""
|
||||||
for key in (
|
for key in (
|
||||||
"OPENROUTER_API_KEY", "OPENAI_BASE_URL", "OPENAI_API_KEY",
|
"OPENROUTER_API_KEY", "OPENAI_BASE_URL", "OPENAI_API_KEY",
|
||||||
|
"NVIDIA_API_KEY", "NVIDIA_BASE_URL",
|
||||||
):
|
):
|
||||||
monkeypatch.delenv(key, raising=False)
|
monkeypatch.delenv(key, raising=False)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNvidiaBillingHeaders:
|
||||||
|
"""NVIDIA NIM billing-origin headers are scoped to NVIDIA cloud."""
|
||||||
|
|
||||||
|
def test_resolve_provider_client_cloud_adds_billing_origin_header(self, monkeypatch):
|
||||||
|
monkeypatch.setenv("NVIDIA_API_KEY", "nvidia-key")
|
||||||
|
monkeypatch.delenv("NVIDIA_BASE_URL", raising=False)
|
||||||
|
mock_openai = MagicMock()
|
||||||
|
mock_openai.return_value = MagicMock(name="nvidia-client")
|
||||||
|
|
||||||
|
with patch("agent.auxiliary_client.OpenAI", mock_openai):
|
||||||
|
client, model = resolve_provider_client(
|
||||||
|
provider="nvidia",
|
||||||
|
model="nvidia/test-model",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert client is not None
|
||||||
|
assert model == "nvidia/test-model"
|
||||||
|
call_kwargs = mock_openai.call_args[1]
|
||||||
|
headers = call_kwargs["default_headers"]
|
||||||
|
assert headers["X-BILLING-INVOKE-ORIGIN"] == "HermesAgent"
|
||||||
|
|
||||||
|
def test_resolve_provider_client_local_nim_skips_billing_origin_header(self, monkeypatch):
|
||||||
|
monkeypatch.setenv("NVIDIA_API_KEY", "nvidia-key")
|
||||||
|
monkeypatch.setenv("NVIDIA_BASE_URL", "http://localhost:8000/v1")
|
||||||
|
mock_openai = MagicMock()
|
||||||
|
mock_openai.return_value = MagicMock(name="nvidia-local-client")
|
||||||
|
|
||||||
|
with patch("agent.auxiliary_client.OpenAI", mock_openai):
|
||||||
|
client, model = resolve_provider_client(
|
||||||
|
provider="nvidia",
|
||||||
|
model="nvidia/test-model",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert client is not None
|
||||||
|
assert model == "nvidia/test-model"
|
||||||
|
call_kwargs = mock_openai.call_args[1]
|
||||||
|
headers = call_kwargs.get("default_headers", {})
|
||||||
|
assert "X-BILLING-INVOKE-ORIGIN" not in headers
|
||||||
|
|
||||||
|
|
||||||
class TestOpenRouterExplicitApiKey:
|
class TestOpenRouterExplicitApiKey:
|
||||||
"""Test that explicit_api_key is correctly propagated to _try_openrouter()."""
|
"""Test that explicit_api_key is correctly propagated to _try_openrouter()."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@ class TestNvidiaProfile:
|
||||||
p = get_provider_profile("nvidia")
|
p = get_provider_profile("nvidia")
|
||||||
assert "nvidia.com" in p.base_url
|
assert "nvidia.com" in p.base_url
|
||||||
|
|
||||||
|
def test_billing_header_not_profile_wide(self):
|
||||||
|
p = get_provider_profile("nvidia")
|
||||||
|
assert p.default_headers == {}
|
||||||
|
|
||||||
|
|
||||||
class TestKimiProfile:
|
class TestKimiProfile:
|
||||||
def test_temperature_omit(self):
|
def test_temperature_omit(self):
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
Mirrors the OpenRouter pattern for the Vercel AI Gateway so that
|
Mirrors the OpenRouter pattern for the Vercel AI Gateway so that
|
||||||
referrerUrl / appName / User-Agent flow into gateway analytics.
|
referrerUrl / appName / User-Agent flow into gateway analytics.
|
||||||
"""
|
"""
|
||||||
|
from types import SimpleNamespace
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from run_agent import AIAgent
|
from run_agent import AIAgent
|
||||||
|
|
@ -65,6 +66,73 @@ def test_routermint_base_url_applies_user_agent_header(mock_openai):
|
||||||
assert headers["User-Agent"].startswith("HermesAgent/")
|
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_nvidia_local_base_url_does_not_apply_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,
|
||||||
|
)
|
||||||
|
agent._client_kwargs["default_headers"] = {
|
||||||
|
"X-BILLING-INVOKE-ORIGIN": "HermesAgent",
|
||||||
|
}
|
||||||
|
|
||||||
|
agent._apply_client_headers_for_base_url("http://localhost:8000/v1")
|
||||||
|
|
||||||
|
assert "default_headers" not in agent._client_kwargs
|
||||||
|
|
||||||
|
|
||||||
|
@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")
|
@patch("run_agent.OpenAI")
|
||||||
def test_gmi_base_url_picks_up_profile_user_agent(mock_openai):
|
def test_gmi_base_url_picks_up_profile_user_agent(mock_openai):
|
||||||
"""GMI declares User-Agent on its ProviderProfile.default_headers.
|
"""GMI declares User-Agent on its ProviderProfile.default_headers.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue