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