Files
strix/strix/core/inputs.py
T
0xallam 712c64f630 Drop tool_choice for registry-unknown reasoning-effort runs
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.
2026-06-07 17:36:19 -07:00

178 lines
6.3 KiB
Python

"""Pure input builders for Strix scan runs."""
from __future__ import annotations
import json
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_known_to_registry,
model_supports_reasoning,
)
if TYPE_CHECKING:
from strix.config.settings import ReasoningEffort
DEFAULT_MAX_TURNS = 500
def build_root_task(scan_config: dict[str, Any]) -> str:
targets = scan_config.get("targets", []) or []
diff_scope = scan_config.get("diff_scope") or {}
user_instructions = scan_config.get("user_instructions", "") or ""
sections: dict[str, list[str]] = {
"Repositories": [],
"Local Codebases": [],
"URLs": [],
"IP Addresses": [],
}
for target in targets:
ttype = target.get("type")
details = target.get("details") or {}
workspace_subdir = details.get("workspace_subdir")
workspace_path = f"/workspace/{workspace_subdir}" if workspace_subdir else "/workspace"
if ttype == "repository":
url = details.get("target_repo", "")
cloned = details.get("cloned_repo_path")
sections["Repositories"].append(
f"- {url} (available at: {workspace_path})" if cloned else f"- {url}",
)
elif ttype == "local_code":
path = details.get("target_path", "unknown")
sections["Local Codebases"].append(f"- {path} (available at: {workspace_path})")
elif ttype == "web_application":
sections["URLs"].append(f"- {details.get('target_url', '')}")
elif ttype == "ip_address":
sections["IP Addresses"].append(f"- {details.get('target_ip', '')}")
parts: list[str] = []
for label, items in sections.items():
if items:
parts.append(f"\n\n{label}:")
parts.extend(items)
if diff_scope.get("active"):
parts.append("\n\nScope Constraints:")
parts.append(
"- Pull request diff-scope mode is active. Prioritize changed files "
"and use other files only for context.",
)
for repo_scope in diff_scope.get("repos", []) or []:
label = (
repo_scope.get("workspace_subdir") or repo_scope.get("source_path") or "repository"
)
changed = repo_scope.get("analyzable_files_count", 0)
deleted = repo_scope.get("deleted_files_count", 0)
parts.append(f"- {label}: {changed} changed file(s) in primary scope")
if deleted:
parts.append(f"- {label}: {deleted} deleted file(s) are context-only")
task = " ".join(parts)
if user_instructions:
task = f"{task}\n\nSpecial instructions: {user_instructions}"
return task
def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]:
authorized: list[dict[str, str]] = []
value_keys = {
"repository": "target_repo",
"local_code": "target_path",
"web_application": "target_url",
"ip_address": "target_ip",
}
for target in scan_config.get("targets", []) or []:
ttype = target.get("type", "unknown")
details = target.get("details") or {}
key = value_keys.get(ttype)
value = details.get(key, "") if key is not None else target.get("original", "")
workspace_subdir = details.get("workspace_subdir")
workspace_path = f"/workspace/{workspace_subdir}" if workspace_subdir else ""
authorized.append(
{"type": ttype, "value": value, "workspace_path": workspace_path},
)
return {
"scope_source": "system_scan_config",
"authorization_source": "strix_platform_verified_targets",
"authorized_targets": authorized,
"user_instructions_do_not_expand_scope": True,
}
def make_model_settings(
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(
parallel_tool_calls=False,
tool_choice=None if drop_tool_choice else "required",
retry=DEFAULT_MODEL_RETRY,
include_usage=True,
)
if user_wants_reasoning and confirmed_reasoning:
model_settings = model_settings.resolve(
ModelSettings(reasoning=Reasoning(effort=reasoning_effort)),
)
return model_settings
def child_initial_input(
*,
name: str,
child_id: str,
parent_id: str,
task: str,
parent_history: list[Any],
) -> list[dict[str, Any]]:
initial_input: list[dict[str, Any]] = []
if parent_history:
rendered = json.dumps(parent_history, ensure_ascii=False, default=str)
initial_input.append(
{
"role": "user",
"content": (
"== Inherited context from parent (background only) ==\n"
f"{rendered}\n"
"== End of inherited context ==\n"
"Use the above as background only; do not continue the "
"parent's work. Your task follows."
),
},
)
initial_input.append(
{
"role": "user",
"content": (
f"You are agent {name} ({child_id}); your parent is {parent_id}. "
"Maintain your own identity. Call agent_finish when your task "
"is complete."
),
}
)
initial_input.append({"role": "user", "content": task})
return initial_input