Fix error when context-resizing (#5860)

Harden local GGUF detection so a .gguf path is not misrouted to the transformers backend during the brief Windows lock window after llama-server is killed. Catch OSError from stat() and treat the path as the file, while a directory named *.gguf still falls through to the directory scan. Adds regression tests for the lock-window and directory cases.
This commit is contained in:
Suyadi 2026-05-31 14:05:42 +07:00 committed by GitHub
commit 87ee06a993
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 112 additions and 5 deletions

View file

@ -0,0 +1,102 @@
# 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 GGUF routing in detect_gguf_model.
Regression test for the bug where a .gguf file temporarily appears
inaccessible on Windows during llama-server process teardown, causing
is_file() to return False and the model to be routed to the transformers
backend instead of llama-server.
"""
import sys
import os
import types
from pathlib import Path
from unittest.mock import patch
# Stub structlog before importing backend modules (mirrors other tests in this suite)
if "structlog" not in sys.modules:
class _DummyLogger:
def __getattr__(self, _):
return lambda *a, **k: None
sys.modules["structlog"] = types.SimpleNamespace(
get_logger = lambda *a, **k: _DummyLogger(),
BoundLogger = _DummyLogger,
)
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from utils.models.model_config import detect_gguf_model
def test_detects_gguf_file_normally(tmp_path):
"""Normal case: .gguf file exists and is accessible."""
gguf = tmp_path / "gpt-oss-20b-MXFP4.gguf"
gguf.write_bytes(b"")
result = detect_gguf_model(str(gguf))
assert result is not None
assert result.endswith("gpt-oss-20b-MXFP4.gguf")
def test_detects_gguf_when_stat_raises_oserror(tmp_path):
"""
Regression: on Windows, both is_file() and exists() call stat() internally.
During the brief lock window after llama-server is killed, stat() raises
OSError, causing both to return False. detect_gguf_model must still route
to llama-server based on the file extension alone.
"""
gguf = tmp_path / "gpt-oss-20b-MXFP4.gguf"
gguf.write_bytes(b"")
original_stat = Path.stat
def flaky_stat(self, **kwargs):
if self.suffix.lower() == ".gguf":
raise OSError("file temporarily inaccessible (Windows lock window)")
return original_stat(self, **kwargs)
with patch.object(Path, "stat", flaky_stat):
result = detect_gguf_model(str(gguf))
assert result is not None, (
"detect_gguf_model returned None when stat() raised OSError. "
"This causes the model to fall through to the transformers backend."
)
def test_does_not_detect_mmproj_as_main_model(tmp_path):
"""mmproj files must never be returned as the primary model."""
mmproj = tmp_path / "mmproj-model-f16.gguf"
mmproj.write_bytes(b"")
result = detect_gguf_model(str(mmproj))
assert result is None
def test_detects_gguf_in_directory(tmp_path):
"""Directory containing a .gguf file is resolved to that file."""
gguf = tmp_path / "model-Q4_K_M.gguf"
gguf.write_bytes(b"")
result = detect_gguf_model(str(tmp_path))
assert result is not None
assert result.endswith("model-Q4_K_M.gguf")
def test_directory_named_like_gguf_scans_inside(tmp_path):
"""A directory named *.gguf resolves the real .gguf inside, not itself."""
gguf_dir = tmp_path / "mymodel.gguf"
gguf_dir.mkdir()
inner = gguf_dir / "model-Q4_K_M.gguf"
inner.write_bytes(b"")
result = detect_gguf_model(str(gguf_dir))
assert result is not None
assert result.endswith("model-Q4_K_M.gguf")
def test_returns_none_for_non_gguf_path(tmp_path):
"""Non-.gguf paths with no .gguf files inside return None."""
result = detect_gguf_model(str(tmp_path))
assert result is None

View file

@ -1156,13 +1156,18 @@ def detect_gguf_model(path: str) -> Optional[str]:
p = Path(path)
# Case 1: direct .gguf file
if p.suffix.lower() == ".gguf" and p.is_file():
if p.suffix.lower() == ".gguf":
if _is_mmproj(p.name):
return None
# Use absolute (not resolve) to preserve symlink names -- e.g.
# Ollama .studio_links/model.gguf -> blobs/sha256-... should
# keep the readable symlink name, not the opaque blob hash.
return str(p.absolute())
# Extension is authoritative: don't gate on is_file()/exists(), which
# can fail in the Windows lock window after llama-server is killed.
try:
is_dir = p.is_dir()
except OSError:
is_dir = False # stat() unavailable in the lock window
if not is_dir:
return str(p.absolute()) # absolute() keeps symlink names readable
# Directory named "*.gguf": fall through to the dir scan below.
# Case 2: directory containing .gguf files (skip mmproj)
if p.is_dir():