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}