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 <provider>/<model> 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.
This commit is contained in:
0xallam
2026-06-08 01:30:21 +03:00
committed by Ahmed Allam
parent 232711be8c
commit 3665a7899f
6 changed files with 13 additions and 105 deletions
-1
View File
@@ -395,7 +395,6 @@ def build_strix_agent(
instructions=instructions, instructions=instructions,
tools=tools, tools=tools,
tool_use_behavior=_finish_tool_use_behavior, tool_use_behavior=_finish_tool_use_behavior,
reset_tool_choice=interactive,
model=None, model=None,
capabilities=[ capabilities=[
Filesystem( Filesystem(
+2 -48
View File
@@ -59,12 +59,7 @@ DEFAULT_MODEL_RETRY = ModelRetrySettings(
def configure_sdk_model_defaults(settings: Settings) -> None: def configure_sdk_model_defaults(settings: Settings) -> None:
"""Apply Strix config to SDK-native defaults. """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`.
"""
llm = settings.llm llm = settings.llm
set_tracing_disabled(True) set_tracing_disabled(True)
_configure_litellm_compatibility() _configure_litellm_compatibility()
@@ -85,7 +80,7 @@ def _mirror_api_key_to_provider_env(model_name: str | None, api_key: str) -> Non
return return
import litellm import litellm
name = normalize_model_name(model_name) name = model_name.strip()
for prefix in ("litellm/", "any-llm/"): for prefix in ("litellm/", "any-llm/"):
if name.lower().startswith(prefix): if name.lower().startswith(prefix):
name = name[len(prefix) :] name = name[len(prefix) :]
@@ -114,50 +109,9 @@ def _configure_litellm_default(name: str, value: str) -> None:
setattr(litellm, name, value) 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: def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bool:
"""Return whether the resolved SDK route can only receive JSON function tools.""" """Return whether the resolved SDK route can only receive JSON function tools."""
model = model_name.strip().lower() model = model_name.strip().lower()
if "/" in model and not model.startswith("openai/"): if "/" in model and not model.startswith("openai/"):
return True return True
return bool(settings.llm.api_base) 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
+3 -24
View File
@@ -8,11 +8,7 @@ from typing import TYPE_CHECKING, Any
from agents.model_settings import ModelSettings from agents.model_settings import ModelSettings
from openai.types.shared import Reasoning from openai.types.shared import Reasoning
from strix.config.models import ( from strix.config.models import DEFAULT_MODEL_RETRY
DEFAULT_MODEL_RETRY,
model_known_to_registry,
model_supports_reasoning,
)
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -110,30 +106,13 @@ def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]:
} }
def make_model_settings( def make_model_settings(reasoning_effort: ReasoningEffort | None) -> ModelSettings:
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)
)
model_settings = ModelSettings( model_settings = ModelSettings(
parallel_tool_calls=False, parallel_tool_calls=False,
tool_choice=None if drop_tool_choice else "required",
retry=DEFAULT_MODEL_RETRY, retry=DEFAULT_MODEL_RETRY,
include_usage=True, 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( model_settings = model_settings.resolve(
ModelSettings(reasoning=Reasoning(effort=reasoning_effort)), ModelSettings(reasoning=Reasoning(effort=reasoning_effort)),
) )
+2 -6
View File
@@ -17,7 +17,6 @@ from strix.config import load_settings
from strix.config.models import ( from strix.config.models import (
StrixProvider, StrixProvider,
configure_sdk_model_defaults, configure_sdk_model_defaults,
normalize_model_name,
uses_chat_completions_tool_schema, uses_chat_completions_tool_schema,
) )
from strix.core.agents import AgentCoordinator from strix.core.agents import AgentCoordinator
@@ -91,7 +90,7 @@ async def run_strix_scan(
settings = load_settings() settings = load_settings()
configure_sdk_model_defaults(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: if not resolved_model:
raise RuntimeError( raise RuntimeError(
"No LLM model configured. Set STRIX_LLM env or pass model= to run_strix_scan().", "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) is_whitebox = any(t.get("type") == "local_code" for t in targets)
skills = list(scan_config.get("skills") or []) skills = list(scan_config.get("skills") or [])
root_task = build_root_task(scan_config) root_task = build_root_task(scan_config)
model_settings = make_model_settings( model_settings = make_model_settings(settings.llm.reasoning_effort)
settings.llm.reasoning_effort,
model_name=resolved_model,
)
run_config = RunConfig( run_config = RunConfig(
model=resolved_model, model=resolved_model,
model_provider=StrixProvider(), model_provider=StrixProvider(),
+3 -22
View File
@@ -5,7 +5,6 @@ Strix Agent Interface
import argparse import argparse
import asyncio import asyncio
import contextlib
import shutil import shutil
import sys import sys
from datetime import UTC, datetime from datetime import UTC, datetime
@@ -23,7 +22,7 @@ from strix.config import (
load_settings, load_settings,
persist_current, 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.core.paths import run_dir_for, runtime_state_dir
from strix.interface.cli import run_cli from strix.interface.cli import run_cli
from strix.interface.tui import run_tui from strix.interface.tui import run_tui
@@ -216,7 +215,7 @@ async def warm_up_llm() -> None:
configure_sdk_model_defaults(settings) configure_sdk_model_defaults(settings)
llm = settings.llm 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( await asyncio.wait_for(
model.get_response( model.get_response(
system_instructions="You are a helpful assistant.", system_instructions="You are a helpful assistant.",
@@ -232,7 +231,7 @@ async def warm_up_llm() -> None:
), ),
timeout=llm.timeout, 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: except Exception as e:
logger.exception("LLM warm-up failed") 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("Please check your configuration and try again.\n", style="white")
error_text.append(f"\nError: {e}", style="dim 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 '<provider>/<model>' form, e.g. "
f"'anthropic/claude-opus-4-7', 'deepseek/deepseek-reasoner', "
f"'openai/gpt-5.4'.",
style="yellow",
)
panel = Panel( panel = Panel(
error_text, error_text,
title="[bold white]STRIX", title="[bold white]STRIX",
+3 -4
View File
@@ -8,14 +8,13 @@ from typing import TYPE_CHECKING, Any
from agents.model_settings import ModelSettings from agents.model_settings import ModelSettings
from agents.models.interface import ModelTracing from agents.models.interface import ModelTracing
from agents.models.multi_provider import MultiProvider
from openai.types.responses import ResponseOutputMessage from openai.types.responses import ResponseOutputMessage
from strix.config import load_settings from strix.config import load_settings
from strix.config.models import ( from strix.config.models import (
DEFAULT_MODEL_RETRY, DEFAULT_MODEL_RETRY,
StrixProvider,
configure_sdk_model_defaults, configure_sdk_model_defaults,
normalize_model_name,
) )
from strix.report.state import get_global_report_state from strix.report.state import get_global_report_state
@@ -188,8 +187,8 @@ async def check_duplicate(
) )
configure_sdk_model_defaults(settings) configure_sdk_model_defaults(settings)
resolved_model = normalize_model_name(model_name) resolved_model = model_name.strip()
model = MultiProvider().get_model(resolved_model) model = StrixProvider().get_model(resolved_model)
response = await model.get_response( response = await model.get_response(
system_instructions=DEDUPE_SYSTEM_PROMPT, system_instructions=DEDUPE_SYSTEM_PROMPT,
input=user_msg, input=user_msg,