use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ModelCapabilities {
pub max_context_tokens: u32,
pub supports_json_mode: bool,
pub supports_tools: bool,
pub supports_streaming: bool,
}
pub trait LlmModelAdapter: Send + Sync {
fn model_id(&self) -> &str;
fn provider_name(&self) -> &str;
fn display_name(&self) -> &str;
fn capabilities(&self) -> ModelCapabilities;
}
pub struct ModelRegistry {
adapters: HashMap<(String, String), Arc<dyn LlmModelAdapter>>,
}
impl ModelRegistry {
#[must_use]
pub fn new() -> Self {
Self { adapters: HashMap::new() }
}
pub fn register(&mut self, adapter: Arc<dyn LlmModelAdapter>) {
let key = (adapter.provider_name().to_string(), adapter.model_id().to_string());
self.adapters.insert(key, adapter);
}
#[must_use]
pub fn get(&self, provider_name: &str, model_id: &str) -> Option<&Arc<dyn LlmModelAdapter>> {
self.adapters.get(&(provider_name.to_string(), model_id.to_string()))
}
#[must_use]
pub fn list_for_provider(&self, provider_name: &str) -> Vec<Arc<dyn LlmModelAdapter>> {
self.adapters
.iter()
.filter(|((p, _), _)| p == provider_name)
.map(|(_, adapter)| Arc::clone(adapter))
.collect()
}
}
impl Default for ModelRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestModelAdapter {
provider: &'static str,
model: &'static str,
display: &'static str,
caps: ModelCapabilities,
}
impl LlmModelAdapter for TestModelAdapter {
fn model_id(&self) -> &str {
self.model
}
fn provider_name(&self) -> &str {
self.provider
}
fn display_name(&self) -> &str {
self.display
}
fn capabilities(&self) -> ModelCapabilities {
self.caps.clone()
}
}
fn default_caps() -> ModelCapabilities {
ModelCapabilities {
max_context_tokens: 8192,
supports_json_mode: true,
supports_tools: false,
supports_streaming: true,
}
}
fn make_adapter(
provider: &'static str,
model: &'static str,
display: &'static str,
) -> Arc<dyn LlmModelAdapter> {
Arc::new(TestModelAdapter { provider, model, display, caps: default_caps() })
}
#[test]
fn model_adapter_capabilities_exposed() {
let adapter = make_adapter("ollama", "llama3.2:3b", "Llama 3.2 3B");
let caps = adapter.capabilities();
assert_eq!(caps.max_context_tokens, 8192);
assert!(caps.supports_json_mode);
assert!(!caps.supports_tools);
assert!(caps.supports_streaming);
}
#[test]
fn model_registry_provider_model_lookup() {
let mut registry = ModelRegistry::new();
registry.register(make_adapter("ollama", "llama3.2:3b", "Llama 3.2 3B"));
assert!(registry.get("ollama", "llama3.2:3b").is_some());
assert!(registry.get("ollama", "nonexistent").is_none());
assert!(registry.get("openai", "llama3.2:3b").is_none());
}
#[test]
fn model_registry_lists_models_by_provider() {
let mut registry = ModelRegistry::new();
registry.register(make_adapter("ollama", "llama3.2:3b", "Llama 3.2 3B"));
registry.register(make_adapter("ollama", "qwen2.5:7b", "Qwen 2.5 7B"));
registry.register(make_adapter("openai", "gpt-4o", "GPT-4o"));
let ollama_models = registry.list_for_provider("ollama");
assert_eq!(ollama_models.len(), 2);
let openai_models = registry.list_for_provider("openai");
assert_eq!(openai_models.len(), 1);
}
}