Coverage for src/lilbee/modelhub/model_info.py: 100%
63 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-17 10:02 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-17 10:02 +0000
1"""Public API for reading model architecture metadata from GGUF files."""
3from __future__ import annotations
5import logging
6from dataclasses import dataclass
8from lilbee.core.config import cfg
10log = logging.getLogger(__name__)
13@dataclass
14class ModelArchInfo:
15 """Architecture metadata for installed models."""
17 chat_arch: str = "unknown"
18 embed_arch: str = "unknown"
19 vision_projector: str = "unknown"
20 active_handler: str = "not loaded"
23# Cache: (chat_model_ref, embed_model_ref, vision_model_ref) -> ModelArchInfo.
24# Reading GGUF headers is hundreds of ms cold (file open + parse); the result
25# is stable as long as the configured refs
26# stay the same. Status screen visits, MCP status calls, and any other
27# read-side caller share this cache. ``invalidate_cache`` lets settings
28# updates clear it explicitly when a model ref changes.
29_arch_cache: dict[tuple[str, str, str], ModelArchInfo] = {}
32def _cache_key() -> tuple[str, str, str]:
33 return (cfg.chat_model or "", cfg.embedding_model or "", cfg.vision_model or "")
36def invalidate_cache() -> None:
37 """Drop the architecture cache. Call when a model ref changes."""
38 _arch_cache.clear()
41def get_model_architecture() -> ModelArchInfo:
42 """Return architecture metadata for the currently configured models.
44 Memoized on (chat_model, embed_model, vision_model). First call
45 reads GGUF headers (binding-free) for each; subsequent calls under
46 the same refs return the cached result instantly. Each reader
47 degrades gracefully when its model is unset or unavailable.
48 """
49 key = _cache_key()
50 cached = _arch_cache.get(key)
51 if cached is not None:
52 return cached
53 info = ModelArchInfo()
54 info = _read_chat_arch(info)
55 info = _read_embed_arch(info)
56 info = _read_vision_arch(info)
57 _arch_cache[key] = info
58 return info
61def _read_chat_arch(info: ModelArchInfo) -> ModelArchInfo:
62 """Read chat model architecture from GGUF metadata."""
63 try:
64 from lilbee.providers.engine_params import resolve_model_path
65 from lilbee.providers.gguf_meta import read_gguf_metadata
67 path = resolve_model_path(cfg.chat_model)
68 meta = read_gguf_metadata(path)
69 if meta:
70 info.chat_arch = meta.get("architecture", "unknown")
71 info.active_handler = "llama-server"
72 except Exception:
73 log.debug("Failed to read chat model architecture", exc_info=True)
74 return info
77def _read_embed_arch(info: ModelArchInfo) -> ModelArchInfo:
78 """Read embedding model architecture from GGUF metadata."""
79 try:
80 from lilbee.providers.engine_params import resolve_model_path
81 from lilbee.providers.gguf_meta import read_gguf_metadata
83 path = resolve_model_path(cfg.embedding_model)
84 meta = read_gguf_metadata(path)
85 if meta:
86 info.embed_arch = meta.get("architecture", "unknown")
87 except Exception:
88 log.debug("Failed to read embedding model architecture", exc_info=True)
89 return info
92def _read_vision_arch(info: ModelArchInfo) -> ModelArchInfo:
93 """Read vision projector type from GGUF metadata for ``cfg.vision_model``.
95 Reads the vision model name from the global ``cfg`` singleton (same
96 pattern as :func:`_read_chat_arch` / :func:`_read_embed_arch`) rather
97 than taking it as a parameter. The chat model is never inspected for
98 vision capability here: role separation is explicit. Returns the
99 input unchanged when no vision model is configured.
100 """
101 if not cfg.vision_model:
102 return info
103 try:
104 from lilbee.providers.engine_params import resolve_model_path
105 from lilbee.providers.gguf_meta import (
106 find_mmproj_for_model,
107 read_mmproj_projector_type,
108 )
110 path = resolve_model_path(cfg.vision_model)
111 mmproj = find_mmproj_for_model(path)
112 proj_type = read_mmproj_projector_type(mmproj)
113 info.vision_projector = proj_type or "unknown"
114 except Exception:
115 log.debug("Failed to read vision projector type", exc_info=True)
116 return info