lc/data/
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 crate::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 audio_path: Option<String>,
48    #[serde(default)]
49    pub speech_path: Option<String>,
50    #[serde(default)]
51    pub headers: HashMap<String, String>,
52    #[serde(default)]
53    pub token_url: Option<String>,
54    #[serde(default)]
55    pub cached_token: Option<CachedToken>,
56    #[serde(default)]
57    pub auth_type: Option<String>, // e.g., "google_sa_jwt"
58    #[serde(default)]
59    pub vars: HashMap<String, String>, // arbitrary provider vars like project, location
60    #[serde(default)]
61    pub chat_templates: Option<HashMap<String, TemplateConfig>>, // Chat endpoint templates
62    #[serde(default)]
63    pub images_templates: Option<HashMap<String, TemplateConfig>>, // Images endpoint templates
64    #[serde(default)]
65    pub embeddings_templates: Option<HashMap<String, TemplateConfig>>, // Embeddings endpoint templates
66    #[serde(default)]
67    pub models_templates: Option<HashMap<String, TemplateConfig>>, // Models endpoint templates
68    #[serde(default)]
69    pub audio_templates: Option<HashMap<String, TemplateConfig>>, // Audio transcription endpoint templates
70    #[serde(default)]
71    pub speech_templates: Option<HashMap<String, TemplateConfig>>, // Speech generation endpoint templates
72}
73
74impl ProviderConfig {
75    /// Check if the chat_path is a full URL (starts with https://)
76    pub fn is_chat_path_full_url(&self) -> bool {
77        self.chat_path.starts_with("https://")
78    }
79
80    /// Get the models endpoint URL
81    pub fn get_models_url(&self) -> String {
82        format!(
83            "{}{}",
84            self.endpoint.trim_end_matches('/'),
85            self.models_path
86        )
87    }
88
89    /// Get the chat completions URL, replacing {model_name} and template variables
90    pub fn get_chat_url(&self, model_name: &str) -> String {
91        crate::debug_log!(
92            "ProviderConfig::get_chat_url called with model: {}",
93            model_name
94        );
95        crate::debug_log!("  chat_path: {}", self.chat_path);
96        crate::debug_log!("  is_full_url: {}", self.is_chat_path_full_url());
97        crate::debug_log!("  vars: {:?}", self.vars);
98
99        if self.is_chat_path_full_url() {
100            // Full URL path - process template variables directly
101            let mut url = self
102                .chat_path
103                .replace("{model}", model_name)
104                .replace("{model_name}", model_name);
105            crate::debug_log!("  after model replacement: {}", url);
106
107            // Interpolate known vars if present
108            for (k, v) in &self.vars {
109                let old_url = url.clone();
110                url = url.replace(&format!("{{{}}}", k), v);
111                crate::debug_log!("  replaced {{{}}} with '{}': {} -> {}", k, v, old_url, url);
112            }
113            crate::debug_log!("  final URL: {}", url);
114            url
115        } else {
116            // Relative path - first process template variables in the path, then combine with endpoint
117            let mut processed_path = self
118                .chat_path
119                .replace("{model}", model_name)
120                .replace("{model_name}", model_name);
121            crate::debug_log!("  after model replacement in path: {}", processed_path);
122
123            // Interpolate known vars in the path
124            for (k, v) in &self.vars {
125                let old_path = processed_path.clone();
126                processed_path = processed_path.replace(&format!("{{{}}}", k), v);
127                crate::debug_log!(
128                    "  replaced {{{}}} with '{}' in path: {} -> {}",
129                    k,
130                    v,
131                    old_path,
132                    processed_path
133                );
134            }
135
136            let url = format!("{}{}", self.endpoint.trim_end_matches('/'), processed_path);
137            crate::debug_log!("  final URL: {}", url);
138            url
139        }
140    }
141
142    /// Get the images generation URL, replacing {model_name} and template variables
143    pub fn get_images_url(&self, model_name: &str) -> String {
144        if let Some(ref images_path) = self.images_path {
145            crate::debug_log!(
146                "ProviderConfig::get_images_url called with model: {}",
147                model_name
148            );
149            crate::debug_log!("  images_path: {}", images_path);
150            crate::debug_log!("  vars: {:?}", self.vars);
151
152            if images_path.starts_with("https://") {
153                // Full URL path - process template variables directly
154                let mut url = images_path
155                    .replace("{model}", model_name)
156                    .replace("{model_name}", model_name);
157                crate::debug_log!("  after model replacement: {}", url);
158
159                // Interpolate known vars if present
160                for (k, v) in &self.vars {
161                    let old_url = url.clone();
162                    url = url.replace(&format!("{{{}}}", k), v);
163                    crate::debug_log!("  replaced {{{}}} with '{}': {} -> {}", k, v, old_url, url);
164                }
165                crate::debug_log!("  final URL: {}", url);
166                url
167            } else {
168                // Relative path - first process template variables in the path, then combine with endpoint
169                let mut processed_path = images_path
170                    .replace("{model}", model_name)
171                    .replace("{model_name}", model_name);
172                crate::debug_log!("  after model replacement in path: {}", processed_path);
173
174                // Interpolate known vars in the path
175                for (k, v) in &self.vars {
176                    let old_path = processed_path.clone();
177                    processed_path = processed_path.replace(&format!("{{{}}}", k), v);
178                    crate::debug_log!(
179                        "  replaced {{{}}} with '{}' in path: {} -> {}",
180                        k,
181                        v,
182                        old_path,
183                        processed_path
184                    );
185                }
186
187                let url = format!("{}{}", self.endpoint.trim_end_matches('/'), processed_path);
188                crate::debug_log!("  final URL: {}", url);
189                url
190            }
191        } else {
192            // Default images path
193            format!("{}/images/generations", self.endpoint.trim_end_matches('/'))
194        }
195    }
196
197    /// Get the speech generation URL, replacing {model_name} and template variables
198    pub fn get_speech_url(&self, model_name: &str) -> String {
199        if let Some(ref speech_path) = self.speech_path {
200            crate::debug_log!(
201                "ProviderConfig::get_speech_url called with model: {}",
202                model_name
203            );
204            crate::debug_log!("  speech_path: {}", speech_path);
205            crate::debug_log!("  vars: {:?}", self.vars);
206
207            if speech_path.starts_with("https://") {
208                // Full URL path - process template variables directly
209                let mut url = speech_path
210                    .replace("{model}", model_name)
211                    .replace("{model_name}", model_name);
212                crate::debug_log!("  after model replacement: {}", url);
213
214                // Interpolate known vars if present
215                for (k, v) in &self.vars {
216                    let old_url = url.clone();
217                    url = url.replace(&format!("{{{}}}", k), v);
218                    crate::debug_log!("  replaced {{{}}} with '{}': {} -> {}", k, v, old_url, url);
219                }
220                crate::debug_log!("  final URL: {}", url);
221                url
222            } else {
223                // Relative path - first process template variables in the path, then combine with endpoint
224                let mut processed_path = speech_path
225                    .replace("{model}", model_name)
226                    .replace("{model_name}", model_name);
227                crate::debug_log!("  after model replacement in path: {}", processed_path);
228
229                // Interpolate known vars in the path
230                for (k, v) in &self.vars {
231                    let old_path = processed_path.clone();
232                    processed_path = processed_path.replace(&format!("{{{}}}", k), v);
233                    crate::debug_log!(
234                        "  replaced {{{}}} with '{}' in path: {} -> {}",
235                        k,
236                        v,
237                        old_path,
238                        processed_path
239                    );
240                }
241
242                let url = format!("{}{}", self.endpoint.trim_end_matches('/'), processed_path);
243                crate::debug_log!("  final URL: {}", url);
244                url
245            }
246        } else {
247            // Default speech path
248            format!("{}/audio/speech", self.endpoint.trim_end_matches('/'))
249        }
250    }
251
252    /// Get the embeddings URL, replacing {model_name} and template variables
253    pub fn get_embeddings_url(&self, model_name: &str) -> String {
254        if let Some(ref embeddings_path) = self.embeddings_path {
255            crate::debug_log!(
256                "ProviderConfig::get_embeddings_url called with model: {}",
257                model_name
258            );
259            crate::debug_log!("  embeddings_path: {}", embeddings_path);
260            crate::debug_log!("  vars: {:?}", self.vars);
261
262            if embeddings_path.starts_with("https://") {
263                // Full URL path - process template variables directly
264                let mut url = embeddings_path
265                    .replace("{model}", model_name)
266                    .replace("{model_name}", model_name);
267                crate::debug_log!("  after model replacement: {}", url);
268
269                // Interpolate known vars if present
270                for (k, v) in &self.vars {
271                    let old_url = url.clone();
272                    url = url.replace(&format!("{{{}}}", k), v);
273                    crate::debug_log!("  replaced {{{}}} with '{}': {} -> {}", k, v, old_url, url);
274                }
275                crate::debug_log!("  final URL: {}", url);
276                url
277            } else {
278                // Relative path - first process template variables in the path, then combine with endpoint
279                let mut processed_path = embeddings_path
280                    .replace("{model}", model_name)
281                    .replace("{model_name}", model_name);
282                crate::debug_log!("  after model replacement in path: {}", processed_path);
283
284                // Interpolate known vars in the path
285                for (k, v) in &self.vars {
286                    let old_path = processed_path.clone();
287                    processed_path = processed_path.replace(&format!("{{{}}}", k), v);
288                    crate::debug_log!(
289                        "  replaced {{{}}} with '{}' in path: {} -> {}",
290                        k,
291                        v,
292                        old_path,
293                        processed_path
294                    );
295                }
296
297                let url = format!("{}{}", self.endpoint.trim_end_matches('/'), processed_path);
298                crate::debug_log!("  final URL: {}", url);
299                url
300            }
301        } else {
302            // Default embeddings path
303            format!("{}/embeddings", self.endpoint.trim_end_matches('/'))
304        }
305    }
306
307    /// Get template for a specific endpoint and model
308    pub fn get_endpoint_template(&self, endpoint: &str, model_name: &str) -> Option<String> {
309        let endpoint_templates = match endpoint {
310            "chat" => self.chat_templates.as_ref()?,
311            "images" => self.images_templates.as_ref()?,
312            "embeddings" => self.embeddings_templates.as_ref()?,
313            "models" => self.models_templates.as_ref()?,
314            "audio" => self.audio_templates.as_ref()?,
315            "speech" => self.speech_templates.as_ref()?,
316            _ => return None,
317        };
318
319        self.get_template_for_model(endpoint_templates, model_name, "request")
320    }
321
322    /// Get response template for a specific endpoint and model
323    pub fn get_endpoint_response_template(
324        &self,
325        endpoint: &str,
326        model_name: &str,
327    ) -> Option<String> {
328        let endpoint_templates = match endpoint {
329            "chat" => self.chat_templates.as_ref()?,
330            "images" => self.images_templates.as_ref()?,
331            "embeddings" => self.embeddings_templates.as_ref()?,
332            "models" => self.models_templates.as_ref()?,
333            "audio" => self.audio_templates.as_ref()?,
334            "speech" => self.speech_templates.as_ref()?,
335            _ => return None,
336        };
337
338        self.get_template_for_model(endpoint_templates, model_name, "response")
339    }
340
341    /// Get template for a specific model from endpoint templates
342    fn get_template_for_model(
343        &self,
344        templates: &HashMap<String, TemplateConfig>,
345        model_name: &str,
346        template_type: &str,
347    ) -> Option<String> {
348        // First check exact match
349        if let Some(template) = templates.get(model_name) {
350            return match template_type {
351                "request" => template.request.clone(),
352                "response" => template.response.clone(),
353                "stream_response" => template.stream_response.clone(),
354                _ => None,
355            };
356        }
357
358        // Then check regex patterns (skip empty string which is the default)
359        for (pattern, template) in templates {
360            if !pattern.is_empty() {
361                if let Ok(re) = regex::Regex::new(pattern) {
362                    if re.is_match(model_name) {
363                        return match template_type {
364                            "request" => template.request.clone(),
365                            "response" => template.response.clone(),
366                            "stream_response" => template.stream_response.clone(),
367                            _ => None,
368                        };
369                    }
370                }
371            }
372        }
373
374        // Finally check for default template (empty key)
375        if let Some(template) = templates.get("") {
376            return match template_type {
377                "request" => template.request.clone(),
378                "response" => template.response.clone(),
379                "stream_response" => template.stream_response.clone(),
380                _ => None,
381            };
382        }
383
384        None
385    }
386}
387
388#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
389pub struct CachedToken {
390    pub token: String,
391    pub expires_at: chrono::DateTime<chrono::Utc>,
392}
393
394fn default_models_path() -> String {
395    "/models".to_string()
396}
397
398fn default_chat_path() -> String {
399    "/chat/completions".to_string()
400}
401
402#[derive(Debug, Clone)]
403pub struct ProviderPaths {
404    pub models_path: String,
405    pub chat_path: String,
406    pub images_path: Option<String>,
407    pub embeddings_path: Option<String>,
408}
409
410impl Config {
411    pub fn load() -> Result<Self> {
412        let config_path = Self::config_file_path()?;
413        let providers_dir = Self::providers_dir()?;
414
415        let mut config = if config_path.exists() {
416            let content = fs::read_to_string(&config_path)?;
417            let mut config: Config = toml::from_str(&content)?;
418
419            // If providers exist in main config, migrate them to separate files
420            if !config.providers.is_empty() {
421                Self::migrate_providers_to_separate_files(&mut config)?;
422            }
423
424            config
425        } else {
426            // Create default config
427            Config {
428                providers: HashMap::new(),
429                default_provider: None,
430                default_model: None,
431                aliases: HashMap::new(),
432                system_prompt: None,
433                templates: HashMap::new(),
434                max_tokens: None,
435                temperature: None,
436                stream: None,
437            }
438        };
439        // Load providers from separate files
440        config.providers = Self::load_providers_from_files(&providers_dir)?;
441
442        // Ensure config directory exists
443        if let Some(parent) = config_path.parent() {
444            fs::create_dir_all(parent)?;
445        }
446
447        // Ensure providers directory exists
448        fs::create_dir_all(&providers_dir)?;
449
450        // Save the main config (without providers)
451        config.save_main_config()?;
452
453        // Migrate API keys to centralized keys.toml if needed
454        if config.has_providers_with_keys() {
455            crate::debug_log!("Detected providers with embedded API keys, initiating migration...");
456            let _ = crate::keys::KeysConfig::migrate_from_provider_configs(&config);
457        }
458
459        Ok(config)
460    }
461
462    pub fn save(&self) -> Result<()> {
463        // Save main config without providers
464        self.save_main_config()?;
465
466        // Save each provider to its own file
467        self.save_providers_to_files()?;
468
469        Ok(())
470    }
471
472    fn save_main_config(&self) -> Result<()> {
473        let config_path = Self::config_file_path()?;
474
475        // Create a config without providers for the main file
476        let main_config = Config {
477            providers: HashMap::new(), // Empty - providers are in separate files
478            default_provider: self.default_provider.clone(),
479            default_model: self.default_model.clone(),
480            aliases: self.aliases.clone(),
481            system_prompt: self.system_prompt.clone(),
482            templates: self.templates.clone(),
483            max_tokens: self.max_tokens,
484            temperature: self.temperature,
485            stream: self.stream,
486        };
487
488        let content = toml::to_string_pretty(&main_config)?;
489        fs::write(&config_path, content)?;
490        Ok(())
491    }
492
493    fn save_providers_to_files(&self) -> Result<()> {
494        let providers_dir = Self::providers_dir()?;
495        fs::create_dir_all(&providers_dir)?;
496
497        for (provider_name, provider_config) in &self.providers {
498            self.save_single_provider_flat(provider_name, provider_config)?;
499        }
500
501        Ok(())
502    }
503
504    fn load_providers_from_files(
505        providers_dir: &PathBuf,
506    ) -> Result<HashMap<String, ProviderConfig>> {
507        let mut providers = HashMap::new();
508
509        if !providers_dir.exists() {
510            return Ok(providers);
511        }
512
513        for entry in fs::read_dir(providers_dir)? {
514            let entry = entry?;
515            let path = entry.path();
516
517            if path.extension().and_then(|s| s.to_str()) == Some("toml") {
518                if let Some(provider_name) = path.file_stem().and_then(|s| s.to_str()) {
519                    let content = fs::read_to_string(&path)?;
520
521                    // Try to parse as new flatter format first
522                    match Self::parse_flat_provider_config(&content) {
523                        Ok(config) => {
524                            providers.insert(provider_name.to_string(), config);
525                        }
526                        Err(flat_error) => {
527                            crate::debug_log!(
528                                "Failed to parse {} as flat format: {}",
529                                provider_name,
530                                flat_error
531                            );
532
533                            // Try to parse as old nested format for backward compatibility
534                            match toml::from_str::<HashMap<String, HashMap<String, ProviderConfig>>>(
535                                &content,
536                            ) {
537                                Ok(provider_data) => {
538                                    if let Some(providers_section) = provider_data.get("providers")
539                                    {
540                                        for (name, config) in providers_section {
541                                            providers.insert(name.clone(), config.clone());
542                                        }
543                                    }
544                                }
545                                Err(nested_error) => {
546                                    crate::debug_log!(
547                                        "Failed to parse {} as nested format: {}",
548                                        provider_name,
549                                        nested_error
550                                    );
551                                    eprintln!(
552                                        "Warning: Failed to parse provider config file '{}': {}",
553                                        path.display(),
554                                        flat_error
555                                    );
556                                    eprintln!("  Also failed as nested format: {}", nested_error);
557                                    // Skip this provider file instead of failing entirely
558                                    continue;
559                                }
560                            }
561                        }
562                    }
563                }
564            }
565        }
566
567        Ok(providers)
568    }
569
570    fn parse_flat_provider_config(content: &str) -> Result<ProviderConfig> {
571        // Directly deserialize the ProviderConfig without the wrapper struct
572        let config: ProviderConfig =
573            toml::from_str(content).map_err(|e| anyhow::anyhow!("TOML parse error: {}", e))?;
574        Ok(config)
575    }
576
577    fn migrate_providers_to_separate_files(config: &mut Config) -> Result<()> {
578        let providers_dir = Self::providers_dir()?;
579        fs::create_dir_all(&providers_dir)?;
580
581        // Save each provider to its own file using the new flat format
582        for (provider_name, provider_config) in &config.providers {
583            Self::save_single_provider_flat_static(&providers_dir, provider_name, provider_config)?;
584        }
585
586        // Clear providers from main config since they're now in separate files
587        config.providers.clear();
588
589        Ok(())
590    }
591
592    pub fn add_provider(&mut self, name: String, endpoint: String) -> Result<()> {
593        self.add_provider_with_paths(name, endpoint, None, None)
594    }
595
596    pub fn add_provider_with_paths(
597        &mut self,
598        name: String,
599        endpoint: String,
600        models_path: Option<String>,
601        chat_path: Option<String>,
602    ) -> Result<()> {
603        let mut provider_config = ProviderConfig {
604            endpoint: endpoint.clone(),
605            api_key: None,
606            models: Vec::new(),
607            models_path: models_path.unwrap_or_else(default_models_path),
608            chat_path: chat_path.unwrap_or_else(default_chat_path),
609            images_path: None,
610            embeddings_path: None,
611            audio_path: None,
612            speech_path: None,
613            headers: HashMap::new(),
614            token_url: None,
615            cached_token: None,
616            auth_type: None,
617            vars: HashMap::new(),
618            chat_templates: None,
619            images_templates: None,
620            embeddings_templates: None,
621            models_templates: None,
622            audio_templates: None,
623            speech_templates: None,
624        };
625
626        // Auto-detect Vertex AI host to mark google_sa_jwt
627        if provider_config
628            .endpoint
629            .contains("aiplatform.googleapis.com")
630        {
631            provider_config.auth_type = Some("google_sa_jwt".to_string());
632            // Default token URL for SA JWT exchange if user later runs lc p t
633            if provider_config.token_url.is_none() {
634                provider_config.token_url = Some("https://oauth2.googleapis.com/token".to_string());
635            }
636        }
637
638        self.providers.insert(name.clone(), provider_config.clone());
639
640        // Set as default if it's the first provider
641        if self.default_provider.is_none() {
642            self.default_provider = Some(name.clone());
643        }
644
645        // Save the provider to its own file
646        self.save_single_provider(&name, &provider_config)?;
647
648        Ok(())
649    }
650
651    pub fn set_api_key(&mut self, provider: String, api_key: String) -> Result<()> {
652        // First check if the provider exists
653        if !self.has_provider(&provider) {
654            anyhow::bail!("Provider '{}' not found", provider);
655        }
656
657        // Store in centralized keys.toml instead of provider config
658        let mut keys = crate::keys::KeysConfig::load()?;
659        keys.set_api_key(provider.clone(), api_key)?;
660
661        // Clear from provider config if it exists there (for migration)
662        if let Some(provider_config) = self.providers.get_mut(&provider) {
663            if provider_config.api_key.is_some() {
664                provider_config.api_key = None;
665                let config_clone = provider_config.clone();
666                self.save_single_provider(&provider, &config_clone)?;
667            }
668        }
669
670        Ok(())
671    }
672
673    /// Check if any providers have embedded API keys (for migration detection)
674    pub fn has_providers_with_keys(&self) -> bool {
675        for (_name, provider_config) in &self.providers {
676            if let Some(api_key) = &provider_config.api_key {
677                if !api_key.is_empty() {
678                    return true;
679                }
680            }
681        }
682        false
683    }
684
685    /// Get provider with authentication from centralized keys
686    pub fn get_provider_with_auth(&self, name: &str) -> Result<ProviderConfig> {
687        let mut provider_config = self.get_provider(name)?.clone();
688
689        // Load authentication from centralized keys
690        if let Some(auth) = crate::keys::get_provider_auth(name)? {
691            match auth {
692                crate::keys::ProviderAuth::ApiKey(key) => {
693                    // Check if provider has custom headers with ${api_key} placeholder
694                    let mut has_custom_auth_header = false;
695                    for (_header_name, header_value) in &provider_config.headers {
696                        if header_value.contains("${api_key}") {
697                            has_custom_auth_header = true;
698                            break;
699                        }
700                    }
701
702                    if has_custom_auth_header {
703                        // Replace ${api_key} in headers
704                        let mut updated_headers = HashMap::new();
705                        for (header_name, header_value) in provider_config.headers.iter() {
706                            let processed_value = header_value.replace("${api_key}", &key);
707                            updated_headers.insert(header_name.clone(), processed_value);
708                        }
709                        provider_config.headers = updated_headers;
710                    } else {
711                        // Use standard Bearer token auth
712                        provider_config.api_key = Some(key);
713                    }
714                }
715                crate::keys::ProviderAuth::ServiceAccount(sa_json) => {
716                    provider_config.api_key = Some(sa_json);
717                }
718                crate::keys::ProviderAuth::OAuthToken(token) => {
719                    provider_config.api_key = Some(token);
720                }
721                crate::keys::ProviderAuth::Token(token) => {
722                    provider_config.api_key = Some(token);
723                }
724                crate::keys::ProviderAuth::Headers(headers) => {
725                    for (k, v) in headers {
726                        provider_config.headers.insert(k, v);
727                    }
728                }
729            }
730        }
731
732        Ok(provider_config)
733    }
734
735    pub fn has_provider(&self, name: &str) -> bool {
736        self.providers.contains_key(name)
737    }
738
739    pub fn get_provider(&self, name: &str) -> Result<&ProviderConfig> {
740        self.providers
741            .get(name)
742            .ok_or_else(|| anyhow::anyhow!("Provider '{}' not found", name))
743    }
744
745    pub fn add_header(
746        &mut self,
747        provider: String,
748        header_name: String,
749        header_value: String,
750    ) -> Result<()> {
751        if let Some(provider_config) = self.providers.get_mut(&provider) {
752            provider_config.headers.insert(header_name, header_value);
753            let config_clone = provider_config.clone();
754            self.save_single_provider(&provider, &config_clone)?;
755            Ok(())
756        } else {
757            anyhow::bail!("Provider '{}' not found", provider);
758        }
759    }
760
761    pub fn remove_header(&mut self, provider: String, header_name: String) -> Result<()> {
762        if let Some(provider_config) = self.providers.get_mut(&provider) {
763            if provider_config.headers.remove(&header_name).is_some() {
764                let config_clone = provider_config.clone();
765                self.save_single_provider(&provider, &config_clone)?;
766                Ok(())
767            } else {
768                anyhow::bail!(
769                    "Header '{}' not found for provider '{}'",
770                    header_name,
771                    provider
772                );
773            }
774        } else {
775            anyhow::bail!("Provider '{}' not found", provider);
776        }
777    }
778
779    pub fn list_headers(&self, provider: &str) -> Result<&HashMap<String, String>> {
780        if let Some(provider_config) = self.providers.get(provider) {
781            Ok(&provider_config.headers)
782        } else {
783            anyhow::bail!("Provider '{}' not found", provider);
784        }
785    }
786
787    pub fn add_alias(&mut self, alias_name: String, provider_model: String) -> Result<()> {
788        // Validate that the provider_model contains a colon
789        if !provider_model.contains(':') {
790            anyhow::bail!(
791                "Alias target must be in format 'provider:model', got '{}'",
792                provider_model
793            );
794        }
795
796        // Extract provider and validate it exists
797        let parts: Vec<&str> = provider_model.splitn(2, ':').collect();
798        let provider_name = parts[0];
799
800        if !self.has_provider(provider_name) {
801            anyhow::bail!(
802                "Provider '{}' not found. Add it first with 'lc providers add'",
803                provider_name
804            );
805        }
806
807        self.aliases.insert(alias_name, provider_model);
808        Ok(())
809    }
810
811    pub fn remove_alias(&mut self, alias_name: String) -> Result<()> {
812        if self.aliases.remove(&alias_name).is_some() {
813            Ok(())
814        } else {
815            anyhow::bail!("Alias '{}' not found", alias_name);
816        }
817    }
818
819    pub fn get_alias(&self, alias_name: &str) -> Option<&String> {
820        self.aliases.get(alias_name)
821    }
822
823    pub fn list_aliases(&self) -> &HashMap<String, String> {
824        &self.aliases
825    }
826
827    pub fn add_template(&mut self, template_name: String, prompt_content: String) -> Result<()> {
828        self.templates.insert(template_name, prompt_content);
829        Ok(())
830    }
831
832    pub fn remove_template(&mut self, template_name: String) -> Result<()> {
833        if self.templates.remove(&template_name).is_some() {
834            Ok(())
835        } else {
836            anyhow::bail!("Template '{}' not found", template_name);
837        }
838    }
839
840    pub fn get_template(&self, template_name: &str) -> Option<&String> {
841        self.templates.get(template_name)
842    }
843
844    pub fn list_templates(&self) -> &HashMap<String, String> {
845        &self.templates
846    }
847
848    pub fn resolve_template_or_prompt(&self, input: &str) -> String {
849        if let Some(template_name) = input.strip_prefix("t:") {
850            if let Some(template_content) = self.get_template(template_name) {
851                template_content.clone()
852            } else {
853                // If template not found, return the original input
854                input.to_string()
855            }
856        } else {
857            input.to_string()
858        }
859    }
860
861    pub fn parse_max_tokens(input: &str) -> Result<u32> {
862        let input = input.to_lowercase();
863        if let Some(num_str) = input.strip_suffix('k') {
864            let num: f32 = num_str
865                .parse()
866                .map_err(|_| anyhow::anyhow!("Invalid max_tokens format: '{}'", input))?;
867            Ok((num * 1000.0) as u32)
868        } else {
869            input
870                .parse()
871                .map_err(|_| anyhow::anyhow!("Invalid max_tokens format: '{}'", input))
872        }
873    }
874
875    pub fn parse_temperature(input: &str) -> Result<f32> {
876        input
877            .parse()
878            .map_err(|_| anyhow::anyhow!("Invalid temperature format: '{}'", input))
879    }
880
881    fn config_file_path() -> Result<PathBuf> {
882        let config_dir = Self::config_dir()?;
883        Ok(config_dir.join("config.toml"))
884    }
885
886    fn providers_dir() -> Result<PathBuf> {
887        let config_dir = Self::config_dir()?;
888        Ok(config_dir.join("providers"))
889    }
890
891    pub fn config_dir() -> Result<PathBuf> {
892        // Check for explicit test environment override first (highest priority)
893        if let Ok(test_dir) = std::env::var("LC_TEST_CONFIG_DIR") {
894            let test_path = PathBuf::from(test_dir);
895            // Create test directory if it doesn't exist
896            if !test_path.exists() {
897                fs::create_dir_all(&test_path)?;
898            }
899            return Ok(test_path);
900        }
901
902        // Automatically detect if we're running in a test environment
903        // This works because cargo test sets CARGO_TARGET_TMPDIR and other test-specific env vars
904        // We can also check if we're running under cargo test by checking for CARGO env vars
905        #[cfg(test)]
906        {
907            // When compiling for tests, always use a temp directory
908            use std::sync::Mutex;
909            use std::sync::Once;
910
911            static INIT: Once = Once::new();
912            static TEST_DIR: Mutex<Option<PathBuf>> = Mutex::new(None);
913
914            // Get or create the test directory
915            let mut test_dir_guard = TEST_DIR
916                .lock()
917                .map_err(|_| anyhow::anyhow!("Failed to acquire test directory lock"))?;
918            if test_dir_guard.is_none() {
919                // Create a unique temp directory for this test run
920                let temp_dir = std::env::temp_dir()
921                    .join("lc_test")
922                    .join(format!("test_{}", std::process::id()));
923
924                // Register cleanup on process exit
925                let cleanup_dir = temp_dir.clone();
926                INIT.call_once(|| {
927                    // Register a cleanup function that runs when tests complete
928                    // This uses a custom panic hook and atexit-like behavior
929                    struct TestDirCleanup(PathBuf);
930                    impl Drop for TestDirCleanup {
931                        fn drop(&mut self) {
932                            // Clean up the test directory when the process exits
933                            if self.0.exists() {
934                                let _ = fs::remove_dir_all(&self.0);
935                            }
936                        }
937                    }
938
939                    // Create a static cleanup object that will be dropped on exit
940                    lazy_static::lazy_static! {
941                        static ref CLEANUP: Mutex<Option<TestDirCleanup>> = Mutex::new(None);
942                    }
943
944                    if let Ok(mut cleanup) = CLEANUP.lock() {
945                        *cleanup = Some(TestDirCleanup(cleanup_dir));
946                    }
947                });
948
949                *test_dir_guard = Some(temp_dir);
950            }
951
952            if let Some(ref test_path) = *test_dir_guard {
953                if !test_path.exists() {
954                    fs::create_dir_all(test_path)?;
955                }
956                return Ok(test_path.clone());
957            }
958        }
959
960        // For non-test builds, check if we're running under cargo test
961        // This catches integration tests that aren't compiled with #[cfg(test)]
962        if std::env::var("CARGO").is_ok() && std::env::var("CARGO_PKG_NAME").is_ok() {
963            // Additional check: see if we're likely in a test by checking the binary name
964            if let Ok(current_exe) = std::env::current_exe() {
965                if let Some(exe_name) = current_exe.file_name() {
966                    let exe_str = exe_name.to_string_lossy();
967                    // Cargo test binaries typically have hashes in their names
968                    if exe_str.contains("test") || exe_str.contains("-") && exe_str.len() > 20 {
969                        // Use a temp directory for tests with automatic cleanup
970                        use std::sync::Mutex;
971                        use tempfile::TempDir;
972
973                        // Store the TempDir in a static to keep it alive for the test duration
974                        lazy_static::lazy_static! {
975                            static ref TEST_TEMP_DIR: Mutex<Option<TempDir>> = Mutex::new(None);
976                        }
977
978                        let mut temp_dir_guard = TEST_TEMP_DIR.lock().map_err(|_| {
979                            anyhow::anyhow!("Failed to acquire temp directory lock")
980                        })?;
981                        if temp_dir_guard.is_none() {
982                            // Create a new temp directory that will be automatically cleaned up
983                            let temp_dir = TempDir::with_prefix("lc_test_")
984                                .map_err(|e| anyhow::anyhow!("Failed to create temp dir: {}", e))?;
985                            *temp_dir_guard = Some(temp_dir);
986                        }
987
988                        if let Some(ref temp_dir) = *temp_dir_guard {
989                            return Ok(temp_dir.path().to_path_buf());
990                        }
991                    }
992                }
993            }
994        }
995
996        // Use data_local_dir for cross-platform data storage to match database location
997        // On macOS: ~/Library/Application Support/lc
998        // On Linux: ~/.local/share/lc
999        // On Windows: %LOCALAPPDATA%/lc
1000        let data_dir = dirs::data_local_dir()
1001            .ok_or_else(|| anyhow::anyhow!("Could not find data directory"))?
1002            .join("lc");
1003
1004        // Only create directory if it doesn't exist to prevent potential recursion
1005        if !data_dir.exists() {
1006            fs::create_dir_all(&data_dir)?;
1007        }
1008        Ok(data_dir)
1009    }
1010
1011    fn save_single_provider(
1012        &self,
1013        provider_name: &str,
1014        provider_config: &ProviderConfig,
1015    ) -> Result<()> {
1016        self.save_single_provider_flat(provider_name, provider_config)
1017    }
1018
1019    fn save_single_provider_flat(
1020        &self,
1021        provider_name: &str,
1022        provider_config: &ProviderConfig,
1023    ) -> Result<()> {
1024        let providers_dir = Self::providers_dir()?;
1025        Self::save_single_provider_flat_static(&providers_dir, provider_name, provider_config)
1026    }
1027
1028    fn save_single_provider_flat_static(
1029        providers_dir: &PathBuf,
1030        provider_name: &str,
1031        provider_config: &ProviderConfig,
1032    ) -> Result<()> {
1033        fs::create_dir_all(providers_dir)?;
1034
1035        let provider_file = providers_dir.join(format!("{}.toml", provider_name));
1036
1037        // Use the new flat format - serialize the ProviderConfig directly
1038        let content = toml::to_string_pretty(provider_config)?;
1039        fs::write(&provider_file, content)?;
1040
1041        Ok(())
1042    }
1043
1044    pub fn set_token_url(&mut self, provider: String, token_url: String) -> Result<()> {
1045        if let Some(provider_config) = self.providers.get_mut(&provider) {
1046            provider_config.token_url = Some(token_url);
1047            // Clear cached token when token_url changes
1048            provider_config.cached_token = None;
1049            let config_clone = provider_config.clone();
1050            self.save_single_provider(&provider, &config_clone)?;
1051            Ok(())
1052        } else {
1053            anyhow::bail!("Provider '{}' not found", provider);
1054        }
1055    }
1056
1057    // Provider vars helpers
1058    pub fn set_provider_var(&mut self, provider: &str, key: &str, value: &str) -> Result<()> {
1059        if let Some(pc) = self.providers.get_mut(provider) {
1060            pc.vars.insert(key.to_string(), value.to_string());
1061            let config_clone = pc.clone();
1062            self.save_single_provider(provider, &config_clone)?;
1063            Ok(())
1064        } else {
1065            anyhow::bail!("Provider '{}' not found", provider);
1066        }
1067    }
1068
1069    pub fn get_provider_var(&self, provider: &str, key: &str) -> Option<&String> {
1070        self.providers.get(provider).and_then(|pc| pc.vars.get(key))
1071    }
1072
1073    pub fn list_provider_vars(&self, provider: &str) -> Result<&HashMap<String, String>> {
1074        if let Some(pc) = self.providers.get(provider) {
1075            Ok(&pc.vars)
1076        } else {
1077            anyhow::bail!("Provider '{}' not found", provider);
1078        }
1079    }
1080
1081    // Provider path management methods
1082    pub fn set_provider_models_path(&mut self, provider: &str, path: &str) -> Result<()> {
1083        if let Some(pc) = self.providers.get_mut(provider) {
1084            pc.models_path = path.to_string();
1085            let config_clone = pc.clone();
1086            self.save_single_provider(provider, &config_clone)?;
1087            Ok(())
1088        } else {
1089            anyhow::bail!("Provider '{}' not found", provider);
1090        }
1091    }
1092
1093    pub fn set_provider_chat_path(&mut self, provider: &str, path: &str) -> Result<()> {
1094        if let Some(pc) = self.providers.get_mut(provider) {
1095            pc.chat_path = path.to_string();
1096            let config_clone = pc.clone();
1097            self.save_single_provider(provider, &config_clone)?;
1098            Ok(())
1099        } else {
1100            anyhow::bail!("Provider '{}' not found", provider);
1101        }
1102    }
1103
1104    pub fn set_provider_images_path(&mut self, provider: &str, path: &str) -> Result<()> {
1105        if let Some(pc) = self.providers.get_mut(provider) {
1106            pc.images_path = Some(path.to_string());
1107            let config_clone = pc.clone();
1108            self.save_single_provider(provider, &config_clone)?;
1109            Ok(())
1110        } else {
1111            anyhow::bail!("Provider '{}' not found", provider);
1112        }
1113    }
1114
1115    pub fn set_provider_embeddings_path(&mut self, provider: &str, path: &str) -> Result<()> {
1116        if let Some(pc) = self.providers.get_mut(provider) {
1117            pc.embeddings_path = Some(path.to_string());
1118            let config_clone = pc.clone();
1119            self.save_single_provider(provider, &config_clone)?;
1120            Ok(())
1121        } else {
1122            anyhow::bail!("Provider '{}' not found", provider);
1123        }
1124    }
1125
1126    pub fn reset_provider_models_path(&mut self, provider: &str) -> Result<()> {
1127        if let Some(pc) = self.providers.get_mut(provider) {
1128            pc.models_path = default_models_path();
1129            let config_clone = pc.clone();
1130            self.save_single_provider(provider, &config_clone)?;
1131            Ok(())
1132        } else {
1133            anyhow::bail!("Provider '{}' not found", provider);
1134        }
1135    }
1136
1137    pub fn reset_provider_chat_path(&mut self, provider: &str) -> Result<()> {
1138        if let Some(pc) = self.providers.get_mut(provider) {
1139            pc.chat_path = default_chat_path();
1140            let config_clone = pc.clone();
1141            self.save_single_provider(provider, &config_clone)?;
1142            Ok(())
1143        } else {
1144            anyhow::bail!("Provider '{}' not found", provider);
1145        }
1146    }
1147
1148    pub fn reset_provider_images_path(&mut self, provider: &str) -> Result<()> {
1149        if let Some(pc) = self.providers.get_mut(provider) {
1150            pc.images_path = None;
1151            let config_clone = pc.clone();
1152            self.save_single_provider(provider, &config_clone)?;
1153            Ok(())
1154        } else {
1155            anyhow::bail!("Provider '{}' not found", provider);
1156        }
1157    }
1158
1159    pub fn reset_provider_embeddings_path(&mut self, provider: &str) -> Result<()> {
1160        if let Some(pc) = self.providers.get_mut(provider) {
1161            pc.embeddings_path = None;
1162            let config_clone = pc.clone();
1163            self.save_single_provider(provider, &config_clone)?;
1164            Ok(())
1165        } else {
1166            anyhow::bail!("Provider '{}' not found", provider);
1167        }
1168    }
1169
1170    pub fn list_provider_paths(&self, provider: &str) -> Result<ProviderPaths> {
1171        if let Some(pc) = self.providers.get(provider) {
1172            Ok(ProviderPaths {
1173                models_path: pc.models_path.clone(),
1174                chat_path: pc.chat_path.clone(),
1175                images_path: pc.images_path.clone(),
1176                embeddings_path: pc.embeddings_path.clone(),
1177            })
1178        } else {
1179            anyhow::bail!("Provider '{}' not found", provider);
1180        }
1181    }
1182
1183    pub fn get_token_url(&self, provider: &str) -> Option<&String> {
1184        self.providers.get(provider)?.token_url.as_ref()
1185    }
1186
1187    pub fn set_cached_token(
1188        &mut self,
1189        provider: String,
1190        token: String,
1191        expires_at: DateTime<Utc>,
1192    ) -> Result<()> {
1193        if let Some(provider_config) = self.providers.get_mut(&provider) {
1194            provider_config.cached_token = Some(CachedToken { token, expires_at });
1195            let config_clone = provider_config.clone();
1196            self.save_single_provider(&provider, &config_clone)?;
1197            Ok(())
1198        } else {
1199            anyhow::bail!("Provider '{}' not found", provider);
1200        }
1201    }
1202
1203    pub fn get_cached_token(&self, provider: &str) -> Option<&CachedToken> {
1204        self.providers.get(provider)?.cached_token.as_ref()
1205    }
1206}