1use 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>, #[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 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>, #[serde(default)]
55 pub vars: HashMap<String, String>, #[serde(default)]
57 pub chat_templates: Option<HashMap<String, TemplateConfig>>, #[serde(default)]
59 pub images_templates: Option<HashMap<String, TemplateConfig>>, #[serde(default)]
61 pub embeddings_templates: Option<HashMap<String, TemplateConfig>>, #[serde(default)]
63 pub models_templates: Option<HashMap<String, TemplateConfig>>, }
65
66impl ProviderConfig {
67 pub fn is_chat_path_full_url(&self) -> bool {
69 self.chat_path.starts_with("https://")
70 }
71
72 pub fn get_models_url(&self) -> String {
74 format!(
75 "{}{}",
76 self.endpoint.trim_end_matches('/'),
77 self.models_path
78 )
79 }
80
81 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 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 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 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 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 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 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 fn get_template_for_model(&self, templates: &HashMap<String, TemplateConfig>, model_name: &str, template_type: &str) -> Option<String> {
162 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 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 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 !config.providers.is_empty() {
235 Self::migrate_providers_to_separate_files(&mut config)?;
236 }
237
238 config
239 } else {
240 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 config.providers = Self::load_providers_from_files(&providers_dir)?;
255
256 if let Some(parent) = config_path.parent() {
258 fs::create_dir_all(parent)?;
259 }
260
261 fs::create_dir_all(&providers_dir)?;
263
264 config.save_main_config()?;
266
267 Ok(config)
268 }
269
270 pub fn save(&self) -> Result<()> {
271 self.save_main_config()?;
273
274 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 let main_config = Config {
285 providers: HashMap::new(), 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 match Self::parse_flat_provider_config(&content) {
329 Ok(config) => {
330 providers.insert(provider_name.to_string(), config);
331 }
332 Err(_flat_error) => {
333 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 for (provider_name, provider_config) in &config.providers {
367 Self::save_single_provider_flat_static(&providers_dir, provider_name, provider_config)?;
368 }
369
370 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 if provider_config
408 .endpoint
409 .contains("aiplatform.googleapis.com")
410 {
411 provider_config.auth_type = Some("google_sa_jwt".to_string());
412 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 if self.default_provider.is_none() {
422 self.default_provider = Some(name.clone());
423 }
424
425 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 if !provider_model.contains(':') {
497 anyhow::bail!(
498 "Alias target must be in format 'provider:model', got '{}'",
499 provider_model
500 );
501 }
502
503 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 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 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 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 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 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 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}