NousResearch/hermes-agent

Init-time fallback when credential pool exhausted

Cluster 17929. Data as of

Nothing open on GitHub. Fix merged in #100135 on 1 Sep 2026. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #100135. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #18762. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #18762 fix(agent): try fallback providers at init when primary credential pool is exhausted (salvage #17958)

    merged to main

    files: run_agent.py, tests/run_agent/test_init_fallback_on_exhausted_pool.py

    Show the diff
    • run_agent.py
      @@ -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
       
      @@ -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
      @@ -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

    files: agent/agent_init.py, contributors/emails/[email protected], tests/run_agent/test_init_fallback_on_exhausted_pool.py

1 PR closed without merging

From the close comments: merged via another change 1

  • #17958 fix(agent): try fallback providers at init when primary credential pool is exhausted · merged via another change → #18762
Closed issues · 1
  • Closed#17929Single-key credential pool: rate-limit cooldown causes init-time RuntimeError, fallback chain never tried
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#18762, #100135
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
1 Sep 2026

For your agent

The same facts, typed.