{"repo":"NousResearch/hermes-agent","public_id":"10149","display_label":"Auxiliary LLM provider fallback and warning fixes","summary":"These issues and PRs address bugs around missing auxiliary LLM provider configuration, improving warning messages and adding fallback behavior to OPENROUTER_API_KEY when the credential pool is missing or exhausted.","subsystem":"agent","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":53906,"merged_at":"2026-06-28T02:09:27+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-28T02:09:27+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"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":10149,"title":"[Bug]: No auxiliary LLM provider configured","url":null,"state":"closed"},{"kind":"pr","number":10655,"title":"fix(auxiliary): fall back to OPENROUTER_API_KEY when pool entry is missing","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":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":36940,"title":"fix(auxiliary): improve \"no auxiliary LLM provider\" warning messages (#10149)","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"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}}