mirror of
https://github.com/usestrix/strix.git
synced 2026-08-18 17:52:32 +02:00
fix(toolchoice): default required tool choice on OpenAI-compatible custom endpoints
Reasoning models on OpenAI-compatible custom endpoints (e.g. GLM / Kimi) often reason and reply with prose instead of emitting native tool calls under the default auto tool choice, stalling the tool-driven scan loop while burning tokens. Default force_required_tool_choice to auto (None): enable tool_choice=required when an api_base is set (still gated to models that accept it), off otherwise. Explicit 0/1 overrides.
This commit is contained in:
@@ -36,8 +36,11 @@ class LlmSettings(BaseSettings):
|
||||
),
|
||||
)
|
||||
reasoning_effort: ReasoningEffort = Field(default="high", alias="STRIX_REASONING_EFFORT")
|
||||
force_required_tool_choice: bool = Field(
|
||||
default=False,
|
||||
# None = auto: force required tool choice on OpenAI-compatible custom
|
||||
# endpoints (where reasoning models otherwise burn tokens thinking without
|
||||
# ever calling a tool), off elsewhere. True/False overrides explicitly.
|
||||
force_required_tool_choice: bool | None = Field(
|
||||
default=None,
|
||||
alias="STRIX_FORCE_REQUIRED_TOOL_CHOICE",
|
||||
)
|
||||
prompt_cache: bool = Field(
|
||||
|
||||
+10
-2
@@ -129,7 +129,8 @@ def make_model_settings(
|
||||
reasoning_effort: ReasoningEffort | None,
|
||||
*,
|
||||
model_name: str,
|
||||
force_required_tool_choice: bool = False,
|
||||
force_required_tool_choice: bool | None = None,
|
||||
custom_api_base: bool = False,
|
||||
request_timeout: float | None = None,
|
||||
prompt_cache: bool = True,
|
||||
) -> ModelSettings:
|
||||
@@ -147,7 +148,14 @@ def make_model_settings(
|
||||
model_settings = model_settings.resolve(
|
||||
ModelSettings(reasoning=Reasoning(effort=reasoning_effort)),
|
||||
)
|
||||
if force_required_tool_choice and _accepts_required_tool_choice(model_name):
|
||||
# Strix is fully tool-driven, so a turn that returns prose instead of a tool
|
||||
# call stalls the scan. Reasoning models on OpenAI-compatible custom
|
||||
# endpoints (e.g. GLM / Kimi via cortecs) do exactly that under the default
|
||||
# ``auto`` tool choice, so default to ``required`` there; ``None`` means auto.
|
||||
use_required = (
|
||||
custom_api_base if force_required_tool_choice is None else force_required_tool_choice
|
||||
)
|
||||
if use_required and _accepts_required_tool_choice(model_name):
|
||||
model_settings = model_settings.resolve(ModelSettings(tool_choice="required"))
|
||||
|
||||
cache_extra_args = _prompt_cache_extra_args(model_name) if prompt_cache else None
|
||||
|
||||
@@ -248,6 +248,7 @@ async def run_strix_scan(
|
||||
settings.llm.reasoning_effort,
|
||||
model_name=resolved_model,
|
||||
force_required_tool_choice=settings.llm.force_required_tool_choice,
|
||||
custom_api_base=bool(settings.llm.api_base),
|
||||
request_timeout=settings.llm.timeout,
|
||||
prompt_cache=settings.llm.prompt_cache,
|
||||
)
|
||||
|
||||
@@ -255,6 +255,44 @@ def test_make_model_settings_forces_required_for_anyllm_routed_openai_model() ->
|
||||
assert settings.tool_choice == "required"
|
||||
|
||||
|
||||
def test_make_model_settings_auto_forces_required_on_custom_openai_endpoint() -> None:
|
||||
# GLM / Kimi via cortecs: openai/-routed reasoning model on a custom base.
|
||||
settings = make_model_settings(
|
||||
None,
|
||||
model_name="openai/glm-5.2",
|
||||
custom_api_base=True,
|
||||
)
|
||||
|
||||
assert settings.tool_choice == "required"
|
||||
|
||||
|
||||
def test_make_model_settings_auto_skips_required_without_custom_endpoint() -> None:
|
||||
settings = make_model_settings(None, model_name="openai/glm-5.2")
|
||||
|
||||
assert settings.tool_choice is None
|
||||
|
||||
|
||||
def test_make_model_settings_explicit_false_overrides_custom_endpoint() -> None:
|
||||
settings = make_model_settings(
|
||||
None,
|
||||
model_name="openai/glm-5.2",
|
||||
force_required_tool_choice=False,
|
||||
custom_api_base=True,
|
||||
)
|
||||
|
||||
assert settings.tool_choice is None
|
||||
|
||||
|
||||
def test_make_model_settings_auto_skips_required_for_non_openai_custom_endpoint() -> None:
|
||||
settings = make_model_settings(
|
||||
None,
|
||||
model_name="anthropic/claude-3-7-sonnet-latest",
|
||||
custom_api_base=True,
|
||||
)
|
||||
|
||||
assert settings.tool_choice is None
|
||||
|
||||
|
||||
def test_make_model_settings_sets_request_timeout() -> None:
|
||||
settings = make_model_settings(
|
||||
"none",
|
||||
|
||||
@@ -38,6 +38,7 @@ async def test_persistent_rate_limit_stops_gracefully(
|
||||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
api_base=None,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
),
|
||||
|
||||
@@ -46,6 +46,7 @@ def _patch_engine_scaffold(
|
||||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
api_base=None,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user