{"repo":"NousResearch/hermes-agent","public_id":"23452","display_label":"OpenRouter credential pool exhaustion and env var fallback","summary":"These issues and pull requests concern a bug where exhausted or stale OpenRouter credential pools block fallback to the OPENROUTER_API_KEY environment variable. Multiple fix attempts address the fallback priority logic in the OpenRouter credential selection path.","subsystem":"agent","counts":{"members":8,"open_issues":1,"open_issues_distinct":1,"open_prs":1,"closed_issues":1,"merged_prs":1,"attempt_prs":5},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":53906,"merged_at":"2026-06-28T02:09:27+00:00"},"resolution":{"state":"partially_resolved","by":"pr-53906"},"freshness":{"state":"verified","last_verified_at":"2026-06-28T02:09:27+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.22714285714285712},"fix_edges":[{"issue_number":10149,"pr_number":53906,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T02:09:27+00:00"},{"issue_number":23452,"pr_number":53906,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-28T02:09:27+00:00"}],"members":[{"kind":"issue","number":23452,"title":"Bug: _try_openrouter() returns (None,None) when credential pool is exhausted, blocking env var fallback","url":null,"state":"closed"},{"kind":"issue","number":48829,"title":"[Bug]: OpenRouter fallback can select stale pool credential before OPENROUTER_API_KEY","url":null,"state":"open"},{"kind":"pr","number":23495,"title":"fix(auxiliary): fall back to env when openrouter pool is exhausted","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"c56b39c11e814ee571e391fc46384b0e7888a490","number":0},{"kind":"pr","value":"#53906","number":53906}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":23903,"title":"fix: fall through to OPENROUTER_API_KEY env var when credential pool is exhausted (#23452)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#23495","number":23495},{"kind":"pr","value":"#23905","number":23905},{"kind":"pr","value":"#23904","number":23904}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":24720,"title":"fix(auxiliary): fallback to OPENROUTER_API_KEY env var when credential pool is exhausted","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"c56b39c11e814ee571e391fc46384b0e7888a490","number":0},{"kind":"pr","value":"#53906","number":53906},{"kind":"pr","value":"#23495","number":23495}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":48395,"title":"fix: prefer Hermes env key for OpenRouter fallback","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":48895,"title":"fix(agent): prefer OPENROUTER_API_KEY env over stale pool entries (#48829)","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":53906,"title":"fix(auxiliary): fall back to OPENROUTER_API_KEY when credential pool exhausted","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":["@@ -1669,13 +1669,14 @@ def _try_openrouter(explicit_api_key: str = None, model: str = None) -> Tuple[Op\n     pool_present, entry = _select_pool_entry(\"openrouter\")\n     if pool_present:\n         or_key = explicit_api_key or _pool_runtime_api_key(entry)\n-        if not or_key:\n-            _mark_provider_unhealthy(\"openrouter\", ttl=60)\n-            return None, None\n-        base_url = _pool_runtime_base_url(entry, OPENROUTER_BASE_URL) or OPENROUTER_BASE_URL\n-        logger.debug(\"Auxiliary client: OpenRouter via pool\")\n-        return OpenAI(api_key=[redacted:secret-assignment] base_url=base_url,\n-                       default_headers=build_or_headers()), model or _OPENROUTER_MODEL\n+        if or_key:\n+            base_url = _pool_runtime_base_url(entry, OPENROUTER_BASE_URL) or OPENROUTER_BASE_URL\n+            logger.debug(\"Auxiliary client: OpenRouter via pool\")\n+            return OpenAI(api_key=[redacted:secret-assignment] base_url=base_url,\n+                           default_headers=build_or_headers()), model or _OPENROUTER_MODEL\n+        # Pool exists but is exhausted (no usable runtime key) — fall through to\n+        # the OPENROUTER_API_KEY env-var path rather than failing outright.\n+        logger.debug(\"Auxiliary client: OpenRouter pool exhausted, trying OPENROUTER_API_KEY\")\n \n     or_key = explicit_api_key or os.getenv(\"OPENROUTER_API_KEY\")\n     if not or_key:"]},{"file":"tests/agent/test_auxiliary_client.py","hunks":["@@ -27,6 +27,9 @@\n     _refresh_nous_recommended_model,\n     _normalize_aux_provider,\n     _try_payment_fallback,\n+    _try_openrouter,\n+    _OPENROUTER_MODEL,\n+    OPENROUTER_BASE_URL,\n     _resolve_auto,\n     _resolve_xai_oauth_for_aux,\n     _CodexCompletionsAdapter,","@@ -919,6 +922,35 @@ def test_explicit_openrouter_missing_env_keeps_not_set_warning(self, monkeypatch\n             for record in caplog.records\n         )\n \n+    def test_try_openrouter_pool_exhausted_falls_back_to_env(self, monkeypatch):\n+        \"\"\"Pool present but exhausted → fall through to OPENROUTER_API_KEY env var.\"\"\"\n+        monkeypatch.setenv(\"OPENROUTER_API_KEY\", \"sk-or-env-fallback\")\n+        with patch(\"agent.auxiliary_client._select_pool_entry\", return_value=(True, None)), \\\n+             patch(\"agent.auxiliary_client.OpenAI\") as mock_openai:\n+            mock_client = MagicMock(name=\"openrouter_client\")\n+            mock_openai.return_value = mock_client\n+\n+            client, model = _try_openrouter()\n+\n+        assert client is mock_client\n+        assert model == _OPENROUTER_MODEL\n+        mock_openai.assert_called_once()\n+        assert mock_openai.call_args.kwargs[\"api_key\"] == \"sk-or-env-fallback\"\n+        assert mock_openai.call_args.kwargs[\"base_url\"] == OPENROUTER_BASE_URL\n+\n+    def test_try_openrouter_pool_exhausted_no_env_marks_unhealthy(self, monkeypatch):\n+        \"\"\"Pool exhausted AND no env var → final failure marks provider unhealthy.\"\"\"\n+        monkeypatch.delenv(\"OPENROUTER_API_KEY\", raising=False)\n+        with patch(\"agent.auxiliary_client._select_pool_entry\", return_value=(True, None)), \\\n+             patch(\"agent.auxiliary_client._mark_provider_unhealthy\") as mock_mark, \\\n+             patch(\"agent.auxiliary_client.OpenAI\") as mock_openai:\n+            client, model = _try_openrouter()\n+\n+        assert client is None\n+        assert model is None\n+        mock_openai.assert_not_called()\n+        mock_mark.assert_called_once_with(\"openrouter\", ttl=60)\n+\n class TestGetTextAuxiliaryClient:\n     \"\"\"Test the full resolution chain for get_text_auxiliary_client.\"\"\"\n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}