mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-23 07:44:06 +00:00
* Studio: honor a request's enable_tools: false instead of overriding it The process-wide tool policy was an override, not a default. unsloth studio run installed set_tool_policy(True) at startup, and _effective_enable_tools returned that value whenever it was non-None, so the request's own enable_tools field was never read. The Studio UI sends its tool pills as an explicit request field, and expresses 'every pill off' by omitting enable_tools entirely. Against a True override that omission read as 'tools on', and with enabled_tools also absent the route selected ALL_TOOLS, so a chat with every tool switched off still advertised web_search, python, terminal and render_html. Thread-title generation, which posts to /v1/chat/completions with no tool fields, picked them up the same way. Only unsloth studio run installed the policy, so unsloth studio, the desktop app and Colab behaved correctly and the two commands disagreed on the same UI. Split the policy into two slots. The override still comes from an explicit --enable-tools/--disable-tools and still beats the request. The new default is what an omitted enable_tools falls back to, installed as True by every launcher, so tools stay on for every bind including --secure. A request that says enable_tools: false now turns them off. Frontend sends the off state explicitly rather than by omission, in the local chat, external provider and token-count paths, and pins enable_tools: false on title generation so a 24-token summarisation never carries tool schemas. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: do not let the tools-on default answer a request that stated its intent The safetensors/MLX path resolves _sf_tools_on straight from _effective_enable_tools, without the two withdrawals the GGUF router applies. So the launcher default introduced here reached requests that had already expressed their own tool intent: - tool_choice: "none" with enable_tools omitted resolved to tools on, and with no enabled_tools allowlist that selected every built-in, python and terminal included, turning a standard opt-out into server-side execution. - A client tools catalog with enable_tools omitted made _sf_client_tools false, so the request left the client-tool passthrough for Unsloth's own loop and the caller got built-ins instead of calls for its own functions. The GGUF router avoids both with _client_disabled_tool_calls and _explicit_studio_tool_loop_requested. Draw the same line on the safetensors gate: the default only answers a request that said nothing, so tool_choice: "none", a client catalog, or tool-result history withdraws it. An explicit enable_tools/mcp_enabled ask, and a CLI --enable-tools or --disable-tools, are unchanged. Also read the resolved _sf_tools_on in the _sf_client_tools gate rather than recomputing _effective_enable_tools, which would have hidden the withdrawal. * Studio: let a response_format contract withdraw the tools-on default too _takes_tool_passthrough already ends with _extract_response_format(payload) is not None, so on the GGUF router a structured-output request keeps the passthrough and never enters the server tool loop. The safetensors withdrawal missed it, so a request supplying response_format while omitting enable_tools still resolved _sf_tools_on to true and could select and run every built-in. response_format is not a declared field on ChatCompletionRequest; the model is extra=allow and OpenAI-SDK clients spread extra_body at the top level, so read it through _extract_response_format rather than an attribute. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: resolve the tool policy before protocol selection, and scope the default to unsloth studio run Two fixes for the same root cause: the tools-on default reaching code built around an omitted enable_tools meaning no tools. _sf_server_tool_intent read the raw policy while the withdrawal ran ~40 lines later, so a tool_choice: "none" or response_format request classified the response protocol on the template's tool_use branch and then generated on the plain one. On a model whose reasoning markers live only in the tool template the extractor starts in the wrong mode and can return the answer as reasoning_content. Resolve _sf_cli_policy / _sf_tools_on / _sf_mcp_allowed once, above the classification, and derive the intent from the resolved value. The default is also no longer installed by run_server. It belongs to unsloth studio run, the launcher that has always forced tools on, and which installs it itself. Installing it in _apply_cli_tool_policy extended it to unsloth studio, the desktop app and Colab, where paths that assume an omitted enable_tools means no tools started seeing it: n > 1 is rejected by the tool loop though the plain path implements it, max_tool_calls_per_message: 0 still advertises schemas and the nudge, and the pre-switch passthrough guard does not recognise tool-result history so it 400s a non-streaming continuation. Those paths predate this PR and are unchanged on unsloth studio run; scoping the default keeps them that way everywhere else. unsloth studio run still defaults tools on for every bind, --secure included, and a request's enable_tools: false is still honored. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: correct the tool-policy help and docstrings for the scoped default Scoping the tools-on default to unsloth studio run left three places claiming it applies everywhere. The --help for plain unsloth studio and for a direct run.py launch both said 'Default: on for every bind', which is now the opposite of what those launchers do, and the tool_policy module said 'Launchers install True'. A --help line about a tool-execution default is worth keeping exact. unsloth studio run's own help is unchanged, since it is the launcher that does default them on. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
40 lines
1.5 KiB
Python
40 lines
1.5 KiB
Python
# Copyright 2025-present the Unsloth AI Inc. team. All rights reserved.
|
|
|
|
"""Pure resolver for `unsloth studio [run] --enable-tools/--disable-tools`.
|
|
|
|
Kept as a standalone module so the truth table can be unit-tested
|
|
without spinning up Typer or the studio venv.
|
|
"""
|
|
|
|
from typing import Callable, Optional
|
|
|
|
import typer
|
|
|
|
# Loopback aliases; any other bind address is treated as network-reachable.
|
|
# Mirrored in studio/backend/utils/host_policy.py (kept separate because the
|
|
# backend is self-contained); keep the two in sync.
|
|
_LOOPBACK_HOSTS = frozenset({"127.0.0.1", "localhost", "::1"})
|
|
|
|
|
|
def is_external_host(host: str) -> bool:
|
|
"""True when `host` is reachable from beyond loopback."""
|
|
return host.lower() not in _LOOPBACK_HOSTS
|
|
|
|
|
|
def resolve_tool_policy(
|
|
host: str,
|
|
flag: Optional[bool],
|
|
yes: bool,
|
|
silent: bool,
|
|
prompt: Callable[[str], bool] = typer.confirm,
|
|
) -> Optional[bool]:
|
|
"""Resolve the process-wide server-side tool OVERRIDE.
|
|
|
|
An explicit --enable-tools/--disable-tools (`flag`) forces tools on/off for
|
|
every request. With no flag the result is None: tools still default on for
|
|
every bind (the backend installs that default in `_apply_cli_tool_policy`),
|
|
but as a default rather than an override, so a request's own
|
|
`enable_tools: false` is honored -- which is what the Studio UI sends with
|
|
its tool pills off. `host`, `yes`, `silent`, `prompt` are kept for signature
|
|
compatibility; no bind prompts."""
|
|
return flag
|