Coverage for src/lilbee/catalog/compat.py: 100%
51 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-31 21:55 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-31 21:55 +0000
1"""Architecture compatibility classification for catalog entries."""
3from __future__ import annotations
5from typing import TYPE_CHECKING
7from huggingface_hub import hf_hub_url
8from huggingface_hub.utils import HFValidationError
10from lilbee._generated.engine_archs import SUPPORTED_ARCHS
11from lilbee.catalog.header_probe import GgufHeader, probe_header
12from lilbee.catalog.refs import (
13 GGUF_SUFFIX,
14 NATIVE_GGUF_REF_MIN_SLASHES,
15 WILDCARD,
16 gguf_filename_from_ref,
17 hf_repo_from_ref,
18)
19from lilbee.catalog.types import ModelCompat
21if TYPE_CHECKING:
22 from lilbee.catalog.hf_client import HfClient
25def classify(architecture: str) -> ModelCompat:
26 """Map a `general.architecture` string to a `ModelCompat` verdict."""
27 if not architecture:
28 return ModelCompat.UNKNOWN
29 return ModelCompat.SUPPORTED if architecture in SUPPORTED_ARCHS else ModelCompat.UNSUPPORTED
32def resolve_arch_for_pull(ref: str, hf_client: HfClient) -> str:
33 """Resolve general.architecture for *ref*: cache hit > Range-GET probe > empty (UNKNOWN).
35 ``probe_header`` returns empty fields on any failure (network, non-200, parse),
36 so an empty result means "undetermined", never a real verdict. Only a non-empty
37 arch is cached; otherwise a transient probe failure would be cached permanently
38 and disable the unsupported-arch guard for this ref on every later pull.
39 """
40 cached = hf_client.get_cached_arch(ref)
41 if cached is not None:
42 return cached
43 url = _resolve_blob_url(ref)
44 if not url:
45 return ""
46 arch = probe_header(url).architecture
47 if arch:
48 hf_client.cache_arch(ref, arch)
49 return arch
52def file_header(hf_repo: str, filename: str) -> GgufHeader:
53 """The GGUF header of one repo file, empty when it cannot be read.
55 An unreadable header is not a verdict: every field stays empty, which reads
56 as "a model of undetermined architecture" so an offline or gated repo
57 resolves the way it did before the probe existed.
58 """
59 try:
60 url = hf_hub_url(hf_repo, filename)
61 except (HFValidationError, ValueError):
62 return GgufHeader()
63 return probe_header(url)
66def _resolve_blob_url(ref: str) -> str:
67 """Return a probable .gguf blob URL for *ref*, or empty string if unresolvable.
69 lilbee's canonical native refs are slash-delimited ``<org>/<repo>/<file>.gguf``
70 (the filename may add subdirs for a quant), so the repo and filename are split
71 on that shape; an ollama-style ``repo:tag`` ref is split on the colon.
72 """
73 if ref.endswith(GGUF_SUFFIX) and ref.count("/") >= NATIVE_GGUF_REF_MIN_SLASHES:
74 repo, filename = hf_repo_from_ref(ref), gguf_filename_from_ref(ref)
75 elif ":" in ref:
76 repo, filename = ref.split(":", 1)
77 else:
78 repo, filename = ref, ""
79 if not filename or WILDCARD in filename:
80 return ""
81 try:
82 return hf_hub_url(repo, filename)
83 except (HFValidationError, ValueError):
84 return ""
87class UnsupportedArchError(Exception):
88 """Raised when a pull is attempted for a model whose architecture isn't supported."""
90 def __init__(self, ref: str, architecture: str) -> None:
91 self.ref = ref
92 self.architecture = architecture
93 super().__init__(
94 f"Model {ref!r} uses architecture {architecture!r}, not supported by this lilbee build."
95 )
98class UnsupportedQuantError(RuntimeError):
99 """Raised when a GGUF's tensors carry a type the bundled engine cannot decode.
101 A RuntimeError so every pull surface renders it the way it already renders a
102 failed pull. Unlike an unsupported architecture there is no preflight route
103 and no list of accepted types to offer, so there is nothing structured for a
104 client to read beyond the message.
105 """
107 def __init__(self, ref: str, quant: str) -> None:
108 self.ref = ref
109 self.quant = quant
110 super().__init__(
111 f"Model {ref!r} is quantized as {quant}, which this lilbee build cannot load. "
112 "Pass --allow-unsupported to download it anyway."
113 )