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    /// Additional metadata key-value pairs from the GGUF file.
64    pub metadata: HashMap<String, String>,
65    /// UTC timestamp of when the model was added to the database.
66    pub added_at: DateTime<Utc>,
67    /// `HuggingFace` repository ID (e.g., "`TheBloke/Llama-2-7B-GGUF`").
68    pub hf_repo_id: Option<String>,
69    /// Git commit SHA from `HuggingFace` Hub.
70    pub hf_commit_sha: Option<String>,
71    /// Original filename on `HuggingFace` Hub.
72    pub hf_filename: Option<String>,
73    /// Timestamp of when this model was downloaded from `HuggingFace`.
74    pub download_date: Option<DateTime<Utc>>,
75    /// Last time we checked for updates on `HuggingFace`.
76    pub last_update_check: Option<DateTime<Utc>>,
77    /// User-defined tags for organizing models.
78    pub tags: Vec<String>,
79    /// Model capabilities inferred from chat template analysis.
80    #[serde(default)]
81    pub capabilities: ModelCapabilities,
82    /// Per-model inference parameter defaults.
83    ///
84    /// These are preferred over global settings when making inference requests.
85    /// If not set, falls back to global settings or hardcoded defaults.
86    #[serde(default)]
87    pub inference_defaults: Option<InferenceConfig>,
88}
89
90/// A model to be inserted into the system (no ID yet).
91///
92/// This represents a model that hasn't been persisted to the database.
93/// After insertion, the repository returns a `Model` with the assigned ID.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct NewModel {
96    /// Human-readable name for the model.
97    pub name: String,
98    /// Absolute path to the GGUF file on the filesystem.
99    pub file_path: PathBuf,
100    /// Number of parameters in the model (in billions).
101    pub param_count_b: f64,
102    /// Model architecture (e.g., "llama", "mistral", "falcon").
103    pub architecture: Option<String>,
104    /// Quantization type (e.g., "`Q4_0`", "`Q8_0`", "`F16`", "`F32`").
105    pub quantization: Option<String>,
106    /// Maximum context length the model supports.
107    pub context_length: Option<u64>,
108    /// Additional metadata key-value pairs from the GGUF file.
109    pub metadata: HashMap<String, String>,
110    /// UTC timestamp of when the model was added to the database.
111    pub added_at: DateTime<Utc>,
112    /// `HuggingFace` repository ID (e.g., "`TheBloke/Llama-2-7B-GGUF`").
113    pub hf_repo_id: Option<String>,
114    /// Git commit SHA from `HuggingFace` Hub.
115    pub hf_commit_sha: Option<String>,
116    /// Original filename on `HuggingFace` Hub.
117    pub hf_filename: Option<String>,
118    /// Timestamp of when this model was downloaded from `HuggingFace`.
119    pub download_date: Option<DateTime<Utc>>,
120    /// Last time we checked for updates on `HuggingFace`.
121    pub last_update_check: Option<DateTime<Utc>>,
122    /// User-defined tags for organizing models.
123    pub tags: Vec<String>,
124    /// Ordered list of all file paths for sharded models (None for single-file models).
125    pub file_paths: Option<Vec<PathBuf>>,
126    /// Model capabilities inferred from chat template analysis.
127    #[serde(default)]
128    pub capabilities: ModelCapabilities,
129    /// Per-model inference parameter defaults.
130    ///
131    /// These are preferred over global settings when making inference requests.
132    /// If not set, falls back to global settings or hardcoded defaults.
133    #[serde(default)]
134    pub inference_defaults: Option<InferenceConfig>,
135}
136
137impl NewModel {
138    /// Create a new model with minimal required fields.
139    ///
140    /// Other fields are set to `None` or empty defaults.
141    #[must_use]
142    pub fn new(
143        name: String,
144        file_path: PathBuf,
145        param_count_b: f64,
146        added_at: DateTime<Utc>,
147    ) -> Self {
148        Self {
149            name,
150            file_path,
151            param_count_b,
152            architecture: None,
153            quantization: None,
154            context_length: None,
155            metadata: HashMap::new(),
156            added_at,
157            hf_repo_id: None,
158            hf_commit_sha: None,
159            hf_filename: None,
160            download_date: None,
161            last_update_check: None,
162            tags: Vec::new(),
163            file_paths: None,
164            capabilities: ModelCapabilities::default(),
165            inference_defaults: None,
166        }
167    }
168}
169
170impl Model {
171    /// Convert this model to a `NewModel` (drops the ID).
172    ///
173    /// Useful when you need to clone a model's data without the ID.
174    #[must_use]
175    pub fn to_new_model(&self) -> NewModel {
176        NewModel {
177            name: self.name.clone(),
178            file_path: self.file_path.clone(),
179            param_count_b: self.param_count_b,
180            architecture: self.architecture.clone(),
181            quantization: self.quantization.clone(),
182            context_length: self.context_length,
183            metadata: self.metadata.clone(),
184            added_at: self.added_at,
185            hf_repo_id: self.hf_repo_id.clone(),
186            hf_commit_sha: self.hf_commit_sha.clone(),
187            hf_filename: self.hf_filename.clone(),
188            download_date: self.download_date,
189            last_update_check: self.last_update_check,
190            tags: self.tags.clone(),
191            file_paths: None, // Not preserved in conversion
192            capabilities: self.capabilities,
193            inference_defaults: self.inference_defaults.clone(),
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use chrono::Utc;
202
203    #[test]
204    fn test_new_model_creation() {
205        let model = NewModel::new(
206            "Test Model".to_string(),
207            PathBuf::from("/path/to/model.gguf"),
208            7.0,
209            Utc::now(),
210        );
211
212        assert_eq!(model.name, "Test Model");
213        assert!((model.param_count_b - 7.0).abs() < f64::EPSILON);
214        assert!(model.architecture.is_none());
215        assert!(model.tags.is_empty());
216    }
217
218    #[test]
219    fn test_model_to_new_model() {
220        let model = Model {
221            id: 42,
222            name: "Persisted Model".to_string(),
223            file_path: PathBuf::from("/path/to/model.gguf"),
224            param_count_b: 13.0,
225            architecture: Some("llama".to_string()),
226            quantization: Some("Q4_0".to_string()),
227            context_length: Some(4096),
228            metadata: HashMap::new(),
229            added_at: Utc::now(),
230            hf_repo_id: Some("TheBloke/Model-GGUF".to_string()),
231            hf_commit_sha: None,
232            hf_filename: None,
233            download_date: None,
234            last_update_check: None,
235            tags: vec!["chat".to_string()],
236            capabilities: ModelCapabilities::default(),
237            inference_defaults: None,
238        };
239
240        let new_model = model.to_new_model();
241        assert_eq!(new_model.name, "Persisted Model");
242        assert_eq!(new_model.architecture, Some("llama".to_string()));
243        assert_eq!(new_model.tags, vec!["chat".to_string()]);
244    }
245}