Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions unsloth_cli/commands/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
help = "Retry once with a nudge when a non-streaming passthrough tool call can't be healed. "
"On by default; when the flag is omitted an inherited UNSLOTH_TOOL_CALL_NUDGE is kept.",
)
_REASONING_OPTION = typer.Option(
None,
"--reasoning",
rich_help_panel = _PANEL_SERVER,
help = (
"llama-server reasoning mode for an auto-started coding-agent server. "
"Defaults to off so tool calls stay in the structured tool channel; use "
"'auto' or 'on' to opt back into model reasoning."
),
)
# Sampling overrides pin a value on the auto-started server (winning over the client and the
# per-model recommendation). Default unset -> the model's recommended sampling is used.
_TEMPERATURE_OPTION = typer.Option(
Expand Down Expand Up @@ -479,6 +489,7 @@ class ServerOptions(NamedTuple):
enable_tools: bool = False
tool_call_healing: Optional[bool] = None
tool_call_nudging: Optional[bool] = None
reasoning: Optional[Literal["on", "off", "auto"]] = None
temperature: Optional[float] = None
top_p: Optional[float] = None
top_k: Optional[int] = None
Expand Down Expand Up @@ -1020,6 +1031,10 @@ def _start_studio_server(
# Own session/process group so a mid-session Ctrl+C (cancel a turn) doesn't reach the
# server. It survives a successful agent session; torn down on startup/launch failure.
child_env = os.environ.copy()
# Current llama-server versions read this documented env equivalent of --reasoning.
# Older managed versions ignore an unknown env variable instead of failing startup on
# an unknown passthrough CLI flag. An omitted start option still defaults to off.
child_env["LLAMA_ARG_REASONING"] = server.reasoning or "off"
# Pass the marker via env so an older launcher ignores it instead of treating an
# unknown CLI flag as a llama-server arg; new launchers preserve it across re-exec.
child_env[_START_API_KEY_MARKER_ENV] = "1"
Expand Down Expand Up @@ -1159,6 +1174,14 @@ def _require_studio(
"and re-run to apply them.",
err = True,
)
if server_options.reasoning is not None:
typer.echo(
f"Warning: an Unsloth server is already running at {base}; "
f"--reasoning {server_options.reasoning} applies only when this command starts "
"the server, so the running server keeps its current reasoning mode. Stop it "
"with `unsloth studio stop` and re-run to apply the override.",
err = True,
)
return base, None
expected = os.environ.get("UNSLOTH_STUDIO_URL", "http://127.0.0.1:8888").rstrip("/")
# Auto-start a local server only for an interactive launch with a model to serve, and
Expand Down Expand Up @@ -3048,6 +3071,7 @@ def claude(
enable_tools: bool = _ENABLE_TOOLS_OPTION,
tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
temperature: Optional[float] = _TEMPERATURE_OPTION,
top_p: Optional[float] = _TOP_P_OPTION,
top_k: Optional[int] = _TOP_K_OPTION,
Expand All @@ -3072,6 +3096,7 @@ def claude(
enable_tools = enable_tools,
tool_call_healing = tool_call_healing,
tool_call_nudging = tool_call_nudging,
reasoning = reasoning,
temperature = temperature,
top_p = top_p,
top_k = top_k,
Expand Down Expand Up @@ -3166,6 +3191,7 @@ def codex(
enable_tools: bool = _ENABLE_TOOLS_OPTION,
tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
temperature: Optional[float] = _TEMPERATURE_OPTION,
top_p: Optional[float] = _TOP_P_OPTION,
top_k: Optional[int] = _TOP_K_OPTION,
Expand All @@ -3190,6 +3216,7 @@ def codex(
enable_tools = enable_tools,
tool_call_healing = tool_call_healing,
tool_call_nudging = tool_call_nudging,
reasoning = reasoning,
temperature = temperature,
top_p = top_p,
top_k = top_k,
Expand Down Expand Up @@ -3265,6 +3292,7 @@ def openclaw(
enable_tools: bool = _ENABLE_TOOLS_OPTION,
tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
temperature: Optional[float] = _TEMPERATURE_OPTION,
top_p: Optional[float] = _TOP_P_OPTION,
top_k: Optional[int] = _TOP_K_OPTION,
Expand All @@ -3289,6 +3317,7 @@ def openclaw(
enable_tools = enable_tools,
tool_call_healing = tool_call_healing,
tool_call_nudging = tool_call_nudging,
reasoning = reasoning,
temperature = temperature,
top_p = top_p,
top_k = top_k,
Expand Down Expand Up @@ -3346,6 +3375,7 @@ def opencode(
enable_tools: bool = _ENABLE_TOOLS_OPTION,
tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
temperature: Optional[float] = _TEMPERATURE_OPTION,
top_p: Optional[float] = _TOP_P_OPTION,
top_k: Optional[int] = _TOP_K_OPTION,
Expand All @@ -3370,6 +3400,7 @@ def opencode(
enable_tools = enable_tools,
tool_call_healing = tool_call_healing,
tool_call_nudging = tool_call_nudging,
reasoning = reasoning,
temperature = temperature,
top_p = top_p,
top_k = top_k,
Expand Down Expand Up @@ -3507,6 +3538,7 @@ def hermes(
enable_tools: bool = _ENABLE_TOOLS_OPTION,
tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
temperature: Optional[float] = _TEMPERATURE_OPTION,
top_p: Optional[float] = _TOP_P_OPTION,
top_k: Optional[int] = _TOP_K_OPTION,
Expand All @@ -3533,6 +3565,7 @@ def hermes(
enable_tools = enable_tools,
tool_call_healing = tool_call_healing,
tool_call_nudging = tool_call_nudging,
reasoning = reasoning,
temperature = temperature,
top_p = top_p,
top_k = top_k,
Expand Down Expand Up @@ -3564,6 +3597,7 @@ def pi(
enable_tools: bool = _ENABLE_TOOLS_OPTION,
tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
temperature: Optional[float] = _TEMPERATURE_OPTION,
top_p: Optional[float] = _TOP_P_OPTION,
top_k: Optional[int] = _TOP_K_OPTION,
Expand All @@ -3588,6 +3622,7 @@ def pi(
enable_tools = enable_tools,
tool_call_healing = tool_call_healing,
tool_call_nudging = tool_call_nudging,
reasoning = reasoning,
temperature = temperature,
top_p = top_p,
top_k = top_k,
Expand Down
36 changes: 34 additions & 2 deletions unsloth_cli/tests/test_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -1993,6 +1993,8 @@ def poll(self):
start._start_studio_server("http://127.0.0.1:8888", "unsloth/M-GGUF", start.LoadOptions())
cmd, env = captured["command"], captured["kwargs"]["env"]
assert "--disable-tools" in cmd and "--enable-tools" not in cmd
assert "--reasoning" not in cmd
assert env["LLAMA_ARG_REASONING"] == "off"
assert "--gpu-memory-mode" not in cmd
assert env["UNSLOTH_DISABLE_TOOL_CALL_HEALING"] == "0"
assert env["UNSLOTH_TOOL_CALL_NUDGE"] == "1"
Expand All @@ -2002,10 +2004,17 @@ def poll(self):
"http://127.0.0.1:8888",
"unsloth/M-GGUF",
start.LoadOptions(),
start.ServerOptions(enable_tools = True, tool_call_healing = False, tool_call_nudging = False),
start.ServerOptions(
enable_tools = True,
tool_call_healing = False,
tool_call_nudging = False,
reasoning = "auto",
),
)
cmd, env = captured["command"], captured["kwargs"]["env"]
assert "--enable-tools" in cmd and "--disable-tools" not in cmd
assert "--reasoning" not in cmd
assert env["LLAMA_ARG_REASONING"] == "auto"
assert env["UNSLOTH_DISABLE_TOOL_CALL_HEALING"] == "1"
assert env["UNSLOTH_TOOL_CALL_NUDGE"] == "0"

Expand Down Expand Up @@ -2118,7 +2127,25 @@ def test_require_studio_no_sampling_warning_without_pins(monkeypatch, capsys):
server_options = start.ServerOptions(enable_tools = True),
)
assert base == BASE and server is None
assert "sampling" not in capsys.readouterr().err.lower()
assert capsys.readouterr().err == ""


@pytest.mark.parametrize("reasoning", ["on", "off", "auto"])
def test_require_studio_warns_on_explicit_reasoning_when_reusing_server(
monkeypatch, capsys, reasoning
):
monkeypatch.setattr(start, "find_studio_server", lambda: BASE)
base, server = start._require_studio(
"unsloth/M-GGUF",
start.LoadOptions(),
serve = True,
server_options = start.ServerOptions(reasoning = reasoning),
)
assert base == BASE and server is None
err = capsys.readouterr().err
assert "already running" in err
assert f"--reasoning {reasoning}" in err
assert "unsloth studio stop" in err


def test_start_claude_parses_sampling_flags(fake_studio, monkeypatch):
Expand Down Expand Up @@ -2153,11 +2180,14 @@ def fake_start(
"0.3",
"--top-k",
"40",
"--reasoning",
"on",
],
)
assert result.exit_code == 0, result.output
so = captured["server_options"]
assert so.temperature == 0.3 and so.top_k == 40 and so.top_p is None
assert so.reasoning == "on"


def test_connect_model_bare_id_matches_loaded_without_reload(fake_studio):
Expand Down Expand Up @@ -2681,6 +2711,8 @@ def poll(self):
cmd = captured["command"]
assert cmd[1] == "run"
assert "--disable-tools" in cmd and "--no-cloudflare" in cmd
assert "--reasoning" not in cmd
assert captured["kwargs"]["env"]["LLAMA_ARG_REASONING"] == "off"
assert cmd[cmd.index("--model") + 1] == "unsloth/Qwen3-1.7B-GGUF:UD-Q4_K_XL"
assert cmd[cmd.index("--gguf-variant") + 1] == "UD-Q4_K_XL"
assert cmd[cmd.index("--context-length") + 1] == "8192"
Expand Down
Loading