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