mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
* remove Vercel AI Gateway provider and Vercel Sandbox terminal backend Both Vercel-hosted integrations are removed end-to-end. Users on the AI Gateway should switch to OpenRouter or one of the other aggregators (Nous Portal, Kilo Code). Users on the Vercel Sandbox backend should switch to Docker, Modal, Daytona, or SSH. What's removed: - `plugins/model-providers/ai-gateway/` provider plugin - `hermes_cli/vercel_auth.py` Vercel-Sandbox auth helper - `tools/environments/vercel_sandbox.py` terminal backend - `ai-gateway` provider wiring across auth, doctor, setup, models, config, status, providers, main, web_server, model_normalize, dump - `vercel_sandbox` backend wiring across terminal_tool, file_tools, code_execution_tool, file_operations, approval, skills_tool, environments/local, credential_files, lazy_deps, prompt_builder, cli, gateway/run - `AI_GATEWAY_BASE_URL` constant, `_AI_GATEWAY_HEADERS` auxiliary-client header set, run_agent base-URL header/reasoning special-cases - `[vercel]` pyproject extra and `vercel`/`vercel-workers` from uv.lock - env vars: `AI_GATEWAY_API_KEY`, `AI_GATEWAY_BASE_URL`, `VERCEL_TOKEN`, `VERCEL_PROJECT_ID`, `VERCEL_TEAM_ID`, `VERCEL_OIDC_TOKEN`, `TERMINAL_VERCEL_RUNTIME` - Tests: deletes test_ai_gateway_models.py and test_vercel_sandbox_environment.py; scrubs references across 23 surviving test files (no entire tests deleted unless they were dedicated to AI Gateway / Sandbox) - Docs: provider tables, env-var reference, setup guides, security notes, tool config, terminal-backend tables — English plus zh-Hans i18n parity - `hermes-agent` skill: provider table entry and remote-backend list What stays (intentional): - `popular-web-designs/templates/vercel.md` — CSS design reference, unrelated to Vercel-the-AI-product - `x-vercel-id` in `stream_diag.py` headers — generic Vercel CDN response header, useful diag signal on any Vercel-hosted endpoint - `vercel-labs/agent-browser` URL in browser config — lightpanda browser project, different OSS effort - `userStories.json` historical contributor entry mentioning Vercel Sandbox — archive, not active docs Validation: - 1153 tests in the 22 targeted files pass (`scripts/run_tests.sh`) - Full repo `py_compile` clean - Live import of every touched module + invariant check (no `ai-gateway` in `PROVIDER_REGISTRY`, no `_AI_GATEWAY_HEADERS`, no `vercel_sandbox` in `_REMOTE_TERMINAL_BACKENDS`) * test: convert profile-count check from change-detector to invariant The hardcoded "== 34" assertion broke when ai-gateway was removed. Per AGENTS.md change-detector-test guidance, assert the relationship (registry count >= number of plugin dirs) instead of a literal count. Counts shift when providers are added/removed; that's expected.
200 lines
6.5 KiB
Python
200 lines
6.5 KiB
Python
"""Attribution default_headers applied per provider via base-URL detection."""
|
|
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_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_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")
|
|
def test_gmi_base_url_picks_up_profile_user_agent(mock_openai):
|
|
"""GMI declares User-Agent on its ProviderProfile.default_headers.
|
|
|
|
The ``_apply_client_headers_for_base_url`` else-branch looks up the
|
|
provider profile and applies its default_headers, so no GMI-specific
|
|
branch is needed in run_agent.
|
|
"""
|
|
mock_openai.return_value = MagicMock()
|
|
agent = AIAgent(
|
|
api_key="test-key",
|
|
base_url="https://api.gmi-serving.com/v1",
|
|
model="test/model",
|
|
provider="gmi",
|
|
quiet_mode=True,
|
|
skip_context_files=True,
|
|
skip_memory=True,
|
|
)
|
|
|
|
agent._apply_client_headers_for_base_url("https://api.gmi-serving.com/v1")
|
|
|
|
headers = agent._client_kwargs["default_headers"]
|
|
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
|
|
|
|
|
|
@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},
|
|
}):
|
|
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"
|
|
|
|
|
|
@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},
|
|
}):
|
|
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
|