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
- 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
Open PRs0
Fixes on record
-
#18762 fix(agent): try fallback providers at init when primary credential pool is exhausted (salvage #17958)
files:
run_agent.py,tests/run_agent/test_init_fallback_on_exhausted_pool.pyShow 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)
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.00means 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