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