gglib_core/domain/
gguf.rs

1//! GGUF domain types.
2//!
3//! This module contains the domain-facing types for GGUF file metadata
4//! and model capabilities. Parsing logic lives in `gglib-gguf`.
5
6use std::collections::{BTreeSet, HashMap};
7use std::fmt;
8
9// =============================================================================
10// Capabilities (Structured, forward-compatible)
11// =============================================================================
12
13bitflags::bitflags! {
14    /// Known model capabilities detected from GGUF metadata.
15    ///
16    /// Uses bitflags for compile-time safety on stable capabilities.
17    /// Unknown/experimental capabilities go in `GgufCapabilities::extensions`.
18    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
19    pub struct CapabilityFlags: u32 {
20        /// Model supports reasoning/thinking (e.g., DeepSeek R1, QwQ).
21        const REASONING = 0b0000_0001;
22        /// Model supports tool/function calling (e.g., Hermes, Functionary).
23        const TOOL_CALLING = 0b0000_0010;
24        /// Model supports vision/image input.
25        const VISION = 0b0000_0100;
26        /// Model supports code generation.
27        const CODE = 0b0000_1000;
28        /// Model is a mixture-of-experts architecture.
29        const MOE = 0b0001_0000;
30    }
31}
32
33/// Model capabilities detected from GGUF metadata.
34///
35/// Combines stable known capabilities (bitflags) with forward-compatible
36/// extension strings for new/experimental capabilities.
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
38pub struct GgufCapabilities {
39    /// Known stable capabilities (compile-time checked).
40    pub flags: CapabilityFlags,
41    /// Unknown/experimental capabilities (forward-compatible).
42    pub extensions: BTreeSet<String>,
43}
44
45impl GgufCapabilities {
46    /// Create empty capabilities.
47    #[must_use]
48    pub const fn empty() -> Self {
49        Self {
50            flags: CapabilityFlags::empty(),
51            extensions: BTreeSet::new(),
52        }
53    }
54
55    /// Check if reasoning is supported.
56    #[must_use]
57    pub const fn has_reasoning(&self) -> bool {
58        self.flags.contains(CapabilityFlags::REASONING)
59    }
60
61    /// Check if tool calling is supported.
62    #[must_use]
63    pub const fn has_tool_calling(&self) -> bool {
64        self.flags.contains(CapabilityFlags::TOOL_CALLING)
65    }
66
67    /// Check if vision is supported.
68    #[must_use]
69    pub const fn has_vision(&self) -> bool {
70        self.flags.contains(CapabilityFlags::VISION)
71    }
72
73    /// Convert capabilities to tag strings for model metadata.
74    ///
75    /// Returns tags like "reasoning", "agent" (for tool calling), etc.
76    #[must_use]
77    pub fn to_tags(&self) -> Vec<String> {
78        let mut tags = Vec::new();
79
80        if self.has_reasoning() {
81            tags.push("reasoning".to_string());
82        }
83        if self.has_tool_calling() {
84            // "agent" tag triggers --jinja auto-enable
85            tags.push("agent".to_string());
86        }
87        if self.has_vision() {
88            tags.push("vision".to_string());
89        }
90        if self.flags.contains(CapabilityFlags::CODE) {
91            tags.push("code".to_string());
92        }
93        if self.flags.contains(CapabilityFlags::MOE) {
94            tags.push("moe".to_string());
95        }
96
97        // Add extension tags
98        for ext in &self.extensions {
99            if !tags.contains(ext) {
100                tags.push(ext.clone());
101            }
102        }
103
104        tags
105    }
106}
107
108// =============================================================================
109// Metadata value types
110// =============================================================================
111
112/// GGUF metadata value types.
113///
114/// Represents all possible value types that can appear in GGUF metadata.
115#[derive(Debug, Clone)]
116pub enum GgufValue {
117    U8(u8),
118    I8(i8),
119    U16(u16),
120    I16(i16),
121    U32(u32),
122    I32(i32),
123    F32(f32),
124    Bool(bool),
125    String(String),
126    Array(Vec<GgufValue>),
127    U64(u64),
128    I64(i64),
129    F64(f64),
130}
131
132impl fmt::Display for GgufValue {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self {
135            Self::U8(v) => write!(f, "{v}"),
136            Self::I8(v) => write!(f, "{v}"),
137            Self::U16(v) => write!(f, "{v}"),
138            Self::I16(v) => write!(f, "{v}"),
139            Self::U32(v) => write!(f, "{v}"),
140            Self::I32(v) => write!(f, "{v}"),
141            Self::F32(v) => write!(f, "{v}"),
142            Self::Bool(v) => write!(f, "{v}"),
143            Self::String(v) => write!(f, "{v}"),
144            Self::U64(v) => write!(f, "{v}"),
145            Self::I64(v) => write!(f, "{v}"),
146            Self::F64(v) => write!(f, "{v}"),
147            Self::Array(arr) => {
148                // Limit array output to prevent massive tokenizer vocab dumps
149                if arr.len() > 10 {
150                    write!(f, "[Array with {} elements]", arr.len())
151                } else {
152                    write!(
153                        f,
154                        "[{}]",
155                        arr.iter()
156                            .map(std::string::ToString::to_string)
157                            .collect::<Vec<_>>()
158                            .join(", ")
159                    )
160                }
161            }
162        }
163    }
164}
165
166impl GgufValue {
167    /// Try to convert the value to a u64.
168    ///
169    /// Attempts to convert various numeric GGUF value types to u64.
170    /// Only converts non-negative values to avoid overflow issues.
171    #[must_use]
172    #[allow(clippy::cast_sign_loss)]
173    pub fn as_u64(&self) -> Option<u64> {
174        match self {
175            Self::U8(v) => Some(u64::from(*v)),
176            Self::U16(v) => Some(u64::from(*v)),
177            Self::U32(v) => Some(u64::from(*v)),
178            Self::U64(v) => Some(*v),
179            Self::I8(v) if *v >= 0 => Some(*v as u64),
180            Self::I16(v) if *v >= 0 => Some(*v as u64),
181            Self::I32(v) if *v >= 0 => Some(*v as u64),
182            Self::I64(v) if *v >= 0 => Some(*v as u64),
183            _ => None,
184        }
185    }
186
187    /// Try to convert the value to a f64.
188    #[must_use]
189    #[allow(clippy::cast_precision_loss)]
190    pub fn as_f64(&self) -> Option<f64> {
191        match self {
192            Self::F32(v) => Some(f64::from(*v)),
193            Self::F64(v) => Some(*v),
194            Self::U8(v) => Some(f64::from(*v)),
195            Self::U16(v) => Some(f64::from(*v)),
196            Self::U32(v) => Some(f64::from(*v)),
197            Self::U64(v) => Some(*v as f64),
198            Self::I8(v) => Some(f64::from(*v)),
199            Self::I16(v) => Some(f64::from(*v)),
200            Self::I32(v) => Some(f64::from(*v)),
201            Self::I64(v) => Some(*v as f64),
202            _ => None,
203        }
204    }
205
206    /// Try to get the value as a string reference.
207    #[must_use]
208    pub fn as_str(&self) -> Option<&str> {
209        match self {
210            Self::String(s) => Some(s),
211            _ => None,
212        }
213    }
214}
215
216// =============================================================================
217// Metadata
218// =============================================================================
219
220/// Parsed metadata from a GGUF file.
221///
222/// This is the domain-facing type used by services and ports.
223/// Parsing logic that produces this type lives in `gglib-gguf`.
224#[derive(Debug, Clone, Default)]
225pub struct GgufMetadata {
226    /// Model name from general.name metadata or filename.
227    pub name: Option<String>,
228    /// Model architecture (e.g., "llama", "mistral").
229    pub architecture: Option<String>,
230    /// Quantization type (e.g., "`Q4_K_M`", "`Q8_0`").
231    pub quantization: Option<String>,
232    /// Number of parameters in billions.
233    pub param_count_b: Option<f64>,
234    /// Maximum context length.
235    pub context_length: Option<u64>,
236    /// Number of experts (for `MoE` models).
237    pub expert_count: Option<u32>,
238    /// Number of experts used during inference (for `MoE` models).
239    pub expert_used_count: Option<u32>,
240    /// Number of shared experts (for `MoE` models).
241    pub expert_shared_count: Option<u32>,
242    /// Additional key-value metadata from the file (string representation).
243    pub metadata: HashMap<String, String>,
244}
245
246/// Raw metadata from GGUF parsing (before string conversion).
247///
248/// Used internally by parsers; services typically use `GgufMetadata`.
249pub type RawMetadata = HashMap<String, GgufValue>;
250
251// =============================================================================
252// Detection results (for detailed analysis)
253// =============================================================================
254
255/// Result of reasoning capability detection.
256#[derive(Debug, Clone, Default)]
257pub struct ReasoningDetection {
258    /// Whether the model appears to support reasoning/thinking.
259    pub supports_reasoning: bool,
260    /// Confidence level of the detection (0.0 to 1.0).
261    pub confidence: f32,
262    /// The specific pattern(s) that matched.
263    pub matched_patterns: Vec<String>,
264    /// Suggested reasoning format for llama-server.
265    pub suggested_format: Option<String>,
266}
267
268/// Result of tool calling capability detection.
269#[derive(Debug, Clone, Default)]
270pub struct ToolCallingDetection {
271    /// Whether the model appears to support tool/function calling.
272    pub supports_tool_calling: bool,
273    /// Confidence level of the detection (0.0 to 1.0).
274    pub confidence: f32,
275    /// The specific pattern(s) that matched.
276    pub matched_patterns: Vec<String>,
277    /// Detected tool calling format (e.g., "hermes", "llama3", "mistral").
278    pub detected_format: Option<String>,
279}
280
281// =============================================================================
282// Tests
283// =============================================================================
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_capabilities_empty() {
291        let caps = GgufCapabilities::empty();
292        assert!(!caps.has_reasoning());
293        assert!(!caps.has_tool_calling());
294        assert!(caps.to_tags().is_empty());
295    }
296
297    #[test]
298    fn test_capabilities_flags() {
299        let caps = GgufCapabilities {
300            flags: CapabilityFlags::REASONING | CapabilityFlags::TOOL_CALLING,
301            extensions: BTreeSet::new(),
302        };
303        assert!(caps.has_reasoning());
304        assert!(caps.has_tool_calling());
305
306        let tags = caps.to_tags();
307        assert!(tags.contains(&"reasoning".to_string()));
308        assert!(tags.contains(&"agent".to_string()));
309    }
310
311    #[test]
312    fn test_capabilities_extensions() {
313        let mut extensions = BTreeSet::new();
314        extensions.insert("experimental-feature".to_string());
315
316        let caps = GgufCapabilities {
317            flags: CapabilityFlags::empty(),
318            extensions,
319        };
320
321        let tags = caps.to_tags();
322        assert!(tags.contains(&"experimental-feature".to_string()));
323    }
324
325    #[test]
326    fn test_gguf_value_as_u64() {
327        assert_eq!(GgufValue::U32(4096).as_u64(), Some(4096));
328        assert_eq!(GgufValue::I32(-1).as_u64(), None);
329        assert_eq!(GgufValue::String("hello".to_string()).as_u64(), None);
330        assert_eq!(GgufValue::I32(100).as_u64(), Some(100));
331    }
332
333    #[test]
334    fn test_gguf_value_as_f64() {
335        assert!((GgufValue::F32(7.5).as_f64().unwrap() - 7.5).abs() < f64::EPSILON);
336        assert!((GgufValue::U64(1000).as_f64().unwrap() - 1000.0).abs() < f64::EPSILON);
337        assert_eq!(GgufValue::Bool(true).as_f64(), None);
338    }
339
340    #[test]
341    fn test_gguf_value_display() {
342        assert_eq!(GgufValue::U32(42).to_string(), "42");
343        assert_eq!(GgufValue::String("test".to_string()).to_string(), "test");
344
345        let large_array = GgufValue::Array(vec![GgufValue::U8(0); 100]);
346        assert!(large_array.to_string().contains("100 elements"));
347    }
348}