From 3665a7899f04a48969b14cecbf3d3913d7445f82 Mon Sep 17 00:00:00 2001 From: 0xallam Date: Mon, 8 Jun 2026 01:30:21 +0300 Subject: [PATCH] Strip model-aware branches from LLM configuration Drop every hand-rolled provider table and per-model gating that had accumulated in the model-handling layer: * normalize_model_name no longer auto-prefixes bare claude-* / gemini-* names. Users supply the full / form. The function became literally model_name.strip(), so callers now inline that and the function is removed. * tool_choice="required" is gone everywhere. Thinking-mode endpoints (Anthropic, DeepSeek /beta) reject it; modern reasoning models don't need it; non-interactive runs already have _append_noninteractive_tool_required_message as the convergence backstop. model_supports_reasoning, model_known_to_registry, and _model_cost_entry were only used to gate this and follow it out. * Reasoning(effort=...) is now attached whenever STRIX_REASONING_EFFORT is non-none. litellm.drop_params=True absorbs it for non-reasoning models. * Warm-up's bare-name OpenAI 401 hint is removed (false-positive prone, relied on substring matching). * reset_tool_choice on SandboxAgent is no-op now (no tool_choice gets set) and is removed. * report/dedupe.py was still routing through stock MultiProvider, so non-OpenAI configs failed the dedupe LLM pass; switch it to StrixProvider. Verified end-to-end against modern provider strings (openai/gpt-5.4, anthropic/claude-opus-4-7, deepseek/deepseek-reasoner, gemini/gemini-2.5-pro, groq/, xai/, mistral/, together_ai/, perplexity/, openrouter/, litellm/ legacy form, and whitespace-padded input): 18/18 cases route correctly, env vars mirror via litellm.validate_environment, and ModelSettings carries no tool_choice. mypy strict passes. --- strix/agents/factory.py | 1 - strix/config/models.py | 50 ++--------------------------------------- strix/core/inputs.py | 27 +++------------------- strix/core/runner.py | 8 ++----- strix/interface/main.py | 25 +++------------------ strix/report/dedupe.py | 7 +++--- 6 files changed, 13 insertions(+), 105 deletions(-) diff --git a/strix/agents/factory.py b/strix/agents/factory.py index f559033..6a5af17 100644 --- a/strix/agents/factory.py +++ b/strix/agents/factory.py @@ -395,7 +395,6 @@ def build_strix_agent( instructions=instructions, tools=tools, tool_use_behavior=_finish_tool_use_behavior, - reset_tool_choice=interactive, model=None, capabilities=[ Filesystem( diff --git a/strix/config/models.py b/strix/config/models.py index 7f949b4..feedbe7 100644 --- a/strix/config/models.py +++ b/strix/config/models.py @@ -59,12 +59,7 @@ DEFAULT_MODEL_RETRY = ModelRetrySettings( def configure_sdk_model_defaults(settings: Settings) -> None: - """Apply Strix config to SDK-native defaults. - - OpenAI-compatible base URLs are handled by the SDK OpenAI provider. - Non-OpenAI providers should use the SDK's native ``litellm/`` or - ``any-llm/`` routing, produced by :func:`normalize_model_name`. - """ + """Apply Strix config to SDK-native defaults.""" llm = settings.llm set_tracing_disabled(True) _configure_litellm_compatibility() @@ -85,7 +80,7 @@ def _mirror_api_key_to_provider_env(model_name: str | None, api_key: str) -> Non return import litellm - name = normalize_model_name(model_name) + name = model_name.strip() for prefix in ("litellm/", "any-llm/"): if name.lower().startswith(prefix): name = name[len(prefix) :] @@ -114,50 +109,9 @@ def _configure_litellm_default(name: str, value: str) -> None: setattr(litellm, name, value) -def normalize_model_name(model_name: str) -> str: - """Normalize friendly Strix model names to SDK-native model ids.""" - model = model_name.strip() - if not model: - return model - - if "/" in model: - return model - - lower = model.lower() - if lower.startswith("claude"): - return f"anthropic/{model}" - if lower.startswith("gemini"): - return f"gemini/{model}" - - return model - - 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 "/" in model and not model.startswith("openai/"): return True return bool(settings.llm.api_base) - - -def _model_cost_entry(model_name: str) -> dict[str, object] | None: - import litellm - - name = model_name.strip().lower() - for prefix in ("litellm/", "any-llm/"): - if name.startswith(prefix): - name = name[len(prefix) :] - break - entry = litellm.model_cost.get(name) - if entry is None and "/" in name: - entry = litellm.model_cost.get(name.rsplit("/", 1)[1]) - return entry - - -def model_supports_reasoning(model_name: str) -> bool: - entry = _model_cost_entry(model_name) - return bool(entry and entry.get("supports_reasoning")) - - -def model_known_to_registry(model_name: str) -> bool: - return _model_cost_entry(model_name) is not None diff --git a/strix/core/inputs.py b/strix/core/inputs.py index 58da3e6..b82d888 100644 --- a/strix/core/inputs.py +++ b/strix/core/inputs.py @@ -8,11 +8,7 @@ from typing import TYPE_CHECKING, Any from agents.model_settings import ModelSettings from openai.types.shared import Reasoning -from strix.config.models import ( - DEFAULT_MODEL_RETRY, - model_known_to_registry, - model_supports_reasoning, -) +from strix.config.models import DEFAULT_MODEL_RETRY if TYPE_CHECKING: @@ -110,30 +106,13 @@ def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]: } -def make_model_settings( - reasoning_effort: ReasoningEffort | None, - *, - model_name: str, -) -> ModelSettings: - # Anthropic + DeepSeek thinking reject ``tool_choice="required"`` outright; - # when reasoning is enabled we let the model self-select tools and rely on - # the system prompt + the ``_finish_tool_use_behavior`` callback to keep - # the loop converging. When the user opted into reasoning but the model - # is unknown to LiteLLM's registry (e.g. a private DeepSeek SKU, a fresh - # release the registry hasn't picked up), drop ``tool_choice`` too — - # server-side thinking-mode endpoints reject it and we can't confirm. - user_wants_reasoning = reasoning_effort is not None and reasoning_effort != "none" - confirmed_reasoning = model_supports_reasoning(model_name) - drop_tool_choice = user_wants_reasoning and ( - confirmed_reasoning or not model_known_to_registry(model_name) - ) +def make_model_settings(reasoning_effort: ReasoningEffort | None) -> ModelSettings: model_settings = ModelSettings( parallel_tool_calls=False, - tool_choice=None if drop_tool_choice else "required", retry=DEFAULT_MODEL_RETRY, include_usage=True, ) - if user_wants_reasoning and confirmed_reasoning: + if reasoning_effort is not None and reasoning_effort != "none": model_settings = model_settings.resolve( ModelSettings(reasoning=Reasoning(effort=reasoning_effort)), ) diff --git a/strix/core/runner.py b/strix/core/runner.py index 4c0c3fb..d042cdf 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -17,7 +17,6 @@ from strix.config import load_settings from strix.config.models import ( StrixProvider, configure_sdk_model_defaults, - normalize_model_name, uses_chat_completions_tool_schema, ) from strix.core.agents import AgentCoordinator @@ -91,7 +90,7 @@ async def run_strix_scan( settings = load_settings() configure_sdk_model_defaults(settings) - resolved_model = normalize_model_name(model or settings.llm.model or "") + resolved_model = (model or settings.llm.model or "").strip() if not resolved_model: raise RuntimeError( "No LLM model configured. Set STRIX_LLM env or pass model= to run_strix_scan().", @@ -154,10 +153,7 @@ async def run_strix_scan( is_whitebox = any(t.get("type") == "local_code" for t in targets) skills = list(scan_config.get("skills") or []) root_task = build_root_task(scan_config) - model_settings = make_model_settings( - settings.llm.reasoning_effort, - model_name=resolved_model, - ) + model_settings = make_model_settings(settings.llm.reasoning_effort) run_config = RunConfig( model=resolved_model, model_provider=StrixProvider(), diff --git a/strix/interface/main.py b/strix/interface/main.py index 8ef2fd2..ac3a86f 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -5,7 +5,6 @@ Strix Agent Interface import argparse import asyncio -import contextlib import shutil import sys from datetime import UTC, datetime @@ -23,7 +22,7 @@ from strix.config import ( load_settings, persist_current, ) -from strix.config.models import StrixProvider, configure_sdk_model_defaults, normalize_model_name +from strix.config.models import StrixProvider, configure_sdk_model_defaults 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 @@ -216,7 +215,7 @@ async def warm_up_llm() -> None: configure_sdk_model_defaults(settings) llm = settings.llm - model = StrixProvider().get_model(normalize_model_name(llm.model or "")) + model = StrixProvider().get_model((llm.model or "").strip()) await asyncio.wait_for( model.get_response( system_instructions="You are a helpful assistant.", @@ -232,7 +231,7 @@ async def warm_up_llm() -> None: ), timeout=llm.timeout, ) - logger.info("LLM warm-up succeeded for model %s", normalize_model_name(llm.model or "")) + logger.info("LLM warm-up succeeded for model %s", (llm.model or "").strip()) except Exception as e: logger.exception("LLM warm-up failed") @@ -243,24 +242,6 @@ async def warm_up_llm() -> None: error_text.append("Please check your configuration and try again.\n", style="white") error_text.append(f"\nError: {e}", style="dim white") - raw_model = "" - resolved = "" - with contextlib.suppress(Exception): - raw_model = (load_settings().llm.model or "").strip() - resolved = normalize_model_name(raw_model) - err_lc = str(e).lower() - unprefixed = bool(resolved) and "/" not in resolved - looks_openai = "platform.openai.com" in err_lc or "openai" in err_lc - if unprefixed and looks_openai: - error_text.append( - 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"'anthropic/claude-opus-4-7', 'deepseek/deepseek-reasoner', " - f"'openai/gpt-5.4'.", - style="yellow", - ) - panel = Panel( error_text, title="[bold white]STRIX", diff --git a/strix/report/dedupe.py b/strix/report/dedupe.py index af468a9..ca2b1fa 100644 --- a/strix/report/dedupe.py +++ b/strix/report/dedupe.py @@ -8,14 +8,13 @@ from typing import TYPE_CHECKING, Any from agents.model_settings import ModelSettings from agents.models.interface import ModelTracing -from agents.models.multi_provider import MultiProvider from openai.types.responses import ResponseOutputMessage from strix.config import load_settings from strix.config.models import ( DEFAULT_MODEL_RETRY, + StrixProvider, configure_sdk_model_defaults, - normalize_model_name, ) from strix.report.state import get_global_report_state @@ -188,8 +187,8 @@ async def check_duplicate( ) configure_sdk_model_defaults(settings) - resolved_model = normalize_model_name(model_name) - model = MultiProvider().get_model(resolved_model) + resolved_model = model_name.strip() + model = StrixProvider().get_model(resolved_model) response = await model.get_response( system_instructions=DEDUPE_SYSTEM_PROMPT, input=user_msg,