gglib_core/ports/model_catalog.rs
1//! Model catalog port for listing and resolving models.
2//!
3//! This port defines the interface for querying the model catalog.
4//! It provides domain-level model information without exposing
5//! database or storage implementation details.
6
7use async_trait::async_trait;
8use std::fmt;
9use std::path::PathBuf;
10use thiserror::Error;
11
12use crate::domain::DefaultsOrigin;
13use crate::domain::InferenceConfig;
14use crate::domain::KvElemsPerToken;
15use crate::domain::ModelCapabilities;
16use crate::domain::ServerConfig;
17
18/// Domain model summary for catalog operations (listing).
19///
20/// This is a domain type (not an `OpenAI` API type). The proxy layer
21/// is responsible for mapping this to OpenAI-compatible formats.
22///
23/// Note: Does NOT include `file_path` to avoid leaking filesystem details
24/// in catalog/listing operations.
25#[derive(Debug, Clone)]
26pub struct ModelSummary {
27 /// Database ID of the model.
28 pub id: u32,
29 /// Model name (used as identifier).
30 pub name: String,
31 /// Tags/labels associated with the model.
32 pub tags: Vec<String>,
33 /// Detected and persisted capability flags for this model.
34 ///
35 /// This is the single source of truth for model behaviour constraints
36 /// (strict-turn alternation, system-role support, tool calls, reasoning).
37 /// The proxy uses these directly rather than inferring from tags at
38 /// request time, eliminating the split-brain between tags and capabilities.
39 pub capabilities: ModelCapabilities,
40 /// Parameter count as string (e.g., "7B", "13B", "70B").
41 pub param_count: String,
42 /// Quantization type (e.g., "`Q4_K_M`", "`Q8_0`").
43 pub quantization: Option<String>,
44 /// Model architecture (e.g., "llama", "mistral", "qwen2").
45 pub architecture: Option<String>,
46 /// Unix timestamp when the model was added.
47 pub created_at: i64,
48 /// File size in bytes.
49 pub file_size: u64,
50 /// Maximum context length the model supports, in tokens (from GGUF
51 /// metadata). `None` when unknown.
52 ///
53 /// This is a static, per-model ceiling — it does not reflect the
54 /// `--ctx-size` a currently-running instance was actually launched
55 /// with, which can be smaller (see `gglib_core::ports::model_runtime::RunningTarget::effective_ctx`
56 /// for the live value). Consumers that need the true, currently-running
57 /// context size should prefer `effective_ctx` when the model is running
58 /// and fall back to this field otherwise.
59 pub context_length: Option<u64>,
60 /// Per-model inference parameter defaults.
61 ///
62 /// When `Some`, these are resolved per-request via
63 /// [`InferenceConfig::resolve_with_defaults`] before forwarding to llama-server.
64 /// Used by `gglib proxy` to inject resolved defaults into OpenAI-format
65 /// request bodies, and by the agentic loop (`gglib chat`, `gglib q`) to
66 /// apply model-specific sampling parameters.
67 pub inference_defaults: Option<InferenceConfig>,
68 /// Whether [`Self::inference_defaults`] was set by the user or
69 /// auto-detected. See [`DefaultsOrigin`] and
70 /// [`InferenceConfig::resolve_with_profile`](crate::domain::InferenceConfig::resolve_with_profile).
71 pub defaults_origin: Option<DefaultsOrigin>,
72 /// Per-model server defaults (`context_length`, etc.) from the database.
73 pub server_defaults: Option<ServerConfig>,
74}
75
76/// Launch specification for running a model.
77///
78/// Contains all information needed to actually launch a model,
79/// including the file path. Separate from `ModelSummary` to avoid
80/// leaking filesystem details in catalog operations.
81#[derive(Debug, Clone)]
82pub struct ModelLaunchSpec {
83 /// Database ID of the model.
84 pub id: u32,
85 /// Model name.
86 pub name: String,
87 /// Absolute path to the GGUF file.
88 pub file_path: PathBuf,
89 /// Tags/labels associated with the model.
90 pub tags: Vec<String>,
91 /// Model architecture (for runtime configuration).
92 pub architecture: Option<String>,
93 /// Quantization label (`Q4_K_M`), when the catalog recorded one.
94 ///
95 /// Carried purely so the launch can name what it loaded — see
96 /// [`crate::domain::LaunchNarration`]. `None` for models whose GGUF
97 /// metadata did not identify a quantization.
98 pub quantization: Option<String>,
99 /// Maximum context length the model supports.
100 pub context_length: Option<u64>,
101 /// Per-model server defaults (e.g., `context_length` for launch).
102 pub server_defaults: Option<ServerConfig>,
103 /// Total on-disk size of the model weights in bytes, summed across all
104 /// shards for multi-part GGUFs.
105 ///
106 /// Used to budget host memory at launch (see
107 /// [`crate::server_config::compute_auto_cache_ram_mb`]). `0` when the
108 /// size could not be determined — callers must treat that as "unknown"
109 /// rather than "free".
110 pub file_size_bytes: u64,
111 /// Estimated K/V element counts consumed per token of context, derived
112 /// from the model's GGUF metadata (see
113 /// [`crate::domain::estimate_kv_elems_per_token`]). Type-agnostic —
114 /// callers convert to bytes via [`crate::domain::kv_bytes_per_token`]
115 /// once the launch's resolved K/V cache types are known.
116 ///
117 /// `None` when the metadata doesn't carry the layer/head counts needed;
118 /// callers substitute a conservative allowance.
119 pub kv_elems_per_token: Option<KvElemsPerToken>,
120 /// True when the model's KV memory retains only part of the token history
121 /// — sliding-window, hybrid, or recurrent attention (see
122 /// [`crate::domain::kv_memory_is_partial`]).
123 ///
124 /// Such models cannot be resumed from llama-server's disk slot files: the
125 /// save/restore path does not carry the context checkpoints they need, so
126 /// a "successful" restore still forces a full prompt re-prefill. Callers
127 /// disable the disk slot layer for these models and rely on the in-RAM
128 /// prompt cache, which does preserve checkpoints.
129 pub kv_memory_is_partial: bool,
130}
131
132impl ModelSummary {
133 /// Create a description string for this model.
134 #[must_use]
135 pub fn description(&self) -> String {
136 let arch = self.architecture.as_deref().unwrap_or("unknown");
137 let quant = self.quantization.as_deref().unwrap_or("unknown");
138 format!("{} - {} parameters, {}", arch, self.param_count, quant)
139 }
140}
141
142/// Errors that can occur during catalog operations.
143#[derive(Debug, Error)]
144pub enum CatalogError {
145 /// Failed to query the catalog.
146 #[error("Failed to query catalog: {0}")]
147 QueryFailed(String),
148
149 /// Internal error during catalog operations.
150 #[error("Internal error: {0}")]
151 Internal(String),
152}
153
154/// Port for querying the model catalog.
155///
156/// This interface provides read-only access to the model catalog
157/// for listing and resolving models. It does not handle model
158/// registration or deletion.
159#[async_trait]
160pub trait ModelCatalogPort: Send + Sync + fmt::Debug {
161 /// List all models in the catalog.
162 ///
163 /// Returns a list of model summaries ordered by name.
164 ///
165 /// # Errors
166 ///
167 /// Returns `CatalogError` if the catalog cannot be queried.
168 async fn list_models(&self) -> Result<Vec<ModelSummary>, CatalogError>;
169
170 /// Resolve a model by name or alias.
171 ///
172 /// This method performs model resolution:
173 /// 1. Exact name match
174 /// 2. Case-insensitive name match
175 /// 3. Fuzzy/partial match (implementation-defined)
176 ///
177 /// Returns `None` if no matching model is found.
178 ///
179 /// # Arguments
180 ///
181 /// * `name` - Model name or alias to resolve
182 ///
183 /// # Errors
184 ///
185 /// Returns `CatalogError` if the catalog cannot be queried.
186 async fn resolve_model(&self, name: &str) -> Result<Option<ModelSummary>, CatalogError>;
187
188 /// Resolve a model for launching.
189 ///
190 /// Returns full launch specification including file path.
191 /// Use this when you need to actually run a model, not just list it.
192 ///
193 /// # Arguments
194 ///
195 /// * `name` - Model name or alias to resolve
196 ///
197 /// # Errors
198 ///
199 /// Returns `CatalogError` if the catalog cannot be queried.
200 async fn resolve_for_launch(&self, name: &str)
201 -> Result<Option<ModelLaunchSpec>, CatalogError>;
202}