Skip to main content

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