# 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 :func:`utils.models.model_config.detect_mmproj_file` (#5347).""" from __future__ import annotations from pathlib import Path import struct from utils.models.model_config import ( _detect_family_token, detect_mmproj_file, mmproj_matches_model_family, ) _GGUF_MAGIC = 0x46554747 def _gguf_with_general(path: Path, fields: dict) -> Path: """Write a minimal GGUF with only ``general.*`` string KVs.""" body = b"" for k, v in fields.items(): kb = k.encode("utf-8") vb = v.encode("utf-8") body += struct.pack(" Path: path.parent.mkdir(parents = True, exist_ok = True) path.write_bytes(b"") return path def test_returns_none_when_no_mmproj(tmp_path: Path): model = _touch(tmp_path / "Qwen3.5-9B-Q4_K_M.gguf") assert detect_mmproj_file(str(model)) is None def test_single_matching_family_mmproj_picked(tmp_path: Path): """Single same-family projector is returned (historical behaviour).""" model = _touch(tmp_path / "Qwen3.5-9B-Q4_K_M.gguf") mmproj = _touch(tmp_path / "Qwen3.5-9B-BF16-mmproj.gguf") assert detect_mmproj_file(str(model)) == str(mmproj.resolve()) def test_hf_style_unprefixed_mmproj_still_works(tmp_path: Path): """HF convention: weight + ``mmproj-F16.gguf`` sibling.""" model = _touch(tmp_path / "model.gguf") mmproj = _touch(tmp_path / "mmproj-F16.gguf") assert detect_mmproj_file(str(model)) == str(mmproj.resolve()) def test_blocks_single_cross_family_projector(tmp_path: Path): """#5347 core: Qwen weight + lone Gemma mmproj returns None.""" model = _touch(tmp_path / "Qwen3.5-9B-Q4_K_M.gguf") _touch(tmp_path / "gemma-4-26B-A4B-it.mmproj-q8_0.gguf") assert detect_mmproj_file(str(model)) is None def test_picks_matching_family_among_mixed_candidates(tmp_path: Path): """Mixed Qwen + Gemma projectors: pick Qwen, drop Gemma.""" model = _touch(tmp_path / "Qwen3.5-9B-Q4_K_M.gguf") qwen_mm = _touch(tmp_path / "Qwen3.5-9B-BF16-mmproj.gguf") _touch(tmp_path / "gemma-4-26B-A4B-it.mmproj-q8_0.gguf") assert detect_mmproj_file(str(model)) == str(qwen_mm.resolve()) def test_prefers_longest_prefix_within_same_family(tmp_path: Path): """Same family, different sizes: longest shared stem prefix wins.""" model = _touch(tmp_path / "Qwen3.5-35B-A3B-UD-Q4_K_L.gguf") _touch(tmp_path / "Qwen3.5-9B-BF16-mmproj.gguf") big_mm = _touch(tmp_path / "Qwen3.5-35B-A3B-BF16-mmproj.gguf") assert detect_mmproj_file(str(model)) == str(big_mm.resolve()) def test_unrecognised_family_does_not_break_detection(tmp_path: Path): """Unknown model family must not return None on a sole candidate.""" model = _touch(tmp_path / "MyCustomBrand-7B-Q4_K_M.gguf") mmproj = _touch(tmp_path / "MyCustomBrand-7B-BF16-mmproj.gguf") assert detect_mmproj_file(str(model)) == str(mmproj.resolve()) def test_directory_path_returns_first_candidate(tmp_path: Path): """Directory path: no model stem to compare; legacy first-candidate.""" _touch(tmp_path / "Qwen3.5-9B-BF16-mmproj.gguf") _touch(tmp_path / "gemma-4-26B-A4B-it.mmproj-q8_0.gguf") result = detect_mmproj_file(str(tmp_path)) assert result is not None assert "mmproj" in Path(result).name.lower() def test_search_root_walk_still_works(tmp_path: Path): """Snapshot layout: weight in quant subdir, mmproj at snapshot root.""" snapshot = tmp_path / "snapshot" weight = _touch(snapshot / "BF16" / "Qwen3.5-9B-BF16.gguf") mmproj = _touch(snapshot / "Qwen3.5-9B-BF16-mmproj.gguf") result = detect_mmproj_file(str(weight), search_root = str(snapshot)) assert result == str(mmproj.resolve()) # -- Family token detection: word-bounded matching ---------------------- def test_family_token_phi_does_not_match_sapphire(): """``phi`` substring inside ``sapphire`` must not tag Phi.""" assert _detect_family_token("sapphire-7b-q4_k_m.gguf") is None def test_family_token_yi_does_not_match_tinyish_names(): """``yi`` must not cross letter boundaries (``yip``).""" assert _detect_family_token("yip-7b.gguf") is None assert _detect_family_token("yi-vl-6b.gguf") == "yi" def test_family_token_mimo_does_not_match_mimosa(): """``mimo`` must not tag ``mimosa``.""" assert _detect_family_token("mimosa-rosa-7b.gguf") is None assert _detect_family_token("MiMo-VL-7B-RL-BF16.gguf") == "mimo" def test_family_token_mistral_does_not_match_ministral(): """Pin Mistral-derivative tagging.""" assert _detect_family_token("Ministral-3-8B-Instruct-2512-BF16.gguf") == "ministral" assert _detect_family_token("Mistral-7B-Instruct-v0.3.gguf") == "mistral" assert _detect_family_token("Magistral-Small-2506-BF16.gguf") == "magistral" assert _detect_family_token("Devstral-Small-2-24B-Instruct-2512-BF16.gguf") == "devstral" def test_family_token_picks_leftmost_when_multiple_present(): """Leftmost family token wins, not tuple order.""" assert _detect_family_token("llama-phi-merge.gguf") == "llama" assert _detect_family_token("phi-llama-merge.gguf") == "phi" assert _detect_family_token("llama3-3b-instruct.gguf") == "llama" def test_family_token_new_families_recognised(): """Catalogue-audit additions tag correctly.""" assert _detect_family_token("NVIDIA-Nemotron-3-Nano-Omni-30B.gguf") == "nemotron" assert _detect_family_token("Kimi-K2.6-BF16.gguf") == "kimi" assert _detect_family_token("Nanonets-OCR-s-BF16.gguf") == "nanonets" assert _detect_family_token("Cosmos-Reason1-7B-BF16.gguf") == "cosmos" assert _detect_family_token("Apriel-1.5-15b-Thinker-BF16.gguf") == "apriel" assert _detect_family_token("LFM2.5-VL-1.6B-BF16.gguf") == "lfm" # -- Cross-family rejection with the expanded token list ---------------- def test_blocks_cross_family_for_new_token_pair(tmp_path: Path): """Nemotron weight + lone Gemma projector returns None.""" model = _touch(tmp_path / "NVIDIA-Nemotron-3-Nano-Omni-30B-A3B-Reasoning-MXFP4_MOE.gguf") _touch(tmp_path / "gemma-4-26B-A4B-it.mmproj-q8_0.gguf") assert detect_mmproj_file(str(model)) is None def test_picks_devstral_mmproj_in_mixed_dir(tmp_path: Path): """Devstral weight + Devstral mmproj + a Qwen mmproj: pick Devstral.""" model = _touch(tmp_path / "Devstral-Small-2-24B-Instruct-2512-BF16.gguf") dev_mm = _touch(tmp_path / "Devstral-Small-2-mmproj-bf16.gguf") _touch(tmp_path / "Qwen3.5-9B-BF16-mmproj.gguf") assert detect_mmproj_file(str(model)) == str(dev_mm.resolve()) # -- Launcher-level family guard ---------------------------------------- def test_mmproj_family_guard_blocks_cross_family(): assert ( mmproj_matches_model_family( "/models/Qwen3.5-9B-Q4_K_M.gguf", "/models/gemma-4-26B-A4B-it.mmproj-q8_0.gguf", ) is False ) def test_mmproj_family_guard_allows_same_family(): assert ( mmproj_matches_model_family( "/models/Qwen3.5-9B-Q4_K_M.gguf", "/models/Qwen3.5-9B-BF16-mmproj.gguf", ) is True ) def test_mmproj_family_guard_allows_generic_hf_mmproj(): """No family token on the projector: wildcard.""" assert ( mmproj_matches_model_family( "/models/Qwen3.5-9B-Q4_K_M.gguf", "/models/mmproj-F16.gguf", ) is True ) def test_mmproj_family_guard_allows_unrecognised_model_family(): """No family token on the model: wildcard.""" assert ( mmproj_matches_model_family( "/models/Apriel-1.5-15b-Thinker-BF16.gguf", "/models/mmproj-F16.gguf", ) is True ) # -- Metadata-primary pairing in detect_mmproj_file --------------------- def test_metadata_url_match_picked_over_filename_lookalike(tmp_path: Path): """URL match beats a longer-prefix sibling.""" weight = _gguf_with_general( tmp_path / "Qwen3.5-9B-Q4_K_M.gguf", { "general.architecture": "qwen2vl", "general.type": "model", "general.basename": "Qwen3.5", "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", }, ) # Closer filename prefix, wrong upstream. _gguf_with_general( tmp_path / "Qwen3.5-9B-mmproj-bf16.gguf", { "general.architecture": "clip", "general.type": "mmproj", "general.basename": "Qwen3.5", "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-1.5B", }, ) # Matching upstream. correct = _gguf_with_general( tmp_path / "mmproj-BF16.gguf", { "general.architecture": "clip", "general.type": "mmproj", "general.basename": "Qwen3.5", "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", }, ) assert detect_mmproj_file(str(weight)) == str(correct.resolve()) def test_metadata_url_mismatch_dropped(tmp_path: Path): """Filenames match family but metadata disagrees: returns None.""" weight = _gguf_with_general( tmp_path / "qwen-9b.gguf", { "general.architecture": "qwen2vl", "general.type": "model", "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", }, ) _gguf_with_general( tmp_path / "qwen-9b-mmproj.gguf", { "general.architecture": "clip", "general.type": "mmproj", "general.base_model.0.repo_url": "https://huggingface.co/google/gemma-3-9B", }, ) assert detect_mmproj_file(str(weight)) is None def test_metadata_identifies_mmproj_without_filename_hint(tmp_path: Path): """Projector named ``vision-projector.gguf`` discovered via header.""" weight = _gguf_with_general( tmp_path / "Qwen3.5-9B.gguf", { "general.architecture": "qwen2vl", "general.type": "model", "general.basename": "Qwen3.5", "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", }, ) projector = _gguf_with_general( tmp_path / "vision-projector.gguf", { "general.architecture": "clip", "general.type": "mmproj", "general.basename": "Qwen3.5", "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", }, ) assert detect_mmproj_file(str(weight)) == str(projector.resolve()) def test_metadata_score_outranks_filename_prefix(tmp_path: Path): """Score 100 (URL match) beats score 0 (long filename prefix).""" weight = _gguf_with_general( tmp_path / "Qwen3.5-9B-Q4_K_M.gguf", { "general.architecture": "qwen2vl", "general.type": "model", "general.basename": "Qwen3.5", "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", }, ) # Headerless: long shared stem, score 0. _touch(tmp_path / "Qwen3.5-9B-Q4_K_M-mmproj.gguf") # Headered: generic name, score 100. correct = _gguf_with_general( tmp_path / "mmproj-BF16.gguf", { "general.architecture": "clip", "general.type": "mmproj", "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", }, ) assert detect_mmproj_file(str(weight)) == str(correct.resolve())