gglib_core/domain/model.rs
1//! Model domain types.
2//!
3//! These types represent models in the system, independent of any
4//! infrastructure concerns (database, filesystem, etc.).
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11use super::capabilities::ModelCapabilities;
12use super::inference::{DefaultsOrigin, InferenceConfig};
13use super::server_config::ServerConfig;
14
15// ─────────────────────────────────────────────────────────────────────────────
16// System tags
17// ─────────────────────────────────────────────────────────────────────────────
18
19/// Prefix marking a tag as runtime-load-bearing.
20///
21/// Tags with this prefix (e.g. `format:qwen-xml`) drive the universal
22/// normalization pipeline's parser selection at compose time. Removing
23/// one would silently break dialect handling for the affected model, so
24/// the standard tag-mutation API rejects deletions while admin/debug
25/// paths can opt in via the `_force` variants.
26pub(super) const SYSTEM_TAG_PREFIX: &str = "format:";
27
28/// Returns `true` when `tag` is a system tag that callers must not
29/// remove through the standard tag-mutation API.
30#[must_use]
31pub fn is_system_tag(tag: &str) -> bool {
32 tag.starts_with(SYSTEM_TAG_PREFIX)
33}
34
35// ─────────────────────────────────────────────────────────────────────────────
36// Filter/Aggregate Types
37// ─────────────────────────────────────────────────────────────────────────────
38
39/// Filter options for the model library UI.
40///
41/// Contains aggregate data about available models for building
42/// dynamic filter controls.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS), ts(export))]
45pub struct ModelFilterOptions {
46 /// All distinct quantization types present in the library.
47 pub quantizations: Vec<String>,
48 /// Minimum and maximum parameter counts (in billions).
49 pub param_range: Option<RangeValues>,
50 /// Minimum and maximum context lengths.
51 pub context_range: Option<RangeValues>,
52 /// Minimum and maximum `latest_tg_tps` from benchmark summaries.
53 /// `None` when no models have been benchmarked.
54 pub speed_range: Option<RangeValues>,
55}
56
57/// A range of numeric values with min and max.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS), ts(export))]
60pub struct RangeValues {
61 pub min: f64,
62 pub max: f64,
63}
64
65// ─────────────────────────────────────────────────────────────────────────────
66// Model Types
67// ─────────────────────────────────────────────────────────────────────────────
68
69/// A model that exists in the system with a database ID.
70///
71/// This represents a persisted model with all its metadata.
72/// Use `NewModel` for models that haven't been persisted yet.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct Model {
75 /// Database ID of the model (always present for persisted models).
76 pub id: i64,
77 /// Human-readable name for the model.
78 pub name: String,
79 /// Canonical deduplication key (e.g., `hf:repo@sha#file`).
80 #[serde(default)]
81 pub model_key: String,
82 /// Absolute path to the GGUF file on the filesystem.
83 pub file_path: PathBuf,
84 /// Number of parameters in the model (in billions).
85 pub param_count_b: f64,
86 /// Model architecture (e.g., "llama", "mistral", "falcon").
87 pub architecture: Option<String>,
88 /// Quantization type (e.g., "`Q4_0`", "`Q8_0`", "`F16`", "`F32`").
89 pub quantization: Option<String>,
90 /// Maximum context length the model supports.
91 pub context_length: Option<u64>,
92 /// Number of experts (for `MoE` models).
93 pub expert_count: Option<u32>,
94 /// Number of experts used during inference (for `MoE` models).
95 pub expert_used_count: Option<u32>,
96 /// Number of shared experts (for `MoE` models).
97 pub expert_shared_count: Option<u32>,
98 /// Additional metadata key-value pairs from the GGUF file.
99 pub metadata: HashMap<String, String>,
100 /// UTC timestamp of when the model was added to the database.
101 pub added_at: DateTime<Utc>,
102 /// `HuggingFace` repository ID (e.g., "`TheBloke/Llama-2-7B-GGUF`").
103 pub hf_repo_id: Option<String>,
104 /// Git commit SHA from `HuggingFace` Hub.
105 pub hf_commit_sha: Option<String>,
106 /// Original filename on `HuggingFace` Hub.
107 pub hf_filename: Option<String>,
108 /// Timestamp of when this model was downloaded from `HuggingFace`.
109 pub download_date: Option<DateTime<Utc>>,
110 /// Last time we checked for updates on `HuggingFace`.
111 pub last_update_check: Option<DateTime<Utc>>,
112 /// User-defined tags for organizing models.
113 pub tags: Vec<String>,
114 /// Model capabilities inferred from chat template analysis.
115 #[serde(default)]
116 pub capabilities: ModelCapabilities,
117 /// Per-model inference parameter defaults.
118 ///
119 /// These are preferred over global settings when making inference requests.
120 /// If not set, falls back to global settings or hardcoded defaults.
121 #[serde(default)]
122 pub inference_defaults: Option<InferenceConfig>,
123 /// Whether [`Self::inference_defaults`] was set by the user or
124 /// auto-detected at import time from the `reasoning` tag.
125 ///
126 /// Always `None` when [`Self::inference_defaults`] is `None` — there is
127 /// nothing to have an origin. See [`DefaultsOrigin`] for why this
128 /// changes how resolution ranks the field.
129 #[serde(default)]
130 pub defaults_origin: Option<DefaultsOrigin>,
131 /// Per-model server-level defaults (`context_length`, etc.).
132 ///
133 /// Stored as JSON in the database. Overrides global settings but can
134 /// be overridden at request time. Part of the 5-level fallback chain.
135 #[serde(default)]
136 pub server_defaults: Option<ServerConfig>,
137 /// Tool-call dialect spec detected at import/retag time.
138 ///
139 /// Stored as JSON in the database. `None` for rows imported before
140 /// specs existed and for models whose dialect could not be derived —
141 /// consumers fall back to the `format:*` tag mapping.
142 #[serde(default)]
143 pub dialect_spec: Option<crate::domain::dialect::DialectSpec>,
144 /// llama-server's template-capability self-report (`chat_template_caps`
145 /// from `GET /props`), recorded once a launch has observed it.
146 ///
147 /// Stored as JSON in the database. `None` means **never observed** — the
148 /// third state of ADR 0007's tri-state, never to be collapsed into "not
149 /// supported". Unlike [`Self::dialect_spec`] this is not derived at
150 /// import time: it is a fact about the binary–model pair, so only a
151 /// launch can learn it.
152 #[serde(default)]
153 pub template_caps: Option<crate::domain::TemplateCaps>,
154 /// Denormalised benchmark summary joined from `model_benchmark_summaries`.
155 ///
156 /// `None` when no benchmark has been run for this model yet, or when the
157 /// model is fetched without the summary join (e.g. lightweight lookups).
158 #[serde(default)]
159 pub benchmark_summary: Option<crate::domain::benchmark::ModelBenchmarkSummary>,
160}
161
162/// A model to be inserted into the system (no ID yet).
163///
164/// This represents a model that hasn't been persisted to the database.
165/// After insertion, the repository returns a `Model` with the assigned ID.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct NewModel {
168 /// Human-readable name for the model.
169 pub name: String,
170 /// Absolute path to the GGUF file on the filesystem.
171 pub file_path: PathBuf,
172 /// Number of parameters in the model (in billions).
173 pub param_count_b: f64,
174 /// Model architecture (e.g., "llama", "mistral", "falcon").
175 pub architecture: Option<String>,
176 /// Quantization type (e.g., "`Q4_0`", "`Q8_0`", "`F16`", "`F32`").
177 pub quantization: Option<String>,
178 /// Maximum context length the model supports.
179 pub context_length: Option<u64>,
180 /// Number of experts (for `MoE` models).
181 pub expert_count: Option<u32>,
182 /// Number of experts used during inference (for `MoE` models).
183 pub expert_used_count: Option<u32>,
184 /// Number of shared experts (for `MoE` models).
185 pub expert_shared_count: Option<u32>,
186 /// Additional metadata key-value pairs from the GGUF file.
187 pub metadata: HashMap<String, String>,
188 /// UTC timestamp of when the model was added to the database.
189 pub added_at: DateTime<Utc>,
190 /// `HuggingFace` repository ID (e.g., "`TheBloke/Llama-2-7B-GGUF`").
191 pub hf_repo_id: Option<String>,
192 /// Git commit SHA from `HuggingFace` Hub.
193 pub hf_commit_sha: Option<String>,
194 /// Original filename on `HuggingFace` Hub.
195 pub hf_filename: Option<String>,
196 /// Timestamp of when this model was downloaded from `HuggingFace`.
197 pub download_date: Option<DateTime<Utc>>,
198 /// Last time we checked for updates on `HuggingFace`.
199 pub last_update_check: Option<DateTime<Utc>>,
200 /// User-defined tags for organizing models.
201 pub tags: Vec<String>,
202 /// Ordered list of all file paths for sharded models (None for single-file models).
203 pub file_paths: Option<Vec<PathBuf>>,
204 /// Model capabilities inferred from chat template analysis.
205 #[serde(default)]
206 pub capabilities: ModelCapabilities,
207 /// Per-model inference parameter defaults.
208 ///
209 /// These are preferred over global settings when making inference requests.
210 /// If not set, falls back to global settings or hardcoded defaults.
211 #[serde(default)]
212 pub inference_defaults: Option<InferenceConfig>,
213 /// See [`Model::defaults_origin`].
214 #[serde(default)]
215 pub defaults_origin: Option<DefaultsOrigin>,
216 /// Per-model server startup defaults.
217 #[serde(default)]
218 pub server_defaults: Option<ServerConfig>,
219 /// See [`Model::dialect_spec`].
220 #[serde(default)]
221 pub dialect_spec: Option<crate::domain::dialect::DialectSpec>,
222}
223
224// ─────────────────────────────────────────────────────────────────────────────
225// Model File Types (for per-shard OID tracking)
226// ─────────────────────────────────────────────────────────────────────────────
227
228/// Represents a single file (shard) belonging to a model.
229///
230/// This tracks per-file metadata including OIDs for verification and update detection.
231/// Models can have multiple files (sharded models) or a single file.
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct ModelFile {
234 /// Database ID of this model file entry.
235 pub id: i64,
236 /// ID of the parent model.
237 pub model_id: i64,
238 /// Relative path to the file within the model directory.
239 pub file_path: String,
240 /// Index of this file in the shard sequence (0 for single-file models).
241 pub file_index: i32,
242 /// Expected file size in bytes (from `HuggingFace` API).
243 pub expected_size: i64,
244 /// `HuggingFace` OID (Git LFS SHA256 hash) for this file.
245 pub hf_oid: Option<String>,
246 /// UTC timestamp of when this file was last verified.
247 pub last_verified_at: Option<DateTime<Utc>>,
248}
249
250/// A model file entry to be inserted into the system (no ID yet).
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct NewModelFile {
253 /// ID of the parent model.
254 pub model_id: i64,
255 /// Relative path to the file within the model directory.
256 pub file_path: String,
257 /// Index of this file in the shard sequence (0 for single-file models).
258 pub file_index: i32,
259 /// Expected file size in bytes (from `HuggingFace` API).
260 pub expected_size: i64,
261 /// `HuggingFace` OID (Git LFS SHA256 hash) for this file.
262 pub hf_oid: Option<String>,
263}
264
265impl NewModelFile {
266 /// Create a new model file entry with minimal required fields.
267 #[must_use]
268 pub const fn new(
269 model_id: i64,
270 file_path: String,
271 file_index: i32,
272 expected_size: i64,
273 hf_oid: Option<String>,
274 ) -> Self {
275 Self {
276 model_id,
277 file_path,
278 file_index,
279 expected_size,
280 hf_oid,
281 }
282 }
283}
284
285impl NewModel {
286 /// Create a new model with minimal required fields.
287 ///
288 /// Other fields are set to `None` or empty defaults.
289 #[must_use]
290 pub fn new(
291 name: String,
292 file_path: PathBuf,
293 param_count_b: f64,
294 added_at: DateTime<Utc>,
295 ) -> Self {
296 Self {
297 name,
298 file_path,
299 param_count_b,
300 architecture: None,
301 quantization: None,
302 context_length: None,
303 expert_count: None,
304 expert_used_count: None,
305 expert_shared_count: None,
306 metadata: HashMap::new(),
307 added_at,
308 hf_repo_id: None,
309 hf_commit_sha: None,
310 hf_filename: None,
311 download_date: None,
312 last_update_check: None,
313 tags: Vec::new(),
314 file_paths: None,
315 capabilities: ModelCapabilities::default(),
316 inference_defaults: None,
317 defaults_origin: None,
318 server_defaults: None,
319 dialect_spec: None,
320 }
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use chrono::Utc;
328
329 #[test]
330 fn test_new_model_creation() {
331 let model = NewModel::new(
332 "Test Model".to_string(),
333 PathBuf::from("/path/to/model.gguf"),
334 7.0,
335 Utc::now(),
336 );
337
338 assert_eq!(model.name, "Test Model");
339 assert!((model.param_count_b - 7.0).abs() < f64::EPSILON);
340 assert!(model.architecture.is_none());
341 assert!(model.tags.is_empty());
342 }
343}