Skip to main content

pacha/
aliases.rs

1//! Model Aliases and Shortcuts
2//!
3//! Provides short aliases for common models, similar to ollama's model naming.
4//!
5//! ## Features
6//!
7//! - Short names: `llama3` -> `meta-llama/Meta-Llama-3-8B`
8//! - Version shortcuts: `llama3:70b` -> `meta-llama/Meta-Llama-3-70B`
9//! - Quantization tags: `llama3:8b-q4` -> Q4_K_M quantized
10//! - Custom alias configuration
11//!
12//! ## Example
13//!
14//! ```rust,ignore
15//! use pacha::aliases::AliasRegistry;
16//!
17//! let aliases = AliasRegistry::default();
18//!
19//! let resolved = aliases.resolve("llama3:8b-q4")?;
20//! // Returns: hf://meta-llama/Meta-Llama-3-8B-Instruct-GGUF:Q4_K_M
21//! ```
22
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25
26// ============================================================================
27// ALIAS-001: Alias Entry
28// ============================================================================
29
30/// An alias entry mapping short name to full reference
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AliasEntry {
33    /// Short name (e.g., "llama3")
34    pub alias: String,
35    /// Full model reference
36    pub target: String,
37    /// Default quantization
38    pub default_quant: Option<String>,
39    /// Available variants (size tags)
40    pub variants: HashMap<String, String>,
41    /// Description
42    pub description: Option<String>,
43}
44
45impl AliasEntry {
46    /// Create a new alias entry
47    #[must_use]
48    pub fn new(alias: impl Into<String>, target: impl Into<String>) -> Self {
49        Self {
50            alias: alias.into(),
51            target: target.into(),
52            default_quant: None,
53            variants: HashMap::new(),
54            description: None,
55        }
56    }
57
58    /// Set default quantization
59    #[must_use]
60    pub fn with_default_quant(mut self, quant: impl Into<String>) -> Self {
61        self.default_quant = Some(quant.into());
62        self
63    }
64
65    /// Add a size variant
66    #[must_use]
67    pub fn with_variant(mut self, tag: impl Into<String>, target: impl Into<String>) -> Self {
68        self.variants.insert(tag.into(), target.into());
69        self
70    }
71
72    /// Set description
73    #[must_use]
74    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
75        self.description = Some(desc.into());
76        self
77    }
78
79    /// Resolve a variant tag to full target
80    #[must_use]
81    pub fn resolve_variant(&self, variant: Option<&str>) -> &str {
82        match variant {
83            Some(v) => self.variants.get(v).map(String::as_str).unwrap_or(&self.target),
84            None => &self.target,
85        }
86    }
87}
88
89// ============================================================================
90// ALIAS-002: Parsed Model Reference
91// ============================================================================
92
93/// Parsed model reference from alias format
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct ParsedRef {
96    /// Model name (alias or full name)
97    pub name: String,
98    /// Size variant (e.g., "8b", "70b")
99    pub variant: Option<String>,
100    /// Quantization tag (e.g., "q4", "q8")
101    pub quantization: Option<String>,
102}
103
104impl ParsedRef {
105    /// Parse a model reference string
106    ///
107    /// Formats:
108    /// - `name` -> name only
109    /// - `name:variant` -> name with variant
110    /// - `name:variant-quant` -> name with variant and quantization
111    /// - `name-quant` -> name with quantization (no variant)
112    /// - `scheme://path` -> full URI (not parsed as alias)
113    #[must_use]
114    pub fn parse(s: &str) -> Self {
115        // Check if this is a full URI with scheme (contains "://")
116        if s.contains("://") {
117            return Self { name: s.to_string(), variant: None, quantization: None };
118        }
119
120        // Split on colon first (but only if not part of a scheme)
121        let (name_part, tag_part) =
122            if let Some(idx) = s.find(':') { (&s[..idx], Some(&s[idx + 1..])) } else { (s, None) };
123
124        // Check for quantization suffix in name part
125        let (name, name_quant) = extract_quant_suffix(name_part);
126
127        // Parse tag part for variant and quantization
128        let (variant, tag_quant) =
129            if let Some(tag) = tag_part { parse_tag(tag) } else { (None, None) };
130
131        // Quantization from tag takes precedence
132        let quantization = tag_quant.or(name_quant);
133
134        Self { name: name.to_string(), variant, quantization }
135    }
136
137    /// Format as string
138    #[must_use]
139    pub fn to_string_repr(&self) -> String {
140        let mut s = self.name.clone();
141        if let Some(ref v) = self.variant {
142            s.push(':');
143            s.push_str(v);
144        }
145        if let Some(ref q) = self.quantization {
146            if self.variant.is_some() {
147                s.push('-');
148            } else {
149                s.push(':');
150            }
151            s.push_str(q);
152        }
153        s
154    }
155}
156
157/// Extract quantization suffix from name (e.g., "llama-q4" -> ("llama", Some("q4")))
158fn extract_quant_suffix(s: &str) -> (&str, Option<String>) {
159    // Common quant suffixes
160    let quant_patterns = [
161        "-q4", "-q5", "-q6", "-q8", "-Q4_K_M", "-Q4_K_S", "-Q5_K_M", "-Q5_K_S", "-Q6_K", "-Q8_0",
162        "-Q8_K", "-f16", "-f32", "-bf16",
163    ];
164
165    for pattern in quant_patterns {
166        if let Some(idx) = s.to_lowercase().rfind(&pattern.to_lowercase()) {
167            return (&s[..idx], Some(s[idx + 1..].to_string()));
168        }
169    }
170
171    (s, None)
172}
173
174/// Parse tag into variant and quantization
175fn parse_tag(tag: &str) -> (Option<String>, Option<String>) {
176    // Check for variant-quant pattern (e.g., "8b-q4")
177    if let Some(idx) = tag.rfind('-') {
178        let (variant, quant) = (&tag[..idx], &tag[idx + 1..]);
179        if is_quant_tag(quant) {
180            return (Some(variant.to_string()), Some(quant.to_string()));
181        }
182    }
183
184    // Check if entire tag is quantization
185    if is_quant_tag(tag) {
186        return (None, Some(tag.to_string()));
187    }
188
189    // Otherwise it's a variant
190    (Some(tag.to_string()), None)
191}
192
193/// Check if a string is a quantization tag
194fn is_quant_tag(s: &str) -> bool {
195    let lower = s.to_lowercase();
196    lower.starts_with("q") && lower.chars().nth(1).is_some_and(|c| c.is_ascii_digit())
197        || lower.starts_with("iq")
198        || lower == "f16"
199        || lower == "f32"
200        || lower == "fp16"
201        || lower == "fp32"
202        || lower == "bf16"
203}
204
205// ============================================================================
206// ALIAS-003: Alias Registry
207// ============================================================================
208
209/// Registry of model aliases
210#[derive(Debug, Clone, Default)]
211pub struct AliasRegistry {
212    /// Alias entries by short name
213    aliases: HashMap<String, AliasEntry>,
214}
215
216impl AliasRegistry {
217    /// Create a new empty registry
218    #[must_use]
219    pub fn new() -> Self {
220        Self::default()
221    }
222
223    /// Create registry with default aliases
224    #[must_use]
225    pub fn with_defaults() -> Self {
226        let mut registry = Self::new();
227
228        // Llama 3 family
229        registry.add(
230            AliasEntry::new("llama3", "hf://meta-llama/Meta-Llama-3-8B-Instruct")
231                .with_default_quant("Q4_K_M")
232                .with_variant("8b", "hf://meta-llama/Meta-Llama-3-8B-Instruct")
233                .with_variant("70b", "hf://meta-llama/Meta-Llama-3-70B-Instruct")
234                .with_description("Meta's Llama 3 family"),
235        );
236
237        // Llama 3.1 family
238        registry.add(
239            AliasEntry::new("llama3.1", "hf://meta-llama/Meta-Llama-3.1-8B-Instruct")
240                .with_default_quant("Q4_K_M")
241                .with_variant("8b", "hf://meta-llama/Meta-Llama-3.1-8B-Instruct")
242                .with_variant("70b", "hf://meta-llama/Meta-Llama-3.1-70B-Instruct")
243                .with_variant("405b", "hf://meta-llama/Meta-Llama-3.1-405B-Instruct")
244                .with_description("Meta's Llama 3.1 family"),
245        );
246
247        // Mistral family
248        registry.add(
249            AliasEntry::new("mistral", "hf://mistralai/Mistral-7B-Instruct-v0.3")
250                .with_default_quant("Q4_K_M")
251                .with_variant("7b", "hf://mistralai/Mistral-7B-Instruct-v0.3")
252                .with_description("Mistral AI's 7B model"),
253        );
254
255        // Mixtral
256        registry.add(
257            AliasEntry::new("mixtral", "hf://mistralai/Mixtral-8x7B-Instruct-v0.1")
258                .with_default_quant("Q4_K_M")
259                .with_variant("8x7b", "hf://mistralai/Mixtral-8x7B-Instruct-v0.1")
260                .with_variant("8x22b", "hf://mistralai/Mixtral-8x22B-Instruct-v0.1")
261                .with_description("Mistral AI's Mixtral MoE"),
262        );
263
264        // Phi family
265        registry.add(
266            AliasEntry::new("phi3", "hf://microsoft/Phi-3-mini-4k-instruct")
267                .with_default_quant("Q4_K_M")
268                .with_variant("mini", "hf://microsoft/Phi-3-mini-4k-instruct")
269                .with_variant("small", "hf://microsoft/Phi-3-small-8k-instruct")
270                .with_variant("medium", "hf://microsoft/Phi-3-medium-4k-instruct")
271                .with_description("Microsoft's Phi-3 family"),
272        );
273
274        // Gemma family
275        registry.add(
276            AliasEntry::new("gemma", "hf://google/gemma-7b-it")
277                .with_default_quant("Q4_K_M")
278                .with_variant("2b", "hf://google/gemma-2b-it")
279                .with_variant("7b", "hf://google/gemma-7b-it")
280                .with_description("Google's Gemma family"),
281        );
282
283        registry.add(
284            AliasEntry::new("gemma2", "hf://google/gemma-2-9b-it")
285                .with_default_quant("Q4_K_M")
286                .with_variant("2b", "hf://google/gemma-2-2b-it")
287                .with_variant("9b", "hf://google/gemma-2-9b-it")
288                .with_variant("27b", "hf://google/gemma-2-27b-it")
289                .with_description("Google's Gemma 2 family"),
290        );
291
292        // Qwen family
293        registry.add(
294            AliasEntry::new("qwen2", "hf://Qwen/Qwen2-7B-Instruct")
295                .with_default_quant("Q4_K_M")
296                .with_variant("0.5b", "hf://Qwen/Qwen2-0.5B-Instruct")
297                .with_variant("1.5b", "hf://Qwen/Qwen2-1.5B-Instruct")
298                .with_variant("7b", "hf://Qwen/Qwen2-7B-Instruct")
299                .with_variant("72b", "hf://Qwen/Qwen2-72B-Instruct")
300                .with_description("Alibaba's Qwen2 family"),
301        );
302
303        // CodeLlama
304        registry.add(
305            AliasEntry::new("codellama", "hf://codellama/CodeLlama-7b-Instruct-hf")
306                .with_default_quant("Q4_K_M")
307                .with_variant("7b", "hf://codellama/CodeLlama-7b-Instruct-hf")
308                .with_variant("13b", "hf://codellama/CodeLlama-13b-Instruct-hf")
309                .with_variant("34b", "hf://codellama/CodeLlama-34b-Instruct-hf")
310                .with_description("Meta's CodeLlama"),
311        );
312
313        // DeepSeek Coder
314        registry.add(
315            AliasEntry::new("deepseek-coder", "hf://deepseek-ai/deepseek-coder-6.7b-instruct")
316                .with_default_quant("Q4_K_M")
317                .with_variant("1.3b", "hf://deepseek-ai/deepseek-coder-1.3b-instruct")
318                .with_variant("6.7b", "hf://deepseek-ai/deepseek-coder-6.7b-instruct")
319                .with_variant("33b", "hf://deepseek-ai/deepseek-coder-33b-instruct")
320                .with_description("DeepSeek AI Coder"),
321        );
322
323        // StarCoder
324        registry.add(
325            AliasEntry::new("starcoder2", "hf://bigcode/starcoder2-7b")
326                .with_default_quant("Q4_K_M")
327                .with_variant("3b", "hf://bigcode/starcoder2-3b")
328                .with_variant("7b", "hf://bigcode/starcoder2-7b")
329                .with_variant("15b", "hf://bigcode/starcoder2-15b")
330                .with_description("BigCode's StarCoder 2"),
331        );
332
333        // Embedding models
334        registry.add(
335            AliasEntry::new("nomic-embed", "hf://nomic-ai/nomic-embed-text-v1.5")
336                .with_description("Nomic AI embedding model"),
337        );
338
339        registry.add(
340            AliasEntry::new("bge", "hf://BAAI/bge-large-en-v1.5")
341                .with_variant("small", "hf://BAAI/bge-small-en-v1.5")
342                .with_variant("base", "hf://BAAI/bge-base-en-v1.5")
343                .with_variant("large", "hf://BAAI/bge-large-en-v1.5")
344                .with_description("BGE embedding models"),
345        );
346
347        registry
348    }
349
350    /// Add an alias entry
351    pub fn add(&mut self, entry: AliasEntry) {
352        self.aliases.insert(entry.alias.clone(), entry);
353    }
354
355    /// Get an alias entry
356    #[must_use]
357    pub fn get(&self, alias: &str) -> Option<&AliasEntry> {
358        self.aliases.get(alias)
359    }
360
361    /// Check if an alias exists
362    #[must_use]
363    pub fn contains(&self, alias: &str) -> bool {
364        self.aliases.contains_key(alias)
365    }
366
367    /// Get all aliases
368    #[must_use]
369    pub fn list(&self) -> Vec<&AliasEntry> {
370        let mut entries: Vec<_> = self.aliases.values().collect();
371        entries.sort_by(|a, b| a.alias.cmp(&b.alias));
372        entries
373    }
374
375    /// Resolve a model reference
376    ///
377    /// Returns the full URI and optional quantization
378    #[must_use]
379    pub fn resolve(&self, reference: &str) -> ResolvedAlias {
380        let parsed = ParsedRef::parse(reference);
381
382        // Check if it's a known alias
383        if let Some(entry) = self.aliases.get(&parsed.name) {
384            let target = entry.resolve_variant(parsed.variant.as_deref());
385            let quant = parsed.quantization.or_else(|| entry.default_quant.clone());
386
387            ResolvedAlias { uri: target.to_string(), quantization: quant, is_alias: true }
388        } else {
389            // Not an alias, return as-is
390            // Determine scheme based on format:
391            // - Contains "://" -> use as-is
392            // - Contains "/" (like "org/repo") -> assume HuggingFace (hf://)
393            // - Otherwise -> assume pacha://
394            let uri = if parsed.name.contains("://") {
395                parsed.name.clone()
396            } else if parsed.name.contains('/') {
397                // HuggingFace-style org/repo format
398                format!("hf://{}", parsed.name)
399            } else {
400                format!("pacha://{}", parsed.name)
401            };
402
403            ResolvedAlias { uri, quantization: parsed.quantization, is_alias: false }
404        }
405    }
406
407    /// Get number of aliases
408    #[must_use]
409    pub fn len(&self) -> usize {
410        self.aliases.len()
411    }
412
413    /// Check if registry is empty
414    #[must_use]
415    pub fn is_empty(&self) -> bool {
416        self.aliases.is_empty()
417    }
418}
419
420/// Resolved alias result
421#[derive(Debug, Clone, PartialEq, Eq)]
422pub struct ResolvedAlias {
423    /// Full model URI
424    pub uri: String,
425    /// Quantization (if specified)
426    pub quantization: Option<String>,
427    /// Whether this was resolved from an alias
428    pub is_alias: bool,
429}
430
431// ============================================================================
432// Tests
433// ============================================================================
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    // ========================================================================
440    // ALIAS-001: Entry Tests
441    // ========================================================================
442
443    #[test]
444    fn test_alias_entry_new() {
445        let entry = AliasEntry::new("llama3", "hf://meta-llama/Llama-3-8B");
446        assert_eq!(entry.alias, "llama3");
447        assert_eq!(entry.target, "hf://meta-llama/Llama-3-8B");
448    }
449
450    #[test]
451    fn test_alias_entry_builder() {
452        let entry = AliasEntry::new("llama3", "hf://meta-llama/Llama-3-8B")
453            .with_default_quant("Q4_K_M")
454            .with_variant("70b", "hf://meta-llama/Llama-3-70B")
455            .with_description("Llama 3 family");
456
457        assert_eq!(entry.default_quant, Some("Q4_K_M".to_string()));
458        assert!(entry.variants.contains_key("70b"));
459        assert!(entry.description.is_some());
460    }
461
462    #[test]
463    fn test_alias_entry_resolve_variant() {
464        let entry = AliasEntry::new("llama3", "hf://default")
465            .with_variant("8b", "hf://8b-model")
466            .with_variant("70b", "hf://70b-model");
467
468        assert_eq!(entry.resolve_variant(None), "hf://default");
469        assert_eq!(entry.resolve_variant(Some("8b")), "hf://8b-model");
470        assert_eq!(entry.resolve_variant(Some("70b")), "hf://70b-model");
471        assert_eq!(entry.resolve_variant(Some("unknown")), "hf://default");
472    }
473
474    // ========================================================================
475    // ALIAS-002: Parsing Tests
476    // ========================================================================
477
478    #[test]
479    fn test_parsed_ref_name_only() {
480        let parsed = ParsedRef::parse("llama3");
481        assert_eq!(parsed.name, "llama3");
482        assert!(parsed.variant.is_none());
483        assert!(parsed.quantization.is_none());
484    }
485
486    #[test]
487    fn test_parsed_ref_with_variant() {
488        let parsed = ParsedRef::parse("llama3:8b");
489        assert_eq!(parsed.name, "llama3");
490        assert_eq!(parsed.variant, Some("8b".to_string()));
491        assert!(parsed.quantization.is_none());
492    }
493
494    #[test]
495    fn test_parsed_ref_with_variant_and_quant() {
496        let parsed = ParsedRef::parse("llama3:8b-q4");
497        assert_eq!(parsed.name, "llama3");
498        assert_eq!(parsed.variant, Some("8b".to_string()));
499        assert_eq!(parsed.quantization, Some("q4".to_string()));
500    }
501
502    #[test]
503    fn test_parsed_ref_quant_only() {
504        let parsed = ParsedRef::parse("llama3:q4");
505        assert_eq!(parsed.name, "llama3");
506        assert!(parsed.variant.is_none());
507        assert_eq!(parsed.quantization, Some("q4".to_string()));
508    }
509
510    #[test]
511    fn test_parsed_ref_uppercase_quant() {
512        let parsed = ParsedRef::parse("llama3:8b-Q4_K_M");
513        assert_eq!(parsed.name, "llama3");
514        assert_eq!(parsed.variant, Some("8b".to_string()));
515        assert_eq!(parsed.quantization, Some("Q4_K_M".to_string()));
516    }
517
518    #[test]
519    fn test_parsed_ref_to_string() {
520        let parsed = ParsedRef::parse("llama3:8b-q4");
521        assert_eq!(parsed.to_string_repr(), "llama3:8b-q4");
522
523        let parsed = ParsedRef::parse("llama3:q4");
524        assert_eq!(parsed.to_string_repr(), "llama3:q4");
525
526        let parsed = ParsedRef::parse("llama3");
527        assert_eq!(parsed.to_string_repr(), "llama3");
528    }
529
530    #[test]
531    fn test_is_quant_tag() {
532        assert!(is_quant_tag("q4"));
533        assert!(is_quant_tag("Q4_K_M"));
534        assert!(is_quant_tag("q8"));
535        assert!(is_quant_tag("f16"));
536        assert!(is_quant_tag("fp16"));
537        assert!(is_quant_tag("bf16"));
538        assert!(is_quant_tag("iq4"));
539        assert!(!is_quant_tag("8b"));
540        assert!(!is_quant_tag("70b"));
541        assert!(!is_quant_tag("instruct"));
542    }
543
544    // ========================================================================
545    // ALIAS-003: Registry Tests
546    // ========================================================================
547
548    #[test]
549    fn test_alias_registry_new() {
550        let registry = AliasRegistry::new();
551        assert!(registry.is_empty());
552        assert_eq!(registry.len(), 0);
553    }
554
555    #[test]
556    fn test_alias_registry_with_defaults() {
557        let registry = AliasRegistry::with_defaults();
558        assert!(!registry.is_empty());
559        assert!(registry.contains("llama3"));
560        assert!(registry.contains("mistral"));
561        assert!(registry.contains("phi3"));
562    }
563
564    #[test]
565    fn test_alias_registry_add() {
566        let mut registry = AliasRegistry::new();
567        registry.add(AliasEntry::new("test", "hf://test/model"));
568
569        assert!(registry.contains("test"));
570        assert_eq!(registry.len(), 1);
571    }
572
573    #[test]
574    fn test_alias_registry_get() {
575        let registry = AliasRegistry::with_defaults();
576        let entry = registry.get("llama3");
577
578        assert!(entry.is_some());
579        assert!(entry.unwrap().target.contains("meta-llama"));
580    }
581
582    #[test]
583    fn test_alias_registry_list() {
584        let mut registry = AliasRegistry::new();
585        registry.add(AliasEntry::new("zzz", "hf://zzz"));
586        registry.add(AliasEntry::new("aaa", "hf://aaa"));
587
588        let list = registry.list();
589        assert_eq!(list.len(), 2);
590        assert_eq!(list[0].alias, "aaa"); // Sorted
591        assert_eq!(list[1].alias, "zzz");
592    }
593
594    #[test]
595    fn test_alias_registry_resolve_known() {
596        let registry = AliasRegistry::with_defaults();
597
598        let resolved = registry.resolve("llama3");
599        assert!(resolved.is_alias);
600        assert!(resolved.uri.contains("meta-llama"));
601    }
602
603    #[test]
604    fn test_alias_registry_resolve_with_variant() {
605        let registry = AliasRegistry::with_defaults();
606
607        let resolved = registry.resolve("llama3:70b");
608        assert!(resolved.is_alias);
609        assert!(resolved.uri.contains("70B"));
610    }
611
612    #[test]
613    fn test_alias_registry_resolve_with_quant() {
614        let registry = AliasRegistry::with_defaults();
615
616        let resolved = registry.resolve("llama3:8b-q8");
617        assert!(resolved.is_alias);
618        assert_eq!(resolved.quantization, Some("q8".to_string()));
619    }
620
621    #[test]
622    fn test_alias_registry_resolve_default_quant() {
623        let registry = AliasRegistry::with_defaults();
624
625        let resolved = registry.resolve("llama3");
626        assert!(resolved.is_alias);
627        // Should have default quant
628        assert_eq!(resolved.quantization, Some("Q4_K_M".to_string()));
629    }
630
631    #[test]
632    fn test_alias_registry_resolve_unknown() {
633        let registry = AliasRegistry::with_defaults();
634
635        let resolved = registry.resolve("unknown-model");
636        assert!(!resolved.is_alias);
637        assert_eq!(resolved.uri, "pacha://unknown-model");
638    }
639
640    #[test]
641    fn test_alias_registry_resolve_huggingface_style() {
642        let registry = AliasRegistry::with_defaults();
643
644        // HuggingFace-style org/repo format should default to hf:// scheme
645        let resolved = registry.resolve("Qwen/Qwen2.5-Coder-0.5B-Instruct");
646        assert!(!resolved.is_alias);
647        assert_eq!(resolved.uri, "hf://Qwen/Qwen2.5-Coder-0.5B-Instruct");
648
649        // Another example
650        let resolved = registry.resolve("TheBloke/Llama-2-7B-GGUF");
651        assert!(!resolved.is_alias);
652        assert_eq!(resolved.uri, "hf://TheBloke/Llama-2-7B-GGUF");
653    }
654
655    #[test]
656    fn test_alias_registry_resolve_full_uri() {
657        let registry = AliasRegistry::with_defaults();
658
659        let resolved = registry.resolve("hf://some/model");
660        assert!(!resolved.is_alias);
661        assert_eq!(resolved.uri, "hf://some/model");
662    }
663
664    // ========================================================================
665    // Serialization Tests
666    // ========================================================================
667
668    #[test]
669    fn test_alias_entry_serialization() {
670        let entry = AliasEntry::new("llama3", "hf://test")
671            .with_default_quant("Q4_K_M")
672            .with_variant("70b", "hf://test-70b");
673
674        let json = serde_json::to_string(&entry).unwrap();
675        assert!(json.contains("llama3"));
676        assert!(json.contains("Q4_K_M"));
677
678        let parsed: AliasEntry = serde_json::from_str(&json).unwrap();
679        assert_eq!(parsed.alias, "llama3");
680        assert_eq!(parsed.default_quant, Some("Q4_K_M".to_string()));
681    }
682
683    // ========================================================================
684    // Edge Cases
685    // ========================================================================
686
687    #[test]
688    fn test_parsed_ref_complex_name() {
689        let parsed = ParsedRef::parse("deepseek-coder:6.7b-q4");
690        assert_eq!(parsed.name, "deepseek-coder");
691        assert_eq!(parsed.variant, Some("6.7b".to_string()));
692        assert_eq!(parsed.quantization, Some("q4".to_string()));
693    }
694
695    #[test]
696    fn test_parsed_ref_numbers_in_name() {
697        let parsed = ParsedRef::parse("llama3.1:8b");
698        assert_eq!(parsed.name, "llama3.1");
699        assert_eq!(parsed.variant, Some("8b".to_string()));
700    }
701
702    #[test]
703    fn test_resolve_quant_override() {
704        let registry = AliasRegistry::with_defaults();
705
706        // Explicit quant should override default
707        let resolved = registry.resolve("llama3:q8");
708        assert_eq!(resolved.quantization, Some("q8".to_string()));
709    }
710
711    #[test]
712    fn test_gemma_variants() {
713        let registry = AliasRegistry::with_defaults();
714
715        let resolved_2b = registry.resolve("gemma:2b");
716        assert!(resolved_2b.uri.contains("2b"));
717
718        let resolved_7b = registry.resolve("gemma:7b");
719        assert!(resolved_7b.uri.contains("7b"));
720    }
721
722    #[test]
723    fn test_embedding_models() {
724        let registry = AliasRegistry::with_defaults();
725
726        assert!(registry.contains("nomic-embed"));
727        assert!(registry.contains("bge"));
728
729        let resolved = registry.resolve("bge:large");
730        assert!(resolved.uri.contains("large"));
731    }
732}