mermaid_cli/models/
model.rs

1/// Unified model implementation
2///
3/// This is the primary Model implementation that wraps the backend router
4/// and provides the clean public API. It handles backend discovery, routing,
5/// and lifecycle management transparently.
6
7use async_trait::async_trait;
8use std::sync::Arc;
9
10use super::backend::Backend;
11use super::config::{BackendConfig, ModelConfig};
12use super::error::Result;
13use super::router::BackendRouter;
14use super::traits::Model;
15use super::types::{ChatMessage, ModelResponse, ProjectContext, StreamCallback};
16
17/// Unified model implementation
18///
19/// This is what users create via ModelFactory. It transparently handles
20/// backend selection, discovery, and routing.
21pub struct UnifiedModel {
22    /// Model specification (e.g., "ollama/qwen3-coder:30b" or just "qwen3-coder:30b")
23    model_spec: String,
24
25    /// Smart router for backend discovery
26    router: Arc<BackendRouter>,
27
28    /// Cached backend (lazy initialization)
29    cached_backend: Option<(Arc<dyn Backend>, String)>,
30}
31
32impl UnifiedModel {
33    /// Create a new unified model
34    pub fn new(model_spec: &str, config: BackendConfig) -> Self {
35        Self {
36            model_spec: model_spec.to_string(),
37            router: Arc::new(BackendRouter::new(config)),
38            cached_backend: None,
39        }
40    }
41
42    /// Get or resolve the backend (lazy initialization)
43    async fn get_backend(&mut self) -> Result<(Arc<dyn Backend>, String)> {
44        // Return cached backend if available
45        if let Some(ref cached) = self.cached_backend {
46            return Ok(cached.clone());
47        }
48
49        // Resolve backend via router
50        let (backend, model_name) = self.router.resolve_model(&self.model_spec).await?;
51
52        // Cache for future use
53        self.cached_backend = Some((backend.clone(), model_name.clone()));
54
55        Ok((backend, model_name))
56    }
57}
58
59#[async_trait]
60impl Model for UnifiedModel {
61    async fn chat(
62        &mut self,
63        messages: &[ChatMessage],
64        context: &ProjectContext,
65        config: &ModelConfig,
66        stream_callback: Option<StreamCallback>,
67    ) -> Result<ModelResponse> {
68        // Get backend (lazy initialization + caching)
69        let (backend, model_name) = self.get_backend().await?;
70
71        // Delegate to backend
72        backend.chat(&model_name, messages, context, config, stream_callback).await
73    }
74
75    fn name(&self) -> &str {
76        &self.model_spec
77    }
78
79    fn is_local(&self) -> bool {
80        // Assume local unless it's explicitly an API provider
81        // This will be accurate after first backend resolution
82        if let Some((ref backend, _)) = self.cached_backend {
83            backend.metadata().is_local
84        } else {
85            // Conservative default: assume not local until we know
86            !self.model_spec.starts_with("openai/")
87                && !self.model_spec.starts_with("anthropic/")
88                && !self.model_spec.starts_with("groq/")
89        }
90    }
91
92    async fn validate_connection(&self) -> Result<bool> {
93        // Quick validation without resolving if not cached yet
94        if let Some((ref backend, _)) = self.cached_backend {
95            Ok(backend.health_check().await.is_ok())
96        } else {
97            // Can't validate without resolving - return optimistic true
98            // Real validation happens on first chat() call
99            Ok(true)
100        }
101    }
102}
103
104/// Factory functions for creating unified models
105
106/// Create a model from a model specification
107///
108/// Examples:
109/// - "ollama/qwen3-coder:30b" - Explicit backend
110/// - "qwen3-coder:30b" - Auto-detect backend
111/// - "gpt-4" - Search backends
112pub async fn create_model(model_spec: &str, backend_config: BackendConfig) -> Result<Box<dyn Model>> {
113    Ok(Box::new(UnifiedModel::new(model_spec, backend_config)))
114}
115
116/// Create a model with default configuration
117pub async fn create_model_default(model_spec: &str) -> Result<Box<dyn Model>> {
118    create_model(model_spec, BackendConfig::default()).await
119}