{"repo":"NousResearch/hermes-agent","public_id":"61099","display_label":"Agent provider headers missing or incorrect intermittently","summary":"These issues and fixes relate to HTTP provider headers (such as OpenRouter HTTP-Referer) not being correctly set or reapplied for agent requests, particularly after model switches.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":61732,"merged_at":"2026-07-10T01:23:11+00:00"},"resolution":{"state":"resolved","by":"pr-61732"},"freshness":{"state":"verified","last_verified_at":"2026-07-10T01:23:11+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":61099,"pr_number":61732,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-10T01:23:11+00:00"}],"members":[{"kind":"issue","number":61099,"title":"[Bug]: OpenRouter logs show 'Unknown' App for Hermes Agent requests intermittently","url":null,"state":"closed"},{"kind":"pr","number":61347,"title":"fix(agent): reapply provider headers after model switch (#61099)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#61732","number":61732}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":61732,"title":"fix(agent): reapply provider headers after model switch (#61099)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/agent_runtime_helpers.py","scripts/release.py","tests/run_agent/test_switch_model_reapplies_headers.py"],"fix_hunks":[{"file":"agent/agent_runtime_helpers.py","hunks":["@@ -1953,6 +1953,11 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo\n             _sm_timeout = get_provider_request_timeout(agent.provider, agent.model)\n             if _sm_timeout is not None:\n                 agent._client_kwargs[\"timeout\"] = _sm_timeout\n+            # Reapply provider-specific headers (e.g. OpenRouter HTTP-Referer,\n+            # X-Title) that were lost when _client_kwargs was rebuilt from\n+            # scratch.  Without this, model switches clear attribution headers\n+            # and OpenRouter logs show \"Unknown\" for subsequent requests.\n+            agent._apply_client_headers_for_base_url(effective_base)\n             agent.client = agent._create_openai_client(\n                 dict(agent._client_kwargs),\n                 reason=\"switch_model\","]},{"file":"scripts/release.py","hunks":["@@ -55,6 +55,7 @@\n     \"[redacted:email]\": \"isheng-eqi\",  # PR #59428 salvage (cron: reject past one-shot timestamps in update_job fallback + resume_job; #59395). Also PR #59446 salvage (cron: advance one-shot next_run_at before dispatch so concurrent gateway+desktop schedulers can't double-execute; #59229).\n     \"[redacted:email]\": \"derek2000139\",  # PR #57838 salvage (desktop/windows: pre-write update marker before quit dwell so the renderer's waitForUpdateToFinish gate parks instead of respawning a backend that re-locks venv .pyd files mid-update)\n     \"AndreasHiltner@users.noreply.github.com\": \"AndreasHiltner\",  # PR #56854 salvage (gateway: route multiplex profile responses through the profile's own adapter — 53-site _adapter_for_source sweep)\n+    \"AlexFucuson9@users.noreply.github.com\": \"AlexFucuson9\",  # PR #61347 salvage (agent: reapply provider headers after model switch; #61099)\n     \"allenliang2022@users.noreply.github.com\": \"allenliang2022\",  # PR #56932 test coverage folded into #56909 salvage (408 → retryable timeout)\n     \"[redacted:email]\": \"ManniBr\",  # PR #57417 partial salvage (gateway: fail-closed adapter resolution for unregistered secondary profiles)\n     \"[redacted:email]\": \"Umi4Life\",  # PR #47377 salvage (agent: emit one-shot fallback switch notice on successful fallback so gateway users see model/provider change; #35419)"]},{"file":"tests/run_agent/test_switch_model_reapplies_headers.py","hunks":["@@ -0,0 +1,100 @@\n+\"\"\"Regression tests for #61099: switch_model must reapply provider-specific\n+default headers when it rebuilds _client_kwargs from scratch.\n+\n+Without _apply_client_headers_for_base_url() in the rebuild path, a /model\n+switch drops OpenRouter attribution headers (HTTP-Referer / X-Title → logs\n+show \"Unknown\") and, worse, functional headers like Kimi's User-Agent\n+sentinel (403 without it).\n+\"\"\"\n+\n+from unittest.mock import MagicMock, patch\n+\n+from run_agent import AIAgent\n+from agent.context_compressor import ContextCompressor\n+\n+\n+def _make_agent(provider=\"copilot\", base_url=\"https://api.githubcopilot.com\") -> AIAgent:\n+    \"\"\"Minimal AIAgent with a context_compressor, skipping __init__.\"\"\"\n+    agent = AIAgent.__new__(AIAgent)\n+\n+    agent.model = \"claude-opus-4.8\"\n+    agent.provider = provider\n+    agent.base_url = base_url\n+    agent.api_key = \"[redacted:secret-assignment]\"\n+    agent.api_mode = \"chat_completions\"\n+    agent.client = MagicMock()\n+    agent.quiet_mode = True\n+    agent._config_context_length = None\n+    agent._client_kwargs = {\"api_key\": \"[redacted:secret-assignment]\", \"base_url\": base_url}\n+\n+    compressor = ContextCompressor(\n+        model=agent.model,\n+        threshold_percent=0.50,\n+        base_url=base_url,\n+        api_key=\"[redacted:secret-assignment]\",\n+        provider=provider,\n+        quiet_mode=True,\n+        config_context_length=None,\n+    )\n+    agent.context_compressor = compressor\n+    agent._primary_runtime = {}\n+\n+    return agent\n+\n+\n+@patch(\"agent.model_metadata.get_model_context_length\", return_value=131_072)\n+def test_switch_to_openrouter_reapplies_attribution_headers(mock_ctx_len):\n+    \"\"\"Switching to an openrouter.ai base_url must attach the OpenRouter\n+    attribution headers (HTTP-Referer / X-Title) to the rebuilt client\n+    kwargs — not ship a bare api_key+base_url client (#61099).\"\"\"\n+    agent = _make_agent(provider=\"copilot\", base_url=\"https://api.githubcopilot.com\")\n+\n+    agent.switch_model(\n+        \"deepseek/deepseek-chat\",\n+        \"openrouter\",\n+        api_key=\"[redacted:secret-assignment]\",\n+        base_url=\"https://openrouter.ai/api/v1\",\n+    )\n+\n+    headers = agent._client_kwargs.get(\"default_headers\") or {}\n+    assert \"HTTP-Referer\" in headers\n+    assert headers.get(\"X-Title\")\n+\n+\n+@patch(\"agent.model_metadata.get_model_context_length\", return_value=131_072)\n+def test_switch_to_kimi_reapplies_user_agent_sentinel(mock_ctx_len):\n+    \"\"\"Kimi requires a User-Agent sentinel; a switch to api.kimi.com must\n+    carry it or every request 403s.\"\"\"\n+    agent = _make_agent(provider=\"openrouter\", base_url=\"https://openrouter.ai/api/v1\")\n+\n+    agent.switch_model(\n+        \"kimi-k2\",\n+        \"kimi\",\n+        api_key=\"[redacted:secret-assignment]\",\n+        base_url=\"https://api.kimi.com/v1\",\n+    )\n+\n+    headers = agent._client_kwargs.get(\"default_headers\") or {}\n+    assert headers.get(\"User-Agent\", \"\").startswith(\"claude-code/\")\n+\n+\n+@patch(\"agent.model_metadata.get_model_context_length\", return_value=131_072)\n+def test_switch_away_from_headered_provider_clears_stale_headers(mock_ctx_len):\n+    \"\"\"Switching FROM a headered provider TO one with no URL-specific headers\n+    must not carry the old provider's headers along.\"\"\"\n+    agent = _make_agent(provider=\"openrouter\", base_url=\"https://openrouter.ai/api/v1\")\n+    agent._client_kwargs[\"default_headers\"] = {\n+        \"HTTP-Referer\": \"https://hermes-agent.nousresearch.com\",\n+        \"X-Title\": \"Hermes Agent\",\n+    }\n+\n+    agent.switch_model(\n+        \"MiniMax-M3\",\n+        \"custom:minimax\",\n+        api_key=\"[redacted:secret-assignment]\",\n+        base_url=\"https://api.minimax.io/v1\",\n+    )\n+\n+    headers = agent._client_kwargs.get(\"default_headers\") or {}\n+    assert \"HTTP-Referer\" not in headers\n+    assert \"X-Title\" not in headers\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}