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 {
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
37async 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 }; 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
101fn 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
121async 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 }; 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 }; 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
164async 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 }; 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
183async 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 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 manager.save_config(&config).await?;
214
215 Ok(())
216}
217
218fn 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 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), "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 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
266async 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
319fn 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
343async 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
358async 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
400fn 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}