diff --git a/strix/config/models.py b/strix/config/models.py index e8544975..f9fcf106 100644 --- a/strix/config/models.py +++ b/strix/config/models.py @@ -733,6 +733,25 @@ def _configure_litellm_default(name: str, value: str) -> None: setattr(litellm, name, value) +def fallback_model_rejection(fallback: str, primary: str, settings: Settings) -> str | None: + """Why ``fallback`` cannot stand in for ``primary`` mid-run, or None if it can. + + Provider credentials, SDK route, and each agent's tool wrappers are all set + up once from the primary model, so a fallback that needs a different + provider or tool schema would be rejected on every request it serves. + """ + if ( + _split_model_provider(_normalized_model_name(fallback))[0] + != (_split_model_provider(_normalized_model_name(primary))[0]) + ): + return "needs a different provider, whose credentials are not configured" + if uses_chat_completions_tool_schema(fallback, settings) != uses_chat_completions_tool_schema( + primary, settings + ): + return "needs a different tool schema than the agents are built with" + return None + + def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bool: """Return whether the resolved SDK route can only receive JSON function tools.""" if codex.subscription_model(model_name): diff --git a/strix/core/runner.py b/strix/core/runner.py index 1012368f..f2212bdc 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -22,6 +22,7 @@ from strix.config import load_settings from strix.config.models import ( StrixProvider, configure_sdk_model_defaults, + fallback_model_rejection, uses_chat_completions_tool_schema, ) from strix.config.settings import DEFAULT_MAX_TURNS @@ -177,14 +178,11 @@ async def run_strix_scan( coordinator.set_snapshot_path(agents_path) fallback_model = settings.llm.fallback_model if fallback_model and ( - uses_chat_completions_tool_schema(fallback_model, settings) != chat_completions_tools + rejection := fallback_model_rejection(fallback_model, resolved_model, settings) ): - # Agents build their tool set once, for the primary model's schema. A - # fallback on the other SDK route would reject every replayed turn, so - # it can never recover a denied agent — fail fast instead. raise RuntimeError( - f"STRIX_LLM_FALLBACK '{fallback_model}' uses a different tool schema than " - f"'{resolved_model}'. Pick a fallback from the same provider family." + f"STRIX_LLM_FALLBACK '{fallback_model}' {rejection}; it could never serve a " + f"turn for '{resolved_model}'. Pick a fallback from the same provider family." ) coordinator.configure_denial_fallback( fallback_model,