use crate::models::catalog::{ModelSpec, MODEL_CATALOG};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::RwLock;
lazy_static! {
static ref GLOBAL_REGISTRY: UniversalModelRegistry = UniversalModelRegistry::new();
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ProviderKind {
Ollama,
QwenCloud,
Anthropic,
Gemini,
LocalBridge,
Custom(String),
}
impl ProviderKind {
fn alias_key(&self) -> String {
match self {
ProviderKind::Ollama => "ollama".to_string(),
ProviderKind::QwenCloud => "qwen".to_string(),
ProviderKind::Anthropic => "anthropic".to_string(),
ProviderKind::Gemini => "gemini".to_string(),
ProviderKind::LocalBridge => "local".to_string(),
ProviderKind::Custom(value) => value.to_lowercase(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelResources {
pub size_gb: f64,
pub min_system_ram_gb: u64,
pub min_vram_gb: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelProfile {
pub name: String,
pub provider: ProviderKind,
pub description: String,
pub is_local: bool,
pub api_key_required: bool,
pub context_window: usize,
pub default_endpoint: Option<String>,
pub resources: ModelResources,
pub tags: Vec<String>,
}
impl From<&ModelSpec> for ModelProfile {
fn from(spec: &ModelSpec) -> Self {
ModelProfile {
name: spec.name.to_string(),
provider: ProviderKind::Ollama,
description: spec.description.to_string(),
is_local: true,
api_key_required: false,
context_window: spec.context_sizes.iter().max().copied().unwrap_or(4096),
default_endpoint: Some("http://localhost:11434".to_string()),
resources: ModelResources {
size_gb: spec.size_gb,
min_system_ram_gb: spec.ram_required_gb,
min_vram_gb: spec.gpu_vram_min,
},
tags: vec!["gguf".to_string(), "local".to_string()],
}
}
}
pub struct UniversalModelRegistry {
builtins: HashMap<String, ModelProfile>,
aliases: RwLock<HashMap<String, String>>,
custom: RwLock<HashMap<String, ModelProfile>>,
}
impl UniversalModelRegistry {
fn new() -> Self {
let mut builtins = HashMap::new();
let mut aliases = HashMap::new();
for spec in MODEL_CATALOG.iter() {
let profile: ModelProfile = spec.into();
aliases.insert(profile.name.to_lowercase(), profile.name.clone());
for tag in &profile.tags {
aliases.insert(
format!("{}:{}", profile.name.to_lowercase(), tag),
profile.name.clone(),
);
}
builtins.insert(profile.name.clone(), profile);
}
Self {
builtins,
aliases: RwLock::new(aliases),
custom: RwLock::new(HashMap::new()),
}
}
pub fn global() -> &'static Self {
&GLOBAL_REGISTRY
}
pub fn list_profiles(&self) -> Vec<ModelProfile> {
let mut profiles: Vec<ModelProfile> = self.builtins.values().cloned().collect();
if let Ok(custom) = self.custom.read() {
profiles.extend(custom.values().cloned());
}
profiles.sort_by(|a, b| a.name.cmp(&b.name));
profiles
}
pub fn get_profile(&self, name: &str) -> Option<ModelProfile> {
let key = name.trim().to_lowercase();
if let Some(actual) = self
.aliases
.read()
.ok()
.and_then(|aliases| aliases.get(&key).cloned())
{
if let Some(profile) = self.builtins.get(&actual) {
return Some(profile.clone());
}
}
if let Some(profile) = self.builtins.get(name) {
return Some(profile.clone());
}
if let Ok(custom) = self.custom.read() {
if let Some(profile) = custom.get(name) {
return Some(profile.clone());
}
if let Some(actual) = self
.aliases
.read()
.ok()
.and_then(|aliases| aliases.get(&key).cloned())
{
if let Some(profile) = custom.get(&actual) {
return Some(profile.clone());
}
}
}
None
}
pub fn register_custom(&self, profile: ModelProfile) -> ModelProfile {
{
let mut custom = self.custom.write().expect("custom registry poisoned");
custom.insert(profile.name.clone(), profile.clone());
}
if let Ok(mut aliases) = self.aliases.write() {
aliases.insert(profile.name.to_lowercase(), profile.name.clone());
}
profile
}
pub fn has_profile(&self, name: &str) -> bool {
self.builtins.contains_key(name)
|| self
.custom
.read()
.map(|c| c.contains_key(name))
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtins_available() {
let registry = UniversalModelRegistry::global();
assert!(registry.has_profile("qwen2.5-coder-7b-q4"));
let list = registry.list_profiles();
assert!(!list.is_empty());
}
#[test]
fn custom_registration_round_trip() {
let registry = UniversalModelRegistry::global();
let profile = ModelProfile {
name: "custom-model".to_string(),
provider: ProviderKind::Custom("acme".to_string()),
description: "User supplied profile".to_string(),
is_local: false,
api_key_required: true,
context_window: 8192,
default_endpoint: Some("https://api.example.dev/v1".to_string()),
resources: ModelResources {
size_gb: 12.0,
min_system_ram_gb: 32,
min_vram_gb: Some(16),
},
tags: vec!["custom".to_string()],
};
registry.register_custom(profile.clone());
let fetched = registry
.get_profile("custom-model")
.expect("profile should exist");
assert_eq!(fetched.name, profile.name);
assert_eq!(fetched.context_window, 8192);
}
}