# NousResearch/hermes-agent · Init-time fallback when credential pool exhausted

## Open now

- Nothing open on GitHub.

These items address a bug where exhausting a single-key credential pool at initialization raises a RuntimeError instead of attempting fallback providers. The fix ensures the fallback chain is tried during init when the primary pool is rate-limited or exhausted.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 2 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #100135. Verify it addresses your case.
- **Recommended:** read PR #100135 (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: `resolved` · deciding PR(s): `pr-18762`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: 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.

## Closed / duplicate issues

- #17929: Single-key credential pool: rate-limit cooldown causes init-time RuntimeError, fallback chain never tried

## Prior & competing PRs

- **#18762: fix(agent): try fallback providers at init when primary credential pool is exhausted (salvage #17958)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `run_agent.py`, `tests/run_agent/test_init_fallback_on_exhausted_pool.py`
  - fix hunks:
  - `run_agent.py`

    ```diff
    @@ -1473,17 +1473,49 @@ def __init__(
                                     _env_hint = _pcfg.api_key_env_vars[0]
                             except Exception:
                                 pass
    +                        # --- Init-time fallback (#17929) ---
    +                        _fb_entries = []
    +                        if isinstance(fallback_model, list):
    +                            _fb_entries = [
    +                                f for f in fallback_model
    +                                if isinstance(f, dict) and f.get("provider") and f.get("model")
    +                            ]
    +                        elif isinstance(fallback_model, dict) and fallback_model.get("provider") and fallback_model.get("model"):
    +                            _fb_entries = [fallback_model]
    +                        _fb_resolved = False
    +                        for _fb in _fb_entries:
    +                            _fb_client, _fb_model = resolve_provider_client(
    +                                _fb["provider"], model=_fb["model"], raw_codex=True,
    +                                explicit_base_url=_fb.get("base_url"),
    +                                explicit_api_key=_fb.get("api_key"),
    +                            )
    +                            if _fb_client is not None:
    +                                self.provider = _fb["provider"]
    +                                self.model = _fb_model or _fb["model"]
    +                                self._fallback_activated = True
    +                                client_kwargs = {
    +                                    "api_key": [redacted:secret-assignment]
    +                                    "base_url": str(_fb_client.base_url),
    +                                }
    +                                if _provider_timeout is not None:
    +                                    client_kwargs["timeout"] = _provider_timeout
    +                                if hasattr(_fb_client, "_default_headers") and _fb_client._default_headers:
    +                                    client_kwargs["default_headers"] = dict(_fb_client._default_headers)
    +                                _fb_resolved = True
    +                                break
    +                        if not _fb_resolved:
    +                            raise RuntimeError(
    +                                f"Provider '{_explicit}' is set in config.yaml but no API key "
    +                                f"was found. Set the {_env_hint} environment "
    +                                f"variable, or switch to a different provider with `hermes model`."
    +                            )
    +                    if not getattr(self, "_fallback_activated", False):
    +                        # No provider configured — reject with a clear message.
                             raise RuntimeError(
    -                            f"Provider '{_explicit}' is set in config.yaml but no API key "
    -                            f"was found. Set the {_env_hint} environment "
    -                            f"variable, or switch to a different provider with `hermes model`."
    +                            "No LLM provider configured. Run `hermes model` to "
    +                            "select a provider, or run `hermes setup` for first-time "
    +                            "configuration."
                             )
    -                    # No provider configured — reject with a clear message.
    -                    raise RuntimeError(
    -                        "No LLM provider configured. Run `hermes model` to "
    -                        "select a provider, or run `hermes setup` for first-time "
    -                        "configuration."
    -                    )
                 
                 self._client_kwargs = client_kwargs  # stored for rebuilding after interrupt
     
    ```

    ```diff
    @@ -1536,7 +1568,7 @@ def __init__(
             else:
                 self._fallback_chain = []
             self._fallback_index = 0
    -        self._fallback_activated = False
    +        self._fallback_activated = getattr(self, "_fallback_activated", False)
             # Legacy attribute kept for backward compat (tests, external callers)
             self._fallback_model = self._fallback_chain[0] if self._fallback_chain else None
             if self._fallback_chain and not self.quiet_mode:
    ```
  - `tests/run_agent/test_init_fallback_on_exhausted_pool.py`

    ```diff
    @@ -0,0 +1,69 @@
    +"""Regression test for #17929: AIAgent.__init__ should try fallback_model
    +when primary provider credentials are exhausted."""
    +import pytest
    +from unittest.mock import patch, MagicMock
    +from run_agent import AIAgent
    +
    +
    +def _make_tool_defs():
    +    return [{"type": "function", "function": {"name": "web_search",
    +             "description": "search", "parameters": {"type": "object", "properties": {}}}}]
    +
    +
    +def _mock_client(api_key="[redacted:secret-assignment]", base_url="https://fb.example.com/v1"):
    +    c = MagicMock()
    +    c.api_key = [redacted:secret-assignment]
    +    c.base_url = base_url
    +    c._default_headers = None
    +    return c
    +
    +
    +def test_init_tries_fallback_when_primary_returns_none():
    +    """When resolve_provider_client returns None for primary but succeeds for
    +    a fallback entry, __init__ should NOT raise RuntimeError."""
    +    fb = _mock_client()
    +
    +    def fake_resolve(provider, model=None, raw_codex=False,
    +                     explicit_base_url=None, explicit_api_key=[redacted:secret-assignment]
    +        if provider == "tencent-token-plan":
    +            return fb, "kimi2.5"
    +        return None, None  # primary exhausted
    +
    +    with patch("agent.auxiliary_client.resolve_provider_client", side_effect=fake_resolve), \
    +         patch("run_agent.get_tool_definitions", return_value=_make_tool_defs()), \
    +         patch("run_agent.check_toolset_requirements", return_value={}), \
    +         patch("run_agent.OpenAI", return_value=MagicMock()):
    +
    +        agent = AIAgent(
    +            provider="alibaba-coding-plan",
    +            model="qwen3.6-plus",
    +            api_key=None,
    +            base_url=None,
    +            quiet_mode=True,
    +            skip_context_files=True,
    +            skip_memory=True,
    +            fallback_model=[{"provider": "tencent-token-plan", "model": "kimi2.5"}],
    +        )
    +        assert agent.provider == "tencent-token-plan"
    +        assert agent.model == "kimi2.5"
    +        assert agent._fallback_activated is True
    +
    +
    +def test_init_raises_when_no_fallback_configured():
    +    """When primary returns None and no fallback is set, should raise."""
    +    with patch("agent.auxiliary_client.resolve_provider_client", return_value=(None, None)), \
    +         patch("run_agent.get_tool_definitions", return_value=_make_tool_defs()), \
    +         patch("run_agent.check_toolset_requirements", return_value={}), \
    +         patch("run_agent.OpenAI", return_value=MagicMock()):
    +
    +        with pytest.raises(RuntimeError, match="no API key was found"):
    +            AIAgent(
    +                provider="alibaba-coding-plan",
    +                model="qwen3.6-plus",
    +                api_key=None,
    +                base_url=None,
    +                quiet_mode=True,
    +                skip_context_files=True,
    +                skip_memory=True,
    +                fallback_model=None,
    +            )
    ```
- **#100135: fix(agent): run init-time fallback for ANY exhausted primary provider (salvage #93439)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/agent_init.py`, `contributors/emails/vjpixel@users.noreply.github.com`, `tests/run_agent/test_init_fallback_on_exhausted_pool.py`
- **#17958: fix(agent): try fallback providers at init when primary credential pool is exhausted**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#18762`

## Fix status

- Merged to main in PR #100135 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-09-01`.
