feat(runner): add model failover with fallback_models
When the primary model returns a non-transient error and no content has been streamed yet, the runner now tries each model listed in the active preset's fallback_models in order. Each fallback model may reside on a different provider — a temporary provider instance is created on-the-fly via make_provider(config, model=...). Key design: - Failover is request-scoped (does not affect subagents/dream/consolidator) - Provider is restored via try/finally after each fallback attempt - Skipped when content was already streamed to avoid duplicate output - Recursive failover prevented by clearing fallback_models on fallback spec - Circuit breaker trips open after 3 consecutive primary failures (60s cooldown) - Cross-provider routing: fallback model prefix (e.g. groq/) determines provider Fixes: cross-provider fallback was broken because the factory passed the original preset (with provider forced to primary's provider) when creating fallback providers. Now uses provider="auto" so the model string prefix correctly routes to the right provider. Also fixes: log messages now distinguish between primary-failed, previous-fallback-failed, and circuit-open scenarios. closes: https://github.com/HKUDS/nanobot/issues/3376
This commit is contained in:
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
|
||||
from nanobot.config.schema import Config, ModelPresetConfig
|
||||
from nanobot.providers.base import LLMProvider
|
||||
from nanobot.providers.fallback_provider import FallbackProvider
|
||||
from nanobot.providers.registry import find_by_name
|
||||
|
||||
|
||||
@@ -27,15 +28,16 @@ def _resolve_model_preset(
|
||||
return preset if preset is not None else config.resolve_preset(preset_name)
|
||||
|
||||
|
||||
def make_provider(
|
||||
def _make_provider_core(
|
||||
config: Config,
|
||||
*,
|
||||
preset_name: str | None = None,
|
||||
preset: ModelPresetConfig | None = None,
|
||||
model: str | None = None,
|
||||
) -> LLMProvider:
|
||||
"""Create the LLM provider implied by config."""
|
||||
"""Create a plain LLM provider without failover wrapping."""
|
||||
resolved = _resolve_model_preset(config, preset_name=preset_name, preset=preset)
|
||||
model = resolved.model
|
||||
model = model or resolved.model
|
||||
provider_name = config.get_provider_name(model, preset=resolved)
|
||||
p = config.get_provider(model, preset=resolved)
|
||||
spec = find_by_name(provider_name) if provider_name else None
|
||||
@@ -102,6 +104,34 @@ def make_provider(
|
||||
return provider
|
||||
|
||||
|
||||
def make_provider(
|
||||
config: Config,
|
||||
*,
|
||||
preset_name: str | None = None,
|
||||
preset: ModelPresetConfig | None = None,
|
||||
model: str | None = None,
|
||||
) -> LLMProvider:
|
||||
"""Create the LLM provider implied by config.
|
||||
|
||||
When *model* is given, it overrides the resolved/preset model — used by
|
||||
the failover path to create providers for fallback models.
|
||||
"""
|
||||
resolved = _resolve_model_preset(config, preset_name=preset_name, preset=preset)
|
||||
provider = _make_provider_core(config, preset_name=preset_name, preset=preset, model=model)
|
||||
|
||||
if resolved.fallback_models:
|
||||
fb_preset = resolved.model_copy(update={"provider": "auto", "fallback_models": []})
|
||||
provider = FallbackProvider(
|
||||
primary=provider,
|
||||
fallback_models=resolved.fallback_models,
|
||||
provider_factory=lambda m: _make_provider_core(
|
||||
config, preset_name=preset_name, preset=fb_preset, model=m
|
||||
),
|
||||
)
|
||||
|
||||
return provider
|
||||
|
||||
|
||||
def provider_signature(
|
||||
config: Config,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user