Gate Reasoning(effort=...) on registry support (#528)

OpenAI's Responses API rejects reasoning.effort on non-reasoning
models like gpt-4o with `unsupported_parameter`, so any scan with
the default STRIX_REASONING_EFFORT=high against gpt-4o crashed at
the first model call. drop_params=True absorbs the rejected param
on LiteLLM-routed models but the SDK's native OpenAI path has no
equivalent.

Lift model_supports_reasoning to a public helper that strips
litellm/, any-llm/, openai/ prefixes and falls back to last-segment
lookup so prefixed forms like anthropic/claude-opus-4-7 resolve
through the bare model_cost entry. make_model_settings regains
model_name and skips Reasoning() when the registry doesn't confirm
support. uses_chat_completions_tool_schema reuses the same helper
(was duplicating the lookup under a misleading name).
This commit is contained in:
Ahmed Allam
2026-06-08 13:18:07 -07:00
committed by GitHub
parent ac0fef2ed7
commit 04eb03febe
3 changed files with 26 additions and 9 deletions
+11 -5
View File
@@ -119,15 +119,21 @@ def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bo
return True return True
if settings.llm.api_base: if settings.llm.api_base:
return True return True
return not _supports_responses_custom_tools(model_name) return not model_supports_reasoning(model_name)
def _supports_responses_custom_tools(model_name: str) -> bool: def model_supports_reasoning(model_name: str) -> bool:
import litellm import litellm
name = model_name.strip().lower().removeprefix("openai/") name = model_name.strip().lower()
entry = litellm.model_cost.get(name) or {} for prefix in ("litellm/", "any-llm/", "openai/"):
return bool(entry.get("supports_reasoning")) 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 bool(entry and entry.get("supports_reasoning"))
def is_known_openai_bare_model(model_name: str) -> bool: def is_known_openai_bare_model(model_name: str) -> bool:
+11 -3
View File
@@ -8,7 +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 DEFAULT_MODEL_RETRY from strix.config.models import DEFAULT_MODEL_RETRY, model_supports_reasoning
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -106,13 +106,21 @@ def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]:
} }
def make_model_settings(reasoning_effort: ReasoningEffort | None) -> ModelSettings: def make_model_settings(
reasoning_effort: ReasoningEffort | None,
*,
model_name: str,
) -> ModelSettings:
model_settings = ModelSettings( model_settings = ModelSettings(
parallel_tool_calls=False, parallel_tool_calls=False,
retry=DEFAULT_MODEL_RETRY, retry=DEFAULT_MODEL_RETRY,
include_usage=True, include_usage=True,
) )
if reasoning_effort is not None and reasoning_effort != "none": if (
reasoning_effort is not None
and reasoning_effort != "none"
and model_supports_reasoning(model_name)
):
model_settings = model_settings.resolve( model_settings = model_settings.resolve(
ModelSettings(reasoning=Reasoning(effort=reasoning_effort)), ModelSettings(reasoning=Reasoning(effort=reasoning_effort)),
) )
+4 -1
View File
@@ -153,7 +153,10 @@ 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(settings.llm.reasoning_effort) model_settings = make_model_settings(
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(),