1use 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>, #[serde(default)]
22 pub system_prompt: Option<String>,
23 #[serde(default)]
24 pub templates: HashMap<String, String>, #[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>, #[serde(default)]
59 pub vars: HashMap<String, String>, #[serde(default)]
61 pub chat_templates: Option<HashMap<String, TemplateConfig>>, #[serde(default)]
63 pub images_templates: Option<HashMap<String, TemplateConfig>>, #[serde(default)]
65 pub embeddings_templates: Option<HashMap<String, TemplateConfig>>, #[serde(default)]
67 pub models_templates: Option<HashMap<String, TemplateConfig>>, #[serde(default)]
69 pub audio_templates: Option<HashMap<String, TemplateConfig>>, #[serde(default)]
71 pub speech_templates: Option<HashMap<String, TemplateConfig>>, }
73
74impl ProviderConfig {
75 pub fn is_chat_path_full_url(&self) -> bool {
77 self.chat_path.starts_with("https://")
78 }
79
80 pub fn get_models_url(&self) -> String {
82 format!(
83 "{}{}",
84 self.endpoint.trim_end_matches('/'),
85 self.models_path
86 )
87 }
88
89 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 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 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 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 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 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 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 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 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 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 format!("{}/images/generations", self.endpoint.trim_end_matches('/'))
194 }
195 }
196
197 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 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 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 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 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 format!("{}/audio/speech", self.endpoint.trim_end_matches('/'))
249 }
250 }
251
252 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 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 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 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 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 format!("{}/embeddings", self.endpoint.trim_end_matches('/'))
304 }
305 }
306
307 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 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 fn get_template_for_model(
343 &self,
344 templates: &HashMap<String, TemplateConfig>,
345 model_name: &str,
346 template_type: &str,
347 ) -> Option<String> {
348 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 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 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 !config.providers.is_empty() {
421 Self::migrate_providers_to_separate_files(&mut config)?;
422 }
423
424 config
425 } else {
426 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 config.providers = Self::load_providers_from_files(&providers_dir)?;
441
442 if let Some(parent) = config_path.parent() {
444 fs::create_dir_all(parent)?;
445 }
446
447 fs::create_dir_all(&providers_dir)?;
449
450 config.save_main_config()?;
452
453 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 self.save_main_config()?;
465
466 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 let main_config = Config {
477 providers: HashMap::new(), 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 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 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 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 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 for (provider_name, provider_config) in &config.providers {
583 Self::save_single_provider_flat_static(&providers_dir, provider_name, provider_config)?;
584 }
585
586 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 if provider_config
628 .endpoint
629 .contains("aiplatform.googleapis.com")
630 {
631 provider_config.auth_type = Some("google_sa_jwt".to_string());
632 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 if self.default_provider.is_none() {
642 self.default_provider = Some(name.clone());
643 }
644
645 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 if !self.has_provider(&provider) {
654 anyhow::bail!("Provider '{}' not found", provider);
655 }
656
657 let mut keys = crate::keys::KeysConfig::load()?;
659 keys.set_api_key(provider.clone(), api_key)?;
660
661 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 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 pub fn get_provider_with_auth(&self, name: &str) -> Result<ProviderConfig> {
687 let mut provider_config = self.get_provider(name)?.clone();
688
689 if let Some(auth) = crate::keys::get_provider_auth(name)? {
691 match auth {
692 crate::keys::ProviderAuth::ApiKey(key) => {
693 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 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 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 if !provider_model.contains(':') {
790 anyhow::bail!(
791 "Alias target must be in format 'provider:model', got '{}'",
792 provider_model
793 );
794 }
795
796 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 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 if let Ok(test_dir) = std::env::var("LC_TEST_CONFIG_DIR") {
894 let test_path = PathBuf::from(test_dir);
895 if !test_path.exists() {
897 fs::create_dir_all(&test_path)?;
898 }
899 return Ok(test_path);
900 }
901
902 #[cfg(test)]
906 {
907 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 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 let temp_dir = std::env::temp_dir()
921 .join("lc_test")
922 .join(format!("test_{}", std::process::id()));
923
924 let cleanup_dir = temp_dir.clone();
926 INIT.call_once(|| {
927 struct TestDirCleanup(PathBuf);
930 impl Drop for TestDirCleanup {
931 fn drop(&mut self) {
932 if self.0.exists() {
934 let _ = fs::remove_dir_all(&self.0);
935 }
936 }
937 }
938
939 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 if std::env::var("CARGO").is_ok() && std::env::var("CARGO_PKG_NAME").is_ok() {
963 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 if exe_str.contains("test") || exe_str.contains("-") && exe_str.len() > 20 {
969 use std::sync::Mutex;
971 use tempfile::TempDir;
972
973 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 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 let data_dir = dirs::data_local_dir()
1001 .ok_or_else(|| anyhow::anyhow!("Could not find data directory"))?
1002 .join("lc");
1003
1004 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 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 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 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 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}