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}