# NousResearch/hermes-agent · OpenRouter credential pool exhaustion and env var fallback

## Open now

- 1 issue, 1 PR open on GitHub.
- #48829: \[Bug\]: OpenRouter fallback can select stale pool credential before OPENROUTER_API_KEY
- PR #48395: fix: prefer Hermes env key for OpenRouter fallback

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.

## TL;DR

- 8 member(s): 1 open issue(s), 1 closed issue(s), 1 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #53906. Verify it addresses your case.
- **Recommended:** read PR #53906 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-53906`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.23`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #48829: \[Bug\]: OpenRouter fallback can select stale pool credential before OPENROUTER_API_KEY

## Closed / duplicate issues

- #23452: Bug: _try_openrouter() returns (None,None) when credential pool is exhausted, blocking env var fallback

## Prior & competing PRs

- **#24720: fix(auxiliary): fallback to OPENROUTER_API_KEY env var when credential pool is exhausted**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `c56b39c11e814ee571e391fc46384b0e7888a490`, `#53906`, `#23495`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#53906: fix(auxiliary): fall back to OPENROUTER_API_KEY when credential pool exhausted**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/auxiliary_client.py`, `tests/agent/test_auxiliary_client.py`
  - fix hunks:
  - `agent/auxiliary_client.py`

    ```diff
    @@ -1669,13 +1669,14 @@ def _try_openrouter(explicit_api_key: str = None, model: str = None) -> Tuple[Op
         pool_present, entry = _select_pool_entry("openrouter")
         if pool_present:
             or_key = explicit_api_key or _pool_runtime_api_key(entry)
    -        if not or_key:
    -            _mark_provider_unhealthy("openrouter", ttl=60)
    -            return None, None
    -        base_url = _pool_runtime_base_url(entry, OPENROUTER_BASE_URL) or OPENROUTER_BASE_URL
    -        logger.debug("Auxiliary client: OpenRouter via pool")
    -        return OpenAI(api_key=[redacted:secret-assignment] base_url=base_url,
    -                       default_headers=build_or_headers()), model or _OPENROUTER_MODEL
    +        if or_key:
    +            base_url = _pool_runtime_base_url(entry, OPENROUTER_BASE_URL) or OPENROUTER_BASE_URL
    +            logger.debug("Auxiliary client: OpenRouter via pool")
    +            return OpenAI(api_key=[redacted:secret-assignment] base_url=base_url,
    +                           default_headers=build_or_headers()), model or _OPENROUTER_MODEL
    +        # Pool exists but is exhausted (no usable runtime key) — fall through to
    +        # the OPENROUTER_API_KEY env-var path rather than failing outright.
    +        logger.debug("Auxiliary client: OpenRouter pool exhausted, trying OPENROUTER_API_KEY")
     
         or_key = explicit_api_key or os.getenv("OPENROUTER_API_KEY")
         if not or_key:
    ```
  - `tests/agent/test_auxiliary_client.py`

    ```diff
    @@ -27,6 +27,9 @@
         _refresh_nous_recommended_model,
         _normalize_aux_provider,
         _try_payment_fallback,
    +    _try_openrouter,
    +    _OPENROUTER_MODEL,
    +    OPENROUTER_BASE_URL,
         _resolve_auto,
         _resolve_xai_oauth_for_aux,
         _CodexCompletionsAdapter,
    ```

    ```diff
    @@ -919,6 +922,35 @@ def test_explicit_openrouter_missing_env_keeps_not_set_warning(self, monkeypatch
                 for record in caplog.records
             )
     
    +    def test_try_openrouter_pool_exhausted_falls_back_to_env(self, monkeypatch):
    +        """Pool present but exhausted → fall through to OPENROUTER_API_KEY env var."""
    +        monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-env-fallback")
    +        with patch("agent.auxiliary_client._select_pool_entry", return_value=(True, None)), \
    +             patch("agent.auxiliary_client.OpenAI") as mock_openai:
    +            mock_client = MagicMock(name="openrouter_client")
    +            mock_openai.return_value = mock_client
    +
    +            client, model = _try_openrouter()
    +
    +        assert client is mock_client
    +        assert model == _OPENROUTER_MODEL
    +        mock_openai.assert_called_once()
    +        assert mock_openai.call_args.kwargs["api_key"] == "sk-or-env-fallback"
    +        assert mock_openai.call_args.kwargs["base_url"] == OPENROUTER_BASE_URL
    +
    +    def test_try_openrouter_pool_exhausted_no_env_marks_unhealthy(self, monkeypatch):
    +        """Pool exhausted AND no env var → final failure marks provider unhealthy."""
    +        monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
    +        with patch("agent.auxiliary_client._select_pool_entry", return_value=(True, None)), \
    +             patch("agent.auxiliary_client._mark_provider_unhealthy") as mock_mark, \
    +             patch("agent.auxiliary_client.OpenAI") as mock_openai:
    +            client, model = _try_openrouter()
    +
    +        assert client is None
    +        assert model is None
    +        mock_openai.assert_not_called()
    +        mock_mark.assert_called_once_with("openrouter", ttl=60)
    +
     class TestGetTextAuxiliaryClient:
         """Test the full resolution chain for get_text_auxiliary_client."""
     
    ```
- **#23495: fix(auxiliary): fall back to env when openrouter pool is exhausted**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `c56b39c11e814ee571e391fc46384b0e7888a490`, `#53906`
- **#23903: fix: fall through to OPENROUTER_API_KEY env var when credential pool is exhausted (#23452)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#23495`, `#23905`, `#23904`
- **#48395: fix: prefer Hermes env key for OpenRouter fallback**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `agent/auxiliary_client.py`, `tests/agent/test_auxiliary_client.py`
- **#48895: fix(agent): prefer OPENROUTER_API_KEY env over stale pool entries (#48829)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`

## Fix status

- Merged to main in PR #53906 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-28`.
- Subsystem: `agent`
