* Studio: trim serving-log noise and surface llama-server engine stats Studio prints one structured line per HTTP request, so the SPA's polling and per-invalidation fan-out bury the lines that matter. - Dedup identical successful GETs within a short window (default 300ms, UNSLOTH_STUDIO_ACCESS_LOG_DEDUP_MS) so a burst logs once. The dedup key includes the query string, so distinct query-driven GETs are not collapsed. Runs after the response is sent, so it adds no request latency; mutations, non-2xx, and loading polls are untouched. - Collapse pure-liveness polls (/api/health, /api/auth/status, /api/inference/status, /api/inference/monitor) to a longer heartbeat (default 10s, UNSLOTH_STUDIO_ACCESS_LOG_POLL_DEDUP_MS). The API monitor console polls /monitor every 1.5s while open. - Translate llama-server's Prometheus /metrics into a periodic vLLM-style engine_stats line (generation/prompt throughput and requests in flight) from a daemon poller, gated on UNSLOTH_STUDIO_ENGINE_STATS. Throughput uses llama-server's predicted_tokens_seconds / prompt_tokens_seconds gauges, with a tokens_predicted_total / prompt_tokens_total counter-delta fallback; it does not use n_decode_total (which counts llama_decode() calls, not tokens). No KV field is emitted, since llama.cpp does not expose kv_cache_usage_ratio. --metrics is added only when probe_server_capabilities reports the binary supports it, so older/custom binaries still load. The poller keeps retrying through transient scrape failures (stop() drives shutdown) and a malformed sample cannot crash its thread. - api_monitor.append_reply: once the preview cap is reached, skip the per-chunk re-concat (avoids O(n^2) on long generations) while still recording the "..." truncation marker for a reply that lands exactly on the cap. - unsloth studio --verbose and unsloth studio run --verbose both restore every per-request log; --verbose before a subcommand is rejected with guidance (matching --secure / --parallel). run --verbose still forwards --log-verbose to llama-server, preserving the pre-existing pass-through verbosity. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
154 lines
5.3 KiB
Python
154 lines
5.3 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
|
|
|
|
"""Tests for the `--verbose/-v` Studio flag: option registration on both the
|
|
plain callback and the `run` subcommand, re-exec forwarding, the access-log
|
|
env override, and rejection before a subcommand. Modeled on
|
|
test_studio_secure_flag.py."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from typer.testing import CliRunner
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
if str(_REPO_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(_REPO_ROOT))
|
|
|
|
_DEDUP = "UNSLOTH_STUDIO_ACCESS_LOG_DEDUP_MS"
|
|
_POLL = "UNSLOTH_STUDIO_ACCESS_LOG_POLL_DEDUP_MS"
|
|
_BASE = ["--model", "unsloth/Qwen3-1.7B-GGUF"]
|
|
|
|
|
|
def _studio():
|
|
from unsloth_cli.commands import studio as _studio_mod
|
|
return _studio_mod
|
|
|
|
|
|
# ── option registration ──────────────────────────────────────────────
|
|
|
|
|
|
def test_run_exposes_verbose_option_default_off():
|
|
import inspect
|
|
|
|
opt = inspect.signature(_studio().run).parameters["verbose"].default
|
|
decls = set(getattr(opt, "param_decls", []) or [])
|
|
assert "--verbose" in decls and "-v" in decls
|
|
assert getattr(opt, "default", None) is False
|
|
|
|
|
|
def test_studio_default_exposes_verbose_option_default_off():
|
|
import inspect
|
|
|
|
opt = inspect.signature(_studio().studio_default).parameters["verbose"].default
|
|
decls = set(getattr(opt, "param_decls", []) or [])
|
|
assert "--verbose" in decls and "-v" in decls
|
|
assert getattr(opt, "default", None) is False
|
|
|
|
|
|
# ── re-exec capture plumbing (mirrors test_studio_secure_flag.py) ─────
|
|
|
|
|
|
class _ExecCaptured(SystemExit):
|
|
def __init__(self, argv):
|
|
super().__init__(0)
|
|
self.argv = list(argv)
|
|
|
|
|
|
def _invoke_run(monkeypatch, args):
|
|
import typer as _typer
|
|
|
|
studio_mod = _studio()
|
|
captured = []
|
|
monkeypatch.setattr(sys, "prefix", "/nonexistent/outer/venv")
|
|
fake_venv = Path("/fake/studio/venv/unsloth_studio")
|
|
monkeypatch.setattr(studio_mod, "_studio_venv_python", lambda: fake_venv / "bin" / "python")
|
|
fake_bin = fake_venv / "bin" / "unsloth"
|
|
real_is_file = Path.is_file
|
|
monkeypatch.setattr(
|
|
Path,
|
|
"is_file",
|
|
lambda self: True if str(self) == str(fake_bin) else real_is_file(self),
|
|
)
|
|
from unsloth_cli import _tool_policy as _tp_mod
|
|
|
|
monkeypatch.setattr(
|
|
_tp_mod,
|
|
"resolve_tool_policy",
|
|
lambda host, flag, yes, silent: False if flag is None else bool(flag),
|
|
)
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
|
|
def fake_execvp(file, argv):
|
|
captured.append(list(argv))
|
|
raise _ExecCaptured(argv)
|
|
|
|
monkeypatch.setattr(studio_mod.os, "execvp", fake_execvp)
|
|
app = _typer.Typer()
|
|
app.command(
|
|
context_settings = {"allow_extra_args": True, "ignore_unknown_options": True},
|
|
)(studio_mod.run)
|
|
CliRunner().invoke(app, args, catch_exceptions = True)
|
|
return captured
|
|
|
|
|
|
# ── re-exec forwarding + env override ─────────────────────────────────
|
|
|
|
|
|
def test_run_verbose_sets_env_and_forwards_on_reexec(monkeypatch):
|
|
monkeypatch.delenv(_DEDUP, raising = False)
|
|
monkeypatch.delenv(_POLL, raising = False)
|
|
captured = _invoke_run(monkeypatch, _BASE + ["--verbose"])
|
|
assert len(captured) == 1, captured
|
|
assert "--verbose" in captured[0], captured[0]
|
|
import os as _os
|
|
|
|
assert _os.environ.get(_DEDUP) == "0"
|
|
assert _os.environ.get(_POLL) == "0"
|
|
|
|
|
|
def test_run_without_verbose_leaves_env_unset(monkeypatch):
|
|
monkeypatch.delenv(_DEDUP, raising = False)
|
|
monkeypatch.delenv(_POLL, raising = False)
|
|
captured = _invoke_run(monkeypatch, _BASE)
|
|
assert len(captured) == 1, captured
|
|
assert "--verbose" not in captured[0], captured[0]
|
|
assert "--log-verbose" not in captured[0], captured[0]
|
|
import os as _os
|
|
|
|
assert _os.environ.get(_DEDUP) is None
|
|
assert _os.environ.get(_POLL) is None
|
|
|
|
|
|
def test_run_verbose_preserves_llama_server_verbosity(monkeypatch):
|
|
# Studio consumes --verbose but still forwards llama-server's own verbosity.
|
|
monkeypatch.delenv(_DEDUP, raising = False)
|
|
monkeypatch.delenv(_POLL, raising = False)
|
|
captured = _invoke_run(monkeypatch, _BASE + ["--verbose"])
|
|
assert len(captured) == 1, captured
|
|
assert "--log-verbose" in captured[0], captured[0]
|
|
|
|
|
|
def test_run_verbose_does_not_duplicate_existing_llama_verbose(monkeypatch):
|
|
monkeypatch.delenv(_DEDUP, raising = False)
|
|
monkeypatch.delenv(_POLL, raising = False)
|
|
captured = _invoke_run(monkeypatch, _BASE + ["--verbose", "--log-verbose"])
|
|
assert len(captured) == 1, captured
|
|
assert captured[0].count("--log-verbose") == 1, captured[0]
|
|
|
|
|
|
# ── --verbose before a subcommand is rejected ─────────────────────────
|
|
|
|
|
|
def test_studio_default_rejects_verbose_with_subcommand():
|
|
import typer as _typer
|
|
|
|
studio_mod = _studio()
|
|
app = _typer.Typer()
|
|
app.add_typer(studio_mod.studio_app, name = "studio")
|
|
result = CliRunner().invoke(app, ["studio", "--verbose", "run", "--model", "X"])
|
|
assert result.exit_code == 2, result.output
|
|
combined = (result.output or "") + (getattr(result, "stderr", "") or "")
|
|
assert "--verbose" in combined, combined
|