{"repo":"NousResearch/hermes-agent","public_id":"18338","display_label":"explicit_api_key not forwarded to _try_openrouter","summary":"A bug where resolve_provider_client fails to pass explicit_api_key when calling _try_openrouter, and multiple fix attempts addressing this propagation gap.","subsystem":null,"counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":18768,"merged_at":"2026-05-02T09:27:50+00:00"},"resolution":{"state":"resolved","by":"pr-18768"},"freshness":{"state":"verified","last_verified_at":"2026-05-02T09:27:50+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":18338,"pr_number":18768,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-02T09:27:50+00:00"}],"members":[{"kind":"issue","number":18338,"title":"[Bug]: resolve_provider_client ignores explicit_api_key when calling _try_openrouter()","url":null,"state":"closed"},{"kind":"pr","number":18341,"title":"fix(auxiliary): propagate explicit_api_key to _try_openrouter()","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#18768","number":18768}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":18618,"title":"fix(auxiliary): forward explicit_api_key to _try_openrouter for credential pool fallback","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#18768","number":18768},{"kind":"pr","value":"#18341","number":18341}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":18768,"title":"fix(auxiliary): propagate explicit_api_key to _try_openrouter (salvage #18341)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/auxiliary_client.py","tests/agent/test_auxiliary_client.py"],"fix_hunks":[{"file":"agent/auxiliary_client.py","hunks":["@@ -1149,10 +1149,10 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:\n \n \n \n-def _try_openrouter() -> Tuple[Optional[OpenAI], Optional[str]]:\n+def _try_openrouter(explicit_api_key: str = None) -> Tuple[Optional[OpenAI], Optional[str]]:\n     pool_present, entry = _select_pool_entry(\"openrouter\")\n     if pool_present:\n-        or_key = _pool_runtime_api_key(entry)\n+        or_key = explicit_api_key or _pool_runtime_api_key(entry)\n         if not or_key:\n             return None, None\n         base_url = _pool_runtime_base_url(entry, OPENROUTER_BASE_URL) or OPENROUTER_BASE_URL","@@ -1160,7 +1160,7 @@ def _try_openrouter() -> Tuple[Optional[OpenAI], Optional[str]]:\n         return OpenAI(api_key=[redacted:secret-assignment] base_url=base_url,\n                        default_headers=_OR_HEADERS), _OPENROUTER_MODEL\n \n-    or_key = os.getenv(\"OPENROUTER_API_KEY\")\n+    or_key = explicit_api_key or os.getenv(\"OPENROUTER_API_KEY\")\n     if not or_key:\n         return None, None\n     logger.debug(\"Auxiliary client: OpenRouter\")","@@ -2053,9 +2053,9 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = \"\",\n         return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode\n                 else (client, final_model))\n \n-    # ── OpenRouter ───────────────────────────────────────────────────\n+    # ── OpenRouter ───────────────────────────────────────────\n     if provider == \"openrouter\":\n-        client, default = _try_openrouter()\n+        client, default = _try_openrouter(explicit_api_key=[redacted:secret-assignment]\n         if client is None:\n             logger.warning(\n                 \"resolve_provider_client: openrouter requested but %s\","]},{"file":"tests/agent/test_auxiliary_client.py","hunks":["@@ -1818,3 +1818,78 @@ def test_none_tools_unchanged(self):\n             provider=\"openai\", model=\"gpt-4o\", messages=[], tools=None,\n         )\n         assert \"tools\" not in kwargs\n+\n+\n[redacted:email](autouse=True)\n+def _clean_env(monkeypatch):\n+    \"\"\"Strip provider env vars so each test starts clean.\"\"\"\n+    for key in (\n+        \"OPENROUTER_API_KEY\", \"OPENAI_BASE_URL\", \"OPENAI_API_KEY\",\n+    ):\n+        monkeypatch.delenv(key, raising=False)\n+\n+\n+class TestOpenRouterExplicitApiKey:\n+    \"\"\"Test that explicit_api_key is correctly propagated to _try_openrouter().\"\"\"\n+\n+    def test_resolve_provider_client_passes_explicit_api_key_to_openrouter(\n+        self, monkeypatch\n+    ):\n+        \"\"\"\n+        When resolve_provider_client() is called with explicit_api_key for OpenRouter,\n+        the explicit key should be passed to the OpenAI client instead of falling back\n+        to OPENROUTER_API_KEY env var.\n+        \"\"\"\n+        # Set up env var as fallback (should NOT be used when explicit_api_key is provided)\n+        monkeypatch.setenv(\"OPENROUTER_API_KEY\", \"env-fallback-key\")\n+\n+        # Mock OpenAI to capture the api_key used\n+        mock_openai = MagicMock()\n+        mock_openai.return_value = MagicMock(name=\"openrouter-client\")\n+\n+        with patch(\"agent.auxiliary_client.OpenAI\", mock_openai):\n+            client, model = resolve_provider_client(\n+                provider=\"openrouter\",\n+                explicit_api_key=\"[redacted:secret-assignment]\",\n+            )\n+\n+            # Verify a client was created\n+            assert client is not None\n+            # Verify the explicit key was used, not the env var fallback\n+            mock_openai.assert_called_once()\n+            call_kwargs = mock_openai.call_args[1]\n+            assert call_kwargs[\"api_key\"] == \"explicit-pool-key\", (\n+                f\"Expected explicit_api_key to be passed, got: {call_kwargs['api_key']}\"\n+            )\n+            assert call_kwargs[\"api_key\"] != \"env-fallback-key\", (\n+                \"Should NOT fall back to OPENROUTER_API_KEY when explicit_api_key is provided\"\n+            )\n+\n+    def test_resolve_provider_client_without_explicit_api_key_falls_back_to_env(\n+        self, monkeypatch\n+    ):\n+        \"\"\"\n+        When resolve_provider_client() is called WITHOUT explicit_api_key for OpenRouter,\n+        it should fall back to OPENROUTER_API_KEY env var.\n+        \"\"\"\n+        # Set up env var as fallback (should be used when explicit_api_key is NOT provided)\n+        monkeypatch.setenv(\"OPENROUTER_API_KEY\", \"env-fallback-key\")\n+\n+        # Mock OpenAI to capture the api_key used\n+        mock_openai = MagicMock()\n+        mock_openai.return_value = MagicMock(name=\"openrouter-client\")\n+\n+        with patch(\"agent.auxiliary_client.OpenAI\", mock_openai):\n+            client, model = resolve_provider_client(\n+                provider=\"openrouter\",\n+                explicit_api_key=None,\n+            )\n+\n+            # Verify a client was created\n+            assert client is not None\n+            # Verify the env var fallback was used\n+            mock_openai.assert_called_once()\n+            call_kwargs = mock_openai.call_args[1]\n+            assert call_kwargs[\"api_key\"] == \"env-fallback-key\", (\n+                f\"Expected env fallback key to be used when explicit_api_key is None, got: {call_kwargs['api_key']}\"\n+            )\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}