Skip to main content

vtcode_core/cli/
models_commands.rs

1//! Model management command handlers with concise, actionable output
2
3use super::args::{Cli, ModelCommands};
4use crate::config::models::supported_models_for_provider;
5use crate::llm::factory::{ProviderConfig, create_provider_with_config, get_factory};
6use crate::utils::colors::{bold, cyan, dimmed, green, red, underline, yellow};
7use crate::utils::dot_config::{DotConfig, get_dot_manager, load_user_config};
8use anyhow::{Context, Result, anyhow};
9
10/// Handle model management commands with concise output
11pub async fn handle_models_command(cli: &Cli, command: &ModelCommands) -> Result<()> {
12    match command {
13        ModelCommands::List => handle_list_models(cli).await,
14        ModelCommands::SetProvider { provider } => handle_set_provider(cli, provider).await,
15        ModelCommands::SetModel { model } => handle_set_model(cli, model).await,
16        ModelCommands::Config {
17            provider,
18            api_key,
19            base_url,
20            model,
21        } => {
22            handle_config_provider(
23                cli,
24                provider,
25                api_key.as_deref(),
26                base_url.as_deref(),
27                model.as_deref(),
28            )
29            .await
30        }
31        ModelCommands::Test { provider } => handle_test_provider(cli, provider).await,
32        ModelCommands::Compare => handle_compare_models(cli).await,
33        ModelCommands::Info { model } => handle_model_info(cli, model).await,
34    }
35}
36
37/// Display available providers and models with status
38async fn handle_list_models(_cli: &Cli) -> Result<()> {
39    println!("{}", underline(&bold("Available Providers & Models")));
40    println!();
41    println!("{}", dimmed("Loading available providers and models..."));
42
43    let config = load_user_config().await.unwrap_or_default();
44    let factory = {
45        let guard = get_factory()
46            .lock()
47            .map_err(|err| anyhow!("LLM factory lock poisoned while listing providers: {err}"))?;
48        guard.list_providers()
49    }; // Lock is released here when guard goes out of scope
50    let providers = factory;
51
52    for provider_name in &providers {
53        let is_current = config.preferences.default_provider == *provider_name;
54        let status = if is_current { "✦" } else { "  " };
55        let provider_display = format!("{}{}", status, provider_name.to_uppercase());
56
57        let colored_provider = if is_current {
58            green(&bold(&provider_display))
59        } else {
60            bold(&provider_display)
61        };
62        println!("{}", colored_provider);
63
64        if let Some(models) = supported_models_for_provider(provider_name) {
65            let current_model = &config.preferences.default_model;
66
67            for model in models.iter().take(3) {
68                let is_current_model = current_model == model;
69                let model_status = if is_current_model { "*" } else { "  " };
70                let colored_model = if is_current_model {
71                    cyan(&bold(model))
72                } else {
73                    cyan(model)
74                };
75                println!("  {}{}", model_status, colored_model);
76            }
77            if models.len() > 3 {
78                println!("  {} +{} more models", dimmed("..."), models.len() - 3);
79            }
80        } else {
81            println!("  {}", yellow("・  Setup required"));
82        }
83
84        let configured = is_provider_configured(&config, provider_name);
85        let config_status = if configured {
86            green("✓ Configured")
87        } else {
88            yellow("・  Not configured")
89        };
90        println!("  {}", config_status);
91        println!();
92    }
93
94    println!("{}", underline(&bold("・ Current Config")));
95    println!("Provider: {}", cyan(&config.preferences.default_provider));
96    println!("Model: {}", cyan(&config.preferences.default_model));
97
98    Ok(())
99}
100
101/// Check if provider is configured
102fn is_provider_configured(config: &DotConfig, provider: &str) -> bool {
103    let (provider_config, default_enabled) = match provider {
104        "openai" => (config.providers.openai.as_ref(), false),
105        "anthropic" => (config.providers.anthropic.as_ref(), false),
106        "gemini" => (config.providers.gemini.as_ref(), false),
107        "deepseek" => (config.providers.deepseek.as_ref(), false),
108        "openrouter" => (config.providers.openrouter.as_ref(), false),
109        "ollama" => (config.providers.ollama.as_ref(), true),
110        "lmstudio" => (config.providers.lmstudio.as_ref(), true),
111        "llamacpp" => (config.providers.llamacpp.as_ref(), true),
112        "stepfun" => (config.providers.stepfun.as_ref(), false),
113        "evolink" => (config.providers.evolink.as_ref(), false),
114        _ => return false,
115    };
116    provider_config
117        .map(|p| p.enabled)
118        .unwrap_or(default_enabled)
119}
120
121/// Set default provider
122async fn handle_set_provider(_cli: &Cli, provider: &str) -> Result<()> {
123    let available = {
124        let factory = get_factory()
125            .lock()
126            .map_err(|err| anyhow!("LLM factory lock poisoned while setting provider: {err}"))?;
127        factory.list_providers()
128    }; // Lock is released here when factory guard goes out of scope
129
130    if !available.iter().any(|p| p == provider) {
131        return Err(anyhow!(
132            "Unknown provider '{}'. Available: {}",
133            provider,
134            available.join(", ")
135        ));
136    }
137
138    let manager = {
139        let guard = get_dot_manager()
140            .context("Failed to initialize dot manager while setting provider")?
141            .lock()
142            .map_err(|err| anyhow!("Dot manager lock poisoned while setting provider: {err}"))?;
143        guard.clone()
144    }; // Lock is released here when guard goes out of scope
145    manager
146        .update_config(|config| {
147            config.preferences.default_provider = provider.to_string();
148        })
149        .await?;
150
151    println!("{} Provider set to: {}", green("✓"), green(&bold(provider)));
152    println!(
153        "{} Configure: {}",
154        cyan("・"),
155        dimmed(&format!(
156            "vtcode models config {} --api-key YOUR_KEY",
157            provider
158        ))
159    );
160
161    Ok(())
162}
163
164/// Set default model
165async fn handle_set_model(_cli: &Cli, model: &str) -> Result<()> {
166    let manager = {
167        let guard = get_dot_manager()
168            .context("Failed to initialize dot manager while setting model")?
169            .lock()
170            .map_err(|err| anyhow!("Dot manager lock poisoned while setting model: {err}"))?;
171        guard.clone()
172    }; // Lock is released here when guard goes out of scope
173    manager
174        .update_config(|config| {
175            config.preferences.default_model = model.to_string();
176        })
177        .await?;
178
179    println!("{} Model set to: {}", green("✓"), green(&bold(model)));
180    Ok(())
181}
182
183/// Configure provider settings
184async fn handle_config_provider(
185    _cli: &Cli,
186    provider: &str,
187    api_key: Option<&str>,
188    base_url: Option<&str>,
189    model: Option<&str>,
190) -> Result<()> {
191    // Clone manager once and reuse for both operations
192    let manager = {
193        let guard = get_dot_manager()
194            .context("Failed to initialize dot manager while configuring provider")?
195            .lock()
196            .map_err(|err| {
197                anyhow!("Dot manager lock poisoned while configuring provider: {err}")
198            })?;
199        guard.clone()
200    };
201
202    let mut config = manager.load_config().await?;
203
204    match provider {
205        "openai" | "anthropic" | "gemini" | "openrouter" | "deepseek" | "ollama" | "lmstudio"
206        | "llamacpp" | "stepfun" | "evolink" => {
207            configure_standard_provider(&mut config, provider, api_key, base_url, model)?;
208        }
209        _ => return Err(anyhow!("Unsupported provider: {}", provider)),
210    }
211
212    // Reuse the same manager instance
213    manager.save_config(&config).await?;
214
215    Ok(())
216}
217
218/// Configure standard providers
219fn configure_standard_provider(
220    config: &mut DotConfig,
221    provider: &str,
222    api_key: Option<&str>,
223    base_url: Option<&str>,
224    model: Option<&str>,
225) -> Result<()> {
226    // Helper macro to reduce boilerplate
227    macro_rules! get_provider_config {
228        ($field:ident) => {
229            config.providers.$field.get_or_insert_with(Default::default)
230        };
231    }
232
233    let provider_config = match provider {
234        "openai" => get_provider_config!(openai),
235        "anthropic" => get_provider_config!(anthropic),
236        "gemini" => get_provider_config!(gemini),
237        "deepseek" => get_provider_config!(deepseek),
238        "openrouter" => get_provider_config!(openrouter),
239        "ollama" => get_provider_config!(ollama),
240        "lmstudio" => get_provider_config!(lmstudio),
241        "llamacpp" => get_provider_config!(llamacpp),
242        "minimax" => get_provider_config!(anthropic), // Note: maps to anthropic
243        "stepfun" => get_provider_config!(stepfun),
244        "evolink" => get_provider_config!(evolink),
245        _ => return Err(anyhow!("Unknown provider: {}", provider)),
246    };
247
248    if let Some(key) = api_key {
249        provider_config.api_key = Some(key.to_owned());
250    }
251    if let Some(url) = base_url {
252        provider_config.base_url = Some(url.to_owned());
253    }
254    if let Some(m) = model {
255        provider_config.model = Some(m.to_owned());
256    }
257
258    // Local providers are enabled by default; others require an API key
259    provider_config.enabled = matches!(provider, "ollama" | "lmstudio" | "llamacpp")
260        || api_key.is_some()
261        || provider_config.api_key.is_some();
262
263    Ok(())
264}
265
266/// Test provider connectivity
267async fn handle_test_provider(_cli: &Cli, provider: &str) -> Result<()> {
268    println!("{} Testing {}...", cyan("・"), bold(provider));
269
270    let config = load_user_config().await?;
271    let (api_key, base_url, model) = get_provider_credentials(&config, provider)?;
272
273    let provider_instance = create_provider_with_config(
274        provider,
275        ProviderConfig {
276            api_key,
277            openai_chatgpt_auth: None,
278            copilot_auth: None,
279            base_url,
280            model: model.clone(),
281            prompt_cache: None,
282            timeouts: None,
283            openai: None,
284            anthropic: None,
285            model_behavior: None,
286            workspace_root: None,
287        },
288    )?;
289
290    let test_request = crate::llm::provider::LLMRequest {
291        messages: vec![crate::llm::provider::Message::user("test".to_owned())],
292        model: model.clone().unwrap_or_else(|| "test".to_owned()),
293        max_tokens: Some(10),
294        temperature: Some(0.0),
295        ..Default::default()
296    };
297
298    match provider_instance.generate(test_request).await {
299        Ok(response) => {
300            let content = response.content.unwrap_or_default();
301            if content.to_lowercase().contains("ok") {
302                println!("{} {} test successful!", green("✓"), green(&bold(provider)));
303            } else {
304                println!(
305                    "{} {} responded unexpectedly",
306                    yellow("・"),
307                    yellow(&bold(provider))
308                );
309            }
310        }
311        Err(e) => {
312            println!("{} {} test failed: {}", red("✦"), red(&bold(provider)), e);
313        }
314    }
315
316    Ok(())
317}
318
319/// Get provider credentials
320fn get_provider_credentials(
321    config: &DotConfig,
322    provider: &str,
323) -> Result<(Option<String>, Option<String>, Option<String>)> {
324    let provider_config = match provider {
325        "openai" => config.providers.openai.as_ref(),
326        "anthropic" => config.providers.anthropic.as_ref(),
327        "gemini" => config.providers.gemini.as_ref(),
328        "deepseek" => config.providers.deepseek.as_ref(),
329        "openrouter" => config.providers.openrouter.as_ref(),
330        "ollama" => config.providers.ollama.as_ref(),
331        "lmstudio" => config.providers.lmstudio.as_ref(),
332        "llamacpp" => config.providers.llamacpp.as_ref(),
333        "stepfun" => config.providers.stepfun.as_ref(),
334        "evolink" => config.providers.evolink.as_ref(),
335        _ => return Err(anyhow!("Unknown provider: {}", provider)),
336    };
337
338    Ok(provider_config
339        .map(|c| (c.api_key.clone(), c.base_url.clone(), c.model.clone()))
340        .unwrap_or((None, None, None)))
341}
342
343/// Compare model performance (placeholder)
344async fn handle_compare_models(_cli: &Cli) -> Result<()> {
345    println!("{}", underline(&bold("✦ Model Performance Comparison")));
346    println!();
347    println!("{} Coming soon! Will compare:", yellow("✦"));
348    println!("• Response times • Token usage • Cost • Quality");
349    println!();
350    println!(
351        "{} Use 'vtcode models list' for available models",
352        cyan("・")
353    );
354
355    Ok(())
356}
357
358/// Show model information
359async fn handle_model_info(_cli: &Cli, model: &str) -> Result<()> {
360    let resolved = crate::llm::ModelResolver::resolve(None, model, &[], None);
361    println!("{} Model Info: {}", cyan("・"), underline(&bold(model)));
362    println!();
363
364    println!("Model: {}", cyan(model));
365    if let Some(resolved) = resolved {
366        println!("Provider: {}", resolved.provider.label());
367        if let Some(context_window) = resolved.context_window() {
368            println!("Context: {}", context_window);
369        }
370        println!(
371            "Reasoning: {}",
372            if resolved.reasoning_supported() {
373                green("Yes")
374            } else {
375                yellow("No")
376            }
377        );
378        println!(
379            "Tools: {}",
380            if resolved.supports_tool_calls() {
381                green("Yes")
382            } else {
383                yellow("No")
384            }
385        );
386        println!(
387            "Availability: {}",
388            model_availability_label(&resolved.availability)
389        );
390    } else {
391        println!("Provider: {}", infer_provider_from_model(model));
392        println!("Availability: {}", yellow("Unknown"));
393    }
394    println!();
395    println!("{} Check docs/models.json for specs", cyan("・"));
396
397    Ok(())
398}
399
400/// Infer provider from model name
401fn infer_provider_from_model(model: &str) -> &'static str {
402    crate::llm::factory::infer_provider(None, model)
403        .map(|provider| provider.label())
404        .unwrap_or("Unknown")
405}
406
407fn model_availability_label(availability: &crate::llm::ModelAvailability) -> &'static str {
408    match availability {
409        crate::llm::ModelAvailability::Available => "Available",
410        crate::llm::ModelAvailability::MissingCredential => "Missing credential",
411        crate::llm::ModelAvailability::ManagedAuthAvailable => "Managed auth",
412        crate::llm::ModelAvailability::Misconfigured => "Misconfigured",
413        crate::llm::ModelAvailability::LocalOnly => "Local only",
414    }
415}