lc/
config.rs

1//! Configuration management for the lc CLI tool
2//!
3//! This module handles loading, saving, and managing configuration for providers,
4//! aliases, templates, and other settings.
5use anyhow::Result;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::PathBuf;
11
12use super::template_processor::TemplateConfig;
13
14#[derive(Debug, Serialize, Deserialize, Clone)]
15pub struct Config {
16    pub providers: HashMap<String, ProviderConfig>,
17    pub default_provider: Option<String>,
18    pub default_model: Option<String>,
19    #[serde(default)]
20    pub aliases: HashMap<String, String>, // alias_name -> provider:model
21    #[serde(default)]
22    pub system_prompt: Option<String>,
23    #[serde(default)]
24    pub templates: HashMap<String, String>, // template_name -> prompt_content
25    #[serde(default)]
26    pub max_tokens: Option<u32>,
27    #[serde(default)]
28    pub temperature: Option<f32>,
29    #[serde(default)]
30    pub stream: Option<bool>,
31}
32
33#[derive(Debug, Serialize, Deserialize, Clone)]
34pub struct ProviderConfig {
35    pub endpoint: String,
36    pub api_key: Option<String>,
37    pub models: Vec<String>,
38    #[serde(default = "default_models_path")]
39    pub models_path: String,
40    #[serde(default = "default_chat_path")]
41    pub chat_path: String,
42    #[serde(default)]
43    pub images_path: Option<String>,
44    #[serde(default)]
45    pub embeddings_path: Option<String>,
46    #[serde(default)]
47    pub headers: HashMap<String, String>,
48    #[serde(default)]
49    pub token_url: Option<String>,
50    #[serde(default)]
51    pub cached_token: Option<CachedToken>,
52    #[serde(default)]
53    pub auth_type: Option<String>, // e.g., "google_sa_jwt"
54    #[serde(default)]
55    pub vars: HashMap<String, String>, // arbitrary provider vars like project, location
56    #[serde(default)]
57    pub chat_templates: Option<HashMap<String, TemplateConfig>>, // Chat endpoint templates
58    #[serde(default)]
59    pub images_templates: Option<HashMap<String, TemplateConfig>>, // Images endpoint templates
60    #[serde(default)]
61    pub embeddings_templates: Option<HashMap<String, TemplateConfig>>, // Embeddings endpoint templates
62    #[serde(default)]
63    pub models_templates: Option<HashMap<String, TemplateConfig>>, // Models endpoint templates
64}
65
66impl ProviderConfig {
67    /// Check if the chat_path is a full URL (starts with https://)
68    pub fn is_chat_path_full_url(&self) -> bool {
69        self.chat_path.starts_with("https://")
70    }
71
72    /// Get the models endpoint URL
73    pub fn get_models_url(&self) -> String {
74        format!(
75            "{}{}",
76            self.endpoint.trim_end_matches('/'),
77            self.models_path
78        )
79    }
80
81    /// Get the chat completions URL, replacing {model_name} and template variables
82    pub fn get_chat_url(&self, model_name: &str) -> String {
83        crate::debug_log!(
84            "ProviderConfig::get_chat_url called with model: {}",
85            model_name
86        );
87        crate::debug_log!("  chat_path: {}", self.chat_path);
88        crate::debug_log!("  is_full_url: {}", self.is_chat_path_full_url());
89        crate::debug_log!("  vars: {:?}", self.vars);
90
91        if self.is_chat_path_full_url() {
92            // Full URL path - process template variables directly
93            let mut url = self
94                .chat_path
95                .replace("{model}", model_name)
96                .replace("{model_name}", model_name);
97            crate::debug_log!("  after model replacement: {}", url);
98
99            // Interpolate known vars if present
100            for (k, v) in &self.vars {
101                let old_url = url.clone();
102                url = url.replace(&format!("{{{}}}", k), v);
103                crate::debug_log!("  replaced {{{}}} with '{}': {} -> {}", k, v, old_url, url);
104            }
105            crate::debug_log!("  final URL: {}", url);
106            url
107        } else {
108            // Relative path - first process template variables in the path, then combine with endpoint
109            let mut processed_path = self
110                .chat_path
111                .replace("{model}", model_name)
112                .replace("{model_name}", model_name);
113            crate::debug_log!("  after model replacement in path: {}", processed_path);
114
115            // Interpolate known vars in the path
116            for (k, v) in &self.vars {
117                let old_path = processed_path.clone();
118                processed_path = processed_path.replace(&format!("{{{}}}", k), v);
119                crate::debug_log!(
120                    "  replaced {{{}}} with '{}' in path: {} -> {}",
121                    k,
122                    v,
123                    old_path,
124                    processed_path
125                );
126            }
127
128            let url = format!("{}{}", self.endpoint.trim_end_matches('/'), processed_path);
129            crate::debug_log!("  final URL: {}", url);
130            url
131        }
132    }
133
134    /// Get template for a specific endpoint and model
135    pub fn get_endpoint_template(&self, endpoint: &str, model_name: &str) -> Option<String> {
136        let endpoint_templates = match endpoint {
137            "chat" => self.chat_templates.as_ref()?,
138            "images" => self.images_templates.as_ref()?,
139            "embeddings" => self.embeddings_templates.as_ref()?,
140            "models" => self.models_templates.as_ref()?,
141            _ => return None,
142        };
143
144        self.get_template_for_model(endpoint_templates, model_name, "request")
145    }
146
147    /// Get response template for a specific endpoint and model
148    pub fn get_endpoint_response_template(&self, endpoint: &str, model_name: &str) -> Option<String> {
149        let endpoint_templates = match endpoint {
150            "chat" => self.chat_templates.as_ref()?,
151            "images" => self.images_templates.as_ref()?,
152            "embeddings" => self.embeddings_templates.as_ref()?,
153            "models" => self.models_templates.as_ref()?,
154            _ => return None,
155        };
156
157        self.get_template_for_model(endpoint_templates, model_name, "response")
158    }
159
160    /// Get template for a specific model from endpoint templates
161    fn get_template_for_model(&self, templates: &HashMap<String, TemplateConfig>, model_name: &str, template_type: &str) -> Option<String> {
162        // First check exact match
163        if let Some(template) = templates.get(model_name) {
164            return match template_type {
165                "request" => template.request.clone(),
166                "response" => template.response.clone(),
167                "stream_response" => template.stream_response.clone(),
168                _ => None,
169            };
170        }
171        
172        // Then check regex patterns (skip empty string which is the default)
173        for (pattern, template) in templates {
174            if !pattern.is_empty() {
175                if let Ok(re) = regex::Regex::new(pattern) {
176                    if re.is_match(model_name) {
177                        return match template_type {
178                            "request" => template.request.clone(),
179                            "response" => template.response.clone(),
180                            "stream_response" => template.stream_response.clone(),
181                            _ => None,
182                        };
183                    }
184                }
185            }
186        }
187        
188        // Finally check for default template (empty key)
189        if let Some(template) = templates.get("") {
190            return match template_type {
191                "request" => template.request.clone(),
192                "response" => template.response.clone(),
193                "stream_response" => template.stream_response.clone(),
194                _ => None,
195            };
196        }
197        
198        None
199    }
200}
201
202#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
203pub struct CachedToken {
204    pub token: String,
205    pub expires_at: chrono::DateTime<chrono::Utc>,
206}
207
208fn default_models_path() -> String {
209    "/models".to_string()
210}
211
212fn default_chat_path() -> String {
213    "/chat/completions".to_string()
214}
215
216#[derive(Debug, Clone)]
217pub struct ProviderPaths {
218    pub models_path: String,
219    pub chat_path: String,
220    pub images_path: Option<String>,
221    pub embeddings_path: Option<String>,
222}
223
224impl Config {
225    pub fn load() -> Result<Self> {
226        let config_path = Self::config_file_path()?;
227        let providers_dir = Self::providers_dir()?;
228
229        let mut config = if config_path.exists() {
230            let content = fs::read_to_string(&config_path)?;
231            let mut config: Config = toml::from_str(&content)?;
232            
233            // If providers exist in main config, migrate them to separate files
234            if !config.providers.is_empty() {
235                Self::migrate_providers_to_separate_files(&mut config)?;
236            }
237            
238            config
239        } else {
240            // Create default config
241            Config {
242                providers: HashMap::new(),
243                default_provider: None,
244                default_model: None,
245                aliases: HashMap::new(),
246                system_prompt: None,
247                templates: HashMap::new(),
248                max_tokens: None,
249                temperature: None,
250                stream: None,
251            }
252        };
253        // Load providers from separate files
254        config.providers = Self::load_providers_from_files(&providers_dir)?;
255
256        // Ensure config directory exists
257        if let Some(parent) = config_path.parent() {
258            fs::create_dir_all(parent)?;
259        }
260
261        // Ensure providers directory exists
262        fs::create_dir_all(&providers_dir)?;
263
264        // Save the main config (without providers)
265        config.save_main_config()?;
266        
267        Ok(config)
268    }
269
270    pub fn save(&self) -> Result<()> {
271        // Save main config without providers
272        self.save_main_config()?;
273        
274        // Save each provider to its own file
275        self.save_providers_to_files()?;
276        
277        Ok(())
278    }
279
280    fn save_main_config(&self) -> Result<()> {
281        let config_path = Self::config_file_path()?;
282        
283        // Create a config without providers for the main file
284        let main_config = Config {
285            providers: HashMap::new(), // Empty - providers are in separate files
286            default_provider: self.default_provider.clone(),
287            default_model: self.default_model.clone(),
288            aliases: self.aliases.clone(),
289            system_prompt: self.system_prompt.clone(),
290            templates: self.templates.clone(),
291            max_tokens: self.max_tokens,
292            temperature: self.temperature,
293            stream: self.stream,
294        };
295        
296        let content = toml::to_string_pretty(&main_config)?;
297        fs::write(&config_path, content)?;
298        Ok(())
299    }
300
301    fn save_providers_to_files(&self) -> Result<()> {
302        let providers_dir = Self::providers_dir()?;
303        fs::create_dir_all(&providers_dir)?;
304
305        for (provider_name, provider_config) in &self.providers {
306            self.save_single_provider_flat(provider_name, provider_config)?;
307        }
308        
309        Ok(())
310    }
311
312    fn load_providers_from_files(providers_dir: &PathBuf) -> Result<HashMap<String, ProviderConfig>> {
313        let mut providers = HashMap::new();
314        
315        if !providers_dir.exists() {
316            return Ok(providers);
317        }
318
319        for entry in fs::read_dir(providers_dir)? {
320            let entry = entry?;
321            let path = entry.path();
322            
323            if path.extension().and_then(|s| s.to_str()) == Some("toml") {
324                if let Some(provider_name) = path.file_stem().and_then(|s| s.to_str()) {
325                    let content = fs::read_to_string(&path)?;
326                    
327                    // Try to parse as new flatter format first
328                    match Self::parse_flat_provider_config(&content) {
329                        Ok(config) => {
330                            providers.insert(provider_name.to_string(), config);
331                        }
332                        Err(_flat_error) => {
333                            // Fall back to old nested format for backward compatibility
334                            let provider_data: HashMap<String, HashMap<String, ProviderConfig>> = toml::from_str(&content)?;
335                            
336                            if let Some(providers_section) = provider_data.get("providers") {
337                                for (name, config) in providers_section {
338                                    providers.insert(name.clone(), config.clone());
339                                }
340                            }
341                        }
342                    }
343                }
344            }
345        }
346        
347        Ok(providers)
348    }
349
350    fn parse_flat_provider_config(content: &str) -> Result<ProviderConfig> {
351        #[derive(Deserialize)]
352        struct FlatProviderConfig {
353            #[serde(flatten)]
354            config: ProviderConfig,
355        }
356
357        let flat_config: FlatProviderConfig = toml::from_str(content)?;
358        Ok(flat_config.config)
359    }
360
361    fn migrate_providers_to_separate_files(config: &mut Config) -> Result<()> {
362        let providers_dir = Self::providers_dir()?;
363        fs::create_dir_all(&providers_dir)?;
364
365        // Save each provider to its own file using the new flat format
366        for (provider_name, provider_config) in &config.providers {
367            Self::save_single_provider_flat_static(&providers_dir, provider_name, provider_config)?;
368        }
369
370        // Clear providers from main config since they're now in separate files
371        config.providers.clear();
372        
373        Ok(())
374    }
375
376    pub fn add_provider(&mut self, name: String, endpoint: String) -> Result<()> {
377        self.add_provider_with_paths(name, endpoint, None, None)
378    }
379
380    pub fn add_provider_with_paths(
381        &mut self,
382        name: String,
383        endpoint: String,
384        models_path: Option<String>,
385        chat_path: Option<String>,
386    ) -> Result<()> {
387        let mut provider_config = ProviderConfig {
388            endpoint: endpoint.clone(),
389            api_key: None,
390            models: Vec::new(),
391            models_path: models_path.unwrap_or_else(default_models_path),
392            chat_path: chat_path.unwrap_or_else(default_chat_path),
393            images_path: None,
394            embeddings_path: None,
395            headers: HashMap::new(),
396            token_url: None,
397            cached_token: None,
398            auth_type: None,
399            vars: HashMap::new(),
400            chat_templates: None,
401            images_templates: None,
402            embeddings_templates: None,
403            models_templates: None,
404        };
405
406        // Auto-detect Vertex AI host to mark google_sa_jwt
407        if provider_config
408            .endpoint
409            .contains("aiplatform.googleapis.com")
410        {
411            provider_config.auth_type = Some("google_sa_jwt".to_string());
412            // Default token URL for SA JWT exchange if user later runs lc p t
413            if provider_config.token_url.is_none() {
414                provider_config.token_url = Some("https://oauth2.googleapis.com/token".to_string());
415            }
416        }
417
418        self.providers.insert(name.clone(), provider_config.clone());
419
420        // Set as default if it's the first provider
421        if self.default_provider.is_none() {
422            self.default_provider = Some(name.clone());
423        }
424
425        // Save the provider to its own file
426        self.save_single_provider(&name, &provider_config)?;
427
428        Ok(())
429    }
430
431    pub fn set_api_key(&mut self, provider: String, api_key: String) -> Result<()> {
432        if let Some(provider_config) = self.providers.get_mut(&provider) {
433            provider_config.api_key = Some(api_key);
434            let config_clone = provider_config.clone();
435            self.save_single_provider(&provider, &config_clone)?;
436            Ok(())
437        } else {
438            anyhow::bail!("Provider '{}' not found", provider);
439        }
440    }
441
442    pub fn has_provider(&self, name: &str) -> bool {
443        self.providers.contains_key(name)
444    }
445
446    pub fn get_provider(&self, name: &str) -> Result<&ProviderConfig> {
447        self.providers
448            .get(name)
449            .ok_or_else(|| anyhow::anyhow!("Provider '{}' not found", name))
450    }
451
452    pub fn add_header(
453        &mut self,
454        provider: String,
455        header_name: String,
456        header_value: String,
457    ) -> Result<()> {
458        if let Some(provider_config) = self.providers.get_mut(&provider) {
459            provider_config.headers.insert(header_name, header_value);
460            let config_clone = provider_config.clone();
461            self.save_single_provider(&provider, &config_clone)?;
462            Ok(())
463        } else {
464            anyhow::bail!("Provider '{}' not found", provider);
465        }
466    }
467
468    pub fn remove_header(&mut self, provider: String, header_name: String) -> Result<()> {
469        if let Some(provider_config) = self.providers.get_mut(&provider) {
470            if provider_config.headers.remove(&header_name).is_some() {
471                let config_clone = provider_config.clone();
472                self.save_single_provider(&provider, &config_clone)?;
473                Ok(())
474            } else {
475                anyhow::bail!(
476                    "Header '{}' not found for provider '{}'",
477                    header_name,
478                    provider
479                );
480            }
481        } else {
482            anyhow::bail!("Provider '{}' not found", provider);
483        }
484    }
485
486    pub fn list_headers(&self, provider: &str) -> Result<&HashMap<String, String>> {
487        if let Some(provider_config) = self.providers.get(provider) {
488            Ok(&provider_config.headers)
489        } else {
490            anyhow::bail!("Provider '{}' not found", provider);
491        }
492    }
493
494    pub fn add_alias(&mut self, alias_name: String, provider_model: String) -> Result<()> {
495        // Validate that the provider_model contains a colon
496        if !provider_model.contains(':') {
497            anyhow::bail!(
498                "Alias target must be in format 'provider:model', got '{}'",
499                provider_model
500            );
501        }
502
503        // Extract provider and validate it exists
504        let parts: Vec<&str> = provider_model.splitn(2, ':').collect();
505        let provider_name = parts[0];
506
507        if !self.has_provider(provider_name) {
508            anyhow::bail!(
509                "Provider '{}' not found. Add it first with 'lc providers add'",
510                provider_name
511            );
512        }
513
514        self.aliases.insert(alias_name, provider_model);
515        Ok(())
516    }
517
518    pub fn remove_alias(&mut self, alias_name: String) -> Result<()> {
519        if self.aliases.remove(&alias_name).is_some() {
520            Ok(())
521        } else {
522            anyhow::bail!("Alias '{}' not found", alias_name);
523        }
524    }
525
526    pub fn get_alias(&self, alias_name: &str) -> Option<&String> {
527        self.aliases.get(alias_name)
528    }
529
530    pub fn list_aliases(&self) -> &HashMap<String, String> {
531        &self.aliases
532    }
533
534    pub fn add_template(&mut self, template_name: String, prompt_content: String) -> Result<()> {
535        self.templates.insert(template_name, prompt_content);
536        Ok(())
537    }
538
539    pub fn remove_template(&mut self, template_name: String) -> Result<()> {
540        if self.templates.remove(&template_name).is_some() {
541            Ok(())
542        } else {
543            anyhow::bail!("Template '{}' not found", template_name);
544        }
545    }
546
547    pub fn get_template(&self, template_name: &str) -> Option<&String> {
548        self.templates.get(template_name)
549    }
550
551    pub fn list_templates(&self) -> &HashMap<String, String> {
552        &self.templates
553    }
554
555    pub fn resolve_template_or_prompt(&self, input: &str) -> String {
556        if let Some(template_name) = input.strip_prefix("t:") {
557            if let Some(template_content) = self.get_template(template_name) {
558                template_content.clone()
559            } else {
560                // If template not found, return the original input
561                input.to_string()
562            }
563        } else {
564            input.to_string()
565        }
566    }
567
568    pub fn parse_max_tokens(input: &str) -> Result<u32> {
569        let input = input.to_lowercase();
570        if let Some(num_str) = input.strip_suffix('k') {
571            let num: f32 = num_str
572                .parse()
573                .map_err(|_| anyhow::anyhow!("Invalid max_tokens format: '{}'", input))?;
574            Ok((num * 1000.0) as u32)
575        } else {
576            input
577                .parse()
578                .map_err(|_| anyhow::anyhow!("Invalid max_tokens format: '{}'", input))
579        }
580    }
581
582    pub fn parse_temperature(input: &str) -> Result<f32> {
583        input
584            .parse()
585            .map_err(|_| anyhow::anyhow!("Invalid temperature format: '{}'", input))
586    }
587
588    fn config_file_path() -> Result<PathBuf> {
589        let config_dir = Self::config_dir()?;
590        Ok(config_dir.join("config.toml"))
591    }
592
593    fn providers_dir() -> Result<PathBuf> {
594        let config_dir = Self::config_dir()?;
595        Ok(config_dir.join("providers"))
596    }
597
598    pub fn config_dir() -> Result<PathBuf> {
599        // Use data_local_dir for cross-platform data storage to match database location
600        // On macOS: ~/Library/Application Support/lc
601        // On Linux: ~/.local/share/lc
602        // On Windows: %LOCALAPPDATA%/lc
603        let data_dir = dirs::data_local_dir()
604            .ok_or_else(|| anyhow::anyhow!("Could not find data directory"))?
605            .join("lc");
606        fs::create_dir_all(&data_dir)?;
607        Ok(data_dir)
608    }
609
610    fn save_single_provider(&self, provider_name: &str, provider_config: &ProviderConfig) -> Result<()> {
611        self.save_single_provider_flat(provider_name, provider_config)
612    }
613
614    fn save_single_provider_flat(&self, provider_name: &str, provider_config: &ProviderConfig) -> Result<()> {
615        let providers_dir = Self::providers_dir()?;
616        Self::save_single_provider_flat_static(&providers_dir, provider_name, provider_config)
617    }
618
619    fn save_single_provider_flat_static(providers_dir: &PathBuf, provider_name: &str, provider_config: &ProviderConfig) -> Result<()> {
620        fs::create_dir_all(providers_dir)?;
621
622        let provider_file = providers_dir.join(format!("{}.toml", provider_name));
623        
624        // Use the new flat format - serialize the ProviderConfig directly
625        let content = toml::to_string_pretty(provider_config)?;
626        fs::write(&provider_file, content)?;
627        
628        Ok(())
629    }
630
631    pub fn set_token_url(&mut self, provider: String, token_url: String) -> Result<()> {
632        if let Some(provider_config) = self.providers.get_mut(&provider) {
633            provider_config.token_url = Some(token_url);
634            // Clear cached token when token_url changes
635            provider_config.cached_token = None;
636            let config_clone = provider_config.clone();
637            self.save_single_provider(&provider, &config_clone)?;
638            Ok(())
639        } else {
640            anyhow::bail!("Provider '{}' not found", provider);
641        }
642    }
643
644    // Provider vars helpers
645    pub fn set_provider_var(&mut self, provider: &str, key: &str, value: &str) -> Result<()> {
646        if let Some(pc) = self.providers.get_mut(provider) {
647            pc.vars.insert(key.to_string(), value.to_string());
648            let config_clone = pc.clone();
649            self.save_single_provider(provider, &config_clone)?;
650            Ok(())
651        } else {
652            anyhow::bail!("Provider '{}' not found", provider);
653        }
654    }
655
656    pub fn get_provider_var(&self, provider: &str, key: &str) -> Option<&String> {
657        self.providers.get(provider).and_then(|pc| pc.vars.get(key))
658    }
659
660    pub fn list_provider_vars(&self, provider: &str) -> Result<&HashMap<String, String>> {
661        if let Some(pc) = self.providers.get(provider) {
662            Ok(&pc.vars)
663        } else {
664            anyhow::bail!("Provider '{}' not found", provider);
665        }
666    }
667
668    // Provider path management methods
669    pub fn set_provider_models_path(&mut self, provider: &str, path: &str) -> Result<()> {
670        if let Some(pc) = self.providers.get_mut(provider) {
671            pc.models_path = path.to_string();
672            let config_clone = pc.clone();
673            self.save_single_provider(provider, &config_clone)?;
674            Ok(())
675        } else {
676            anyhow::bail!("Provider '{}' not found", provider);
677        }
678    }
679
680    pub fn set_provider_chat_path(&mut self, provider: &str, path: &str) -> Result<()> {
681        if let Some(pc) = self.providers.get_mut(provider) {
682            pc.chat_path = path.to_string();
683            let config_clone = pc.clone();
684            self.save_single_provider(provider, &config_clone)?;
685            Ok(())
686        } else {
687            anyhow::bail!("Provider '{}' not found", provider);
688        }
689    }
690
691    pub fn set_provider_images_path(&mut self, provider: &str, path: &str) -> Result<()> {
692        if let Some(pc) = self.providers.get_mut(provider) {
693            pc.images_path = Some(path.to_string());
694            let config_clone = pc.clone();
695            self.save_single_provider(provider, &config_clone)?;
696            Ok(())
697        } else {
698            anyhow::bail!("Provider '{}' not found", provider);
699        }
700    }
701
702    pub fn set_provider_embeddings_path(&mut self, provider: &str, path: &str) -> Result<()> {
703        if let Some(pc) = self.providers.get_mut(provider) {
704            pc.embeddings_path = Some(path.to_string());
705            let config_clone = pc.clone();
706            self.save_single_provider(provider, &config_clone)?;
707            Ok(())
708        } else {
709            anyhow::bail!("Provider '{}' not found", provider);
710        }
711    }
712
713    pub fn reset_provider_models_path(&mut self, provider: &str) -> Result<()> {
714        if let Some(pc) = self.providers.get_mut(provider) {
715            pc.models_path = default_models_path();
716            let config_clone = pc.clone();
717            self.save_single_provider(provider, &config_clone)?;
718            Ok(())
719        } else {
720            anyhow::bail!("Provider '{}' not found", provider);
721        }
722    }
723
724    pub fn reset_provider_chat_path(&mut self, provider: &str) -> Result<()> {
725        if let Some(pc) = self.providers.get_mut(provider) {
726            pc.chat_path = default_chat_path();
727            let config_clone = pc.clone();
728            self.save_single_provider(provider, &config_clone)?;
729            Ok(())
730        } else {
731            anyhow::bail!("Provider '{}' not found", provider);
732        }
733    }
734
735    pub fn reset_provider_images_path(&mut self, provider: &str) -> Result<()> {
736        if let Some(pc) = self.providers.get_mut(provider) {
737            pc.images_path = None;
738            let config_clone = pc.clone();
739            self.save_single_provider(provider, &config_clone)?;
740            Ok(())
741        } else {
742            anyhow::bail!("Provider '{}' not found", provider);
743        }
744    }
745
746    pub fn reset_provider_embeddings_path(&mut self, provider: &str) -> Result<()> {
747        if let Some(pc) = self.providers.get_mut(provider) {
748            pc.embeddings_path = None;
749            let config_clone = pc.clone();
750            self.save_single_provider(provider, &config_clone)?;
751            Ok(())
752        } else {
753            anyhow::bail!("Provider '{}' not found", provider);
754        }
755    }
756
757    pub fn list_provider_paths(&self, provider: &str) -> Result<ProviderPaths> {
758        if let Some(pc) = self.providers.get(provider) {
759            Ok(ProviderPaths {
760                models_path: pc.models_path.clone(),
761                chat_path: pc.chat_path.clone(),
762                images_path: pc.images_path.clone(),
763                embeddings_path: pc.embeddings_path.clone(),
764            })
765        } else {
766            anyhow::bail!("Provider '{}' not found", provider);
767        }
768    }
769
770    pub fn get_token_url(&self, provider: &str) -> Option<&String> {
771        self.providers.get(provider)?.token_url.as_ref()
772    }
773
774    pub fn set_cached_token(
775        &mut self,
776        provider: String,
777        token: String,
778        expires_at: DateTime<Utc>,
779    ) -> Result<()> {
780        if let Some(provider_config) = self.providers.get_mut(&provider) {
781            provider_config.cached_token = Some(CachedToken { token, expires_at });
782            let config_clone = provider_config.clone();
783            self.save_single_provider(&provider, &config_clone)?;
784            Ok(())
785        } else {
786            anyhow::bail!("Provider '{}' not found", provider);
787        }
788    }
789
790    pub fn get_cached_token(&self, provider: &str) -> Option<&CachedToken> {
791        self.providers.get(provider)?.cached_token.as_ref()
792    }
793}