From 712c64f6300dcab65a7a734bddd303c86884924f Mon Sep 17 00:00:00 2001 From: 0xallam Date: Mon, 8 Jun 2026 00:18:44 +0300 Subject: [PATCH] Drop tool_choice for registry-unknown reasoning-effort runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user opts into reasoning_effort but the configured model isn't in litellm.model_cost at all (private SKUs, fresh releases the registry hasn't picked up — e.g. deepseek/deepseek-v4-pro), we can't confirm thinking support and were sending tool_choice="required", which thinking-mode endpoints reject ("Thinking mode does not support this tool_choice"). Add model_known_to_registry() and split the decision: when the user wants reasoning AND the model is either confirmed-reasoning OR unknown-to-registry, drop tool_choice. The Reasoning(effort=...) param still only attaches for confirmed-reasoning models, so we don't send reasoning hints to known non-reasoning models. Known non-reasoning models (gpt-4o, registry-confirmed) keep tool_choice="required" unchanged. --- strix/config/models.py | 11 ++++++++++- strix/core/inputs.py | 30 ++++++++++++++++++------------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/strix/config/models.py b/strix/config/models.py index 59a52a4..83f8e91 100644 --- a/strix/config/models.py +++ b/strix/config/models.py @@ -121,7 +121,7 @@ def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bo return bool(settings.llm.api_base) -def model_supports_reasoning(model_name: str) -> bool: +def _model_cost_entry(model_name: str) -> dict[str, object] | None: import litellm name = model_name.strip().lower() @@ -132,4 +132,13 @@ def model_supports_reasoning(model_name: str) -> bool: 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 148bf09..58da3e6 100644 --- a/strix/core/inputs.py +++ b/strix/core/inputs.py @@ -8,7 +8,11 @@ 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_supports_reasoning +from strix.config.models import ( + DEFAULT_MODEL_RETRY, + model_known_to_registry, + model_supports_reasoning, +) if TYPE_CHECKING: @@ -111,23 +115,25 @@ def make_model_settings( *, model_name: str, ) -> ModelSettings: - # Anthropic + DeepSeek thinking reject ``tool_choice="required"`` outright - # when reasoning is enabled; OpenAI o-series accepts both but doesn't need - # the safety net. When reasoning is on we let the model self-select tools - # and rely on the system prompt + the ``_finish_tool_use_behavior`` callback - # to keep the loop converging on a lifecycle tool. - use_reasoning = ( - reasoning_effort is not None - and reasoning_effort != "none" - and model_supports_reasoning(model_name) + # 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( parallel_tool_calls=False, - tool_choice=None if use_reasoning else "required", + tool_choice=None if drop_tool_choice else "required", retry=DEFAULT_MODEL_RETRY, include_usage=True, ) - if use_reasoning: + if user_wants_reasoning and confirmed_reasoning: model_settings = model_settings.resolve( ModelSettings(reasoning=Reasoning(effort=reasoning_effort)), )