1use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AliasEntry {
33 pub alias: String,
35 pub target: String,
37 pub default_quant: Option<String>,
39 pub variants: HashMap<String, String>,
41 pub description: Option<String>,
43}
44
45impl AliasEntry {
46 #[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 #[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 #[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 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct ParsedRef {
96 pub name: String,
98 pub variant: Option<String>,
100 pub quantization: Option<String>,
102}
103
104impl ParsedRef {
105 #[must_use]
114 pub fn parse(s: &str) -> Self {
115 if s.contains("://") {
117 return Self { name: s.to_string(), variant: None, quantization: None };
118 }
119
120 let (name_part, tag_part) =
122 if let Some(idx) = s.find(':') { (&s[..idx], Some(&s[idx + 1..])) } else { (s, None) };
123
124 let (name, name_quant) = extract_quant_suffix(name_part);
126
127 let (variant, tag_quant) =
129 if let Some(tag) = tag_part { parse_tag(tag) } else { (None, None) };
130
131 let quantization = tag_quant.or(name_quant);
133
134 Self { name: name.to_string(), variant, quantization }
135 }
136
137 #[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
157fn extract_quant_suffix(s: &str) -> (&str, Option<String>) {
159 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
174fn parse_tag(tag: &str) -> (Option<String>, Option<String>) {
176 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 if is_quant_tag(tag) {
186 return (None, Some(tag.to_string()));
187 }
188
189 (Some(tag.to_string()), None)
191}
192
193fn 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#[derive(Debug, Clone, Default)]
211pub struct AliasRegistry {
212 aliases: HashMap<String, AliasEntry>,
214}
215
216impl AliasRegistry {
217 #[must_use]
219 pub fn new() -> Self {
220 Self::default()
221 }
222
223 #[must_use]
225 pub fn with_defaults() -> Self {
226 let mut registry = Self::new();
227
228 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 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 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 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 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 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 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 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 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 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 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 pub fn add(&mut self, entry: AliasEntry) {
352 self.aliases.insert(entry.alias.clone(), entry);
353 }
354
355 #[must_use]
357 pub fn get(&self, alias: &str) -> Option<&AliasEntry> {
358 self.aliases.get(alias)
359 }
360
361 #[must_use]
363 pub fn contains(&self, alias: &str) -> bool {
364 self.aliases.contains_key(alias)
365 }
366
367 #[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 #[must_use]
379 pub fn resolve(&self, reference: &str) -> ResolvedAlias {
380 let parsed = ParsedRef::parse(reference);
381
382 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 let uri = if parsed.name.contains("://") {
395 parsed.name.clone()
396 } else if parsed.name.contains('/') {
397 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 #[must_use]
409 pub fn len(&self) -> usize {
410 self.aliases.len()
411 }
412
413 #[must_use]
415 pub fn is_empty(&self) -> bool {
416 self.aliases.is_empty()
417 }
418}
419
420#[derive(Debug, Clone, PartialEq, Eq)]
422pub struct ResolvedAlias {
423 pub uri: String,
425 pub quantization: Option<String>,
427 pub is_alias: bool,
429}
430
431#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[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 #[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 #[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"); 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 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 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 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 #[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 #[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 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}