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:
@@ -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
@@ -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
@@ -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)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user