unsloth/tests/studio/test_cli_run_alias.py
Roland Tannous dbea77e347
Studio: forward llama-server args from unsloth studio run , activate unsloth run , and allow passing model:quant to load models (#5271)
* Studio: forward unknown CLI args directly to llama-server

`unsloth studio run --model X --top-k 20 --chat-template-file foo.jinja`
now passes the unknown flags through to the llama-server subprocess.
Adds a denylist for flags Studio manages (port, -m, -c, --api-key, -ngl,
--flash-attn, --no-context-shift, --jinja, GPU-fit, model-identity, ...)
that returns HTTP 400 on collision. HTTP callers can supply the same
list via LoadRequest.llama_extra_args.

* Studio: accept `--model org/repo:variant` shorthand in `unsloth studio run`

Mirrors llama.cpp's `-hf <repo>:<quant>` and ollama's pull syntax so
`unsloth studio run --model unsloth/gpt-oss-20b-GGUF:UD-Q4_K_XL` is
equivalent to `--model unsloth/... --gguf-variant UD-Q4_K_XL`. Local
paths and Windows drive letters are preserved verbatim. If both an
embedded variant and an explicit `--gguf-variant` are given and they
disagree, the command fails with a clear error.

* Studio: register `unsloth run` as alias for `unsloth studio run`

Top-level `unsloth run --model ...` is now equivalent to
`unsloth studio run --model ...`. Same context_settings, so unknown
flags continue to pass through to llama-server.

* Studio: let users override soft-managed llama-server flags from CLI

Trims the denylist to flags Studio fundamentally cannot share with
the user (model identity, --host/--port/--path/--api-prefix,
--api-key, --ssl-*, --webui, --models-*). Soft-managed flags --
-c/--ctx-size, --parallel, --flash-attn, --no-context-shift,
--jinja, -ngl, -t/--threads, --fit* -- now pass through and override
Studio's auto-set version via llama.cpp's last-wins CLI parsing.

Lets users tune their run on the spot:
  unsloth run --model X -c 131072 --parallel 1 --threads 32

* Studio: accept `-hf` / `-hfr` / `--hf-repo` as aliases for `--model`

Matches llama-server's `-hf <repo>:<quant>` spelling so users coming
from llama.cpp can use the same flag. Typer claims the aliases before
the pass-through validator runs, so the HTTP-API denylist on those
flags is unaffected.

  unsloth run -hf unsloth/gpt-oss-20b-GGUF:UD-Q4_K_XL
2026-05-04 17:08:04 +04:00

69 lines
2.4 KiB
Python

"""Tests that ``unsloth run`` is registered as a top-level alias for
``unsloth studio run``.
AST-based to avoid importing ``unsloth_cli`` (which pulls in the heavy
training stack) at test-collection time.
"""
from __future__ import annotations
import ast
from pathlib import Path
_CLI_INIT = Path(__file__).resolve().parents[2] / "unsloth_cli" / "__init__.py"
def _module_calls(source: str):
tree = ast.parse(source)
for node in ast.walk(tree):
if isinstance(node, ast.Call):
yield node
def test_top_level_run_alias_registered():
"""`app.command("run", ...)` must be invoked with studio_run as its target."""
source = _CLI_INIT.read_text()
# Find ``app.command("run", ...)`` call -- the decorator-call form.
found_decorator_call = False
for call in _module_calls(source):
# Match ``app.command(...)`` syntactically.
if not (
isinstance(call.func, ast.Attribute)
and call.func.attr == "command"
and isinstance(call.func.value, ast.Name)
and call.func.value.id == "app"
):
continue
# Decorator-call form has a string literal "run" as the first
# positional or as keyword ``name="run"``.
first_pos = call.args[0] if call.args else None
keyword_name = next(
(kw.value for kw in call.keywords if kw.arg == "name"), None
)
is_run = (isinstance(first_pos, ast.Constant) and first_pos.value == "run") or (
isinstance(keyword_name, ast.Constant) and keyword_name.value == "run"
)
if is_run:
found_decorator_call = True
break
assert (
found_decorator_call
), 'Expected `app.command("run", ...)` registration in unsloth_cli/__init__.py'
def test_studio_run_imported_for_alias():
"""The alias must wire up to the studio.run function, not redefine it."""
source = _CLI_INIT.read_text()
tree = ast.parse(source)
has_import = False
for node in ast.walk(tree):
if not isinstance(node, ast.ImportFrom):
continue
if node.module != "unsloth_cli.commands.studio":
continue
for alias in node.names:
if alias.name == "run":
has_import = True
break
assert has_import, "Expected `from unsloth_cli.commands.studio import run` in unsloth_cli/__init__.py"