Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -921,18 +921,42 @@ def _load_provider_routing() -> dict:
def _load_fallback_model() -> dict | None:
"""Load fallback model config from config.yaml.

Returns a dict with 'provider' and 'model' keys, or None if
not configured / both fields empty.
Supports both single ``fallback_model`` (dict) and
``fallback_models`` (list of dicts). The list form is
preferred — each entry is tried in order when the previous
provider is exhausted. The legacy single-dict form is wrapped
into a one-element list for uniform handling.

Returns a dict with 'provider' and 'model' keys (and an
optional '_chain' key holding additional fallbacks), or None.
"""
try:
import yaml as _y
cfg_path = _hermes_home / "config.yaml"
if cfg_path.exists():
with open(cfg_path, encoding="utf-8") as _f:
cfg = _y.safe_load(_f) or {}
fb = cfg.get("fallback_model", {}) or {}
if fb.get("provider") and fb.get("model"):
return fb

chain: list[dict] = []

# New list form: fallback_models
fb_list = cfg.get("fallback_models")
if isinstance(fb_list, list):
for entry in fb_list:
if isinstance(entry, dict) and entry.get("provider") and entry.get("model"):
chain.append(entry)

# Legacy single form: fallback_model
if not chain:
fb = cfg.get("fallback_model", {}) or {}
if fb.get("provider") and fb.get("model"):
chain.append(fb)

if chain:
first = dict(chain[0])
if len(chain) > 1:
first["_chain"] = chain[1:]
return first
except Exception:
pass
return None
Expand Down
44 changes: 32 additions & 12 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -896,16 +896,25 @@ def __init__(
except Exception as e:
raise RuntimeError(f"Failed to initialize OpenAI client: {e}")

# Provider fallback — a single backup model/provider tried when the
# primary is exhausted (rate-limit, overload, connection failure).
# Config shape: {"provider": "openrouter", "model": "anthropic/claude-sonnet-4"}
self._fallback_model = fallback_model if isinstance(fallback_model, dict) else None
# Provider fallback chain — one or more backup model/providers tried
# in order when the primary is exhausted (rate-limit, overload, etc.).
# Config shape: {"provider": "...", "model": "...", "_chain": [{...}, ...]}
# The first entry is popped into _fallback_model; _chain holds the rest.
self._fallback_chain: list[dict] = []
self._fallback_model = None
self._fallback_activated = False
if self._fallback_model:
if isinstance(fallback_model, dict):
# Extract chain if present, then set first fallback
chain_rest = fallback_model.pop("_chain", None) or []
self._fallback_model = fallback_model
self._fallback_chain = list(chain_rest)
if self._fallback_model and not self.quiet_mode:
fb_p = self._fallback_model.get("provider", "")
fb_m = self._fallback_model.get("model", "")
if fb_p and fb_m and not self.quiet_mode:
print(f"🔄 Fallback model: {fb_m} ({fb_p})")
chain_len = len(self._fallback_chain)
chain_suffix = f" (+{chain_len} more)" if chain_len else ""
if fb_p and fb_m:
print(f"🔄 Fallback model: {fb_m} ({fb_p}){chain_suffix}")

# Get available tools with filtering
self.tools = get_tool_definitions(
Expand Down Expand Up @@ -4318,18 +4327,29 @@ def _call():
# ── Provider fallback ──────────────────────────────────────────────────

def _try_activate_fallback(self) -> bool:
"""Switch to the configured fallback model/provider.
"""Switch to the next fallback model/provider in the chain.

Called when the primary model is failing after retries. Swaps the
Called when the current model is failing after retries. Swaps the
OpenAI client, model slug, and provider in-place so the retry loop
can continue with the new backend. One-shot: returns False if
already activated or not configured.
can continue with the new backend.

Supports a fallback chain: if the current fallback is already active
and there are more entries in ``_fallback_chain``, the next one is
tried. Returns False only when all fallbacks are exhausted.

Uses the centralized provider router (resolve_provider_client) for
auth resolution and client construction — no duplicated provider→key
mappings.
"""
if self._fallback_activated or not self._fallback_model:
if self._fallback_activated:
# Already on a fallback — try the next one in the chain
if self._fallback_chain:
self._fallback_model = self._fallback_chain.pop(0)
self._fallback_activated = False # reset so the logic below runs
else:
return False # chain exhausted

if not self._fallback_model:
return False

fb = self._fallback_model
Expand Down
Loading