Both review points were correct. - detect_mtp_file takes an optional accept callback, so a caller with extra rules keeps scanning its candidates in preference order. A native load whose size-preferred MTP/ copy resolved out of the grant was treating that rejection as exhaustion and disabling MTP, even with a valid copy beside it. Both the load path and reload dedup now pass their admissibility check straight in, which also removes the manual two-step retry - clear activeModelIsLocal and specFallbackReason in setCheckpoint when the checkpoint really changes. Both describe the model being replaced, so a selection change was classifying the newly chosen model by the previous one's provenance. They are cleared together: dropping one alone pairs a stale reason with the wrong recovery text, which is the flip fixed earlier for the no-active-model branch. The load or status response reseeds both.
386 lines
15 KiB
Python
386 lines
15 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
|
|
|
|
"""Native GGUF companion path validation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent)
|
|
if _BACKEND_DIR not in sys.path:
|
|
sys.path.insert(0, _BACKEND_DIR)
|
|
|
|
from routes.inference import _validate_native_gguf_companion
|
|
from routes.inference import _request_matches_loaded_settings
|
|
from routes.inference import _validate_native_mtp_drafter
|
|
from routes.inference import _loaded_is_local_model
|
|
from routes.inference import _native_gguf_companion_usable
|
|
from utils.models.model_config import detect_mtp_file
|
|
from core.inference.llama_cpp import LlamaCppBackend
|
|
from models.inference import LoadRequest
|
|
|
|
|
|
def _write_pair(tmp_path: Path, folder: str | None = None) -> tuple[Path, Path]:
|
|
tmp_path.mkdir(parents = True, exist_ok = True)
|
|
weight = tmp_path / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
parent = tmp_path if folder is None else tmp_path / folder
|
|
parent.mkdir(parents = True, exist_ok = True)
|
|
companion = parent / "mtp-model.gguf"
|
|
companion.write_bytes(b"draft")
|
|
return weight, companion
|
|
|
|
|
|
def test_native_companion_allows_model_directory(tmp_path):
|
|
weight, companion = _write_pair(tmp_path)
|
|
_validate_native_gguf_companion(str(companion), str(weight), "vision companion")
|
|
|
|
|
|
@pytest.mark.parametrize("folder", ["MTP", "mtp", "MtP"])
|
|
def test_native_mtp_companion_allows_mtp_directory(tmp_path, folder):
|
|
weight, companion = _write_pair(tmp_path, folder)
|
|
_validate_native_gguf_companion(
|
|
str(companion), str(weight), "MTP drafter", allow_mtp_subdir = True
|
|
)
|
|
|
|
|
|
def test_native_mtp_companion_allows_repo_root_mtp_directory(tmp_path):
|
|
quant_dir = tmp_path / "Q4_0"
|
|
weight, _ = _write_pair(quant_dir)
|
|
companion_dir = tmp_path / "MTP"
|
|
companion_dir.mkdir()
|
|
companion = companion_dir / "mtp-model.gguf"
|
|
companion.write_bytes(b"draft")
|
|
|
|
_validate_native_gguf_companion(
|
|
str(companion),
|
|
str(weight),
|
|
"MTP drafter",
|
|
allow_mtp_subdir = True,
|
|
mtp_search_root = str(tmp_path),
|
|
)
|
|
|
|
|
|
def test_native_mtp_companion_rejects_unrelated_search_root(tmp_path):
|
|
quant_dir = tmp_path / "repo" / "Q4_0"
|
|
weight, _ = _write_pair(quant_dir)
|
|
companion_dir = tmp_path / "MTP"
|
|
companion_dir.mkdir()
|
|
companion = companion_dir / "mtp-model.gguf"
|
|
companion.write_bytes(b"draft")
|
|
|
|
with pytest.raises(HTTPException, match = "must live beside"):
|
|
_validate_native_gguf_companion(
|
|
str(companion),
|
|
str(weight),
|
|
"MTP drafter",
|
|
allow_mtp_subdir = True,
|
|
mtp_search_root = str(tmp_path),
|
|
)
|
|
|
|
|
|
def test_reload_dedup_finds_repo_root_mtp_companion(tmp_path, monkeypatch):
|
|
quant_dir = tmp_path / "Q4_0"
|
|
quant_dir.mkdir()
|
|
weight = quant_dir / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
companion_dir = tmp_path / "MTP"
|
|
companion_dir.mkdir()
|
|
companion = companion_dir / "mtp-model.gguf"
|
|
companion.write_bytes(b"draft")
|
|
|
|
monkeypatch.setattr(LlamaCppBackend, "_kill_orphaned_servers", staticmethod(lambda: 0))
|
|
backend = LlamaCppBackend()
|
|
backend._gguf_path = str(weight)
|
|
backend._mtp_draft_path = str(companion)
|
|
|
|
request = LoadRequest(model_path = str(weight))
|
|
assert _request_matches_loaded_settings(request, backend)
|
|
|
|
|
|
def test_reload_dedup_matches_quant_directory_selection(tmp_path, monkeypatch):
|
|
quant_dir = tmp_path / "Q4_0"
|
|
quant_dir.mkdir()
|
|
weight = quant_dir / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
companion_dir = tmp_path / "MTP"
|
|
companion_dir.mkdir()
|
|
companion = companion_dir / "mtp-model.gguf"
|
|
companion.write_bytes(b"draft")
|
|
|
|
monkeypatch.setattr(LlamaCppBackend, "_kill_orphaned_servers", staticmethod(lambda: 0))
|
|
backend = LlamaCppBackend()
|
|
backend._gguf_path = str(weight)
|
|
backend._mtp_draft_path = str(companion)
|
|
|
|
request = LoadRequest(model_path = str(quant_dir), gguf_variant = "Q4_0")
|
|
assert _request_matches_loaded_settings(request, backend)
|
|
|
|
|
|
def test_native_vision_companion_rejects_mtp_directory(tmp_path):
|
|
weight, companion = _write_pair(tmp_path, "MTP")
|
|
with pytest.raises(HTTPException, match = "must live next to"):
|
|
_validate_native_gguf_companion(str(companion), str(weight), "vision companion")
|
|
|
|
|
|
@pytest.mark.parametrize("folder", ["other", "MTP/deeper", "mtp/deeper"])
|
|
def test_native_companion_rejects_arbitrary_nesting(tmp_path, folder):
|
|
weight, companion = _write_pair(tmp_path, folder)
|
|
with pytest.raises(HTTPException, match = "must live beside") as error:
|
|
_validate_native_gguf_companion(
|
|
str(companion), str(weight), "MTP drafter", allow_mtp_subdir = True
|
|
)
|
|
assert error.value.status_code == 400
|
|
|
|
|
|
def test_native_companion_rejects_file_symlink(tmp_path):
|
|
weight, companion = _write_pair(tmp_path)
|
|
link = tmp_path / "mtp-link.gguf"
|
|
try:
|
|
link.symlink_to(companion)
|
|
except OSError as exc:
|
|
pytest.skip(f"symlinks unavailable: {exc}")
|
|
with pytest.raises(HTTPException, match = "regular file"):
|
|
_validate_native_gguf_companion(str(link), str(weight), "MTP drafter")
|
|
|
|
|
|
def test_native_companion_rejects_directory_symlink_escape(tmp_path):
|
|
model_dir = tmp_path / "model"
|
|
outside = tmp_path / "outside"
|
|
model_dir.mkdir()
|
|
outside.mkdir()
|
|
weight = model_dir / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
companion = outside / "mtp-model.gguf"
|
|
companion.write_bytes(b"draft")
|
|
try:
|
|
(model_dir / "MTP").symlink_to(outside, target_is_directory = True)
|
|
except OSError as exc:
|
|
pytest.skip(f"symlinks unavailable: {exc}")
|
|
with pytest.raises(HTTPException, match = "must live beside"):
|
|
_validate_native_gguf_companion(
|
|
str(model_dir / "MTP" / companion.name),
|
|
str(weight),
|
|
"MTP drafter",
|
|
allow_mtp_subdir = True,
|
|
)
|
|
|
|
|
|
def test_native_companion_rejects_missing_file(tmp_path):
|
|
weight = tmp_path / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
with pytest.raises(HTTPException, match = "no longer accessible"):
|
|
_validate_native_gguf_companion(str(tmp_path / "missing.gguf"), str(weight), "MTP drafter")
|
|
|
|
|
|
def test_native_companion_rejects_directory(tmp_path):
|
|
weight = tmp_path / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
companion = tmp_path / "mtp-model.gguf"
|
|
companion.mkdir()
|
|
with pytest.raises(HTTPException, match = "regular file"):
|
|
_validate_native_gguf_companion(str(companion), str(weight), "MTP drafter")
|
|
|
|
|
|
def test_native_companion_rejects_missing_weight(tmp_path):
|
|
companion = tmp_path / "mtp-model.gguf"
|
|
companion.write_bytes(b"draft")
|
|
with pytest.raises(HTTPException, match = "no longer accessible"):
|
|
_validate_native_gguf_companion(
|
|
str(companion), str(tmp_path / "missing.gguf"), "MTP drafter"
|
|
)
|
|
|
|
|
|
def test_native_companion_none_is_noop():
|
|
_validate_native_gguf_companion(None, None, "MTP drafter")
|
|
|
|
|
|
def test_reload_dedup_accepts_native_subdir_fallback(tmp_path, monkeypatch):
|
|
"""A native load whose root drafter was out of bounds launches the MTP/
|
|
copy, so root-first detection never matches it. Dedup must still hold."""
|
|
quant_dir = tmp_path / "Q4_0"
|
|
quant_dir.mkdir()
|
|
weight = quant_dir / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
(tmp_path / "mtp-model.gguf").write_bytes(b"root drafter")
|
|
companion_dir = tmp_path / "MTP"
|
|
companion_dir.mkdir()
|
|
companion = companion_dir / "mtp-model-Q4_0.gguf"
|
|
companion.write_bytes(b"draft")
|
|
|
|
monkeypatch.setattr(LlamaCppBackend, "_kill_orphaned_servers", staticmethod(lambda: 0))
|
|
backend = LlamaCppBackend()
|
|
backend._gguf_path = str(weight)
|
|
backend._mtp_draft_path = str(companion)
|
|
|
|
request = LoadRequest(model_path = str(weight))
|
|
assert _request_matches_loaded_settings(request, backend, None, native_grant_backed = True)
|
|
|
|
|
|
def test_reload_dedup_still_reloads_when_drafter_disappears(tmp_path, monkeypatch):
|
|
"""The fallback comparison must not mask a deleted drafter."""
|
|
quant_dir = tmp_path / "Q4_0"
|
|
quant_dir.mkdir()
|
|
weight = quant_dir / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
companion_dir = tmp_path / "MTP"
|
|
companion_dir.mkdir()
|
|
companion = companion_dir / "mtp-model-Q4_0.gguf"
|
|
companion.write_bytes(b"draft")
|
|
|
|
monkeypatch.setattr(LlamaCppBackend, "_kill_orphaned_servers", staticmethod(lambda: 0))
|
|
backend = LlamaCppBackend()
|
|
backend._gguf_path = str(weight)
|
|
backend._mtp_draft_path = str(companion)
|
|
|
|
companion.unlink()
|
|
request = LoadRequest(model_path = str(weight))
|
|
assert not _request_matches_loaded_settings(request, backend)
|
|
|
|
|
|
def test_reload_dedup_reloads_for_ordinary_load_when_root_drafter_appears(tmp_path, monkeypatch):
|
|
"""The native fallback exception must not swallow a newly added root
|
|
drafter on an ordinary local load, which can reach it."""
|
|
quant_dir = tmp_path / "Q4_0"
|
|
quant_dir.mkdir()
|
|
weight = quant_dir / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
companion_dir = tmp_path / "MTP"
|
|
companion_dir.mkdir()
|
|
companion = companion_dir / "mtp-model-Q4_0.gguf"
|
|
companion.write_bytes(b"draft")
|
|
|
|
monkeypatch.setattr(LlamaCppBackend, "_kill_orphaned_servers", staticmethod(lambda: 0))
|
|
backend = LlamaCppBackend()
|
|
backend._gguf_path = str(weight)
|
|
backend._mtp_draft_path = str(companion)
|
|
|
|
request = LoadRequest(model_path = str(weight))
|
|
# No root drafter yet: both routes dedupe.
|
|
assert _request_matches_loaded_settings(request, backend, None, native_grant_backed = True)
|
|
assert _request_matches_loaded_settings(request, backend, None, native_grant_backed = False)
|
|
|
|
(tmp_path / "mtp-model.gguf").write_bytes(b"root drafter")
|
|
# Native cannot reach the root drafter, so the subdir copy stays current.
|
|
assert _request_matches_loaded_settings(request, backend, None, native_grant_backed = True)
|
|
# An ordinary load would pick the root drafter, so it must reload.
|
|
assert not _request_matches_loaded_settings(request, backend, None, native_grant_backed = False)
|
|
|
|
|
|
def test_reload_dedup_native_load_with_no_admissible_drafter(tmp_path, monkeypatch):
|
|
"""Root drafter out of the grant and no MTP/ copy: the load stores no
|
|
drafter, so dedup must compare against None rather than the root file."""
|
|
quant_dir = tmp_path / "Q4_0"
|
|
quant_dir.mkdir()
|
|
weight = quant_dir / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
(tmp_path / "mtp-model.gguf").write_bytes(b"root drafter")
|
|
|
|
monkeypatch.setattr(LlamaCppBackend, "_kill_orphaned_servers", staticmethod(lambda: 0))
|
|
backend = LlamaCppBackend()
|
|
backend._gguf_path = str(weight)
|
|
backend._mtp_draft_path = None
|
|
|
|
request = LoadRequest(model_path = str(weight))
|
|
assert _request_matches_loaded_settings(request, backend, None, native_grant_backed = True)
|
|
# An ordinary load would launch the root drafter, so it must reload.
|
|
assert not _request_matches_loaded_settings(request, backend, None, native_grant_backed = False)
|
|
|
|
|
|
def test_native_mtp_drafter_rejects_symlinked_later_shard(tmp_path):
|
|
"""llama-server opens sibling shards implicitly, so validating only the
|
|
launch path would let a later shard escape the permitted directory."""
|
|
weight = tmp_path / "model-Q4_0.gguf"
|
|
weight.write_bytes(b"model")
|
|
sub = tmp_path / "MTP"
|
|
sub.mkdir()
|
|
first = sub / "mtp-model-Q4_0-00001-of-00002.gguf"
|
|
first.write_bytes(b"draft")
|
|
outside = tmp_path / "outside.bin"
|
|
outside.write_bytes(b"secret")
|
|
try:
|
|
(sub / "mtp-model-Q4_0-00002-of-00002.gguf").symlink_to(outside)
|
|
except OSError as exc:
|
|
pytest.skip(f"symlinks unavailable: {exc}")
|
|
|
|
with pytest.raises(HTTPException, match = "regular file"):
|
|
_validate_native_mtp_drafter(str(first), str(weight), mtp_search_root = str(tmp_path))
|
|
|
|
|
|
def test_native_mtp_drafter_accepts_regular_shard_set(tmp_path):
|
|
weight = tmp_path / "model-Q4_0.gguf"
|
|
weight.write_bytes(b"model")
|
|
sub = tmp_path / "MTP"
|
|
sub.mkdir()
|
|
first = sub / "mtp-model-Q4_0-00001-of-00002.gguf"
|
|
first.write_bytes(b"draft")
|
|
(sub / "mtp-model-Q4_0-00002-of-00002.gguf").write_bytes(b"draft")
|
|
|
|
_validate_native_mtp_drafter(str(first), str(weight), mtp_search_root = str(tmp_path))
|
|
|
|
|
|
def test_status_provenance_survives_deleted_model_directory(tmp_path, monkeypatch):
|
|
"""Provenance is a load-time fact: a directory removed underneath a running
|
|
server must not turn a local model into a remote one."""
|
|
monkeypatch.setattr(LlamaCppBackend, "_kill_orphaned_servers", staticmethod(lambda: 0))
|
|
backend = LlamaCppBackend()
|
|
backend._is_local_model = True
|
|
# "outputs/gemma" no longer exists, so is_local_path would call it a repo id.
|
|
assert _loaded_is_local_model(backend, False, "outputs/gemma")
|
|
|
|
stale = LlamaCppBackend()
|
|
assert not _loaded_is_local_model(stale, False, "unsloth/gemma-4-12b")
|
|
assert _loaded_is_local_model(stale, True, None)
|
|
|
|
|
|
def test_native_load_skips_rejected_mtp_candidate_for_next_one(tmp_path):
|
|
"""MTP/ can hold several compatible copies. If the size-preferred one is
|
|
out of the grant, the next must be tried instead of disabling MTP."""
|
|
quant_dir = tmp_path / "Q4_0"
|
|
quant_dir.mkdir()
|
|
weight = quant_dir / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
outside = tmp_path.parent / "outside-blob.gguf"
|
|
outside.write_bytes(b"d")
|
|
companion_dir = tmp_path / "MTP"
|
|
companion_dir.mkdir()
|
|
try:
|
|
(companion_dir / "mtp-model-Q4_0.gguf").symlink_to(outside)
|
|
except OSError as exc:
|
|
pytest.skip(f"symlinks unavailable: {exc}")
|
|
larger = companion_dir / "mtp-model-Q8_0.gguf"
|
|
larger.write_bytes(b"d" * 5000)
|
|
|
|
def _usable(candidate: str) -> bool:
|
|
return _native_gguf_companion_usable(candidate, str(weight), mtp_search_root = str(tmp_path))
|
|
|
|
# Preferred by size, but it resolves out of the permitted directory.
|
|
assert not _usable(detect_mtp_file(str(weight), str(tmp_path), skip_root = True))
|
|
assert detect_mtp_file(str(weight), str(tmp_path), skip_root = True, accept = _usable) == str(
|
|
larger.resolve()
|
|
)
|
|
|
|
|
|
def test_native_load_returns_none_when_no_candidate_passes(tmp_path):
|
|
quant_dir = tmp_path / "Q4_0"
|
|
quant_dir.mkdir()
|
|
weight = quant_dir / "model.gguf"
|
|
weight.write_bytes(b"model")
|
|
outside = tmp_path.parent / "outside-only.gguf"
|
|
outside.write_bytes(b"d")
|
|
companion_dir = tmp_path / "MTP"
|
|
companion_dir.mkdir()
|
|
try:
|
|
(companion_dir / "mtp-model-Q4_0.gguf").symlink_to(outside)
|
|
except OSError as exc:
|
|
pytest.skip(f"symlinks unavailable: {exc}")
|
|
|
|
def _usable(candidate: str) -> bool:
|
|
return _native_gguf_companion_usable(candidate, str(weight), mtp_search_root = str(tmp_path))
|
|
|
|
assert detect_mtp_file(str(weight), str(tmp_path), skip_root = True, accept = _usable) is None
|