1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use crate::error::{LetheError, Result};
4
5#[cfg(test)]
6use regex;
7
8#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
10pub struct Alpha(f64);
11
12impl Alpha {
13 pub fn new(value: f64) -> Result<Self> {
14 if !value.is_finite() || value < 0.0 || value > 1.0 {
15 Err(LetheError::validation("alpha", "Must be between 0 and 1"))
16 } else {
17 Ok(Alpha(value))
18 }
19 }
20
21 pub fn value(self) -> f64 {
22 self.0
23 }
24}
25
26impl Default for Alpha {
27 fn default() -> Self {
28 Alpha(0.7) }
30}
31
32#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
34pub struct Beta(f64);
35
36impl Beta {
37 pub fn new(value: f64) -> Result<Self> {
38 if !value.is_finite() || value < 0.0 || value > 1.0 {
39 Err(LetheError::validation("beta", "Must be between 0 and 1"))
40 } else {
41 Ok(Beta(value))
42 }
43 }
44
45 pub fn value(self) -> f64 {
46 self.0
47 }
48}
49
50impl Default for Beta {
51 fn default() -> Self {
52 Beta(0.5) }
54}
55
56#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
58pub struct PositiveTokens(i32);
59
60impl PositiveTokens {
61 pub fn new(value: i32) -> Result<Self> {
62 if value <= 0 {
63 Err(LetheError::validation("tokens", "Must be positive"))
64 } else {
65 Ok(PositiveTokens(value))
66 }
67 }
68
69 pub fn value(self) -> i32 {
70 self.0
71 }
72}
73
74impl Default for PositiveTokens {
75 fn default() -> Self {
76 PositiveTokens(320) }
78}
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
82pub struct TimeoutMs(u64);
83
84impl TimeoutMs {
85 pub fn new(value: u64) -> Result<Self> {
86 if value == 0 {
87 Err(LetheError::validation("timeout", "Must be positive"))
88 } else {
89 Ok(TimeoutMs(value))
90 }
91 }
92
93 pub fn value(self) -> u64 {
94 self.0
95 }
96}
97
98impl Default for TimeoutMs {
99 fn default() -> Self {
100 TimeoutMs(10000) }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct LetheConfig {
107 pub version: String,
108 pub description: Option<String>,
109 pub retrieval: RetrievalConfig,
110 pub chunking: ChunkingConfig,
111 pub timeouts: TimeoutsConfig,
112 pub features: Option<FeaturesConfig>,
113 pub query_understanding: Option<QueryUnderstandingConfig>,
114 pub ml: Option<MlConfig>,
115 pub development: Option<DevelopmentConfig>,
116 pub lens: Option<LensConfig>,
117 pub proxy: Option<ProxyConfig>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct RetrievalConfig {
123 pub alpha: Alpha,
124 pub beta: Beta,
125 #[serde(default = "default_gamma_kind_boost")]
126 pub gamma_kind_boost: HashMap<String, f64>,
127 #[serde(default)]
128 pub fusion: Option<FusionConfig>,
129 #[serde(default)]
130 pub llm_rerank: Option<LlmRerankConfig>,
131}
132
133fn default_gamma_kind_boost() -> HashMap<String, f64> {
134 let mut map = HashMap::new();
135 map.insert("code".to_string(), 0.1);
136 map.insert("text".to_string(), 0.0);
137 map
138}
139
140impl Default for RetrievalConfig {
141 fn default() -> Self {
142 Self {
143 alpha: Alpha::default(),
144 beta: Beta::default(),
145 gamma_kind_boost: default_gamma_kind_boost(),
146 fusion: Some(FusionConfig::default()),
147 llm_rerank: Some(LlmRerankConfig::default()),
148 }
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct FusionConfig {
155 #[serde(default)]
156 pub dynamic: bool,
157}
158
159impl Default for FusionConfig {
160 fn default() -> Self {
161 Self { dynamic: false }
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct LlmRerankConfig {
168 #[serde(default)]
169 pub use_llm: bool,
170 #[serde(default = "default_llm_budget")]
171 pub llm_budget_ms: u64,
172 #[serde(default = "default_llm_model")]
173 pub llm_model: String,
174 #[serde(default)]
175 pub contradiction_enabled: bool,
176 #[serde(default = "default_contradiction_penalty")]
177 pub contradiction_penalty: f64,
178}
179
180fn default_llm_budget() -> u64 { 1200 }
181fn default_llm_model() -> String { "llama3.2:1b".to_string() }
182fn default_contradiction_penalty() -> f64 { 0.15 }
183
184impl Default for LlmRerankConfig {
185 fn default() -> Self {
186 Self {
187 use_llm: false,
188 llm_budget_ms: default_llm_budget(),
189 llm_model: default_llm_model(),
190 contradiction_enabled: false,
191 contradiction_penalty: default_contradiction_penalty(),
192 }
193 }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct ChunkingConfig {
199 pub target_tokens: PositiveTokens,
200 pub overlap: i32, #[serde(default = "default_chunking_method")]
202 pub method: String,
203}
204
205fn default_chunking_method() -> String {
206 "semantic".to_string()
207}
208
209impl ChunkingConfig {
210 pub fn validate(&self) -> Result<()> {
211 if self.overlap < 0 || self.overlap >= self.target_tokens.value() {
212 return Err(LetheError::validation(
213 "chunking.overlap",
214 "Must be non-negative and less than target_tokens"
215 ));
216 }
217 Ok(())
218 }
219}
220
221impl Default for ChunkingConfig {
222 fn default() -> Self {
223 Self {
224 target_tokens: PositiveTokens::default(),
225 overlap: 64,
226 method: default_chunking_method(),
227 }
228 }
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct TimeoutsConfig {
234 #[serde(default)]
235 pub hyde_ms: TimeoutMs,
236 #[serde(default)]
237 pub summarize_ms: TimeoutMs,
238 #[serde(default = "default_connect_timeout")]
239 pub ollama_connect_ms: TimeoutMs,
240 pub ml_prediction_ms: Option<TimeoutMs>,
241}
242
243fn default_connect_timeout() -> TimeoutMs {
244 TimeoutMs::new(500).unwrap()
245}
246
247impl Default for TimeoutsConfig {
248 fn default() -> Self {
249 Self {
250 hyde_ms: TimeoutMs::default(),
251 summarize_ms: TimeoutMs::default(),
252 ollama_connect_ms: default_connect_timeout(),
253 ml_prediction_ms: Some(TimeoutMs::new(2000).unwrap()),
254 }
255 }
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct FeaturesConfig {
261 #[serde(default = "default_true")]
262 pub enable_hyde: bool,
263 #[serde(default = "default_true")]
264 pub enable_summarization: bool,
265 #[serde(default = "default_true")]
266 pub enable_plan_selection: bool,
267 #[serde(default = "default_true")]
268 pub enable_query_understanding: bool,
269 #[serde(default)]
270 pub enable_ml_prediction: bool,
271 #[serde(default = "default_true")]
272 pub enable_state_tracking: bool,
273}
274
275fn default_true() -> bool { true }
276
277impl Default for FeaturesConfig {
278 fn default() -> Self {
279 Self {
280 enable_hyde: true,
281 enable_summarization: true,
282 enable_plan_selection: true,
283 enable_query_understanding: true,
284 enable_ml_prediction: false,
285 enable_state_tracking: true,
286 }
287 }
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct QueryUnderstandingConfig {
293 #[serde(default = "default_true")]
294 pub rewrite_enabled: bool,
295 #[serde(default = "default_true")]
296 pub decompose_enabled: bool,
297 #[serde(default = "default_max_subqueries")]
298 pub max_subqueries: i32,
299 #[serde(default = "default_llm_model")]
300 pub llm_model: String,
301 #[serde(default = "default_temperature")]
302 pub temperature: f64,
303}
304
305fn default_max_subqueries() -> i32 { 3 }
306fn default_temperature() -> f64 { 0.1 }
307
308impl Default for QueryUnderstandingConfig {
309 fn default() -> Self {
310 Self {
311 rewrite_enabled: true,
312 decompose_enabled: true,
313 max_subqueries: default_max_subqueries(),
314 llm_model: default_llm_model(),
315 temperature: default_temperature(),
316 }
317 }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct MlConfig {
323 #[serde(default)]
324 pub prediction_service: Option<PredictionServiceConfig>,
325 #[serde(default)]
326 pub models: Option<ModelsConfig>,
327}
328
329impl Default for MlConfig {
330 fn default() -> Self {
331 Self {
332 prediction_service: Some(PredictionServiceConfig::default()),
333 models: Some(ModelsConfig::default()),
334 }
335 }
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct PredictionServiceConfig {
341 #[serde(default)]
342 pub enabled: bool,
343 #[serde(default = "default_host")]
344 pub host: String,
345 #[serde(default = "default_port")]
346 pub port: u16,
347 #[serde(default = "default_service_timeout")]
348 pub timeout_ms: u64,
349 #[serde(default = "default_true")]
350 pub fallback_to_static: bool,
351}
352
353fn default_host() -> String { "127.0.0.1".to_string() }
354fn default_port() -> u16 { 8080 }
355fn default_service_timeout() -> u64 { 2000 }
356
357impl PredictionServiceConfig {
358 pub fn validate(&self) -> Result<()> {
359 if self.enabled {
360 if self.port == 0 {
361 return Err(LetheError::validation(
362 "ml.prediction_service.port",
363 "Must be a valid port number"
364 ));
365 }
366 if self.timeout_ms == 0 {
367 return Err(LetheError::validation(
368 "ml.prediction_service.timeout_ms",
369 "Must be positive"
370 ));
371 }
372 }
373 Ok(())
374 }
375}
376
377impl Default for PredictionServiceConfig {
378 fn default() -> Self {
379 Self {
380 enabled: false,
381 host: default_host(),
382 port: default_port(),
383 timeout_ms: default_service_timeout(),
384 fallback_to_static: true,
385 }
386 }
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct ModelsConfig {
392 #[serde(default = "default_plan_selector")]
393 pub plan_selector: Option<String>,
394 #[serde(default = "default_fusion_weights")]
395 pub fusion_weights: Option<String>,
396 #[serde(default = "default_feature_extractor")]
397 pub feature_extractor: Option<String>,
398}
399
400fn default_plan_selector() -> Option<String> {
401 Some("learned_plan_selector.joblib".to_string())
402}
403fn default_fusion_weights() -> Option<String> {
404 Some("dynamic_fusion_model.joblib".to_string())
405}
406fn default_feature_extractor() -> Option<String> {
407 Some("feature_extractor.json".to_string())
408}
409
410impl Default for ModelsConfig {
411 fn default() -> Self {
412 Self {
413 plan_selector: default_plan_selector(),
414 fusion_weights: default_fusion_weights(),
415 feature_extractor: default_feature_extractor(),
416 }
417 }
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct DevelopmentConfig {
423 #[serde(default)]
424 pub debug_enabled: bool,
425 #[serde(default)]
426 pub profiling_enabled: bool,
427 #[serde(default = "default_log_level")]
428 pub log_level: String,
429}
430
431fn default_log_level() -> String { "info".to_string() }
432
433impl Default for DevelopmentConfig {
434 fn default() -> Self {
435 Self {
436 debug_enabled: false,
437 profiling_enabled: false,
438 log_level: default_log_level(),
439 }
440 }
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct LensConfig {
446 #[serde(default)]
447 pub enabled: bool,
448 #[serde(default = "default_lens_base_url")]
449 pub base_url: String,
450 #[serde(default = "default_lens_connect_timeout")]
451 pub connect_timeout_ms: u64,
452 #[serde(default = "default_lens_request_timeout")]
453 pub request_timeout_ms: u64,
454 #[serde(default = "default_lens_request_timeout")]
455 pub sla_recall_ms: u64,
456 #[serde(default = "default_topic_fanout_k")]
457 pub topic_fanout_k: i32,
458 #[serde(default = "default_weight_cap")]
459 pub weight_cap: f64,
460 #[serde(default = "default_max_tokens_per_response")]
461 pub max_tokens_per_response: i32,
462 #[serde(default = "default_lens_mode")]
463 pub mode: String,
464 #[serde(default = "default_dpp_rank")]
465 pub dpp_rank: i32,
466 #[serde(default = "default_true")]
467 pub enable_facility_location: bool,
468 #[serde(default = "default_true")]
469 pub enable_log_det_dpp: bool,
470 #[serde(default = "default_lambda_multiplier")]
471 pub lambda_multiplier: f64,
472 #[serde(default = "default_mu_multiplier")]
473 pub mu_multiplier: f64,
474 #[serde(default = "default_max_tokens_per_response")]
475 pub lens_tokens_cap: i32,
476}
477
478fn default_lens_base_url() -> String { "http://localhost:8081".to_string() }
479fn default_lens_connect_timeout() -> u64 { 500 }
480fn default_lens_request_timeout() -> u64 { 150 }
481fn default_topic_fanout_k() -> i32 { 240 }
482fn default_weight_cap() -> f64 { 0.4 }
483fn default_max_tokens_per_response() -> i32 { 4000 }
484fn default_lens_mode() -> String { "auto".to_string() }
485fn default_dpp_rank() -> i32 { 14 }
486fn default_lambda_multiplier() -> f64 { 1.2 }
487fn default_mu_multiplier() -> f64 { 1.0 }
488
489impl LensConfig {
490 pub fn validate(&self) -> Result<()> {
491 if self.enabled {
492 if self.sla_recall_ms == 0 || self.sla_recall_ms > 1000 {
493 return Err(LetheError::validation(
494 "lens.sla_recall_ms",
495 "Must be between 0 and 1000"
496 ));
497 }
498 if self.topic_fanout_k <= 0 || self.topic_fanout_k > 1000 {
499 return Err(LetheError::validation(
500 "lens.topic_fanout_k",
501 "Must be between 0 and 1000"
502 ));
503 }
504 if self.weight_cap <= 0.0 || self.weight_cap > 1.0 {
505 return Err(LetheError::validation(
506 "lens.weight_cap",
507 "Must be between 0 and 1.0"
508 ));
509 }
510 if !self.base_url.starts_with("http") {
511 return Err(LetheError::validation(
512 "lens.base_url",
513 "Must be a valid HTTP URL"
514 ));
515 }
516 }
517 Ok(())
518 }
519}
520
521impl Default for LensConfig {
522 fn default() -> Self {
523 Self {
524 enabled: false,
525 base_url: default_lens_base_url(),
526 connect_timeout_ms: default_lens_connect_timeout(),
527 request_timeout_ms: default_lens_request_timeout(),
528 sla_recall_ms: default_lens_request_timeout(),
529 topic_fanout_k: default_topic_fanout_k(),
530 weight_cap: default_weight_cap(),
531 max_tokens_per_response: default_max_tokens_per_response(),
532 mode: default_lens_mode(),
533 dpp_rank: default_dpp_rank(),
534 enable_facility_location: true,
535 enable_log_det_dpp: true,
536 lambda_multiplier: default_lambda_multiplier(),
537 mu_multiplier: default_mu_multiplier(),
538 lens_tokens_cap: default_max_tokens_per_response(),
539 }
540 }
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct ProxyConfig {
546 #[serde(default = "default_true")]
547 pub enabled: bool,
548 #[serde(default)]
549 pub openai: ProviderConfig,
550 #[serde(default)]
551 pub anthropic: ProviderConfig,
552 #[serde(default)]
553 pub auth: AuthConfig,
554 #[serde(default)]
555 pub rewrite: RewriteConfig,
556 #[serde(default)]
557 pub security: SecurityConfig,
558 #[serde(default)]
559 pub timeouts: ProxyTimeoutsConfig,
560 #[serde(default)]
561 pub logging: ProxyLoggingConfig,
562}
563
564impl Default for ProxyConfig {
565 fn default() -> Self {
566 Self {
567 enabled: true,
568 openai: ProviderConfig::default_openai(),
569 anthropic: ProviderConfig::default_anthropic(),
570 auth: AuthConfig::default(),
571 rewrite: RewriteConfig::default(),
572 security: SecurityConfig::default(),
573 timeouts: ProxyTimeoutsConfig::default(),
574 logging: ProxyLoggingConfig::default(),
575 }
576 }
577}
578
579impl ProxyConfig {
580 pub fn validate(&self) -> Result<()> {
581 if self.enabled {
582 self.openai.validate()?;
583 self.anthropic.validate()?;
584 self.auth.validate()?;
585 self.rewrite.validate()?;
586 self.security.validate()?;
587 self.timeouts.validate()?;
588 self.logging.validate()?;
589 }
590 Ok(())
591 }
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
596pub struct ProviderConfig {
597 #[serde(default)]
598 pub base_url: String,
599}
600
601impl ProviderConfig {
602 pub fn default_openai() -> Self {
603 Self {
604 base_url: "https://api.openai.com".to_string(),
605 }
606 }
607
608 pub fn default_anthropic() -> Self {
609 Self {
610 base_url: "https://api.anthropic.com".to_string(),
611 }
612 }
613
614 pub fn validate(&self) -> Result<()> {
615 if !self.base_url.starts_with("http") {
616 return Err(LetheError::validation(
617 "proxy.provider.base_url",
618 "Must be a valid HTTP URL"
619 ));
620 }
621 Ok(())
622 }
623}
624
625impl Default for ProviderConfig {
626 fn default() -> Self {
627 Self::default_openai()
628 }
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct AuthConfig {
634 #[serde(default = "default_auth_mode")]
635 pub mode: String, #[serde(default)]
637 pub inject: InjectConfig,
638}
639
640fn default_auth_mode() -> String {
641 "passthrough".to_string()
642}
643
644impl Default for AuthConfig {
645 fn default() -> Self {
646 Self {
647 mode: default_auth_mode(),
648 inject: InjectConfig::default(),
649 }
650 }
651}
652
653impl AuthConfig {
654 pub fn validate(&self) -> Result<()> {
655 match self.mode.as_str() {
656 "passthrough" | "inject" => Ok(()),
657 _ => Err(LetheError::validation(
658 "proxy.auth.mode",
659 "Must be 'passthrough' or 'inject'"
660 ))
661 }
662 }
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize)]
667pub struct InjectConfig {
668 pub openai_api_key: Option<String>,
669 pub anthropic_api_key: Option<String>,
670}
671
672impl Default for InjectConfig {
673 fn default() -> Self {
674 Self {
675 openai_api_key: std::env::var("OPENAI_API_KEY").ok(),
676 anthropic_api_key: std::env::var("ANTHROPIC_API_KEY").ok(),
677 }
678 }
679}
680
681#[derive(Debug, Clone, Serialize, Deserialize)]
683pub struct RewriteConfig {
684 #[serde(default = "default_true")]
685 pub enabled: bool,
686 #[serde(default = "default_max_request_bytes")]
687 pub max_request_bytes: u64,
688 pub prelude_system: Option<String>,
689}
690
691fn default_max_request_bytes() -> u64 {
692 2_000_000 }
694
695impl Default for RewriteConfig {
696 fn default() -> Self {
697 Self {
698 enabled: true,
699 max_request_bytes: default_max_request_bytes(),
700 prelude_system: None,
701 }
702 }
703}
704
705impl RewriteConfig {
706 pub fn validate(&self) -> Result<()> {
707 if self.max_request_bytes == 0 {
708 return Err(LetheError::validation(
709 "proxy.rewrite.max_request_bytes",
710 "Must be positive"
711 ));
712 }
713 Ok(())
714 }
715}
716
717#[derive(Debug, Clone, Serialize, Deserialize)]
719pub struct SecurityConfig {
720 #[serde(default = "default_allowed_providers")]
721 pub allowed_providers: Vec<String>,
722}
723
724fn default_allowed_providers() -> Vec<String> {
725 vec!["openai".to_string(), "anthropic".to_string()]
726}
727
728impl Default for SecurityConfig {
729 fn default() -> Self {
730 Self {
731 allowed_providers: default_allowed_providers(),
732 }
733 }
734}
735
736impl SecurityConfig {
737 pub fn validate(&self) -> Result<()> {
738 if self.allowed_providers.is_empty() {
739 return Err(LetheError::validation(
740 "proxy.security.allowed_providers",
741 "Must have at least one allowed provider"
742 ));
743 }
744 for provider in &self.allowed_providers {
745 match provider.as_str() {
746 "openai" | "anthropic" => {},
747 _ => return Err(LetheError::validation(
748 "proxy.security.allowed_providers",
749 "Only 'openai' and 'anthropic' are supported"
750 ))
751 }
752 }
753 Ok(())
754 }
755}
756
757#[derive(Debug, Clone, Serialize, Deserialize)]
759pub struct ProxyTimeoutsConfig {
760 #[serde(default = "default_proxy_connect_timeout")]
761 pub connect_ms: u64,
762 #[serde(default = "default_proxy_read_timeout")]
763 pub read_ms: u64,
764}
765
766fn default_proxy_connect_timeout() -> u64 {
767 5000 }
769
770fn default_proxy_read_timeout() -> u64 {
771 60000 }
773
774impl Default for ProxyTimeoutsConfig {
775 fn default() -> Self {
776 Self {
777 connect_ms: default_proxy_connect_timeout(),
778 read_ms: default_proxy_read_timeout(),
779 }
780 }
781}
782
783impl ProxyTimeoutsConfig {
784 pub fn validate(&self) -> Result<()> {
785 if self.connect_ms == 0 {
786 return Err(LetheError::validation(
787 "proxy.timeouts.connect_ms",
788 "Must be positive"
789 ));
790 }
791 if self.read_ms == 0 {
792 return Err(LetheError::validation(
793 "proxy.timeouts.read_ms",
794 "Must be positive"
795 ));
796 }
797 Ok(())
798 }
799}
800
801#[derive(Debug, Clone, Serialize, Deserialize)]
803pub struct ProxyLoggingConfig {
804 #[serde(default = "default_proxy_log_level")]
805 pub level: String, #[serde(default = "default_true")]
807 pub include_payloads: bool,
808 #[serde(default = "default_true")]
809 pub redact_sensitive: bool,
810 #[serde(default)]
811 pub redaction_patterns: Vec<String>,
812 #[serde(default = "default_log_destination")]
813 pub destination: String, pub file_path: Option<String>,
815 #[serde(default = "default_true")]
816 pub enable_correlation_ids: bool,
817 #[serde(default = "default_true")]
818 pub log_performance_metrics: bool,
819}
820
821fn default_proxy_log_level() -> String {
822 "basic".to_string()
823}
824
825fn default_log_destination() -> String {
826 "stdout".to_string()
827}
828
829impl Default for ProxyLoggingConfig {
830 fn default() -> Self {
831 Self {
832 level: default_proxy_log_level(),
833 include_payloads: true,
834 redact_sensitive: true,
835 redaction_patterns: vec![
836 "sk-[A-Za-z0-9]{48}".to_string(), "Bearer\\s+[A-Za-z0-9._-]+".to_string(), "x-api-key:\\s*[A-Za-z0-9._-]+".to_string(), "\"password\":\\s*\"[^\"]*\"".to_string(), "\"api_key\":\\s*\"[^\"]*\"".to_string(), ],
842 destination: default_log_destination(),
843 file_path: None,
844 enable_correlation_ids: true,
845 log_performance_metrics: true,
846 }
847 }
848}
849
850impl ProxyLoggingConfig {
851 pub fn validate(&self) -> Result<()> {
852 match self.level.as_str() {
853 "off" | "basic" | "detailed" | "debug" => {},
854 _ => return Err(LetheError::validation(
855 "proxy.logging.level",
856 "Must be 'off', 'basic', 'detailed', or 'debug'"
857 )),
858 }
859
860 match self.destination.as_str() {
861 "stdout" | "file" | "structured" => {},
862 _ => return Err(LetheError::validation(
863 "proxy.logging.destination",
864 "Must be 'stdout', 'file', or 'structured'"
865 )),
866 }
867
868 if self.destination == "file" && self.file_path.is_none() {
869 return Err(LetheError::validation(
870 "proxy.logging.file_path",
871 "file_path is required when destination is 'file'"
872 ));
873 }
874
875 for pattern in &self.redaction_patterns {
877 if let Err(e) = regex::Regex::new(pattern) {
878 return Err(LetheError::validation(
879 "proxy.logging.redaction_patterns",
880 &format!("Invalid regex pattern '{}': {}", pattern, e)
881 ));
882 }
883 }
884
885 Ok(())
886 }
887
888 pub fn should_log(&self) -> bool {
889 self.level != "off"
890 }
891
892 pub fn should_log_payloads(&self) -> bool {
893 self.include_payloads && matches!(self.level.as_str(), "detailed" | "debug")
894 }
895
896 pub fn should_log_debug_info(&self) -> bool {
897 self.level == "debug"
898 }
899}
900
901impl Default for LetheConfig {
902 fn default() -> Self {
903 Self {
904 version: "1.0.0".to_string(),
905 description: Some("Default Lethe configuration".to_string()),
906 retrieval: RetrievalConfig::default(),
907 chunking: ChunkingConfig::default(),
908 timeouts: TimeoutsConfig::default(),
909 features: Some(FeaturesConfig::default()),
910 query_understanding: Some(QueryUnderstandingConfig::default()),
911 ml: Some(MlConfig::default()),
912 development: Some(DevelopmentConfig::default()),
913 lens: Some(LensConfig::default()),
914 proxy: Some(ProxyConfig::default()),
915 }
916 }
917}
918
919impl LetheConfig {
920 pub fn from_file(path: &std::path::Path) -> Result<Self> {
922 let content = std::fs::read_to_string(path)
923 .map_err(|e| LetheError::config(format!("Failed to read config file: {}", e)))?;
924
925 let config: Self = serde_json::from_str(&content)
926 .map_err(|e| LetheError::config(format!("Failed to parse config: {}", e)))?;
927
928 config.validate()?;
929 Ok(config)
930 }
931
932 pub fn to_file(&self, path: &std::path::Path) -> Result<()> {
934 let content = serde_json::to_string_pretty(self)?;
935 std::fs::write(path, content)
936 .map_err(|e| LetheError::config(format!("Failed to write config file: {}", e)))?;
937 Ok(())
938 }
939
940 pub fn validate(&self) -> Result<()> {
942 self.chunking.validate()?;
946
947 if let Some(ml) = &self.ml {
951 if let Some(service) = &ml.prediction_service {
952 service.validate()?;
953 }
954 }
955
956 if let Some(lens) = &self.lens {
958 lens.validate()?;
959 }
960
961 if let Some(proxy) = &self.proxy {
963 proxy.validate()?;
964 }
965
966 Ok(())
967 }
968
969 pub fn merge_with(&mut self, other: &Self) {
971 self.version = other.version.clone();
972
973 if other.description.is_some() {
975 self.description = other.description.clone();
976 }
977
978 self.retrieval = other.retrieval.clone();
980 self.chunking = other.chunking.clone();
981 self.timeouts = other.timeouts.clone();
982
983 self.features = other.features.clone().or_else(|| self.features.clone());
985 self.query_understanding = other.query_understanding.clone().or_else(|| self.query_understanding.clone());
986 self.ml = other.ml.clone().or_else(|| self.ml.clone());
987 self.development = other.development.clone().or_else(|| self.development.clone());
988 self.lens = other.lens.clone().or_else(|| self.lens.clone());
989 self.proxy = other.proxy.clone().or_else(|| self.proxy.clone());
990 }
991
992 pub fn builder() -> LetheConfigBuilder {
994 LetheConfigBuilder::default()
995 }
996}
997
998#[derive(Debug, Default)]
1000pub struct LetheConfigBuilder {
1001 config: LetheConfig,
1002}
1003
1004impl LetheConfigBuilder {
1005 pub fn version<S: Into<String>>(mut self, version: S) -> Self {
1006 self.config.version = version.into();
1007 self
1008 }
1009
1010 pub fn description<S: Into<String>>(mut self, description: S) -> Self {
1011 self.config.description = Some(description.into());
1012 self
1013 }
1014
1015 pub fn retrieval(mut self, retrieval: RetrievalConfig) -> Self {
1016 self.config.retrieval = retrieval;
1017 self
1018 }
1019
1020 pub fn chunking(mut self, chunking: ChunkingConfig) -> Self {
1021 self.config.chunking = chunking;
1022 self
1023 }
1024
1025 pub fn features(mut self, features: FeaturesConfig) -> Self {
1026 self.config.features = Some(features);
1027 self
1028 }
1029
1030 pub fn build(self) -> Result<LetheConfig> {
1031 let config = self.config;
1032 config.validate()?;
1033 Ok(config)
1034 }
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use super::*;
1040 use std::collections::HashMap;
1041 use tempfile::NamedTempFile;
1042 use std::io::Write;
1043 use proptest::prelude::*;
1044 use approx::assert_relative_eq;
1045
1046 #[test]
1048 fn test_alpha_valid_values() {
1049 assert!(Alpha::new(0.0).is_ok());
1050 assert!(Alpha::new(0.5).is_ok());
1051 assert!(Alpha::new(1.0).is_ok());
1052
1053 let alpha = Alpha::new(0.7).unwrap();
1054 assert_eq!(alpha.value(), 0.7);
1055 }
1056
1057 #[test]
1058 fn test_alpha_invalid_values() {
1059 assert!(Alpha::new(-0.1).is_err());
1060 assert!(Alpha::new(1.1).is_err());
1061 assert!(Alpha::new(f64::NAN).is_err());
1062 assert!(Alpha::new(f64::INFINITY).is_err());
1063 assert!(Alpha::new(f64::NEG_INFINITY).is_err());
1064 }
1065
1066 #[test]
1067 fn test_alpha_default() {
1068 let alpha = Alpha::default();
1069 assert_eq!(alpha.value(), 0.7);
1070 }
1071
1072 #[test]
1073 fn test_alpha_serialization() {
1074 let alpha = Alpha::new(0.8).unwrap();
1075 let serialized = serde_json::to_string(&alpha).unwrap();
1076 assert_eq!(serialized, "0.8");
1077
1078 let deserialized: Alpha = serde_json::from_str(&serialized).unwrap();
1079 assert_eq!(deserialized.value(), 0.8);
1080 }
1081
1082 #[test]
1084 fn test_beta_valid_values() {
1085 assert!(Beta::new(0.0).is_ok());
1086 assert!(Beta::new(0.5).is_ok());
1087 assert!(Beta::new(1.0).is_ok());
1088
1089 let beta = Beta::new(0.3).unwrap();
1090 assert_eq!(beta.value(), 0.3);
1091 }
1092
1093 #[test]
1094 fn test_beta_invalid_values() {
1095 assert!(Beta::new(-0.1).is_err());
1096 assert!(Beta::new(1.1).is_err());
1097 }
1098
1099 #[test]
1100 fn test_beta_default() {
1101 let beta = Beta::default();
1102 assert_eq!(beta.value(), 0.5);
1103 }
1104
1105 #[test]
1107 fn test_positive_tokens_valid() {
1108 assert!(PositiveTokens::new(1).is_ok());
1109 assert!(PositiveTokens::new(1000).is_ok());
1110
1111 let tokens = PositiveTokens::new(320).unwrap();
1112 assert_eq!(tokens.value(), 320);
1113 }
1114
1115 #[test]
1116 fn test_positive_tokens_invalid() {
1117 assert!(PositiveTokens::new(0).is_err());
1118 assert!(PositiveTokens::new(-1).is_err());
1119 assert!(PositiveTokens::new(-100).is_err());
1120 }
1121
1122 #[test]
1123 fn test_positive_tokens_default() {
1124 let tokens = PositiveTokens::default();
1125 assert_eq!(tokens.value(), 320);
1126 }
1127
1128 #[test]
1130 fn test_timeout_ms_valid() {
1131 assert!(TimeoutMs::new(1).is_ok());
1132 assert!(TimeoutMs::new(10000).is_ok());
1133
1134 let timeout = TimeoutMs::new(5000).unwrap();
1135 assert_eq!(timeout.value(), 5000);
1136 }
1137
1138 #[test]
1139 fn test_timeout_ms_invalid() {
1140 assert!(TimeoutMs::new(0).is_err());
1141 }
1142
1143 #[test]
1144 fn test_timeout_ms_default() {
1145 let timeout = TimeoutMs::default();
1146 assert_eq!(timeout.value(), 10000);
1147 }
1148
1149 #[test]
1151 fn test_retrieval_config_default() {
1152 let config = RetrievalConfig::default();
1153 assert_eq!(config.alpha.value(), 0.7);
1154 assert_eq!(config.beta.value(), 0.5);
1155 assert!(config.gamma_kind_boost.contains_key("code"));
1156 assert!(config.gamma_kind_boost.contains_key("text"));
1157 assert_eq!(config.gamma_kind_boost["code"], 0.1);
1158 assert_eq!(config.gamma_kind_boost["text"], 0.0);
1159 assert!(config.fusion.is_some());
1160 assert!(config.llm_rerank.is_some());
1161 }
1162
1163 #[test]
1164 fn test_retrieval_config_serialization() {
1165 let config = RetrievalConfig::default();
1166 let serialized = serde_json::to_string(&config).unwrap();
1167 let deserialized: RetrievalConfig = serde_json::from_str(&serialized).unwrap();
1168
1169 assert_eq!(deserialized.alpha.value(), config.alpha.value());
1170 assert_eq!(deserialized.beta.value(), config.beta.value());
1171 assert_eq!(deserialized.gamma_kind_boost, config.gamma_kind_boost);
1172 }
1173
1174 #[test]
1176 fn test_chunking_config_valid() {
1177 let config = ChunkingConfig {
1178 target_tokens: PositiveTokens::new(320).unwrap(),
1179 overlap: 64,
1180 method: "semantic".to_string(),
1181 };
1182 assert!(config.validate().is_ok());
1183 }
1184
1185 #[test]
1186 fn test_chunking_config_invalid_overlap() {
1187 let config = ChunkingConfig {
1188 target_tokens: PositiveTokens::new(100).unwrap(),
1189 overlap: 100, method: "semantic".to_string(),
1191 };
1192 assert!(config.validate().is_err());
1193
1194 let config = ChunkingConfig {
1195 target_tokens: PositiveTokens::new(100).unwrap(),
1196 overlap: -1, method: "semantic".to_string(),
1198 };
1199 assert!(config.validate().is_err());
1200 }
1201
1202 #[test]
1203 fn test_chunking_config_boundary_values() {
1204 let config = ChunkingConfig {
1206 target_tokens: PositiveTokens::new(100).unwrap(),
1207 overlap: 99,
1208 method: "semantic".to_string(),
1209 };
1210 assert!(config.validate().is_ok());
1211 }
1212
1213 #[test]
1215 fn test_timeouts_config_default() {
1216 let config = TimeoutsConfig::default();
1217 assert_eq!(config.hyde_ms.value(), 10000);
1218 assert_eq!(config.summarize_ms.value(), 10000);
1219 assert_eq!(config.ollama_connect_ms.value(), 500);
1220 assert!(config.ml_prediction_ms.is_some());
1221 assert_eq!(config.ml_prediction_ms.unwrap().value(), 2000);
1222 }
1223
1224 #[test]
1226 fn test_features_config_default() {
1227 let config = FeaturesConfig::default();
1228 assert!(config.enable_hyde);
1229 assert!(config.enable_summarization);
1230 assert!(config.enable_plan_selection);
1231 assert!(config.enable_query_understanding);
1232 assert!(!config.enable_ml_prediction); assert!(config.enable_state_tracking);
1234 }
1235
1236 #[test]
1238 fn test_query_understanding_config_default() {
1239 let config = QueryUnderstandingConfig::default();
1240 assert!(config.rewrite_enabled);
1241 assert!(config.decompose_enabled);
1242 assert_eq!(config.max_subqueries, 3);
1243 assert_eq!(config.llm_model, "llama3.2:1b");
1244 assert_relative_eq!(config.temperature, 0.1);
1245 }
1246
1247 #[test]
1249 fn test_prediction_service_config_disabled() {
1250 let config = PredictionServiceConfig {
1251 enabled: false,
1252 port: 0, ..Default::default()
1254 };
1255 assert!(config.validate().is_ok());
1256 }
1257
1258 #[test]
1259 fn test_prediction_service_config_enabled_valid() {
1260 let config = PredictionServiceConfig {
1261 enabled: true,
1262 host: "localhost".to_string(),
1263 port: 8080,
1264 timeout_ms: 5000,
1265 fallback_to_static: true,
1266 };
1267 assert!(config.validate().is_ok());
1268 }
1269
1270 #[test]
1271 fn test_prediction_service_config_enabled_invalid_port() {
1272 let config = PredictionServiceConfig {
1273 enabled: true,
1274 port: 0,
1275 ..Default::default()
1276 };
1277 assert!(config.validate().is_err());
1278 }
1279
1280 #[test]
1281 fn test_prediction_service_config_enabled_invalid_timeout() {
1282 let config = PredictionServiceConfig {
1283 enabled: true,
1284 timeout_ms: 0,
1285 ..Default::default()
1286 };
1287 assert!(config.validate().is_err());
1288 }
1289
1290 #[test]
1292 fn test_lens_config_disabled() {
1293 let config = LensConfig {
1294 enabled: false,
1295 base_url: "invalid-url".to_string(), ..Default::default()
1297 };
1298 assert!(config.validate().is_ok());
1299 }
1300
1301 #[test]
1302 fn test_lens_config_enabled_valid() {
1303 let config = LensConfig {
1304 enabled: true,
1305 base_url: "http://localhost:8081".to_string(),
1306 sla_recall_ms: 150,
1307 topic_fanout_k: 240,
1308 weight_cap: 0.4,
1309 ..Default::default()
1310 };
1311 assert!(config.validate().is_ok());
1312 }
1313
1314 #[test]
1315 fn test_lens_config_invalid_sla_recall() {
1316 let config = LensConfig {
1317 enabled: true,
1318 sla_recall_ms: 0,
1319 ..Default::default()
1320 };
1321 assert!(config.validate().is_err());
1322
1323 let config = LensConfig {
1324 enabled: true,
1325 sla_recall_ms: 1001,
1326 ..Default::default()
1327 };
1328 assert!(config.validate().is_err());
1329 }
1330
1331 #[test]
1332 fn test_lens_config_invalid_topic_fanout_k() {
1333 let config = LensConfig {
1334 enabled: true,
1335 topic_fanout_k: 0,
1336 ..Default::default()
1337 };
1338 assert!(config.validate().is_err());
1339
1340 let config = LensConfig {
1341 enabled: true,
1342 topic_fanout_k: 1001,
1343 ..Default::default()
1344 };
1345 assert!(config.validate().is_err());
1346 }
1347
1348 #[test]
1349 fn test_lens_config_invalid_weight_cap() {
1350 let config = LensConfig {
1351 enabled: true,
1352 weight_cap: 0.0,
1353 ..Default::default()
1354 };
1355 assert!(config.validate().is_err());
1356
1357 let config = LensConfig {
1358 enabled: true,
1359 weight_cap: 1.1,
1360 ..Default::default()
1361 };
1362 assert!(config.validate().is_err());
1363 }
1364
1365 #[test]
1366 fn test_lens_config_invalid_base_url() {
1367 let config = LensConfig {
1368 enabled: true,
1369 base_url: "not-a-url".to_string(),
1370 ..Default::default()
1371 };
1372 assert!(config.validate().is_err());
1373
1374 let config = LensConfig {
1375 enabled: true,
1376 base_url: "ftp://localhost:8081".to_string(),
1377 ..Default::default()
1378 };
1379 assert!(config.validate().is_err());
1380 }
1381
1382 #[test]
1384 fn test_auth_config_valid_modes() {
1385 let config = AuthConfig {
1386 mode: "passthrough".to_string(),
1387 ..Default::default()
1388 };
1389 assert!(config.validate().is_ok());
1390
1391 let config = AuthConfig {
1392 mode: "inject".to_string(),
1393 ..Default::default()
1394 };
1395 assert!(config.validate().is_ok());
1396 }
1397
1398 #[test]
1399 fn test_auth_config_invalid_mode() {
1400 let config = AuthConfig {
1401 mode: "invalid".to_string(),
1402 ..Default::default()
1403 };
1404 assert!(config.validate().is_err());
1405 }
1406
1407 #[test]
1409 fn test_provider_config_valid_urls() {
1410 let config = ProviderConfig {
1411 base_url: "https://api.openai.com".to_string(),
1412 };
1413 assert!(config.validate().is_ok());
1414
1415 let config = ProviderConfig {
1416 base_url: "http://localhost:8080".to_string(),
1417 };
1418 assert!(config.validate().is_ok());
1419 }
1420
1421 #[test]
1422 fn test_provider_config_invalid_urls() {
1423 let config = ProviderConfig {
1424 base_url: "not-a-url".to_string(),
1425 };
1426 assert!(config.validate().is_err());
1427
1428 let config = ProviderConfig {
1429 base_url: "ftp://example.com".to_string(),
1430 };
1431 assert!(config.validate().is_err());
1432 }
1433
1434 #[test]
1436 fn test_rewrite_config_valid() {
1437 let config = RewriteConfig {
1438 enabled: true,
1439 max_request_bytes: 1_000_000,
1440 prelude_system: Some("System message".to_string()),
1441 };
1442 assert!(config.validate().is_ok());
1443 }
1444
1445 #[test]
1446 fn test_rewrite_config_invalid_max_bytes() {
1447 let config = RewriteConfig {
1448 enabled: true,
1449 max_request_bytes: 0,
1450 prelude_system: None,
1451 };
1452 assert!(config.validate().is_err());
1453 }
1454
1455 #[test]
1457 fn test_security_config_valid_providers() {
1458 let config = SecurityConfig {
1459 allowed_providers: vec!["openai".to_string(), "anthropic".to_string()],
1460 };
1461 assert!(config.validate().is_ok());
1462
1463 let config = SecurityConfig {
1464 allowed_providers: vec!["openai".to_string()],
1465 };
1466 assert!(config.validate().is_ok());
1467 }
1468
1469 #[test]
1470 fn test_security_config_empty_providers() {
1471 let config = SecurityConfig {
1472 allowed_providers: vec![],
1473 };
1474 assert!(config.validate().is_err());
1475 }
1476
1477 #[test]
1478 fn test_security_config_invalid_providers() {
1479 let config = SecurityConfig {
1480 allowed_providers: vec!["openai".to_string(), "invalid".to_string()],
1481 };
1482 assert!(config.validate().is_err());
1483 }
1484
1485 #[test]
1487 fn test_proxy_timeouts_config_valid() {
1488 let config = ProxyTimeoutsConfig {
1489 connect_ms: 5000,
1490 read_ms: 60000,
1491 };
1492 assert!(config.validate().is_ok());
1493 }
1494
1495 #[test]
1496 fn test_proxy_timeouts_config_invalid() {
1497 let config = ProxyTimeoutsConfig {
1498 connect_ms: 0,
1499 read_ms: 60000,
1500 };
1501 assert!(config.validate().is_err());
1502
1503 let config = ProxyTimeoutsConfig {
1504 connect_ms: 5000,
1505 read_ms: 0,
1506 };
1507 assert!(config.validate().is_err());
1508 }
1509
1510 #[test]
1512 fn test_proxy_logging_config_valid_levels() {
1513 for level in ["off", "basic", "detailed", "debug"] {
1514 let config = ProxyLoggingConfig {
1515 level: level.to_string(),
1516 ..Default::default()
1517 };
1518 assert!(config.validate().is_ok());
1519 }
1520 }
1521
1522 #[test]
1523 fn test_proxy_logging_config_invalid_level() {
1524 let config = ProxyLoggingConfig {
1525 level: "invalid".to_string(),
1526 ..Default::default()
1527 };
1528 assert!(config.validate().is_err());
1529 }
1530
1531 #[test]
1532 fn test_proxy_logging_config_valid_destinations() {
1533 for dest in ["stdout", "file", "structured"] {
1534 let config = ProxyLoggingConfig {
1535 destination: dest.to_string(),
1536 file_path: if dest == "file" { Some("/tmp/test.log".to_string()) } else { None },
1537 ..Default::default()
1538 };
1539 assert!(config.validate().is_ok());
1540 }
1541 }
1542
1543 #[test]
1544 fn test_proxy_logging_config_file_destination_missing_path() {
1545 let config = ProxyLoggingConfig {
1546 destination: "file".to_string(),
1547 file_path: None,
1548 ..Default::default()
1549 };
1550 assert!(config.validate().is_err());
1551 }
1552
1553 #[test]
1554 fn test_proxy_logging_config_invalid_regex_patterns() {
1555 let config = ProxyLoggingConfig {
1556 redaction_patterns: vec!["[invalid regex".to_string()],
1557 ..Default::default()
1558 };
1559 assert!(config.validate().is_err());
1560 }
1561
1562 #[test]
1563 fn test_proxy_logging_config_helper_methods() {
1564 let config = ProxyLoggingConfig {
1565 level: "off".to_string(),
1566 include_payloads: true,
1567 ..Default::default()
1568 };
1569 assert!(!config.should_log());
1570 assert!(!config.should_log_payloads());
1571 assert!(!config.should_log_debug_info());
1572
1573 let config = ProxyLoggingConfig {
1574 level: "detailed".to_string(),
1575 include_payloads: true,
1576 ..Default::default()
1577 };
1578 assert!(config.should_log());
1579 assert!(config.should_log_payloads());
1580 assert!(!config.should_log_debug_info());
1581
1582 let config = ProxyLoggingConfig {
1583 level: "debug".to_string(),
1584 include_payloads: true,
1585 ..Default::default()
1586 };
1587 assert!(config.should_log());
1588 assert!(config.should_log_payloads());
1589 assert!(config.should_log_debug_info());
1590 }
1591
1592 #[test]
1594 fn test_inject_config_default() {
1595 let config = InjectConfig::default();
1596 let openai_key = std::env::var("OPENAI_API_KEY").ok();
1598 let anthropic_key = std::env::var("ANTHROPIC_API_KEY").ok();
1599 assert_eq!(config.openai_api_key, openai_key);
1600 assert_eq!(config.anthropic_api_key, anthropic_key);
1601 }
1602
1603 #[test]
1605 fn test_proxy_config_validation_cascade() {
1606 let mut config = ProxyConfig::default();
1607 config.enabled = true;
1608
1609 assert!(config.validate().is_ok());
1611
1612 config.security.allowed_providers = vec![];
1614 assert!(config.validate().is_err());
1615 }
1616
1617 #[test]
1619 fn test_lethe_config_default() {
1620 let config = LetheConfig::default();
1621 assert_eq!(config.version, "1.0.0");
1622 assert!(config.description.is_some());
1623 assert!(config.features.is_some());
1624 assert!(config.proxy.is_some());
1625 assert!(config.lens.is_some());
1626
1627 assert!(config.validate().is_ok());
1629 }
1630
1631 #[test]
1632 fn test_lethe_config_validation_cascade() {
1633 let mut config = LetheConfig::default();
1634
1635 config.chunking.overlap = config.chunking.target_tokens.value();
1637 assert!(config.validate().is_err());
1638
1639 config.chunking.overlap = 0;
1641 if let Some(ml) = &mut config.ml {
1642 if let Some(service) = &mut ml.prediction_service {
1643 service.enabled = true;
1644 service.port = 0;
1645 }
1646 }
1647 assert!(config.validate().is_err());
1648
1649 if let Some(ml) = &mut config.ml {
1651 if let Some(service) = &mut ml.prediction_service {
1652 service.port = 8080;
1653 }
1654 }
1655 if let Some(lens) = &mut config.lens {
1656 lens.enabled = true;
1657 lens.base_url = "invalid".to_string();
1658 }
1659 assert!(config.validate().is_err());
1660
1661 if let Some(lens) = &mut config.lens {
1663 lens.base_url = "http://localhost:8081".to_string();
1664 }
1665 if let Some(proxy) = &mut config.proxy {
1666 proxy.security.allowed_providers = vec![];
1667 }
1668 assert!(config.validate().is_err());
1669 }
1670
1671 #[test]
1673 fn test_config_file_serialization_roundtrip() {
1674 let original_config = LetheConfig::default();
1675
1676 let mut temp_file = NamedTempFile::new().unwrap();
1677 let temp_path = temp_file.path();
1678
1679 original_config.to_file(temp_path).unwrap();
1681
1682 let loaded_config = LetheConfig::from_file(temp_path).unwrap();
1684
1685 assert_eq!(loaded_config.version, original_config.version);
1687 assert_eq!(loaded_config.description, original_config.description);
1688 assert_eq!(loaded_config.retrieval.alpha.value(), original_config.retrieval.alpha.value());
1689 assert_eq!(loaded_config.chunking.target_tokens.value(), original_config.chunking.target_tokens.value());
1690 }
1691
1692 #[test]
1693 fn test_config_file_invalid_json() {
1694 let mut temp_file = NamedTempFile::new().unwrap();
1695 writeln!(temp_file, "{{invalid json}}").unwrap();
1696
1697 let result = LetheConfig::from_file(temp_file.path());
1698 assert!(result.is_err());
1699 match result.unwrap_err() {
1700 LetheError::Config { .. } => {}, _ => panic!("Expected Config error"),
1702 }
1703 }
1704
1705 #[test]
1706 fn test_config_file_nonexistent() {
1707 let result = LetheConfig::from_file(std::path::Path::new("/nonexistent/path"));
1708 assert!(result.is_err());
1709 match result.unwrap_err() {
1710 LetheError::Config { .. } => {}, _ => panic!("Expected Config error"),
1712 }
1713 }
1714
1715 #[test]
1717 fn test_config_merge_basic() {
1718 let mut base_config = LetheConfig::default();
1719 base_config.version = "1.0.0".to_string();
1720 base_config.description = Some("Base".to_string());
1721
1722 let other_config = LetheConfig {
1723 version: "2.0.0".to_string(),
1724 description: Some("Other".to_string()),
1725 retrieval: RetrievalConfig {
1726 alpha: Alpha::new(0.8).unwrap(),
1727 ..Default::default()
1728 },
1729 ..Default::default()
1730 };
1731
1732 base_config.merge_with(&other_config);
1733
1734 assert_eq!(base_config.version, "2.0.0");
1735 assert_eq!(base_config.description, Some("Other".to_string()));
1736 assert_eq!(base_config.retrieval.alpha.value(), 0.8);
1737 }
1738
1739 #[test]
1740 fn test_config_merge_none_values() {
1741 let mut base_config = LetheConfig {
1742 features: Some(FeaturesConfig::default()),
1743 ..Default::default()
1744 };
1745
1746 let other_config = LetheConfig {
1747 features: None,
1748 ..Default::default()
1749 };
1750
1751 base_config.merge_with(&other_config);
1752
1753 assert!(base_config.features.is_some());
1755 }
1756
1757 #[test]
1759 fn test_config_builder() {
1760 let config = LetheConfig::builder()
1761 .version("test-version")
1762 .description("test-description")
1763 .features(FeaturesConfig {
1764 enable_hyde: false,
1765 ..Default::default()
1766 })
1767 .build()
1768 .unwrap();
1769
1770 assert_eq!(config.version, "test-version");
1771 assert_eq!(config.description, Some("test-description".to_string()));
1772 assert!(!config.features.unwrap().enable_hyde);
1773 }
1774
1775 #[test]
1776 fn test_config_builder_invalid() {
1777 let result = LetheConfig::builder()
1778 .chunking(ChunkingConfig {
1779 target_tokens: PositiveTokens::new(100).unwrap(),
1780 overlap: 100, method: "semantic".to_string(),
1782 })
1783 .build();
1784
1785 assert!(result.is_err());
1786 }
1787
1788 proptest! {
1790 #[test]
1791 fn test_alpha_proptest(value in 0.0_f64..=1.0) {
1792 let alpha = Alpha::new(value);
1793 assert!(alpha.is_ok());
1794 assert_eq!(alpha.unwrap().value(), value);
1795 }
1796
1797 #[test]
1798 fn test_beta_proptest(value in 0.0_f64..=1.0) {
1799 let beta = Beta::new(value);
1800 assert!(beta.is_ok());
1801 assert_eq!(beta.unwrap().value(), value);
1802 }
1803
1804 #[test]
1805 fn test_positive_tokens_proptest(value in 1_i32..10000) {
1806 let tokens = PositiveTokens::new(value);
1807 assert!(tokens.is_ok());
1808 assert_eq!(tokens.unwrap().value(), value);
1809 }
1810
1811 #[test]
1812 fn test_timeout_ms_proptest(value in 1_u64..1000000) {
1813 let timeout = TimeoutMs::new(value);
1814 assert!(timeout.is_ok());
1815 assert_eq!(timeout.unwrap().value(), value);
1816 }
1817
1818 #[test]
1819 fn test_chunking_config_valid_overlap_proptest(
1820 target_tokens in 1_i32..1000,
1821 overlap_ratio in 0.0_f64..0.99
1822 ) {
1823 let overlap = (target_tokens as f64 * overlap_ratio) as i32;
1824 let config = ChunkingConfig {
1825 target_tokens: PositiveTokens::new(target_tokens).unwrap(),
1826 overlap,
1827 method: "semantic".to_string(),
1828 };
1829 assert!(config.validate().is_ok());
1830 }
1831
1832 #[test]
1833 fn test_lens_config_valid_ranges_proptest(
1834 sla_recall in 1_u64..1000,
1835 topic_fanout_k in 1_i32..1000,
1836 weight_cap in 0.01_f64..1.0
1837 ) {
1838 let config = LensConfig {
1839 enabled: true,
1840 base_url: "http://localhost:8081".to_string(),
1841 sla_recall_ms: sla_recall,
1842 topic_fanout_k,
1843 weight_cap,
1844 ..Default::default()
1845 };
1846 assert!(config.validate().is_ok());
1847 }
1848 }
1849
1850 #[test]
1852 fn test_config_with_minimal_values() {
1853 let config = LetheConfig {
1854 version: "0.0.1".to_string(),
1855 description: None,
1856 retrieval: RetrievalConfig {
1857 alpha: Alpha::new(0.0).unwrap(),
1858 beta: Beta::new(0.0).unwrap(),
1859 gamma_kind_boost: HashMap::new(),
1860 fusion: None,
1861 llm_rerank: None,
1862 },
1863 chunking: ChunkingConfig {
1864 target_tokens: PositiveTokens::new(1).unwrap(),
1865 overlap: 0,
1866 method: "simple".to_string(),
1867 },
1868 timeouts: TimeoutsConfig {
1869 hyde_ms: TimeoutMs::new(1).unwrap(),
1870 summarize_ms: TimeoutMs::new(1).unwrap(),
1871 ollama_connect_ms: TimeoutMs::new(1).unwrap(),
1872 ml_prediction_ms: None,
1873 },
1874 features: None,
1875 query_understanding: None,
1876 ml: None,
1877 development: None,
1878 lens: None,
1879 proxy: None,
1880 };
1881
1882 assert!(config.validate().is_ok());
1883 }
1884
1885 #[test]
1886 fn test_config_with_maximal_values() {
1887 let config = LetheConfig {
1888 version: "999.999.999".to_string(),
1889 description: Some("Maximum configuration".to_string()),
1890 retrieval: RetrievalConfig {
1891 alpha: Alpha::new(1.0).unwrap(),
1892 beta: Beta::new(1.0).unwrap(),
1893 gamma_kind_boost: {
1894 let mut map = HashMap::new();
1895 map.insert("code".to_string(), 1.0);
1896 map.insert("text".to_string(), 1.0);
1897 map.insert("markdown".to_string(), 1.0);
1898 map
1899 },
1900 fusion: Some(FusionConfig { dynamic: true }),
1901 llm_rerank: Some(LlmRerankConfig {
1902 use_llm: true,
1903 llm_budget_ms: 10000,
1904 llm_model: "gpt-4".to_string(),
1905 contradiction_enabled: true,
1906 contradiction_penalty: 1.0,
1907 }),
1908 },
1909 chunking: ChunkingConfig {
1910 target_tokens: PositiveTokens::new(i32::MAX).unwrap(),
1911 overlap: i32::MAX - 1,
1912 method: "advanced_semantic_ai_powered".to_string(),
1913 },
1914 timeouts: TimeoutsConfig {
1915 hyde_ms: TimeoutMs::new(u64::MAX).unwrap(),
1916 summarize_ms: TimeoutMs::new(u64::MAX).unwrap(),
1917 ollama_connect_ms: TimeoutMs::new(u64::MAX).unwrap(),
1918 ml_prediction_ms: Some(TimeoutMs::new(u64::MAX).unwrap()),
1919 },
1920 features: Some(FeaturesConfig {
1921 enable_hyde: true,
1922 enable_summarization: true,
1923 enable_plan_selection: true,
1924 enable_query_understanding: true,
1925 enable_ml_prediction: true,
1926 enable_state_tracking: true,
1927 }),
1928 query_understanding: Some(QueryUnderstandingConfig {
1929 rewrite_enabled: true,
1930 decompose_enabled: true,
1931 max_subqueries: i32::MAX,
1932 llm_model: "custom-model-ultra-pro".to_string(),
1933 temperature: 2.0, }),
1935 ml: Some(MlConfig {
1936 prediction_service: Some(PredictionServiceConfig {
1937 enabled: true,
1938 host: "production.ml.service.internal".to_string(),
1939 port: 65535,
1940 timeout_ms: u64::MAX,
1941 fallback_to_static: true,
1942 }),
1943 models: Some(ModelsConfig {
1944 plan_selector: Some("advanced_model_v3.joblib".to_string()),
1945 fusion_weights: Some("neural_fusion_model.pkl".to_string()),
1946 feature_extractor: Some("transformer_extractor.json".to_string()),
1947 }),
1948 }),
1949 development: Some(DevelopmentConfig {
1950 debug_enabled: true,
1951 profiling_enabled: true,
1952 log_level: "trace".to_string(),
1953 }),
1954 lens: Some(LensConfig {
1955 enabled: true,
1956 base_url: "https://lens.production.service".to_string(),
1957 connect_timeout_ms: u64::MAX,
1958 request_timeout_ms: u64::MAX,
1959 sla_recall_ms: 999, topic_fanout_k: 999, weight_cap: 0.99, max_tokens_per_response: i32::MAX,
1963 mode: "ultra".to_string(),
1964 dpp_rank: i32::MAX,
1965 enable_facility_location: true,
1966 enable_log_det_dpp: true,
1967 lambda_multiplier: f64::MAX,
1968 mu_multiplier: f64::MAX,
1969 lens_tokens_cap: i32::MAX,
1970 }),
1971 proxy: Some(ProxyConfig {
1972 enabled: true,
1973 openai: ProviderConfig {
1974 base_url: "https://api.openai.com/v2/ultra".to_string(),
1975 },
1976 anthropic: ProviderConfig {
1977 base_url: "https://api.anthropic.com/v2/pro".to_string(),
1978 },
1979 auth: AuthConfig {
1980 mode: "inject".to_string(),
1981 inject: InjectConfig {
1982 openai_api_key: Some("sk-test-key-123".to_string()),
1983 anthropic_api_key: Some("ant-key-456".to_string()),
1984 },
1985 },
1986 rewrite: RewriteConfig {
1987 enabled: true,
1988 max_request_bytes: u64::MAX,
1989 prelude_system: Some("Advanced system prompt with maximum customization".to_string()),
1990 },
1991 security: SecurityConfig {
1992 allowed_providers: vec!["openai".to_string(), "anthropic".to_string()],
1993 },
1994 timeouts: ProxyTimeoutsConfig {
1995 connect_ms: u64::MAX,
1996 read_ms: u64::MAX,
1997 },
1998 logging: ProxyLoggingConfig {
1999 level: "debug".to_string(),
2000 include_payloads: true,
2001 redact_sensitive: true,
2002 redaction_patterns: vec![
2003 "sk-[A-Za-z0-9]{48}".to_string(),
2004 "Bearer\\s+[A-Za-z0-9._-]+".to_string(),
2005 ".*secret.*".to_string(),
2006 ],
2007 destination: "structured".to_string(),
2008 file_path: Some("/var/log/lethe/proxy-debug.log".to_string()),
2009 enable_correlation_ids: true,
2010 log_performance_metrics: true,
2011 },
2012 }),
2013 };
2014
2015 assert!(config.validate().is_ok());
2016 }
2017
2018 #[test]
2019 fn test_default_true_helper() {
2020 assert_eq!(default_true(), true);
2022
2023 let features = FeaturesConfig::default();
2025 assert!(features.enable_hyde);
2026 assert!(features.enable_summarization);
2027 assert!(features.enable_plan_selection);
2028 assert!(features.enable_query_understanding);
2029 assert!(!features.enable_ml_prediction); assert!(features.enable_state_tracking);
2031 }
2032}