lethe_core_rust/
config.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use crate::error::{LetheError, Result};
4
5#[cfg(test)]
6use regex;
7
8/// Newtype for alpha values ensuring 0.0 <= alpha <= 1.0
9#[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) // Safe default
29    }
30}
31
32/// Newtype for beta values ensuring 0.0 <= beta <= 1.0
33#[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) // Safe default
53    }
54}
55
56/// Newtype for positive token counts
57#[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) // Safe default
77    }
78}
79
80/// Newtype for timeout values in milliseconds
81#[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) // Safe default
101    }
102}
103
104/// Main configuration structure
105#[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/// Retrieval algorithm configuration
121#[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/// Fusion configuration for dynamic parameter adjustment
153#[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/// LLM reranking configuration
166#[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/// Text chunking configuration
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct ChunkingConfig {
199    pub target_tokens: PositiveTokens,
200    pub overlap: i32, // Can be 0, validated relative to target_tokens
201    #[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/// Operation timeout configuration
232#[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/// Feature toggles
259#[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/// Query understanding configuration
291#[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/// Machine learning configuration
321#[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/// ML prediction service configuration
339#[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/// ML models configuration
390#[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/// Development-specific configuration
421#[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/// Lens integration configuration
444#[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/// Proxy configuration for reverse-proxy functionality
544#[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/// Provider-specific configuration (OpenAI, Anthropic)
595#[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/// Authentication configuration for proxy
632#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct AuthConfig {
634    #[serde(default = "default_auth_mode")]
635    pub mode: String, // "passthrough" or "inject"
636    #[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/// API key injection configuration
666#[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/// Request rewriting configuration
682#[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 // 2MB
693}
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/// Security configuration for proxy
718#[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/// Proxy-specific timeout configuration
758#[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 // 5 seconds
768}
769
770fn default_proxy_read_timeout() -> u64 {
771    60000 // 60 seconds
772}
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/// Proxy logging configuration for debugging and analysis
802#[derive(Debug, Clone, Serialize, Deserialize)]
803pub struct ProxyLoggingConfig {
804    #[serde(default = "default_proxy_log_level")]
805    pub level: String, // "off", "basic", "detailed", "debug"
806    #[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, // "stdout", "file", "structured"
814    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(),        // OpenAI API keys
837                "Bearer\\s+[A-Za-z0-9._-]+".to_string(), // Bearer tokens
838                "x-api-key:\\s*[A-Za-z0-9._-]+".to_string(), // Anthropic API keys
839                "\"password\":\\s*\"[^\"]*\"".to_string(),   // Password fields
840                "\"api_key\":\\s*\"[^\"]*\"".to_string(),    // Generic API key fields
841            ],
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        // Validate regex patterns
876        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    /// Load configuration from file
921    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    /// Save configuration to file
933    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    /// Validate configuration values
941    pub fn validate(&self) -> Result<()> {
942        // Alpha and Beta are now validated at construction time via newtype wrappers
943        
944        // Validate chunking configuration
945        self.chunking.validate()?;
946        
947        // Timeout validation is now handled by TimeoutMs newtype
948        
949        // Validate ML service configuration
950        if let Some(ml) = &self.ml {
951            if let Some(service) = &ml.prediction_service {
952                service.validate()?;
953            }
954        }
955        
956        // Validate Lens configuration
957        if let Some(lens) = &self.lens {
958            lens.validate()?;
959        }
960        
961        // Validate Proxy configuration
962        if let Some(proxy) = &self.proxy {
963            proxy.validate()?;
964        }
965        
966        Ok(())
967    }
968
969    /// Merge with another configuration, preferring other's values
970    pub fn merge_with(&mut self, other: &Self) {
971        self.version = other.version.clone();
972        
973        // Use Option::or to prefer other's value when it exists
974        if other.description.is_some() {
975            self.description = other.description.clone();
976        }
977        
978        // Always merge core configs (they should always exist)
979        self.retrieval = other.retrieval.clone();
980        self.chunking = other.chunking.clone();
981        self.timeouts = other.timeouts.clone();
982        
983        // Use or_else for optional configs to maintain existing values when other is None
984        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    /// Builder pattern for creating configurations
993    pub fn builder() -> LetheConfigBuilder {
994        LetheConfigBuilder::default()
995    }
996}
997
998/// Builder for LetheConfig to make complex configurations easier
999#[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    // Alpha tests - bounded value type
1047    #[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    // Beta tests - similar to Alpha
1083    #[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    // PositiveTokens tests
1106    #[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    // TimeoutMs tests
1129    #[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    // RetrievalConfig tests
1150    #[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    // ChunkingConfig tests with validation
1175    #[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, // >= target_tokens
1190            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, // negative
1197            method: "semantic".to_string(),
1198        };
1199        assert!(config.validate().is_err());
1200    }
1201    
1202    #[test]
1203    fn test_chunking_config_boundary_values() {
1204        // overlap = target_tokens - 1 should be valid
1205        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    // TimeoutsConfig tests
1214    #[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    // FeaturesConfig tests
1225    #[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); // defaults to false
1233        assert!(config.enable_state_tracking);
1234    }
1235
1236    // QueryUnderstandingConfig tests
1237    #[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    // PredictionServiceConfig tests with validation
1248    #[test]
1249    fn test_prediction_service_config_disabled() {
1250        let config = PredictionServiceConfig {
1251            enabled: false,
1252            port: 0, // Invalid port, but should pass validation when disabled
1253            ..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    // LensConfig tests with comprehensive validation
1291    #[test]
1292    fn test_lens_config_disabled() {
1293        let config = LensConfig {
1294            enabled: false,
1295            base_url: "invalid-url".to_string(), // Invalid, but should pass when disabled
1296            ..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    // AuthConfig tests
1383    #[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    // ProviderConfig tests
1408    #[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    // RewriteConfig tests
1435    #[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    // SecurityConfig tests
1456    #[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    // ProxyTimeoutsConfig tests
1486    #[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    // ProxyLoggingConfig tests - comprehensive validation
1511    #[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    // InjectConfig tests
1593    #[test]
1594    fn test_inject_config_default() {
1595        let config = InjectConfig::default();
1596        // Should load from environment variables if present
1597        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    // ProxyConfig comprehensive tests
1604    #[test]
1605    fn test_proxy_config_validation_cascade() {
1606        let mut config = ProxyConfig::default();
1607        config.enabled = true;
1608        
1609        // Should validate all sub-components
1610        assert!(config.validate().is_ok());
1611        
1612        // Break one sub-component
1613        config.security.allowed_providers = vec![];
1614        assert!(config.validate().is_err());
1615    }
1616
1617    // Full LetheConfig integration tests
1618    #[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        // Validation should pass for default config
1628        assert!(config.validate().is_ok());
1629    }
1630    
1631    #[test]
1632    fn test_lethe_config_validation_cascade() {
1633        let mut config = LetheConfig::default();
1634        
1635        // Break chunking validation
1636        config.chunking.overlap = config.chunking.target_tokens.value();
1637        assert!(config.validate().is_err());
1638        
1639        // Fix chunking, break ML service
1640        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        // Fix ML service, break Lens
1650        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        // Fix Lens, break Proxy
1662        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    // File I/O tests
1672    #[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        // Save to file
1680        original_config.to_file(temp_path).unwrap();
1681        
1682        // Load from file
1683        let loaded_config = LetheConfig::from_file(temp_path).unwrap();
1684        
1685        // Compare key fields (can't do direct equality due to complexity)
1686        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 { .. } => {}, // Expected
1701            _ => 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 { .. } => {}, // Expected
1711            _ => panic!("Expected Config error"),
1712        }
1713    }
1714
1715    // Config merging tests
1716    #[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        // Should keep original features since other is None
1754        assert!(base_config.features.is_some());
1755    }
1756
1757    // Builder pattern tests
1758    #[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, // Invalid: overlap >= target_tokens
1781                method: "semantic".to_string(),
1782            })
1783            .build();
1784        
1785        assert!(result.is_err());
1786    }
1787
1788    // Property-based tests using proptest
1789    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    // Edge case and stress tests
1851    #[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, // Above normal range but not validated
1934            }),
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, // Max valid value
1960                topic_fanout_k: 999, // Max valid value
1961                weight_cap: 0.99, // Max valid value
1962                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        // Test the default_true helper function
2021        assert_eq!(default_true(), true);
2022        
2023        // Test that FeaturesConfig uses this default
2024        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); // This defaults to false
2030        assert!(features.enable_state_tracking);
2031    }
2032}