Skip to main content

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}