unsloth/studio/backend/tests/test_nudge_tool_calls_wiring.py
Daniel Han 3506371677
Studio: keep the nudge wiring test collectable without the unsloth stack (#6924)
test_nudge_tool_calls_wiring.py imported InferenceBackend from
core.inference.inference, which pulls in unsloth (and thus unsloth_zoo)
at module scope. The dependency-light backend CI matrix job does not
install unsloth_zoo, so the import raised at collection time and aborted
the whole job (831 tests never ran). Guard that one import and fold the
safetensors InferenceBackend checks in only when the unsloth stack is
importable; the orchestrator/llama_cpp/safetensors_agentic wiring is
still asserted unconditionally, and local/full-stack runs keep the
InferenceBackend coverage.
2026-07-06 21:57:56 -07:00

99 lines
4.6 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Wiring guard for the plan-without-action ``nudge_tool_calls`` policy.
Decided policy: the re-prompt is ALWAYS ON for the Studio inference paths
(safetensors, GGUF/llama_cpp, MLX) and OPT-IN for the API (/v1 OpenAI-compat +
Anthropic-compat, controlled by the request's ``nudge_tool_calls``, default off).
Mechanism (verified here without loading a model):
* every backend tool-loop entry point accepts and forwards ``nudge_tool_calls``
(safetensors -> ``InferenceBackend``; MLX -> ``InferenceOrchestrator``; both
call the shared ``run_safetensors_tool_loop``; GGUF -> ``LlamaCppBackend``);
* the safetensors/MLX loop gates the retry on a truthy flag (new retry ->
opt-in), while the GGUF loop keeps its pre-existing default-on behaviour
(``None`` keeps nudging) so an omitted flag never disables GGUF;
* the API request models default the flag to ``None`` (opt-in / off);
* the Studio-facing routes forward the request's flag, and the Studio frontend
sends ``nudge_tool_calls: true`` -- exercised behaviourally in
``test_safetensors_tool_loop.py`` and ``test_llama_cpp_tool_loop.py``.
"""
import inspect
from core.inference.llama_cpp import LlamaCppBackend
from core.inference.orchestrator import InferenceOrchestrator
from core.inference.safetensors_agentic import run_safetensors_tool_loop
try:
# core.inference.inference imports unsloth at module scope, which requires
# unsloth_zoo. The dependency-light backend CI matrix job does not install
# it, so the safetensors InferenceBackend is folded into the checks below
# only when the unsloth stack is importable (local runs / full CI); the
# other entry points are always checked.
from core.inference.inference import InferenceBackend
except ImportError:
InferenceBackend = None
def _params(fn):
return inspect.signature(fn).parameters
def test_shared_loop_accepts_nudge_flag():
assert "nudge_tool_calls" in _params(run_safetensors_tool_loop)
def test_backends_accept_the_flag():
methods = [
InferenceOrchestrator.generate_chat_completion_with_tools,
LlamaCppBackend.generate_chat_completion_with_tools,
]
if InferenceBackend is not None: # safetensors path; needs the unsloth stack
methods.append(InferenceBackend.generate_chat_completion_with_tools)
for method in methods:
assert "nudge_tool_calls" in _params(method), method.__qualname__
def test_delegating_backends_forward_the_flag_to_the_shared_loop():
# safetensors (in-process transformers) and MLX (parent-process orchestrator)
# both delegate to run_safetensors_tool_loop; GGUF runs its own in-file loop
# and consumes the flag directly (asserted separately by the gate test).
methods = [InferenceOrchestrator.generate_chat_completion_with_tools]
if InferenceBackend is not None: # safetensors path; needs the unsloth stack
methods.append(InferenceBackend.generate_chat_completion_with_tools)
for method in methods:
src = inspect.getsource(method)
assert "nudge_tool_calls = nudge_tool_calls" in src, method.__qualname__
def test_safetensors_loop_is_opt_in_while_gguf_stays_default_on():
# Safetensors/MLX: the retry is new here, so it requires a truthy flag.
sf_src = inspect.getsource(run_safetensors_tool_loop)
assert "and nudge_tool_calls" in sf_src
# GGUF: pre-existing nudge must not be accidentally disabled -- an omitted
# (None) flag keeps nudging; only an explicit False turns it off.
gguf_src = inspect.getsource(LlamaCppBackend.generate_chat_completion_with_tools)
assert "nudge_tool_calls is None or nudge_tool_calls" in gguf_src
def test_api_request_models_default_the_flag_off():
from models.inference import AnthropicMessagesRequest, ChatCompletionRequest
for model in (ChatCompletionRequest, AnthropicMessagesRequest):
field = model.model_fields["nudge_tool_calls"]
assert field.default is None, model.__name__
def test_studio_routes_forward_the_request_flag():
# The Studio chat frontend posts to /v1/chat/completions and /v1/messages
# with nudge_tool_calls=true; the route handlers forward the request value
# (external API clients that omit it fall back to the opt-in default).
from routes import inference as routes_inference
for handler in (
routes_inference.openai_chat_completions,
routes_inference.anthropic_messages,
):
src = inspect.getsource(handler)
assert "nudge_tool_calls = payload.nudge_tool_calls" in src, handler.__name__