vtcode_core/cli/
models_commands.rs

1//! Model management command handlers with concise, actionable output
2
3use super::args::{Cli, ModelCommands};
4use crate::llm::factory::{create_provider_with_config, get_factory};
5use crate::utils::dot_config::{DotConfig, get_dot_manager, load_user_config};
6use anyhow::{Result, anyhow};
7use colored::*;
8
9/// Handle model management commands with concise output
10pub async fn handle_models_command(cli: &Cli, command: &ModelCommands) -> Result<()> {
11    match command {
12        ModelCommands::List => handle_list_models(cli).await,
13        ModelCommands::SetProvider { provider } => handle_set_provider(cli, provider).await,
14        ModelCommands::SetModel { model } => handle_set_model(cli, model).await,
15        ModelCommands::Config {
16            provider,
17            api_key,
18            base_url,
19            model,
20        } => {
21            handle_config_provider(
22                cli,
23                provider,
24                api_key.as_deref(),
25                base_url.as_deref(),
26                model.as_deref(),
27            )
28            .await
29        }
30        ModelCommands::Test { provider } => handle_test_provider(cli, provider).await,
31        ModelCommands::Compare => handle_compare_models(cli).await,
32        ModelCommands::Info { model } => handle_model_info(cli, model).await,
33    }
34}
35
36/// Display available providers and models with status
37async fn handle_list_models(_cli: &Cli) -> Result<()> {
38    println!("{}", "Available Providers & Models".bold().underline());
39    println!();
40
41    let factory = get_factory().lock().unwrap();
42    let config = load_user_config().unwrap_or_default();
43    let providers = factory.list_providers();
44
45    for provider_name in &providers {
46        let is_current = config.preferences.default_provider == *provider_name;
47        let status = if is_current { "▶️" } else { "  " };
48        let provider_display = format!("{}{}", status, provider_name.to_uppercase());
49
50        // Color the provider name based on whether it's the current provider
51        let colored_provider = if is_current {
52            format!("{}", provider_display.bold().green())
53        } else {
54            format!("{}", provider_display.bold())
55        };
56        println!("{}", colored_provider);
57
58        // Show models concisely
59        if let Ok(provider) =
60            create_provider_with_config(provider_name, Some("dummy".to_string()), None, None, None)
61        {
62            let models = provider.supported_models();
63            let current_model = &config.preferences.default_model;
64
65            for model in models.iter().take(3) {
66                // Show first 3 models
67                let is_current_model = current_model == model;
68                let model_status = if is_current_model { "⭐" } else { "  " };
69                let colored_model = if is_current_model {
70                    format!("{}", model.clone().bold().cyan())
71                } else {
72                    format!("{}", model.clone().cyan())
73                };
74                println!("  {}{}", model_status, colored_model);
75            }
76            if models.len() > 3 {
77                println!("  {} +{} more models", "...".dimmed(), models.len() - 3);
78            }
79        } else {
80            println!("  {}", "⚠️  Setup required".yellow());
81        }
82
83        // Configuration status
84        let configured = is_provider_configured(&config, provider_name);
85        let config_status = if configured {
86            format!("{}", "✅ Configured".green())
87        } else {
88            format!("{}", "⚠️  Not configured".yellow())
89        };
90        println!("  {}", config_status);
91        println!();
92    }
93
94    // Current config summary
95    println!("{}", "📋 Current Config".bold().underline());
96    println!("Provider: {}", config.preferences.default_provider.cyan());
97    println!("Model: {}", config.preferences.default_model.cyan());
98
99    Ok(())
100}
101
102/// Check if provider is configured
103fn is_provider_configured(config: &DotConfig, provider: &str) -> bool {
104    match provider {
105        "openai" => config
106            .providers
107            .openai
108            .as_ref()
109            .map(|p| p.enabled)
110            .unwrap_or(false),
111        "anthropic" => config
112            .providers
113            .anthropic
114            .as_ref()
115            .map(|p| p.enabled)
116            .unwrap_or(false),
117        "gemini" => config
118            .providers
119            .gemini
120            .as_ref()
121            .map(|p| p.enabled)
122            .unwrap_or(false),
123        "openrouter" => config
124            .providers
125            .openrouter
126            .as_ref()
127            .map(|p| p.enabled)
128            .unwrap_or(false),
129        _ => false,
130    }
131}
132
133/// Set default provider
134async fn handle_set_provider(_cli: &Cli, provider: &str) -> Result<()> {
135    let factory = get_factory().lock().unwrap();
136    let available = factory.list_providers();
137
138    if !available.contains(&provider.to_string()) {
139        return Err(anyhow!(
140            "Unknown provider '{}'. Available: {}",
141            provider,
142            available.join(", ")
143        ));
144    }
145
146    let manager = get_dot_manager().lock().unwrap();
147    manager.update_config(|config| {
148        config.preferences.default_provider = provider.to_string();
149    })?;
150
151    println!(
152        "{} Provider set to: {}",
153        "✅".green(),
154        provider.bold().green()
155    );
156    println!(
157        "{} Configure: {}",
158        "💡".blue(),
159        format!("vtcode models config {} --api-key YOUR_KEY", provider).dimmed()
160    );
161
162    Ok(())
163}
164
165/// Set default model
166async fn handle_set_model(_cli: &Cli, model: &str) -> Result<()> {
167    let manager = get_dot_manager().lock().unwrap();
168    manager.update_config(|config| {
169        config.preferences.default_model = model.to_string();
170    })?;
171
172    println!("{} Model set to: {}", "✅".green(), model.bold().green());
173    Ok(())
174}
175
176/// Configure provider settings
177async fn handle_config_provider(
178    _cli: &Cli,
179    provider: &str,
180    api_key: Option<&str>,
181    base_url: Option<&str>,
182    model: Option<&str>,
183) -> Result<()> {
184    let manager = get_dot_manager().lock().unwrap();
185    let mut config = manager.load_config()?;
186
187    match provider {
188        "openai" | "anthropic" | "gemini" | "openrouter" => {
189            configure_standard_provider(&mut config, provider, api_key, model)?;
190        }
191        _ => return Err(anyhow!("Unsupported provider: {}", provider)),
192    }
193
194    manager.save_config(&config)?;
195    println!("{} {} configured!", "✅".green(), provider.bold().green());
196
197    if let Some(key) = api_key {
198        let masked = mask_api_key(key);
199        println!("  API Key: {}", masked.dimmed());
200    }
201    if let Some(url) = base_url {
202        println!("  Base URL: {}", url.dimmed());
203    }
204    if let Some(m) = model {
205        println!("  Model: {}", m.dimmed());
206    }
207
208    Ok(())
209}
210
211/// Configure standard providers
212fn configure_standard_provider(
213    config: &mut DotConfig,
214    provider: &str,
215    api_key: Option<&str>,
216    model: Option<&str>,
217) -> Result<()> {
218    let provider_config = match provider {
219        "openai" => config.providers.openai.get_or_insert_with(Default::default),
220        "anthropic" => config
221            .providers
222            .anthropic
223            .get_or_insert_with(Default::default),
224        "gemini" => config.providers.gemini.get_or_insert_with(Default::default),
225        "openrouter" => config
226            .providers
227            .openrouter
228            .get_or_insert_with(Default::default),
229        "xai" => config.providers.xai.get_or_insert_with(Default::default),
230        _ => return Err(anyhow!("Unknown provider: {}", provider)),
231    };
232
233    if let Some(key) = api_key {
234        provider_config.api_key = Some(key.to_string());
235    }
236    if let Some(m) = model {
237        provider_config.model = Some(m.to_string());
238    }
239    provider_config.enabled = api_key.is_some() || provider_config.api_key.is_some();
240
241    Ok(())
242}
243
244/// Test provider connectivity
245async fn handle_test_provider(_cli: &Cli, provider: &str) -> Result<()> {
246    println!("{} Testing {}...", "🔍".blue(), provider.bold());
247
248    let config = load_user_config()?;
249    let (api_key, base_url, model) = get_provider_credentials(&config, provider)?;
250
251    let provider_instance =
252        create_provider_with_config(provider, api_key, base_url, model.clone(), None)?;
253
254    let test_request = crate::llm::provider::LLMRequest {
255        messages: vec![crate::llm::provider::Message {
256            role: crate::llm::provider::MessageRole::User,
257            content: "Respond with 'OK' if you receive this message.".to_string(),
258            tool_calls: None,
259            tool_call_id: None,
260        }],
261        system_prompt: None,
262        tools: None,
263        model: model.unwrap_or_else(|| "test".to_string()),
264        max_tokens: Some(10),
265        temperature: Some(0.1),
266        stream: false,
267        tool_choice: None,
268        parallel_tool_calls: None,
269        parallel_tool_config: None,
270        reasoning_effort: None,
271    };
272
273    match provider_instance.generate(test_request).await {
274        Ok(response) => {
275            let content = response.content.unwrap_or_default();
276            if content.to_lowercase().contains("ok") {
277                println!(
278                    "{} {} test successful!",
279                    "✅".green(),
280                    provider.bold().green()
281                );
282            } else {
283                println!(
284                    "{} {} responded unexpectedly",
285                    "⚠️".yellow(),
286                    provider.bold().yellow()
287                );
288            }
289        }
290        Err(e) => {
291            println!(
292                "{} {} test failed: {}",
293                "❌".red(),
294                provider.bold().red(),
295                e
296            );
297        }
298    }
299
300    Ok(())
301}
302
303/// Get provider credentials
304fn get_provider_credentials(
305    config: &DotConfig,
306    provider: &str,
307) -> Result<(Option<String>, Option<String>, Option<String>)> {
308    let get_config = |p: Option<&crate::utils::dot_config::ProviderConfig>| {
309        p.map(|c| (c.api_key.clone(), c.base_url.clone(), c.model.clone()))
310            .unwrap_or((None, None, None))
311    };
312
313    match provider {
314        "openai" => Ok(get_config(config.providers.openai.as_ref())),
315        "anthropic" => Ok(get_config(config.providers.anthropic.as_ref())),
316        "gemini" => Ok(get_config(config.providers.gemini.as_ref())),
317        "openrouter" => Ok(get_config(config.providers.openrouter.as_ref())),
318        "xai" => Ok(get_config(config.providers.xai.as_ref())),
319        _ => Err(anyhow!("Unknown provider: {}", provider)),
320    }
321}
322
323/// Compare model performance (placeholder)
324async fn handle_compare_models(_cli: &Cli) -> Result<()> {
325    println!("{}", "📊 Model Performance Comparison".bold().underline());
326    println!();
327    println!("{} Coming soon! Will compare:", "🚧".yellow());
328    println!("• Response times • Token usage • Cost • Quality");
329    println!();
330    println!(
331        "{} Use 'vtcode models list' for available models",
332        "💡".blue()
333    );
334
335    Ok(())
336}
337
338/// Show model information
339async fn handle_model_info(_cli: &Cli, model: &str) -> Result<()> {
340    println!("{} Model Info: {}", "📋".blue(), model.bold().underline());
341    println!();
342
343    println!("Model: {}", model.cyan());
344    println!("Provider: {}", infer_provider_from_model(model));
345    println!("Status: {}", "Available".green());
346    println!();
347    println!("{} Check docs/models.json for specs", "💡".blue());
348
349    Ok(())
350}
351
352/// Infer provider from model name
353fn infer_provider_from_model(model: &str) -> &'static str {
354    if model.starts_with("gpt-") {
355        "OpenAI"
356    } else if model.starts_with("claude-") {
357        "Anthropic"
358    } else if model.starts_with("gemini-") {
359        "Google Gemini"
360    } else if model.starts_with("grok-") {
361        "xAI"
362    } else if model.starts_with("deepseek-") {
363        "DeepSeek"
364    } else {
365        "Unknown"
366    }
367}
368
369/// Mask API key for display
370fn mask_api_key(key: &str) -> String {
371    if key.len() > 8 {
372        format!("{}****{}", &key[..4], &key[key.len().saturating_sub(4)..])
373    } else {
374        "****".to_string()
375    }
376}