# 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 :mod:`utils.models.gguf_metadata`. Synthesise small GGUF headers in tmp dirs so we never depend on real model files.""" from __future__ import annotations import struct from pathlib import Path from typing import Iterable, Mapping from utils.models.gguf_metadata import ( is_mmproj_by_metadata, pairing_score, read_gguf_context_length, read_gguf_general_metadata, read_mmproj_audio_capability, ) _GGUF_MAGIC = 0x46554747 _VTYPE_STRING = 8 _VTYPE_UINT32 = 4 _VTYPE_UINT64 = 10 _VTYPE_ARRAY = 9 _VTYPE_BOOL = 7 def _enc_string(s: str) -> bytes: b = s.encode("utf-8") return struct.pack(" bytes: return _enc_string(key) + struct.pack(" bytes: return _enc_string(key) + struct.pack(" bytes: return _enc_string(key) + struct.pack(" bytes: return _enc_string(key) + struct.pack(" bytes: vals = list(values) out = _enc_string(key) + struct.pack(" Path: """Minimal GGUF: header + KV body, no tensors.""" extra_uint32 = extra_uint32 or {} extra_uint64 = extra_uint64 or {} extra_string_arrays = extra_string_arrays or {} extra_bools = extra_bools or {} kv_count = ( len(general_strings) + len(extra_uint32) + len(extra_uint64) + len(extra_string_arrays) + len(extra_bools) ) body = b"" for k, v in general_strings.items(): body += _enc_kv_string(k, v) for k, v in extra_uint32.items(): body += _enc_kv_uint32(k, v) for k, v in extra_uint64.items(): body += _enc_kv_uint64(k, v) for k, v in extra_string_arrays.items(): body += _enc_kv_string_array(k, v) for k, v in extra_bools.items(): body += _enc_kv_bool(k, v) header = struct.pack( ".context_length key. p = _write_synthetic_gguf( tmp_path / "model.gguf", {"general.architecture": "llama"}, extra_uint32 = {"llama.block_count": 32}, ) assert read_gguf_context_length(str(p)) is None def test_context_length_ignores_foreign_arch_key(tmp_path: Path): # A context_length under a different arch namespace must not match. p = _write_synthetic_gguf( tmp_path / "model.gguf", {"general.architecture": "llama"}, extra_uint32 = {"qwen2.context_length": 8192}, ) assert read_gguf_context_length(str(p)) is None def test_context_length_read_from_uint64(tmp_path: Path): # Some models store context_length as a uint64 (vtype 10). p = _write_synthetic_gguf( tmp_path / "model.gguf", {"general.architecture": "qwen3"}, extra_uint64 = {"qwen3.context_length": 262144}, ) assert read_gguf_context_length(str(p)) == 262144 def test_context_length_zero_treated_as_absent(tmp_path: Path): # A zero/garbage ceiling must read as None so the UI can't build a slider # with max < min. p = _write_synthetic_gguf( tmp_path / "model.gguf", {"general.architecture": "llama"}, extra_uint32 = {"llama.context_length": 0}, ) assert read_gguf_context_length(str(p)) is None def test_extracts_general_string_fields(tmp_path: Path): p = _write_synthetic_gguf( tmp_path / "model.gguf", { "general.architecture": "qwen2vl", "general.type": "model", "general.basename": "Qwen3.5", "general.organization": "Qwen", "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", "general.base_model.0.name": "Qwen3.5 9B", "general.base_model.0.organization": "Qwen", }, ) meta = read_gguf_general_metadata(str(p)) assert meta is not None assert meta["general.architecture"] == "qwen2vl" assert meta["general.basename"] == "Qwen3.5" assert meta["general.base_model.0.repo_url"] == "https://huggingface.co/Qwen/Qwen3.5-9B" def test_skips_unrelated_fields_without_breaking(tmp_path: Path): """Skip unwanted arrays and uint32s without losing position.""" p = _write_synthetic_gguf( tmp_path / "model.gguf", {"general.basename": "Foo"}, extra_uint32 = {"qwen2vl.context_length": 32768}, extra_string_arrays = {"tokenizer.ggml.tokens": ["a", "bc", "def"]}, ) meta = read_gguf_general_metadata(str(p)) assert meta == {"general.basename": "Foo"} def test_metadata_is_cached(tmp_path: Path): """Cache invalidates on size change.""" p = _write_synthetic_gguf( tmp_path / "model.gguf", {"general.basename": "First"}, ) first = read_gguf_general_metadata(str(p)) assert first == {"general.basename": "First"} # Change size so the (path, mtime, size) cache key invalidates. _write_synthetic_gguf( tmp_path / "model.gguf", {"general.basename": "Second", "general.organization": "X"}, ) second = read_gguf_general_metadata(str(p)) assert second == {"general.basename": "Second", "general.organization": "X"} # --- is_mmproj_by_metadata -------------------------------------------- def test_is_mmproj_by_metadata_signals(): assert is_mmproj_by_metadata({"general.type": "mmproj"}) is True assert is_mmproj_by_metadata({"general.type": "MMProj"}) is True assert is_mmproj_by_metadata({"general.type": "model"}) is False assert is_mmproj_by_metadata({"general.basename": "foo"}) is None assert is_mmproj_by_metadata({}) is None assert is_mmproj_by_metadata(None) is None # --- pairing_score ----------------------------------------------------- def test_pairing_score_base_model_url_match(): weight = { "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", } mmproj = { "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", } assert pairing_score(weight, mmproj) == 100 def test_pairing_score_base_model_url_mismatch(): weight = { "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", } mmproj = { "general.base_model.0.repo_url": "https://huggingface.co/google/gemma-3-9B", } assert pairing_score(weight, mmproj) == -1 def test_pairing_score_base_model_url_trailing_slash_normalised(): weight = { "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B/", } mmproj = { "general.base_model.0.repo_url": "https://huggingface.co/Qwen/Qwen3.5-9B", } assert pairing_score(weight, mmproj) == 100 def test_pairing_score_basename_plus_org_fallback(): weight = { "general.basename": "Nanonets-Ocr-S", "general.base_model.0.organization": "Nanonets", } mmproj = { "general.basename": "Nanonets-Ocr-S", "general.base_model.0.organization": "Nanonets", } assert pairing_score(weight, mmproj) == 80 def test_pairing_score_basename_only_fallback(): assert ( pairing_score( {"general.basename": "Nanonets-Ocr-S"}, {"general.basename": "Nanonets-Ocr-S"}, ) == 60 ) def test_pairing_score_no_overlap_returns_zero(): """One side empty: scorer punts to filename fallback.""" assert pairing_score({"general.basename": "Foo"}, {}) == 0 assert pairing_score({}, {"general.basename": "Foo"}) == 0 assert pairing_score(None, {"general.basename": "Foo"}) == 0 # --- read_mmproj_audio_capability -------------------------------------- def test_mmproj_audio_capability_true(tmp_path: Path): """clip.has_audio_encoder=True (e.g. Gemma 4's gemma4ua projector).""" p = _write_synthetic_gguf( tmp_path / "mmproj.gguf", {"general.type": "mmproj"}, extra_bools = { "clip.has_vision_encoder": True, "clip.has_audio_encoder": True, }, ) assert read_mmproj_audio_capability(str(p)) is True def test_mmproj_audio_capability_false(tmp_path: Path): """Vision-only projector: key present but false.""" p = _write_synthetic_gguf( tmp_path / "mmproj.gguf", {"general.type": "mmproj"}, extra_bools = { "clip.has_vision_encoder": True, "clip.has_audio_encoder": False, }, ) assert read_mmproj_audio_capability(str(p)) is False def test_mmproj_audio_capability_absent_returns_none(tmp_path: Path): """Key absent (older/vision-only mmproj): None, not False.""" p = _write_synthetic_gguf( tmp_path / "mmproj.gguf", {"general.type": "mmproj"}, extra_bools = {"clip.has_vision_encoder": True}, ) assert read_mmproj_audio_capability(str(p)) is None def test_mmproj_audio_capability_missing_or_non_gguf(tmp_path: Path): assert read_mmproj_audio_capability(str(tmp_path / "nope.gguf")) is None junk = tmp_path / "garbage.gguf" junk.write_bytes(b"not a gguf header at all") assert read_mmproj_audio_capability(str(junk)) is None