lc/cli/
providers.rs

1//! Provider management commands
2
3use crate::cli::{HeaderCommands, ProviderCommands, ProviderPathCommands, ProviderVarsCommands};
4use crate::provider_installer::{AuthType, ProviderInstaller};
5use crate::{chat, config, debug_log};
6use anyhow::Result;
7use colored::Colorize;
8
9/// Handle provider-related commands
10pub async fn handle(command: ProviderCommands) -> Result<()> {
11    match command {
12        ProviderCommands::Install { name, force } => {
13            let installer = ProviderInstaller::new()?;
14            installer.install_provider(&name, force).await?;
15        }
16        ProviderCommands::Upgrade { name } => {
17            let installer = ProviderInstaller::new()?;
18            if let Some(provider_name) = name.as_deref() {
19                installer.update_provider(&provider_name).await?;
20            } else {
21                installer.update_all_providers().await?;
22            }
23        }
24        ProviderCommands::Uninstall { name } => {
25            let installer = ProviderInstaller::new()?;
26            installer.uninstall_provider(&name)?;
27        }
28        ProviderCommands::Available { official, tag } => {
29            let installer = ProviderInstaller::new()?;
30            let providers = installer.list_available().await?;
31
32            println!("\n{}", "Available Providers:".bold().blue());
33
34            let mut displayed_count = 0;
35            for (id, metadata) in providers {
36                // Apply filters
37                if official && !metadata.official {
38                    continue;
39                }
40                if let Some(ref filter_tag) = tag {
41                    if !metadata.tags.contains(filter_tag) {
42                        continue;
43                    }
44                }
45
46                displayed_count += 1;
47
48                print!("  {} {} - {}", "•".blue(), id.bold(), metadata.name);
49
50                if metadata.official {
51                    print!(" {}", "✓ official".green());
52                }
53
54                if !metadata.tags.is_empty() {
55                    print!(" [{}]", metadata.tags.join(", ").dimmed());
56                }
57
58                println!("\n    {}", metadata.description.dimmed());
59
60                // Show auth type
61                let auth_str = match metadata.auth_type {
62                    AuthType::ApiKey => "API Key",
63                    AuthType::ServiceAccount => "Service Account",
64                    AuthType::OAuth => "OAuth",
65                    AuthType::Token => "Token",
66                    AuthType::Headers => "Custom Headers",
67                    AuthType::None => "None",
68                };
69                println!("    Auth: {}", auth_str.yellow());
70            }
71
72            if displayed_count == 0 {
73                if official {
74                    println!("No official providers found.");
75                } else if tag.is_some() {
76                    println!("No providers found with the specified tag.");
77                } else {
78                    println!("No providers available.");
79                }
80            } else {
81                println!(
82                    "\n{} Use 'lc providers install <name>' to install a provider",
83                    "💡".yellow()
84                );
85            }
86        }
87        ProviderCommands::Add {
88            name,
89            url,
90            models_path,
91            chat_path,
92        } => {
93            let mut config = config::Config::load()?;
94            config.add_provider_with_paths(name.clone(), url, models_path, chat_path)?;
95            config.save()?;
96            println!("{} Provider '{}' added successfully", "✓".green(), name);
97        }
98        ProviderCommands::Update { name, url } => {
99            let mut config = config::Config::load()?;
100            if !config.has_provider(&name) {
101                anyhow::bail!("Provider '{}' not found", name);
102            }
103            config.add_provider(name.clone(), url)?; // add_provider also updates
104            config.save()?;
105            println!("{} Provider '{}' updated successfully", "✓".green(), name);
106        }
107        ProviderCommands::Remove { name } => {
108            let mut config = config::Config::load()?;
109            if !config.has_provider(&name) {
110                anyhow::bail!("Provider '{}' not found", name);
111            }
112            config.providers.remove(&name);
113            config.save()?;
114            println!("{} Provider '{}' removed successfully", "✓".green(), name);
115        }
116        ProviderCommands::List => {
117            let config = config::Config::load()?;
118            if config.providers.is_empty() {
119                println!("No providers configured.");
120                return Ok(());
121            }
122
123            println!("\n{}", "Configured Providers:".bold().blue());
124
125            // Load keys config to check authentication status
126            let keys =
127                crate::keys::KeysConfig::load().unwrap_or_else(|_| crate::keys::KeysConfig::new());
128
129            // Sort providers by name for easier lookup
130            let mut sorted_providers: Vec<_> = config.providers.iter().collect();
131            sorted_providers.sort_by(|a, b| a.0.cmp(b.0));
132
133            for (name, provider_config) in sorted_providers {
134                // Check if provider has authentication in keys.toml
135                let has_key = keys.has_auth(name);
136                let key_status = if has_key { "✓".green() } else { "✗".red() };
137                println!(
138                    "  {} {} - {} (API Key: {})",
139                    "•".blue(),
140                    name.bold(),
141                    provider_config.endpoint,
142                    key_status
143                );
144            }
145        }
146        ProviderCommands::Models { name, refresh } => {
147            debug_log!(
148                "Handling provider models command for '{}', refresh: {}",
149                name,
150                refresh
151            );
152
153            let config = config::Config::load()?;
154            let _provider_config = config.get_provider(&name)?;
155
156            debug_log!("Provider '{}' found in config", name);
157
158            // Use unified cache system
159            match crate::unified_cache::UnifiedCache::fetch_and_cache_provider_models(
160                &name, refresh,
161            )
162            .await
163            {
164                Ok(models) => {
165                    debug_log!(
166                        "Successfully fetched {} models for provider '{}'",
167                        models.len(),
168                        name
169                    );
170                    println!("\n{} Available models:", "Models:".bold());
171                    display_provider_models(&models)?;
172                }
173                Err(e) => {
174                    debug_log!("Unified cache failed for provider '{}': {}", name, e);
175                    eprintln!("Error fetching models from provider '{}': {}", name, e);
176
177                    // Fallback to basic listing if unified cache fails
178                    debug_log!(
179                        "Attempting fallback to basic client listing for provider '{}'",
180                        name
181                    );
182                    let mut config_mut = config.clone();
183                    match chat::create_authenticated_client(&mut config_mut, &name).await {
184                        Ok(client) => {
185                            debug_log!("Created fallback client for provider '{}'", name);
186                            // Save config if tokens were updated
187                            if config_mut.get_cached_token(&name) != config.get_cached_token(&name)
188                            {
189                                debug_log!("Tokens updated for provider '{}', saving config", name);
190                                config_mut.save()?;
191                            }
192
193                            match client.list_models().await {
194                                Ok(models) => {
195                                    debug_log!(
196                                        "Fallback client returned {} models for provider '{}'",
197                                        models.len(),
198                                        name
199                                    );
200                                    println!(
201                                        "\n{} Available models (basic listing):",
202                                        "Models:".bold()
203                                    );
204                                    for model in models {
205                                        println!("  • {}", model.id);
206                                    }
207                                }
208                                Err(e2) => {
209                                    debug_log!(
210                                        "Fallback client failed for provider '{}': {}",
211                                        name,
212                                        e2
213                                    );
214                                    anyhow::bail!("Failed to fetch models: {}", e2);
215                                }
216                            }
217                        }
218                        Err(e2) => {
219                            debug_log!(
220                                "Failed to create fallback client for provider '{}': {}",
221                                name,
222                                e2
223                            );
224                            anyhow::bail!("Failed to create client: {}", e2);
225                        }
226                    }
227                }
228            }
229        }
230        ProviderCommands::Headers { provider, command } => {
231            let mut config = config::Config::load()?;
232
233            if !config.has_provider(&provider) {
234                anyhow::bail!("Provider '{}' not found", provider);
235            }
236
237            match command {
238                HeaderCommands::Add { name, value } => {
239                    config.add_header(provider.clone(), name.clone(), value.clone())?;
240                    config.save()?;
241                    println!(
242                        "{} Header '{}' added to provider '{}'",
243                        "✓".green(),
244                        name,
245                        provider
246                    );
247                }
248                HeaderCommands::Delete { name } => {
249                    config.remove_header(provider.clone(), name.clone())?;
250                    config.save()?;
251                    println!(
252                        "{} Header '{}' removed from provider '{}'",
253                        "✓".green(),
254                        name,
255                        provider
256                    );
257                }
258                HeaderCommands::List => {
259                    let headers = config.list_headers(&provider)?;
260                    if headers.is_empty() {
261                        println!("No custom headers configured for provider '{}'", provider);
262                    } else {
263                        println!(
264                            "\n{} Custom headers for provider '{}':",
265                            "Headers:".bold().blue(),
266                            provider
267                        );
268                        for (name, value) in headers {
269                            println!("  {} {}: {}", "•".blue(), name.bold(), value);
270                        }
271                    }
272                }
273            }
274        }
275        ProviderCommands::TokenUrl { provider, url } => {
276            let mut config = config::Config::load()?;
277
278            if !config.has_provider(&provider) {
279                anyhow::bail!("Provider '{}' not found", provider);
280            }
281
282            config.set_token_url(provider.clone(), url.clone())?;
283            config.save()?;
284            println!("{} Token URL set for provider '{}'", "✓".green(), provider);
285        }
286        ProviderCommands::Vars { provider, command } => {
287            let mut config = config::Config::load()?;
288            if !config.has_provider(&provider) {
289                anyhow::bail!("Provider '{}' not found", provider);
290            }
291            match command {
292                ProviderVarsCommands::Set { key, value } => {
293                    config.set_provider_var(&provider, &key, &value)?;
294                    config.save()?;
295                    println!(
296                        "{} Set var '{}'='{}' for provider '{}'",
297                        "✓".green(),
298                        key,
299                        value,
300                        provider
301                    );
302                }
303                ProviderVarsCommands::Get { key } => {
304                    match config.get_provider_var(&provider, &key) {
305                        Some(val) => println!("{}", val),
306                        None => anyhow::bail!("Var '{}' not set for provider '{}'", key, provider),
307                    }
308                }
309                ProviderVarsCommands::List => {
310                    let vars = config.list_provider_vars(&provider)?;
311                    if vars.is_empty() {
312                        println!("No vars set for provider '{}'", provider);
313                    } else {
314                        println!(
315                            "\n{} Vars for provider '{}':",
316                            "Vars:".bold().blue(),
317                            provider
318                        );
319                        for (k, v) in vars {
320                            println!("  {} {} = {}", "•".blue(), k.bold(), v);
321                        }
322                    }
323                }
324            }
325        }
326        ProviderCommands::Paths { provider, command } => {
327            let mut config = config::Config::load()?;
328            if !config.has_provider(&provider) {
329                anyhow::bail!("Provider '{}' not found", provider);
330            }
331            match command {
332                ProviderPathCommands::Add {
333                    models_path,
334                    chat_path,
335                    images_path,
336                    embeddings_path,
337                } => {
338                    let mut updated = false;
339                    if let Some(path) = models_path.as_deref() {
340                        config.set_provider_models_path(&provider, &path)?;
341                        println!(
342                            "{} Models path set to '{}' for provider '{}'",
343                            "✓".green(),
344                            path,
345                            provider
346                        );
347                        updated = true;
348                    }
349                    if let Some(path) = chat_path.as_deref() {
350                        config.set_provider_chat_path(&provider, &path)?;
351                        println!(
352                            "{} Chat path set to '{}' for provider '{}'",
353                            "✓".green(),
354                            path,
355                            provider
356                        );
357                        updated = true;
358                    }
359                    if let Some(path) = images_path.as_deref() {
360                        config.set_provider_images_path(&provider, &path)?;
361                        println!(
362                            "{} Images path set to '{}' for provider '{}'",
363                            "✓".green(),
364                            path,
365                            provider
366                        );
367                        updated = true;
368                    }
369                    if let Some(path) = embeddings_path.as_deref() {
370                        config.set_provider_embeddings_path(&provider, &path)?;
371                        println!(
372                            "{} Embeddings path set to '{}' for provider '{}'",
373                            "✓".green(),
374                            path,
375                            provider
376                        );
377                        updated = true;
378                    }
379                    if !updated {
380                        anyhow::bail!("No paths specified. Use -m, -c, -i, or -e to set paths.");
381                    }
382                    config.save()?;
383                }
384                ProviderPathCommands::Delete {
385                    models,
386                    chat,
387                    images,
388                    embeddings,
389                } => {
390                    let mut updated = false;
391                    if models {
392                        config.reset_provider_models_path(&provider)?;
393                        println!(
394                            "{} Models path reset to default for provider '{}'",
395                            "✓".green(),
396                            provider
397                        );
398                        updated = true;
399                    }
400                    if chat {
401                        config.reset_provider_chat_path(&provider)?;
402                        println!(
403                            "{} Chat path reset to default for provider '{}'",
404                            "✓".green(),
405                            provider
406                        );
407                        updated = true;
408                    }
409                    if images {
410                        config.reset_provider_images_path(&provider)?;
411                        println!(
412                            "{} Images path reset to default for provider '{}'",
413                            "✓".green(),
414                            provider
415                        );
416                        updated = true;
417                    }
418                    if embeddings {
419                        config.reset_provider_embeddings_path(&provider)?;
420                        println!(
421                            "{} Embeddings path reset to default for provider '{}'",
422                            "✓".green(),
423                            provider
424                        );
425                        updated = true;
426                    }
427                    if !updated {
428                        anyhow::bail!("No paths specified for deletion. Use -m, -c, -i, or -e to delete paths.");
429                    }
430                    config.save()?;
431                }
432                ProviderPathCommands::List => {
433                    let paths = config.list_provider_paths(&provider)?;
434                    println!(
435                        "\n{} API paths for provider '{}':",
436                        "Paths:".bold().blue(),
437                        provider
438                    );
439                    println!("  {} Models: {}", "•".blue(), paths.models_path.bold());
440                    println!("  {} Chat: {}", "•".blue(), paths.chat_path.bold());
441                    if let Some(ref images_path) = paths.images_path {
442                        println!("  {} Images: {}", "•".blue(), images_path.bold());
443                    } else {
444                        println!("  {} Images: {}", "•".blue(), "not set".dimmed());
445                    }
446                    if let Some(ref embeddings_path) = paths.embeddings_path {
447                        println!("  {} Embeddings: {}", "•".blue(), embeddings_path.bold());
448                    } else {
449                        println!("  {} Embeddings: {}", "•".blue(), "not set".dimmed());
450                    }
451                }
452            }
453        }
454    }
455    Ok(())
456}
457
458// Display provider models with metadata
459fn display_provider_models(models: &[crate::model_metadata::ModelMetadata]) -> Result<()> {
460    use colored::Colorize;
461
462    for model in models {
463        // Safety check: log if all capability flags are false to catch defaulting bugs
464        if !model.supports_tools
465            && !model.supports_function_calling
466            && !model.supports_vision
467            && !model.supports_audio
468            && !model.supports_reasoning
469            && !model.supports_code
470        {
471            debug_log!("All capability flags are false for model '{}' - this might indicate a defaulting bug", model.id);
472        }
473
474        // Build capability indicators
475        let mut capabilities = Vec::new();
476        if model.supports_tools || model.supports_function_calling {
477            capabilities.push("🔧 tools".blue());
478        }
479        if model.supports_vision {
480            capabilities.push("👁 vision".magenta());
481        }
482        if model.supports_audio {
483            capabilities.push("🔊 audio".yellow());
484        }
485        if model.supports_reasoning {
486            capabilities.push("🧠 reasoning".cyan());
487        }
488        if model.supports_code {
489            capabilities.push("💻 code".green());
490        }
491
492        // Build context and pricing info
493        let mut info_parts = Vec::new();
494        if let Some(ctx) = model.context_length {
495            if ctx >= 1000000 {
496                info_parts.push(format!("{}m ctx", ctx / 1000000));
497            } else if ctx >= 1000 {
498                info_parts.push(format!("{}k ctx", ctx / 1000));
499            } else {
500                info_parts.push(format!("{} ctx", ctx));
501            }
502        }
503        if let Some(max_out) = model.max_output_tokens {
504            if max_out >= 1000 {
505                info_parts.push(format!("{}k out", max_out / 1000));
506            } else {
507                info_parts.push(format!("{} out", max_out));
508            }
509        }
510        if let Some(input_price) = model.input_price_per_m {
511            info_parts.push(format!("${:.2}/M in", input_price));
512        }
513        if let Some(output_price) = model.output_price_per_m {
514            info_parts.push(format!("${:.2}/M out", output_price));
515        }
516
517        // Display model with metadata
518        let model_display = if let Some(ref display_name) = model.display_name {
519            if display_name != &model.id {
520                format!("{} ({})", model.id, display_name)
521            } else {
522                model.id.clone()
523            }
524        } else {
525            model.id.clone()
526        };
527
528        print!("  {} {}", "•".blue(), model_display.bold());
529
530        if !capabilities.is_empty() {
531            let capability_strings: Vec<String> =
532                capabilities.iter().map(|c| c.to_string()).collect();
533            print!(" [{}]", capability_strings.join(" "));
534        }
535
536        if !info_parts.is_empty() {
537            print!(" ({})", info_parts.join(", ").dimmed());
538        }
539
540        println!();
541    }
542
543    Ok(())
544}