diff --git a/strix/config/models.py b/strix/config/models.py index 83f8e91..7f949b4 100644 --- a/strix/config/models.py +++ b/strix/config/models.py @@ -6,6 +6,7 @@ import os from typing import TYPE_CHECKING from agents import set_default_openai_api, set_default_openai_key, set_tracing_disabled +from agents.models.multi_provider import MultiProvider from agents.retry import ( ModelRetryBackoffSettings, ModelRetrySettings, @@ -14,10 +15,31 @@ from agents.retry import ( if TYPE_CHECKING: + from agents.models.interface import ModelProvider + from strix.config.settings import Settings -_SDK_PREFIXES = {"any-llm", "litellm", "openai"} +class StrixProvider(MultiProvider): + """Route any non-OpenAI prefix through LiteLLM with the prefix preserved, + so users type ``deepseek/deepseek-chat`` rather than + ``litellm/deepseek/deepseek-chat``. + """ + + def _resolve_prefixed_model( + self, + *, + original_model_name: str, + prefix: str, + stripped_model_name: str | None, + ) -> tuple[ModelProvider, str | None]: + if prefix in {"openai", "litellm", "any-llm"}: + return super()._resolve_prefixed_model( + original_model_name=original_model_name, + prefix=prefix, + stripped_model_name=stripped_model_name, + ) + return self._get_fallback_provider("litellm"), original_model_name DEFAULT_MODEL_RETRY = ModelRetrySettings( @@ -99,16 +121,13 @@ def normalize_model_name(model_name: str) -> str: return model if "/" in model: - prefix = model.split("/", 1)[0].lower() - if prefix in _SDK_PREFIXES: - return model - return f"litellm/{model}" + return model lower = model.lower() if lower.startswith("claude"): - return f"litellm/anthropic/{model}" + return f"anthropic/{model}" if lower.startswith("gemini"): - return f"litellm/gemini/{model}" + return f"gemini/{model}" return model @@ -116,7 +135,7 @@ def normalize_model_name(model_name: str) -> str: def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bool: """Return whether the resolved SDK route can only receive JSON function tools.""" model = model_name.strip().lower() - if model.startswith(("litellm/", "any-llm/")): + if "/" in model and not model.startswith("openai/"): return True return bool(settings.llm.api_base) diff --git a/strix/core/runner.py b/strix/core/runner.py index e69e632..4c0c3fb 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -15,6 +15,7 @@ from agents.sandbox import SandboxRunConfig from strix.agents.factory import build_strix_agent, make_child_factory from strix.config import load_settings from strix.config.models import ( + StrixProvider, configure_sdk_model_defaults, normalize_model_name, uses_chat_completions_tool_schema, @@ -159,6 +160,7 @@ async def run_strix_scan( ) run_config = RunConfig( model=resolved_model, + model_provider=StrixProvider(), model_settings=model_settings, sandbox=SandboxRunConfig(client=bundle["client"], session=bundle["session"]), trace_include_sensitive_data=False, diff --git a/strix/interface/main.py b/strix/interface/main.py index 9bb86ae..8ef2fd2 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -13,7 +13,6 @@ from pathlib import Path from agents.model_settings import ModelSettings from agents.models.interface import ModelTracing -from agents.models.multi_provider import MultiProvider from docker.errors import DockerException from rich.console import Console from rich.panel import Panel @@ -24,7 +23,7 @@ from strix.config import ( load_settings, persist_current, ) -from strix.config.models import configure_sdk_model_defaults, normalize_model_name +from strix.config.models import StrixProvider, configure_sdk_model_defaults, normalize_model_name from strix.core.paths import run_dir_for, runtime_state_dir from strix.interface.cli import run_cli from strix.interface.tui import run_tui @@ -99,7 +98,8 @@ def validate_environment() -> None: error_text.append("• ", style="white") error_text.append("STRIX_LLM", style="bold cyan") error_text.append( - " - Model name to use (e.g., 'gpt-5.4' or 'claude-sonnet-4-6')\n", + " - Model name to use (e.g., 'openai/gpt-5.4' or " + "'anthropic/claude-opus-4-7')\n", style="white", ) @@ -138,7 +138,7 @@ def validate_environment() -> None: ) error_text.append("\nExample setup:\n", style="white") - error_text.append("export STRIX_LLM='gpt-5.4'\n", style="dim white") + error_text.append("export STRIX_LLM='openai/gpt-5.4'\n", style="dim white") if missing_optional_vars: for var in missing_optional_vars: @@ -216,7 +216,7 @@ async def warm_up_llm() -> None: configure_sdk_model_defaults(settings) llm = settings.llm - model = MultiProvider().get_model(normalize_model_name(llm.model or "")) + model = StrixProvider().get_model(normalize_model_name(llm.model or "")) await asyncio.wait_for( model.get_response( system_instructions="You are a helpful assistant.", @@ -256,8 +256,8 @@ async def warm_up_llm() -> None: f"\n\nHint: '{raw_model}' has no provider prefix, so the SDK " f"routed it through OpenAI by default. For non-OpenAI providers " f"use the '/' form, e.g. " - f"'deepseek/deepseek-chat', 'groq/llama-3.1-70b', " - f"'anthropic/claude-3-5-sonnet'.", + f"'anthropic/claude-opus-4-7', 'deepseek/deepseek-reasoner', " + f"'openai/gpt-5.4'.", style="yellow", )