1#[derive(Debug, Clone, Serialize, Deserialize)]
2pub struct ServerConfig {
3 #[serde(default = "default_port")]
4 pub port: u16,
5 #[serde(default = "default_bind")]
6 pub bind: String,
7 #[serde(default, skip_serializing)]
8 pub api_key: Option<String>,
9 #[serde(default = "default_log_dir")]
10 pub log_dir: PathBuf,
11 #[serde(default = "default_log_max_days")]
12 pub log_max_days: u32,
13 #[serde(default = "default_rate_limit_requests")]
14 pub rate_limit_requests: u32,
15 #[serde(default = "default_rate_limit_window_secs")]
16 pub rate_limit_window_secs: u64,
17 #[serde(default = "default_per_ip_rate_limit_requests")]
18 pub per_ip_rate_limit_requests: u32,
19 #[serde(default = "default_per_actor_rate_limit_requests")]
20 pub per_actor_rate_limit_requests: u32,
21 #[serde(default)]
22 pub trusted_proxy_cidrs: Vec<String>,
23 #[serde(default = "default_cron_max_concurrency")]
24 pub cron_max_concurrency: u8,
25}
26
27impl Default for ServerConfig {
28 fn default() -> Self {
29 Self {
30 port: default_port(),
31 bind: default_bind(),
32 api_key: None,
33 log_dir: default_log_dir(),
34 log_max_days: default_log_max_days(),
35 rate_limit_requests: default_rate_limit_requests(),
36 rate_limit_window_secs: default_rate_limit_window_secs(),
37 per_ip_rate_limit_requests: default_per_ip_rate_limit_requests(),
38 per_actor_rate_limit_requests: default_per_actor_rate_limit_requests(),
39 trusted_proxy_cidrs: Vec::new(),
40 cron_max_concurrency: default_cron_max_concurrency(),
41 }
42 }
43}
44
45fn default_rate_limit_requests() -> u32 {
46 100
47}
48
49fn default_rate_limit_window_secs() -> u64 {
50 60
51}
52
53fn default_per_ip_rate_limit_requests() -> u32 {
54 300
55}
56
57fn default_per_actor_rate_limit_requests() -> u32 {
58 200
59}
60
61fn default_cron_max_concurrency() -> u8 {
62 8
63}
64
65fn default_port() -> u16 {
66 18789
67}
68
69fn default_bind() -> String {
70 "localhost".into()
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ContextBudgetConfig {
77 #[serde(default = "default_budget_l0")]
79 pub l0: usize,
80 #[serde(default = "default_budget_l1")]
82 pub l1: usize,
83 #[serde(default = "default_budget_l2")]
85 pub l2: usize,
86 #[serde(default = "default_budget_l3")]
88 pub l3: usize,
89 #[serde(default = "default_channel_minimum")]
91 pub channel_minimum: String,
92 #[serde(default = "default_soul_max_context_pct")]
95 pub soul_max_context_pct: f64,
96}
97
98impl ContextBudgetConfig {
99 pub const MIN_BUDGET: usize = 512;
101 pub const MAX_BUDGET: usize = 2_000_000;
103
104 pub fn validate_and_clamp(&mut self) {
107 self.l0 = self.l0.clamp(Self::MIN_BUDGET, Self::MAX_BUDGET);
108 self.l1 = self.l1.clamp(Self::MIN_BUDGET, Self::MAX_BUDGET);
109 self.l2 = self.l2.clamp(Self::MIN_BUDGET, Self::MAX_BUDGET);
110 self.l3 = self.l3.clamp(Self::MIN_BUDGET, Self::MAX_BUDGET);
111 self.soul_max_context_pct = self.soul_max_context_pct.clamp(0.05, 1.0);
112 self.l1 = self.l1.max(self.l0);
114 self.l2 = self.l2.max(self.l1);
115 self.l3 = self.l3.max(self.l2);
116 }
117
118 pub fn soul_token_cap(&self, total_budget: usize) -> usize {
122 if total_budget == 0 {
123 return 0;
124 }
125 (total_budget as f64 * self.soul_max_context_pct) as usize
126 }
127}
128
129impl Default for ContextBudgetConfig {
130 fn default() -> Self {
131 Self {
132 l0: default_budget_l0(),
133 l1: default_budget_l1(),
134 l2: default_budget_l2(),
135 l3: default_budget_l3(),
136 channel_minimum: default_channel_minimum(),
137 soul_max_context_pct: default_soul_max_context_pct(),
138 }
139 }
140}
141
142fn default_budget_l0() -> usize { 4_000 }
143fn default_budget_l1() -> usize { 8_000 }
144fn default_budget_l2() -> usize { 16_000 }
145fn default_budget_l3() -> usize { 32_000 }
146fn default_channel_minimum() -> String { "L1".into() }
147fn default_soul_max_context_pct() -> f64 { 0.4 }
148
149#[cfg(test)]
150mod context_budget_tests {
151 use super::ContextBudgetConfig;
152
153 #[test]
154 fn context_budget_validates_and_clamps() {
155 let mut cfg = ContextBudgetConfig {
156 l0: 0,
157 l1: 0,
158 l2: 0,
159 l3: 999_999_999,
160 channel_minimum: "L1".into(),
161 soul_max_context_pct: 2.5,
162 };
163 cfg.validate_and_clamp();
164
165 assert!(cfg.l0 >= ContextBudgetConfig::MIN_BUDGET, "l0 must be >= MIN_BUDGET");
166 assert!(cfg.l3 <= ContextBudgetConfig::MAX_BUDGET, "l3 must be <= MAX_BUDGET");
167 assert!(cfg.soul_max_context_pct <= 1.0, "soul_max_context_pct must be <= 1.0");
168 assert!(cfg.soul_max_context_pct >= 0.05, "soul_max_context_pct must be >= 0.05");
169 assert!(cfg.l0 <= cfg.l1);
171 assert!(cfg.l1 <= cfg.l2);
172 assert!(cfg.l2 <= cfg.l3);
173 }
174
175 #[test]
176 fn context_budget_validate_preserves_valid_values() {
177 let mut cfg = ContextBudgetConfig::default();
178 let before = cfg.clone();
179 cfg.validate_and_clamp();
180
181 assert_eq!(cfg.l0, before.l0);
182 assert_eq!(cfg.l1, before.l1);
183 assert_eq!(cfg.l2, before.l2);
184 assert_eq!(cfg.l3, before.l3);
185 assert_eq!(cfg.soul_max_context_pct, before.soul_max_context_pct);
186 }
187
188 #[test]
189 fn soul_token_cap_safe_with_zero_budget() {
190 let cfg = ContextBudgetConfig::default();
191 let cap = cfg.soul_token_cap(0);
192 assert_eq!(cap, 0, "soul_token_cap must return 0 for zero total_budget");
193 }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct DatabaseConfig {
198 #[serde(default = "default_db_path")]
199 pub path: PathBuf,
200}
201
202impl Default for DatabaseConfig {
203 fn default() -> Self {
204 Self {
205 path: default_db_path(),
206 }
207 }
208}
209
210fn default_db_path() -> PathBuf {
211 dirs_next().join("state.db")
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ModelsConfig {
216 pub primary: String,
217 #[serde(default)]
218 pub fallbacks: Vec<String>,
219 #[serde(default)]
220 pub routing: RoutingConfig,
221 #[serde(default)]
222 pub model_overrides: HashMap<String, ModelOverride>,
223 #[serde(default)]
224 pub stream_by_default: bool,
225 #[serde(default)]
226 pub tiered_inference: TieredInferenceConfig,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct TieredInferenceConfig {
231 #[serde(default)]
232 pub enabled: bool,
233 #[serde(default = "default_confidence_floor")]
234 pub confidence_floor: f64,
235 #[serde(default = "default_escalation_latency_ms")]
236 pub escalation_latency_budget_ms: u64,
237}
238
239fn default_confidence_floor() -> f64 {
240 0.6
241}
242fn default_escalation_latency_ms() -> u64 {
243 3000
244}
245
246impl Default for TieredInferenceConfig {
247 fn default() -> Self {
248 Self {
249 enabled: false,
250 confidence_floor: default_confidence_floor(),
251 escalation_latency_budget_ms: default_escalation_latency_ms(),
252 }
253 }
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct RoutingConfig {
258 #[serde(default = "default_routing_mode")]
259 pub mode: String,
260 #[serde(default = "default_confidence_threshold")]
261 pub confidence_threshold: f64,
262 #[serde(default = "default_true")]
263 pub local_first: bool,
264 #[serde(default)]
265 pub cost_aware: bool,
266 #[serde(default = "default_estimated_output_tokens")]
267 pub estimated_output_tokens: u32,
268 #[serde(default)]
272 pub accuracy_floor: f64,
273 #[serde(default = "default_accuracy_min_obs")]
275 pub accuracy_min_obs: usize,
276 #[serde(default)]
281 pub cost_weight: Option<f64>,
282 #[serde(default)]
286 pub canary_model: Option<String>,
287 #[serde(default)]
290 pub canary_fraction: f64,
291 #[serde(default)]
295 pub blocked_models: Vec<String>,
296 #[serde(default = "default_per_provider_timeout")]
300 pub per_provider_timeout_seconds: u64,
301 #[serde(default = "default_max_total_inference")]
305 pub max_total_inference_seconds: u64,
306 #[serde(default = "default_max_fallback_attempts")]
308 pub max_fallback_attempts: usize,
309}
310
311impl Default for RoutingConfig {
312 fn default() -> Self {
313 Self {
314 mode: default_routing_mode(),
315 confidence_threshold: default_confidence_threshold(),
316 local_first: true,
317 cost_aware: false,
318 estimated_output_tokens: default_estimated_output_tokens(),
319 accuracy_floor: 0.0,
320 accuracy_min_obs: default_accuracy_min_obs(),
321 cost_weight: None,
322 canary_model: None,
323 canary_fraction: 0.0,
324 blocked_models: Vec::new(),
325 per_provider_timeout_seconds: default_per_provider_timeout(),
326 max_total_inference_seconds: default_max_total_inference(),
327 max_fallback_attempts: default_max_fallback_attempts(),
328 }
329 }
330}
331
332fn default_accuracy_min_obs() -> usize {
333 10
334}
335
336fn default_per_provider_timeout() -> u64 {
337 30
338}
339
340fn default_max_total_inference() -> u64 {
341 120
342}
343
344fn default_max_fallback_attempts() -> usize {
345 6
346}
347
348fn default_estimated_output_tokens() -> u32 {
349 500
350}
351
352fn default_routing_mode() -> String {
353 "metascore".into()
354}
355
356fn default_confidence_threshold() -> f64 {
357 0.9
358}
359
360fn default_true() -> bool {
361 true
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct ProviderConfig {
366 pub url: String,
367 pub tier: String,
368 #[serde(default)]
369 pub format: Option<String>,
370 #[serde(default)]
371 pub api_key_env: Option<String>,
372 #[serde(default)]
373 pub chat_path: Option<String>,
374 #[serde(default)]
375 pub embedding_path: Option<String>,
376 #[serde(default)]
377 pub embedding_model: Option<String>,
378 #[serde(default)]
379 pub embedding_dimensions: Option<usize>,
380 #[serde(default)]
381 pub is_local: Option<bool>,
382 #[serde(default)]
383 pub cost_per_input_token: Option<f64>,
384 #[serde(default)]
385 pub cost_per_output_token: Option<f64>,
386 #[serde(default)]
387 pub auth_header: Option<String>,
388 #[serde(default)]
389 pub extra_headers: Option<HashMap<String, String>>,
390 #[serde(default)]
391 pub tpm_limit: Option<u64>,
392 #[serde(default)]
393 pub rpm_limit: Option<u64>,
394 #[serde(default)]
395 pub auth_mode: Option<String>,
396 #[serde(default)]
397 pub oauth_client_id: Option<String>,
398 #[serde(default)]
399 pub oauth_redirect_uri: Option<String>,
400 #[serde(default)]
401 pub api_key_ref: Option<String>,
402}
403
404impl ProviderConfig {
405 pub fn new(url: impl Into<String>, tier: impl Into<String>) -> Self {
406 Self {
407 url: url.into(),
408 tier: tier.into(),
409 format: None,
410 api_key_env: None,
411 chat_path: None,
412 embedding_path: None,
413 embedding_model: None,
414 embedding_dimensions: None,
415 is_local: None,
416 cost_per_input_token: None,
417 cost_per_output_token: None,
418 auth_header: None,
419 extra_headers: None,
420 tpm_limit: None,
421 rpm_limit: None,
422 auth_mode: None,
423 oauth_client_id: None,
424 oauth_redirect_uri: None,
425 api_key_ref: None,
426 }
427 }
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct ModelOverride {
432 #[serde(default)]
433 pub tier: Option<String>,
434 #[serde(default)]
435 pub cost_per_input_token: Option<f64>,
436 #[serde(default)]
437 pub cost_per_output_token: Option<f64>,
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct TierAdaptConfig {
442 #[serde(default)]
443 pub t1_strip_system: bool,
444 #[serde(default)]
445 pub t1_condense_turns: bool,
446 #[serde(default = "default_t2_preamble")]
447 pub t2_default_preamble: Option<String>,
448 #[serde(default = "default_true")]
449 pub t3_t4_passthrough: bool,
450}
451
452impl Default for TierAdaptConfig {
453 fn default() -> Self {
454 Self {
455 t1_strip_system: false,
456 t1_condense_turns: false,
457 t2_default_preamble: default_t2_preamble(),
458 t3_t4_passthrough: true,
459 }
460 }
461}
462
463fn default_t2_preamble() -> Option<String> {
464 Some("Be concise and direct. Focus on accuracy.".into())
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct CircuitBreakerConfig {
469 #[serde(default = "default_cb_threshold")]
470 pub threshold: u32,
471 #[serde(default = "default_cb_window")]
472 pub window_seconds: u64,
473 #[serde(default = "default_cb_cooldown")]
474 pub cooldown_seconds: u64,
475 #[serde(default = "default_cb_max_cooldown")]
476 pub max_cooldown_seconds: u64,
477}
478
479impl Default for CircuitBreakerConfig {
480 fn default() -> Self {
481 Self {
482 threshold: default_cb_threshold(),
483 window_seconds: default_cb_window(),
484 cooldown_seconds: default_cb_cooldown(),
485 max_cooldown_seconds: default_cb_max_cooldown(),
486 }
487 }
488}
489
490fn default_cb_threshold() -> u32 {
491 3
492}
493fn default_cb_window() -> u64 {
494 60
495}
496fn default_cb_cooldown() -> u64 {
497 60
498}
499fn default_cb_max_cooldown() -> u64 {
500 900
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
504pub struct MemoryConfig {
505 #[serde(default = "default_working_pct")]
506 pub working_budget_pct: f64,
507 #[serde(default = "default_episodic_pct")]
508 pub episodic_budget_pct: f64,
509 #[serde(default = "default_semantic_pct")]
510 pub semantic_budget_pct: f64,
511 #[serde(default = "default_procedural_pct")]
512 pub procedural_budget_pct: f64,
513 #[serde(default = "default_relationship_pct")]
514 pub relationship_budget_pct: f64,
515 #[serde(default)]
516 pub embedding_provider: Option<String>,
517 #[serde(default)]
518 pub embedding_model: Option<String>,
519 #[serde(default = "default_hybrid_weight")]
520 pub hybrid_weight: f64,
521 #[serde(default)]
522 pub ann_index: bool,
523 #[serde(default)]
525 pub similarity_threshold: f64,
526 #[serde(default = "default_decay_half_life")]
528 pub decay_half_life_days: f64,
529 #[serde(default = "default_ann_threshold")]
531 pub ann_activation_threshold: usize,
532}
533
534impl Default for MemoryConfig {
535 fn default() -> Self {
536 Self {
537 working_budget_pct: default_working_pct(),
538 episodic_budget_pct: default_episodic_pct(),
539 semantic_budget_pct: default_semantic_pct(),
540 procedural_budget_pct: default_procedural_pct(),
541 relationship_budget_pct: default_relationship_pct(),
542 embedding_provider: None,
543 embedding_model: None,
544 hybrid_weight: default_hybrid_weight(),
545 ann_index: false,
546 similarity_threshold: 0.0,
547 decay_half_life_days: default_decay_half_life(),
548 ann_activation_threshold: default_ann_threshold(),
549 }
550 }
551}
552
553fn default_decay_half_life() -> f64 {
554 7.0
555}
556
557fn default_ann_threshold() -> usize {
558 1000
559}
560
561fn default_hybrid_weight() -> f64 {
562 0.5
563}
564
565fn default_working_pct() -> f64 {
566 30.0
567}
568fn default_episodic_pct() -> f64 {
569 25.0
570}
571fn default_semantic_pct() -> f64 {
572 20.0
573}
574fn default_procedural_pct() -> f64 {
575 15.0
576}
577fn default_relationship_pct() -> f64 {
578 10.0
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct CacheConfig {
583 #[serde(default = "default_true")]
584 pub enabled: bool,
585 #[serde(default = "default_cache_ttl")]
586 pub exact_match_ttl_seconds: u64,
587 #[serde(default = "default_semantic_threshold")]
588 pub semantic_threshold: f64,
589 #[serde(default = "default_max_entries")]
590 pub max_entries: usize,
591 #[serde(default)]
592 pub prompt_compression: bool,
593 #[serde(default = "default_compression_ratio")]
594 pub compression_target_ratio: f64,
595}
596
597impl Default for CacheConfig {
598 fn default() -> Self {
599 Self {
600 enabled: true,
601 exact_match_ttl_seconds: default_cache_ttl(),
602 semantic_threshold: default_semantic_threshold(),
603 max_entries: default_max_entries(),
604 prompt_compression: false,
605 compression_target_ratio: default_compression_ratio(),
606 }
607 }
608}
609
610fn default_compression_ratio() -> f64 {
611 0.5
612}
613
614fn default_cache_ttl() -> u64 {
615 3600
616}
617fn default_semantic_threshold() -> f64 {
618 0.95
619}
620fn default_max_entries() -> usize {
621 10000
622}
623
624#[cfg(test)]
625mod memory_config_tests {
626 use super::MemoryConfig;
627
628 #[test]
629 fn memory_config_tuning_defaults() {
630 let cfg = MemoryConfig::default();
631 assert_eq!(cfg.similarity_threshold, 0.0);
632 assert_eq!(cfg.decay_half_life_days, 7.0);
633 assert_eq!(cfg.ann_activation_threshold, 1000);
634 }
635
636 #[test]
637 fn memory_config_tuning_deserializes() {
638 let toml = r#"
639 similarity_threshold = 0.3
640 decay_half_life_days = 14.0
641 ann_activation_threshold = 500
642 "#;
643 let cfg: MemoryConfig = toml::from_str(toml).unwrap();
644 assert!((cfg.similarity_threshold - 0.3).abs() < f64::EPSILON);
645 assert!((cfg.decay_half_life_days - 14.0).abs() < f64::EPSILON);
646 assert_eq!(cfg.ann_activation_threshold, 500);
647 }
648}
649