1use ferrum_types::Result;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use tracing::{debug, info};
7
8#[derive(Debug, Clone)]
10pub struct ModelAlias {
11 pub name: String,
13 pub target: String,
15 pub description: Option<String>,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
21pub enum Architecture {
22 Llama,
23 Qwen2,
24 Qwen3,
25 Qwen3Moe,
28 Gemma3,
33 Mistral,
34 Phi,
35 GPT2,
36 Bert,
37 Clip,
38 Whisper,
39 Qwen3TTS,
40 Unknown,
41}
42
43impl Architecture {
44 pub fn from_str(s: &str) -> Self {
45 match s.to_lowercase().as_str() {
46 "llama" | "llamaforcausallm" => Architecture::Llama,
47 "qwen2" | "qwen2forcausallm" => Architecture::Qwen2,
48 "qwen3" | "qwen3forcausallm" => Architecture::Qwen3,
49 "qwen3_moe" | "qwen3moe" | "qwen3moeforcausallm" => Architecture::Qwen3Moe,
50 "gemma3" | "gemma3_text" | "gemma3forcausallm" | "gemma3forconditionalgeneration" => {
51 Architecture::Gemma3
52 }
53 "mistral" | "mistralforcausallm" => Architecture::Mistral,
54 "phi" | "phiforcausallm" => Architecture::Phi,
55 "gpt2" | "gpt2lmheadmodel" => Architecture::GPT2,
56 "bert" | "bertmodel" | "bertformaskedlm" | "bertforsequenceclassification" => {
57 Architecture::Bert
58 }
59 "clip" | "clipmodel" => Architecture::Clip,
60 "chinese_clip" | "chineseclipmodel" => Architecture::Clip,
61 "siglip" | "siglipmodel" => Architecture::Clip,
62 "whisper" | "whisperforconditionalgeneration" => Architecture::Whisper,
63 "qwen3_tts" | "qwen3ttsforconditionalgeneration" => Architecture::Qwen3TTS,
64 _ => Architecture::Unknown,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum ModelFormatType {
72 SafeTensors,
73 PyTorch,
74 GGUF,
75 Unknown,
76}
77
78#[derive(Debug, Clone)]
80pub struct ModelDiscoveryEntry {
81 pub id: String,
83 pub path: PathBuf,
85 pub format: ModelFormatType,
87 pub architecture: Option<Architecture>,
89 pub is_valid: bool,
91}
92
93#[derive(Debug)]
95pub struct DefaultModelRegistry {
96 aliases: HashMap<String, String>,
98 discovered_models: Vec<ModelDiscoveryEntry>,
100}
101
102impl DefaultModelRegistry {
103 pub fn new() -> Self {
105 Self {
106 aliases: HashMap::new(),
107 discovered_models: Vec::new(),
108 }
109 }
110
111 pub fn with_defaults() -> Self {
113 let mut registry = Self::new();
114
115 registry.register_alias("tinyllama", "TinyLlama/TinyLlama-1.1B-Chat-v1.0");
117 registry.register_alias("llama2-7b", "meta-llama/Llama-2-7b-hf");
118 registry.register_alias("llama2-7b-chat", "meta-llama/Llama-2-7b-chat-hf");
119 registry.register_alias("llama3-8b", "meta-llama/Meta-Llama-3-8B");
120 registry.register_alias("llama3-8b-instruct", "meta-llama/Meta-Llama-3-8B-Instruct");
121 registry.register_alias("qwen2-7b", "Qwen/Qwen2-7B");
122 registry.register_alias("qwen2-7b-instruct", "Qwen/Qwen2-7B-Instruct");
123 registry.register_alias("qwen3-0.6b", "Qwen/Qwen3-0.6B");
124 registry.register_alias("qwen3-1.7b", "Qwen/Qwen3-1.7B");
125 registry.register_alias("qwen3-4b", "Qwen/Qwen3-4B");
126 registry.register_alias("mistral-7b", "mistralai/Mistral-7B-v0.1");
127 registry.register_alias("mistral-7b-instruct", "mistralai/Mistral-7B-Instruct-v0.2");
128 registry.register_alias("phi3-mini", "microsoft/Phi-3-mini-4k-instruct");
129
130 registry.register_alias("whisper-tiny", "openai/whisper-tiny");
132 registry.register_alias("whisper-base", "openai/whisper-base");
133 registry.register_alias("whisper-small", "openai/whisper-small");
134 registry.register_alias("whisper-medium", "openai/whisper-medium");
135 registry.register_alias("whisper-large-v3", "openai/whisper-large-v3");
136 registry.register_alias("whisper-turbo", "openai/whisper-large-v3-turbo");
137 registry.register_alias("whisper-large-v3-turbo", "openai/whisper-large-v3-turbo");
138
139 registry
140 }
141
142 pub fn register_alias(&mut self, alias: impl Into<String>, target: impl Into<String>) {
144 let alias_str = alias.into();
145 let target_str = target.into();
146 debug!("Registering alias: {} -> {}", alias_str, target_str);
147 self.aliases.insert(alias_str, target_str);
148 }
149
150 pub fn add_alias(&mut self, alias: ModelAlias) -> Result<()> {
152 self.register_alias(alias.name, alias.target);
153 Ok(())
154 }
155
156 pub fn resolve_model_id(&self, name: &str) -> String {
158 self.aliases
159 .get(name)
160 .cloned()
161 .unwrap_or_else(|| name.to_string())
162 }
163
164 pub fn list_aliases(&self) -> Vec<ModelAlias> {
166 self.aliases
167 .iter()
168 .map(|(name, target)| ModelAlias {
169 name: name.clone(),
170 target: target.clone(),
171 description: None,
172 })
173 .collect()
174 }
175
176 pub async fn discover_models(&mut self, root: &Path) -> Result<Vec<ModelDiscoveryEntry>> {
178 if !root.exists() || !root.is_dir() {
179 return Ok(Vec::new());
180 }
181
182 info!("Discovering models in: {:?}", root);
183
184 let mut discovered = Vec::new();
185
186 if let Some(model_entry) = self.inspect_model_dir(root).await {
188 discovered.push(model_entry);
189 } else {
190 if let Ok(entries) = std::fs::read_dir(root) {
192 for entry in entries.filter_map(|e| e.ok()) {
193 let path = entry.path();
194 if path.is_dir() {
195 if let Some(model_entry) = self.inspect_model_dir(&path).await {
196 discovered.push(model_entry);
197 }
198 }
199 }
200 }
201 }
202
203 self.discovered_models = discovered.clone();
204 Ok(discovered)
205 }
206
207 async fn inspect_model_dir(&self, path: &Path) -> Option<ModelDiscoveryEntry> {
209 let config_path = path.join("config.json");
211 if !config_path.exists() {
212 debug!("No config.json in: {:?}", path);
213 return None;
214 }
215
216 let format = self.detect_model_format(path);
218 if format == ModelFormatType::Unknown {
219 debug!("Unknown format in: {:?}", path);
220 return None;
221 }
222
223 debug!("Found valid model at: {:?}, format: {:?}", path, format);
224
225 let architecture = self.read_architecture(&config_path);
227
228 let id = if let Some(parent) = path.parent() {
230 if let Some(grandparent) = parent.parent() {
231 if let Some(name) = grandparent.file_name().and_then(|n| n.to_str()) {
233 if name.starts_with("models--") {
234 name[8..].replace("--", "/")
235 } else {
236 path.file_name()
237 .and_then(|n| n.to_str())
238 .unwrap_or("unknown")
239 .to_string()
240 }
241 } else {
242 path.file_name()
243 .and_then(|n| n.to_str())
244 .unwrap_or("unknown")
245 .to_string()
246 }
247 } else {
248 path.file_name()
249 .and_then(|n| n.to_str())
250 .unwrap_or("unknown")
251 .to_string()
252 }
253 } else {
254 path.file_name()
255 .and_then(|n| n.to_str())
256 .unwrap_or("unknown")
257 .to_string()
258 };
259
260 Some(ModelDiscoveryEntry {
261 id,
262 path: path.to_path_buf(),
263 format,
264 architecture,
265 is_valid: true,
266 })
267 }
268
269 fn detect_model_format(&self, path: &Path) -> ModelFormatType {
271 if path.join("model.safetensors").exists()
272 || path.join("model.safetensors.index.json").exists()
273 {
274 ModelFormatType::SafeTensors
275 } else if path.join("pytorch_model.bin").exists()
276 || path.join("pytorch_model.bin.index.json").exists()
277 {
278 ModelFormatType::PyTorch
279 } else if std::fs::read_dir(path)
280 .ok()
281 .and_then(|entries| {
282 entries
283 .filter_map(|e| e.ok())
284 .find(|e| e.path().extension().and_then(|s| s.to_str()) == Some("gguf"))
285 })
286 .is_some()
287 {
288 ModelFormatType::GGUF
289 } else {
290 ModelFormatType::Unknown
291 }
292 }
293
294 fn read_architecture(&self, config_path: &Path) -> Option<Architecture> {
296 let content = std::fs::read_to_string(config_path).ok()?;
297 let config: serde_json::Value = serde_json::from_str(&content).ok()?;
298
299 if let Some(model_type) = config.get("model_type").and_then(|v| v.as_str()) {
301 return Some(Architecture::from_str(model_type));
302 }
303
304 if let Some(architectures) = config.get("architectures").and_then(|v| v.as_array()) {
306 if let Some(arch) = architectures.first().and_then(|v| v.as_str()) {
307 return Some(Architecture::from_str(arch));
308 }
309 }
310
311 None
312 }
313}
314
315impl Default for DefaultModelRegistry {
316 fn default() -> Self {
317 Self::new()
318 }
319}
320
321#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_architecture_from_str() {
331 assert_eq!(Architecture::from_str("llama"), Architecture::Llama);
332 assert_eq!(
333 Architecture::from_str("LlamaForCausalLM"),
334 Architecture::Llama
335 );
336 assert_eq!(Architecture::from_str("qwen2"), Architecture::Qwen2);
337 assert_eq!(Architecture::from_str("mistral"), Architecture::Mistral);
338 assert_eq!(Architecture::from_str("phi"), Architecture::Phi);
339 assert_eq!(Architecture::from_str("gpt2"), Architecture::GPT2);
340 assert_eq!(
341 Architecture::from_str("unknown_arch"),
342 Architecture::Unknown
343 );
344 }
345
346 #[test]
347 fn test_architecture_copy() {
348 let arch = Architecture::Llama;
349 let arch2 = arch;
350 assert_eq!(arch, arch2);
351 }
352
353 #[test]
354 fn test_model_format_type_eq() {
355 assert_eq!(ModelFormatType::SafeTensors, ModelFormatType::SafeTensors);
356 assert_ne!(ModelFormatType::SafeTensors, ModelFormatType::PyTorch);
357 }
358
359 #[test]
360 fn test_model_alias_creation() {
361 let alias = ModelAlias {
362 name: "test".to_string(),
363 target: "test/model".to_string(),
364 description: Some("Test model".to_string()),
365 };
366
367 assert_eq!(alias.name, "test");
368 assert_eq!(alias.target, "test/model");
369 assert!(alias.description.is_some());
370 }
371
372 #[test]
373 fn test_model_alias_clone() {
374 let alias = ModelAlias {
375 name: "test".to_string(),
376 target: "test/model".to_string(),
377 description: None,
378 };
379
380 let cloned = alias.clone();
381 assert_eq!(alias.name, cloned.name);
382 assert_eq!(alias.target, cloned.target);
383 }
384
385 #[test]
386 fn test_model_discovery_entry() {
387 let entry = ModelDiscoveryEntry {
388 id: "test-model".to_string(),
389 path: PathBuf::from("/path/to/model"),
390 format: ModelFormatType::SafeTensors,
391 architecture: Some(Architecture::Llama),
392 is_valid: true,
393 };
394
395 assert_eq!(entry.id, "test-model");
396 assert_eq!(entry.format, ModelFormatType::SafeTensors);
397 assert!(entry.is_valid);
398 }
399
400 #[test]
401 fn test_registry_creation() {
402 let registry = DefaultModelRegistry::new();
403 assert_eq!(registry.aliases.len(), 0);
404 assert_eq!(registry.discovered_models.len(), 0);
405 }
406
407 #[test]
408 fn test_registry_default() {
409 let registry = DefaultModelRegistry::default();
410 assert_eq!(registry.aliases.len(), 0);
411 }
412
413 #[test]
414 fn test_registry_with_defaults() {
415 let registry = DefaultModelRegistry::with_defaults();
416
417 assert!(registry.aliases.len() > 0);
419
420 assert!(registry.aliases.contains_key("tinyllama"));
422 assert!(registry.aliases.contains_key("llama2-7b"));
423 }
424
425 #[test]
426 fn test_registry_register_alias() {
427 let mut registry = DefaultModelRegistry::new();
428
429 registry.register_alias("test", "test/model");
430
431 assert_eq!(
432 registry.aliases.get("test"),
433 Some(&"test/model".to_string())
434 );
435 }
436
437 #[test]
438 fn test_registry_resolve_model_id() {
439 let mut registry = DefaultModelRegistry::new();
440
441 registry.register_alias("mymodel", "org/actual-model");
442
443 let resolved = registry.resolve_model_id("mymodel");
444 assert_eq!(resolved, "org/actual-model");
445
446 let unresolved = registry.resolve_model_id("unknown");
448 assert_eq!(unresolved, "unknown");
449 }
450
451 #[test]
452 fn test_registry_list_aliases() {
453 let mut registry = DefaultModelRegistry::new();
454
455 registry.register_alias("model1", "org/model1");
456 registry.register_alias("model2", "org/model2");
457
458 let aliases = registry.list_aliases();
459 assert_eq!(aliases.len(), 2);
460 }
461
462 #[test]
463 fn test_architecture_debug() {
464 let arch = Architecture::Llama;
465 let debug_str = format!("{:?}", arch);
466 assert!(debug_str.contains("Llama"));
467 }
468
469 #[test]
470 fn test_model_format_debug() {
471 let format = ModelFormatType::SafeTensors;
472 let debug_str = format!("{:?}", format);
473 assert!(debug_str.contains("SafeTensors"));
474 }
475
476 #[test]
477 fn test_model_discovery_entry_clone() {
478 let entry = ModelDiscoveryEntry {
479 id: "test".to_string(),
480 path: PathBuf::from("/path"),
481 format: ModelFormatType::GGUF,
482 architecture: Some(Architecture::Mistral),
483 is_valid: false,
484 };
485
486 let cloned = entry.clone();
487 assert_eq!(entry.id, cloned.id);
488 assert_eq!(entry.format, cloned.format);
489 assert_eq!(entry.is_valid, cloned.is_valid);
490 }
491
492 #[test]
493 fn test_registry_multiple_aliases_same_target() {
494 let mut registry = DefaultModelRegistry::new();
495
496 registry.register_alias("alias1", "org/model");
497 registry.register_alias("alias2", "org/model");
498
499 assert_eq!(registry.resolve_model_id("alias1"), "org/model");
500 assert_eq!(registry.resolve_model_id("alias2"), "org/model");
501 }
502
503 #[test]
504 fn test_architecture_serialization() {
505 let arch = Architecture::Qwen2;
506 let json = serde_json::to_string(&arch).unwrap();
507 assert!(json.contains("Qwen2"));
508
509 let deserialized: Architecture = serde_json::from_str(&json).unwrap();
510 assert_eq!(deserialized, arch);
511 }
512}