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 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)]
44pub struct ModelFilterOptions {
45 /// All distinct quantization types present in the library.
46 pub quantizations: Vec<String>,
47 /// Minimum and maximum parameter counts (in billions).
48 pub param_range: Option<RangeValues>,
49 /// Minimum and maximum context lengths.
50 pub context_range: Option<RangeValues>,
51 /// Minimum and maximum `latest_tg_tps` from benchmark summaries.
52 /// `None` when no models have been benchmarked.
53 pub speed_range: Option<RangeValues>,
54}
55
56/// A range of numeric values with min and max.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct RangeValues {
59 pub min: f64,
60 pub max: f64,
61}
62
63// ─────────────────────────────────────────────────────────────────────────────
64// Model Types
65// ─────────────────────────────────────────────────────────────────────────────
66
67/// A model that exists in the system with a database ID.
68///
69/// This represents a persisted model with all its metadata.
70/// Use `NewModel` for models that haven't been persisted yet.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Model {
73 /// Database ID of the model (always present for persisted models).
74 pub id: i64,
75 /// Human-readable name for the model.
76 pub name: String,
77 /// Canonical deduplication key (e.g., `hf:repo@sha#file`).
78 #[serde(default)]
79 pub model_key: String,
80 /// Absolute path to the GGUF file on the filesystem.
81 pub file_path: PathBuf,
82 /// Number of parameters in the model (in billions).
83 pub param_count_b: f64,
84 /// Model architecture (e.g., "llama", "mistral", "falcon").
85 pub architecture: Option<String>,
86 /// Quantization type (e.g., "`Q4_0`", "`Q8_0`", "`F16`", "`F32`").
87 pub quantization: Option<String>,
88 /// Maximum context length the model supports.
89 pub context_length: Option<u64>,
90 /// Number of experts (for `MoE` models).
91 pub expert_count: Option<u32>,
92 /// Number of experts used during inference (for `MoE` models).
93 pub expert_used_count: Option<u32>,
94 /// Number of shared experts (for `MoE` models).
95 pub expert_shared_count: Option<u32>,
96 /// Additional metadata key-value pairs from the GGUF file.
97 pub metadata: HashMap<String, String>,
98 /// UTC timestamp of when the model was added to the database.
99 pub added_at: DateTime<Utc>,
100 /// `HuggingFace` repository ID (e.g., "`TheBloke/Llama-2-7B-GGUF`").
101 pub hf_repo_id: Option<String>,
102 /// Git commit SHA from `HuggingFace` Hub.
103 pub hf_commit_sha: Option<String>,
104 /// Original filename on `HuggingFace` Hub.
105 pub hf_filename: Option<String>,
106 /// Timestamp of when this model was downloaded from `HuggingFace`.
107 pub download_date: Option<DateTime<Utc>>,
108 /// Last time we checked for updates on `HuggingFace`.
109 pub last_update_check: Option<DateTime<Utc>>,
110 /// User-defined tags for organizing models.
111 pub tags: Vec<String>,
112 /// Model capabilities inferred from chat template analysis.
113 #[serde(default)]
114 pub capabilities: ModelCapabilities,
115 /// Per-model inference parameter defaults.
116 ///
117 /// These are preferred over global settings when making inference requests.
118 /// If not set, falls back to global settings or hardcoded defaults.
119 #[serde(default)]
120 pub inference_defaults: Option<InferenceConfig>,
121 /// Whether [`Self::inference_defaults`] was set by the user or
122 /// auto-detected at import time from the `reasoning` tag.
123 ///
124 /// Always `None` when [`Self::inference_defaults`] is `None` — there is
125 /// nothing to have an origin. See [`DefaultsOrigin`] for why this
126 /// changes how resolution ranks the field.
127 #[serde(default)]
128 pub defaults_origin: Option<DefaultsOrigin>,
129 /// Per-model server-level defaults (`context_length`, etc.).
130 ///
131 /// Stored as JSON in the database. Overrides global settings but can
132 /// be overridden at request time. Part of the 4-level fallback chain.
133 #[serde(default)]
134 pub server_defaults: Option<ServerConfig>,
135 /// Denormalised benchmark summary joined from `model_benchmark_summaries`.
136 ///
137 /// `None` when no benchmark has been run for this model yet, or when the
138 /// model is fetched without the summary join (e.g. lightweight lookups).
139 #[serde(default)]
140 pub benchmark_summary: Option<crate::domain::benchmark::ModelBenchmarkSummary>,
141}
142
143/// A model to be inserted into the system (no ID yet).
144///
145/// This represents a model that hasn't been persisted to the database.
146/// After insertion, the repository returns a `Model` with the assigned ID.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct NewModel {
149 /// Human-readable name for the model.
150 pub name: String,
151 /// Absolute path to the GGUF file on the filesystem.
152 pub file_path: PathBuf,
153 /// Number of parameters in the model (in billions).
154 pub param_count_b: f64,
155 /// Model architecture (e.g., "llama", "mistral", "falcon").
156 pub architecture: Option<String>,
157 /// Quantization type (e.g., "`Q4_0`", "`Q8_0`", "`F16`", "`F32`").
158 pub quantization: Option<String>,
159 /// Maximum context length the model supports.
160 pub context_length: Option<u64>,
161 /// Number of experts (for `MoE` models).
162 pub expert_count: Option<u32>,
163 /// Number of experts used during inference (for `MoE` models).
164 pub expert_used_count: Option<u32>,
165 /// Number of shared experts (for `MoE` models).
166 pub expert_shared_count: Option<u32>,
167 /// Additional metadata key-value pairs from the GGUF file.
168 pub metadata: HashMap<String, String>,
169 /// UTC timestamp of when the model was added to the database.
170 pub added_at: DateTime<Utc>,
171 /// `HuggingFace` repository ID (e.g., "`TheBloke/Llama-2-7B-GGUF`").
172 pub hf_repo_id: Option<String>,
173 /// Git commit SHA from `HuggingFace` Hub.
174 pub hf_commit_sha: Option<String>,
175 /// Original filename on `HuggingFace` Hub.
176 pub hf_filename: Option<String>,
177 /// Timestamp of when this model was downloaded from `HuggingFace`.
178 pub download_date: Option<DateTime<Utc>>,
179 /// Last time we checked for updates on `HuggingFace`.
180 pub last_update_check: Option<DateTime<Utc>>,
181 /// User-defined tags for organizing models.
182 pub tags: Vec<String>,
183 /// Ordered list of all file paths for sharded models (None for single-file models).
184 pub file_paths: Option<Vec<PathBuf>>,
185 /// Model capabilities inferred from chat template analysis.
186 #[serde(default)]
187 pub capabilities: ModelCapabilities,
188 /// Per-model inference parameter defaults.
189 ///
190 /// These are preferred over global settings when making inference requests.
191 /// If not set, falls back to global settings or hardcoded defaults.
192 #[serde(default)]
193 pub inference_defaults: Option<InferenceConfig>,
194 /// See [`Model::defaults_origin`].
195 #[serde(default)]
196 pub defaults_origin: Option<DefaultsOrigin>,
197 /// Per-model server startup defaults.
198 #[serde(default)]
199 pub server_defaults: Option<ServerConfig>,
200}
201
202// ─────────────────────────────────────────────────────────────────────────────
203// Model File Types (for per-shard OID tracking)
204// ─────────────────────────────────────────────────────────────────────────────
205
206/// Represents a single file (shard) belonging to a model.
207///
208/// This tracks per-file metadata including OIDs for verification and update detection.
209/// Models can have multiple files (sharded models) or a single file.
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct ModelFile {
212 /// Database ID of this model file entry.
213 pub id: i64,
214 /// ID of the parent model.
215 pub model_id: i64,
216 /// Relative path to the file within the model directory.
217 pub file_path: String,
218 /// Index of this file in the shard sequence (0 for single-file models).
219 pub file_index: i32,
220 /// Expected file size in bytes (from `HuggingFace` API).
221 pub expected_size: i64,
222 /// `HuggingFace` OID (Git LFS SHA256 hash) for this file.
223 pub hf_oid: Option<String>,
224 /// UTC timestamp of when this file was last verified.
225 pub last_verified_at: Option<DateTime<Utc>>,
226}
227
228/// A model file entry to be inserted into the system (no ID yet).
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct NewModelFile {
231 /// ID of the parent model.
232 pub model_id: i64,
233 /// Relative path to the file within the model directory.
234 pub file_path: String,
235 /// Index of this file in the shard sequence (0 for single-file models).
236 pub file_index: i32,
237 /// Expected file size in bytes (from `HuggingFace` API).
238 pub expected_size: i64,
239 /// `HuggingFace` OID (Git LFS SHA256 hash) for this file.
240 pub hf_oid: Option<String>,
241}
242
243impl NewModelFile {
244 /// Create a new model file entry with minimal required fields.
245 #[must_use]
246 pub const fn new(
247 model_id: i64,
248 file_path: String,
249 file_index: i32,
250 expected_size: i64,
251 hf_oid: Option<String>,
252 ) -> Self {
253 Self {
254 model_id,
255 file_path,
256 file_index,
257 expected_size,
258 hf_oid,
259 }
260 }
261}
262
263impl NewModel {
264 /// Create a new model with minimal required fields.
265 ///
266 /// Other fields are set to `None` or empty defaults.
267 #[must_use]
268 pub fn new(
269 name: String,
270 file_path: PathBuf,
271 param_count_b: f64,
272 added_at: DateTime<Utc>,
273 ) -> Self {
274 Self {
275 name,
276 file_path,
277 param_count_b,
278 architecture: None,
279 quantization: None,
280 context_length: None,
281 expert_count: None,
282 expert_used_count: None,
283 expert_shared_count: None,
284 metadata: HashMap::new(),
285 added_at,
286 hf_repo_id: None,
287 hf_commit_sha: None,
288 hf_filename: None,
289 download_date: None,
290 last_update_check: None,
291 tags: Vec::new(),
292 file_paths: None,
293 capabilities: ModelCapabilities::default(),
294 inference_defaults: None,
295 defaults_origin: None,
296 server_defaults: None,
297 }
298 }
299}
300
301impl Model {
302 /// Convert this model to a `NewModel` (drops the ID).
303 ///
304 /// Useful when you need to clone a model's data without the ID.
305 #[must_use]
306 pub fn to_new_model(&self) -> NewModel {
307 NewModel {
308 name: self.name.clone(),
309 file_path: self.file_path.clone(),
310 param_count_b: self.param_count_b,
311 architecture: self.architecture.clone(),
312 quantization: self.quantization.clone(),
313 context_length: self.context_length,
314 expert_count: self.expert_count,
315 expert_used_count: self.expert_used_count,
316 expert_shared_count: self.expert_shared_count,
317 metadata: self.metadata.clone(),
318 added_at: self.added_at,
319 hf_repo_id: self.hf_repo_id.clone(),
320 hf_commit_sha: self.hf_commit_sha.clone(),
321 hf_filename: self.hf_filename.clone(),
322 download_date: self.download_date,
323 last_update_check: self.last_update_check,
324 tags: self.tags.clone(),
325 file_paths: None, // Not preserved in conversion
326 capabilities: self.capabilities,
327 inference_defaults: self.inference_defaults.clone(),
328 defaults_origin: self.defaults_origin,
329 server_defaults: self.server_defaults.clone(),
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use chrono::Utc;
338
339 #[test]
340 fn test_new_model_creation() {
341 let model = NewModel::new(
342 "Test Model".to_string(),
343 PathBuf::from("/path/to/model.gguf"),
344 7.0,
345 Utc::now(),
346 );
347
348 assert_eq!(model.name, "Test Model");
349 assert!((model.param_count_b - 7.0).abs() < f64::EPSILON);
350 assert!(model.architecture.is_none());
351 assert!(model.tags.is_empty());
352 }
353
354 #[test]
355 fn test_model_to_new_model() {
356 let model = Model {
357 id: 42,
358 name: "Persisted Model".to_string(),
359 model_key: String::new(),
360 file_path: PathBuf::from("/path/to/model.gguf"),
361 param_count_b: 13.0,
362 architecture: Some("llama".to_string()),
363 quantization: Some("Q4_0".to_string()),
364 context_length: Some(4096),
365 expert_count: None,
366 expert_used_count: None,
367 expert_shared_count: None,
368 metadata: HashMap::new(),
369 added_at: Utc::now(),
370 hf_repo_id: Some("TheBloke/Model-GGUF".to_string()),
371 hf_commit_sha: None,
372 hf_filename: None,
373 download_date: None,
374 last_update_check: None,
375 tags: vec!["chat".to_string()],
376 capabilities: ModelCapabilities::default(),
377 inference_defaults: None,
378 defaults_origin: None,
379 server_defaults: None,
380 benchmark_summary: None,
381 };
382
383 let new_model = model.to_new_model();
384 assert_eq!(new_model.name, "Persisted Model");
385 assert_eq!(new_model.architecture, Some("llama".to_string()));
386 assert_eq!(new_model.tags, vec!["chat".to_string()]);
387 }
388}