1use std::collections::VecDeque;
41use std::sync::{Arc, Mutex};
42use std::time::{Duration, Instant};
43
44use serde::{Deserialize, Serialize};
45
46use crate::core::ocla::types::ResponseOptimizationRequest;
47use crate::core::savings_ledger::{self, SavingsEvent};
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(default)]
52pub struct ResponseOptimizerConfig {
53 pub enabled: bool,
55 pub cache_enabled: bool,
57 pub dedup_enabled: bool,
59 pub cache_ttl_secs: u64,
61 pub cache_capacity: usize,
63 pub dedup_window: usize,
65}
66
67impl Default for ResponseOptimizerConfig {
68 fn default() -> Self {
69 Self {
70 enabled: false,
71 cache_enabled: true,
72 dedup_enabled: true,
73 cache_ttl_secs: 300,
74 cache_capacity: 64,
75 dedup_window: 16,
76 }
77 }
78}
79
80#[derive(Debug, Clone)]
82struct CacheEntry {
83 response_body: String,
84 created_at: Instant,
85}
86
87#[derive(Debug)]
89pub struct ResponseCache {
90 entries: VecDeque<(u64, CacheEntry)>,
91 capacity: usize,
92 ttl: Duration,
93}
94
95impl ResponseCache {
96 pub fn new(capacity: usize, ttl: Duration) -> Self {
97 Self {
98 entries: VecDeque::with_capacity(capacity),
99 capacity,
100 ttl,
101 }
102 }
103
104 pub fn get(&mut self, key: u64) -> Option<&str> {
106 self.evict_expired();
107 let pos = self.entries.iter().position(|(k, _)| *k == key)?;
108 let entry = self.entries.remove(pos)?;
110 self.entries.push_back(entry);
111 self.entries.back().map(|(_, e)| e.response_body.as_str())
113 }
114
115 pub fn put(&mut self, key: u64, response: String, _output_tokens: u64) {
117 self.evict_expired();
118 self.entries.retain(|(k, _)| *k != key);
120 while self.entries.len() >= self.capacity {
122 self.entries.pop_front();
123 }
124 self.entries.push_back((
125 key,
126 CacheEntry {
127 response_body: response,
128 created_at: Instant::now(),
129 },
130 ));
131 }
132
133 fn evict_expired(&mut self) {
135 let now = Instant::now();
136 self.entries
137 .retain(|(_, e)| now.duration_since(e.created_at) < self.ttl);
138 }
139
140 pub fn len(&self) -> usize {
141 self.entries.len()
142 }
143
144 pub fn is_empty(&self) -> bool {
145 self.entries.is_empty()
146 }
147}
148
149#[derive(Debug)]
151pub struct DedupTracker {
152 fingerprints: VecDeque<u64>,
153 window: usize,
154}
155
156impl DedupTracker {
157 pub fn new(window: usize) -> Self {
158 Self {
159 fingerprints: VecDeque::with_capacity(window),
160 window,
161 }
162 }
163
164 pub fn record(&mut self, fingerprint: u64) -> bool {
167 let is_dup = self.fingerprints.contains(&fingerprint);
168 if self.fingerprints.len() >= self.window {
169 self.fingerprints.pop_front();
170 }
171 self.fingerprints.push_back(fingerprint);
172 is_dup
173 }
174
175 pub fn clear(&mut self) {
176 self.fingerprints.clear();
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182pub struct OptimizationDecision {
183 pub cache_hit: bool,
185 pub is_duplicate: bool,
187 pub cache_key: u64,
189 pub tokens_saved: u64,
191 pub source: OptimizationSource,
193}
194
195#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
197pub enum OptimizationSource {
198 None,
200 Cache,
202 Dedup,
204 CacheAndDedup,
206}
207
208#[derive(Debug)]
210pub struct SessionOptimizer {
211 pub cache: ResponseCache,
212 pub dedup: DedupTracker,
213 pub config: ResponseOptimizerConfig,
214 pub stats: OptimizerStats,
215}
216
217#[derive(Debug, Clone, Default, Serialize, Deserialize)]
219pub struct OptimizerStats {
220 pub cache_hits: u64,
221 pub cache_misses: u64,
222 pub dedup_detections: u64,
223 pub total_tokens_saved: u64,
224}
225
226impl SessionOptimizer {
227 pub fn new(config: ResponseOptimizerConfig) -> Self {
228 let cache = ResponseCache::new(
229 config.cache_capacity,
230 Duration::from_secs(config.cache_ttl_secs),
231 );
232 let dedup = DedupTracker::new(config.dedup_window);
233 Self {
234 cache,
235 dedup,
236 config,
237 stats: OptimizerStats::default(),
238 }
239 }
240
241 pub fn try_cache_hit(&mut self, cache_key: u64) -> Option<&str> {
244 if !self.config.cache_enabled {
245 return None;
246 }
247 let hit = self.cache.get(cache_key);
248 if hit.is_some() {
249 self.stats.cache_hits += 1;
250 } else {
251 self.stats.cache_misses += 1;
252 }
253 hit
254 }
255
256 pub fn record_response(
258 &mut self,
259 cache_key: u64,
260 response: &str,
261 output_tokens: u64,
262 ) -> OptimizationDecision {
263 let fingerprint = fingerprint_response(response);
264 let is_dup = if self.config.dedup_enabled {
265 let dup = self.dedup.record(fingerprint);
266 if dup {
267 self.stats.dedup_detections += 1;
268 }
269 dup
270 } else {
271 false
272 };
273
274 if self.config.cache_enabled {
275 self.cache
276 .put(cache_key, response.to_string(), output_tokens);
277 }
278
279 OptimizationDecision {
280 cache_hit: false,
281 is_duplicate: is_dup,
282 cache_key,
283 tokens_saved: 0,
284 source: if is_dup {
285 OptimizationSource::Dedup
286 } else {
287 OptimizationSource::None
288 },
289 }
290 }
291
292 pub fn cache_hit_decision(&self, cache_key: u64, tokens_saved: u64) -> OptimizationDecision {
294 OptimizationDecision {
295 cache_hit: true,
296 is_duplicate: false,
297 cache_key,
298 tokens_saved,
299 source: OptimizationSource::Cache,
300 }
301 }
302}
303
304pub fn compute_cache_key(model: &str, system: Option<&str>, messages: &[&str]) -> u64 {
307 let mut hasher = SimpleHasher::new();
308 hasher.write(model.as_bytes());
309 hasher.write(b"\x00");
310 if let Some(sys) = system {
311 hasher.write(sys.as_bytes());
312 }
313 hasher.write(b"\x00");
314 for msg in messages {
315 hasher.write(msg.as_bytes());
316 hasher.write(b"\x01");
317 }
318 hasher.finish()
319}
320
321pub fn fingerprint_response(response: &str) -> u64 {
324 let prefix = if response.len() > 200 {
325 &response[..200]
326 } else {
327 response
328 };
329 let mut hasher = SimpleHasher::new();
330 hasher.write(prefix.as_bytes());
331 hasher.finish()
332}
333
334struct SimpleHasher {
338 state: u64,
339}
340
341impl SimpleHasher {
342 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
343 const PRIME: u64 = 0x0100_0000_01b3;
344
345 fn new() -> Self {
346 Self {
347 state: Self::OFFSET,
348 }
349 }
350
351 fn write(&mut self, bytes: &[u8]) {
352 for &b in bytes {
353 self.state ^= u64::from(b);
354 self.state = self.state.wrapping_mul(Self::PRIME);
355 }
356 }
357
358 fn finish(&self) -> u64 {
359 self.state
360 }
361}
362
363static OPTIMIZERS: std::sync::OnceLock<
366 Mutex<std::collections::HashMap<String, Arc<Mutex<SessionOptimizer>>>>,
367> = std::sync::OnceLock::new();
368
369fn registry() -> &'static Mutex<std::collections::HashMap<String, Arc<Mutex<SessionOptimizer>>>> {
370 OPTIMIZERS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
371}
372
373pub fn get_or_create(
375 session_id: &str,
376 config: &ResponseOptimizerConfig,
377) -> Arc<Mutex<SessionOptimizer>> {
378 let mut reg = registry()
379 .lock()
380 .unwrap_or_else(std::sync::PoisonError::into_inner);
381 reg.entry(session_id.to_string())
382 .or_insert_with(|| Arc::new(Mutex::new(SessionOptimizer::new(config.clone()))))
383 .clone()
384}
385
386pub fn optimize_response(request: &ResponseOptimizationRequest) -> OptimizationDecision {
388 let config = ResponseOptimizerConfig {
389 enabled: true,
390 ..Default::default()
391 };
392 let optimizer = get_or_create(&request.context.session_id, &config);
393 let mut optimizer = optimizer
394 .lock()
395 .unwrap_or_else(std::sync::PoisonError::into_inner);
396 let cache_key = compute_cache_key("ocla-response", None, &[&request.response_ref]);
397
398 let decision = if optimizer.try_cache_hit(cache_key).is_some() {
399 optimizer.cache_hit_decision(
400 cache_key,
401 request
402 .original_tokens
403 .saturating_sub(request.target_tokens),
404 )
405 } else {
406 optimizer.record_response(
407 cache_key,
408 &request.response_ref,
409 request.target_tokens.min(request.original_tokens),
410 )
411 };
412
413 let delivered_tokens = if decision.cache_hit {
414 0
415 } else {
416 request.target_tokens.min(request.original_tokens)
417 };
418 record_response_measurement(request, delivered_tokens);
419 decision
420}
421
422fn record_response_measurement(request: &ResponseOptimizationRequest, delivered_tokens: u64) {
423 let ledger_disabled = std::env::var("LEAN_CTX_SAVINGS_LEDGER")
424 .ok()
425 .is_some_and(|value| {
426 matches!(
427 value.trim().to_ascii_lowercase().as_str(),
428 "off" | "0" | "false" | "no"
429 )
430 });
431 if request.original_tokens <= delivered_tokens || ledger_disabled {
432 return;
433 }
434 let Some(path) = savings_ledger::store::default_path() else {
435 return;
436 };
437
438 let quote = crate::core::gain::model_pricing::ModelPricing::load().quote(None);
439 let saved_tokens = request.original_tokens - delivered_tokens;
440 let event = SavingsEvent {
441 ts: chrono::Utc::now().to_rfc3339(),
442 tool: "proxy_response_optimizer".into(),
443 mechanism: savings_ledger::MECHANISM_COMPRESSION.into(),
444 model_id: quote.model_key.clone(),
445 tokenizer: crate::core::tokens::detect_tokenizer("e.model_key).to_string(),
446 baseline_tokens: request.original_tokens,
447 actual_tokens: delivered_tokens,
448 saved_tokens,
449 bounce_adjustment: 0,
450 unit_price_per_m_usd: quote.cost.input_per_m,
451 saved_usd: saved_tokens as f64 * quote.cost.input_per_m / 1_000_000.0,
452 repo_hash: String::new(),
453 agent_id: request.context.agent_id.clone(),
454 prev_hash: String::new(),
455 entry_hash: String::new(),
456 version: env!("CARGO_PKG_VERSION").into(),
457 intent_tag: None,
458 outcome: None,
459 model_original: None,
460 model_routed: None,
461 routing_savings: None,
462 response_original_tokens: Some(request.original_tokens),
463 response_delivered_tokens: Some(delivered_tokens),
464 agent_chain_id: None,
465 chain_depth: None,
466 measurement_method: Some(savings_ledger::event::MeasurementMethod::DirectCount),
467 evidence_class: Some(savings_ledger::event::EvidenceClass::Measured),
468 confidence: Some(1.0),
469 request_id: None,
470 session_id: None,
471 trace_id: None,
472 quality_signal: None,
473 attribution_group: None,
474 attribution_id: Some(request.response_ref.clone()),
475 baseline_ref: None,
476 price_version: None,
477 customer_approval: None,
478 settlement_status: None,
479 is_first_inject: None,
480 cache_read_per_m_usd: Some(quote.cost.cache_read_per_m),
481 cache_write_per_m_usd: Some(quote.cost.cache_write_per_m),
482 };
483 let _ = savings_ledger::store::append(&path, event);
484}
485
486pub fn remove_session(session_id: &str) {
488 let mut reg = registry()
489 .lock()
490 .unwrap_or_else(std::sync::PoisonError::into_inner);
491 reg.remove(session_id);
492}
493
494pub fn global_stats() -> OptimizerStats {
496 let reg = registry()
497 .lock()
498 .unwrap_or_else(std::sync::PoisonError::into_inner);
499 let mut total = OptimizerStats::default();
500 for opt in reg.values() {
501 let guard = opt
502 .lock()
503 .unwrap_or_else(std::sync::PoisonError::into_inner);
504 total.cache_hits += guard.stats.cache_hits;
505 total.cache_misses += guard.stats.cache_misses;
506 total.dedup_detections += guard.stats.dedup_detections;
507 total.total_tokens_saved += guard.stats.total_tokens_saved;
508 }
509 total
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 fn default_config() -> ResponseOptimizerConfig {
517 ResponseOptimizerConfig {
518 enabled: true,
519 ..Default::default()
520 }
521 }
522
523 #[test]
526 fn cache_stores_and_retrieves() {
527 let mut cache = ResponseCache::new(8, Duration::from_mins(1));
528 cache.put(42, "hello world".to_string(), 5);
529 assert_eq!(cache.get(42), Some("hello world"));
530 }
531
532 #[test]
533 fn cache_miss_returns_none() {
534 let mut cache = ResponseCache::new(8, Duration::from_mins(1));
535 assert_eq!(cache.get(99), None);
536 }
537
538 #[test]
539 fn cache_respects_capacity() {
540 let mut cache = ResponseCache::new(3, Duration::from_mins(1));
541 cache.put(1, "a".into(), 1);
542 cache.put(2, "b".into(), 1);
543 cache.put(3, "c".into(), 1);
544 cache.put(4, "d".into(), 1);
545 assert_eq!(cache.get(1), None);
547 assert_eq!(cache.get(2), Some("b"));
548 assert_eq!(cache.get(4), Some("d"));
549 assert_eq!(cache.len(), 3);
550 }
551
552 #[test]
553 fn cache_updates_existing_key() {
554 let mut cache = ResponseCache::new(8, Duration::from_mins(1));
555 cache.put(1, "old".into(), 5);
556 cache.put(1, "new".into(), 5);
557 assert_eq!(cache.get(1), Some("new"));
558 assert_eq!(cache.len(), 1);
559 }
560
561 #[test]
564 fn dedup_detects_repeated_fingerprint() {
565 let mut dedup = DedupTracker::new(8);
566 assert!(!dedup.record(100), "first occurrence");
567 assert!(!dedup.record(200), "different fingerprint");
568 assert!(dedup.record(100), "repeated");
569 }
570
571 #[test]
572 fn dedup_window_evicts_old_entries() {
573 let mut dedup = DedupTracker::new(3);
574 dedup.record(1);
575 dedup.record(2);
576 dedup.record(3);
577 dedup.record(4);
579 assert!(!dedup.record(1), "1 was evicted from window");
580 assert!(dedup.record(3), "3 still in window");
582 assert!(!dedup.record(2), "2 was evicted when 1 was added");
583 }
584
585 #[test]
588 fn cache_key_is_deterministic() {
589 let k1 = compute_cache_key("gpt-4o", Some("sys"), &["hello", "world"]);
590 let k2 = compute_cache_key("gpt-4o", Some("sys"), &["hello", "world"]);
591 assert_eq!(k1, k2);
592 }
593
594 #[test]
595 fn cache_key_differs_for_different_inputs() {
596 let k1 = compute_cache_key("gpt-4o", Some("sys"), &["hello"]);
597 let k2 = compute_cache_key("gpt-4o", Some("sys"), &["world"]);
598 assert_ne!(k1, k2);
599
600 let k3 = compute_cache_key("gpt-4o", None, &["hello"]);
601 let k4 = compute_cache_key("claude-sonnet-4", None, &["hello"]);
602 assert_ne!(k3, k4);
603 }
604
605 #[test]
606 fn cache_key_order_matters() {
607 let k1 = compute_cache_key("m", None, &["a", "b"]);
608 let k2 = compute_cache_key("m", None, &["b", "a"]);
609 assert_ne!(k1, k2, "message order must affect key");
610 }
611
612 #[test]
615 fn fingerprint_uses_prefix() {
616 let short = "hello";
617 let long = format!("{}{}", "x".repeat(200), "DIFFERENT_TAIL");
618 let long2 = format!("{}{}", "x".repeat(200), "OTHER_TAIL");
619 assert_eq!(fingerprint_response(&long), fingerprint_response(&long2));
621 assert_ne!(fingerprint_response(short), fingerprint_response(&long));
623 }
624
625 #[test]
628 fn session_optimizer_cache_flow() {
629 let mut opt = SessionOptimizer::new(default_config());
630 let key = compute_cache_key("gpt-4o", None, &["what is rust?"]);
631
632 assert!(opt.try_cache_hit(key).is_none());
634 assert_eq!(opt.stats.cache_misses, 1);
635
636 let decision = opt.record_response(key, "Rust is a systems programming language.", 12);
638 assert!(!decision.cache_hit);
639 assert!(!decision.is_duplicate);
640
641 let hit = opt.try_cache_hit(key);
643 assert_eq!(hit, Some("Rust is a systems programming language."));
644 assert_eq!(opt.stats.cache_hits, 1);
645 }
646
647 #[test]
648 fn session_optimizer_dedup_flow() {
649 let mut opt = SessionOptimizer::new(default_config());
650 let key1 = 100;
651 let key2 = 200;
652
653 let response = "Rust is a systems programming language.";
655 let d1 = opt.record_response(key1, response, 12);
656 assert!(!d1.is_duplicate);
657
658 let d2 = opt.record_response(key2, response, 12);
659 assert!(d2.is_duplicate);
660 assert_eq!(d2.source, OptimizationSource::Dedup);
661 assert_eq!(opt.stats.dedup_detections, 1);
662 }
663
664 #[test]
665 fn disabled_optimizer_is_noop() {
666 let config = ResponseOptimizerConfig {
667 enabled: true,
668 cache_enabled: false,
669 dedup_enabled: false,
670 ..Default::default()
671 };
672 let mut opt = SessionOptimizer::new(config);
673 let key = 42;
674
675 assert!(opt.try_cache_hit(key).is_none());
676 let d = opt.record_response(key, "response", 10);
677 assert!(!d.is_duplicate);
678 assert!(opt.cache.is_empty());
680 }
681
682 #[test]
683 fn global_registry_creates_and_retrieves() {
684 let config = default_config();
685 let opt1 = get_or_create("session-test-1", &config);
686 let opt2 = get_or_create("session-test-1", &config);
687 assert!(Arc::ptr_eq(&opt1, &opt2));
689
690 let opt3 = get_or_create("session-test-2", &config);
691 assert!(!Arc::ptr_eq(&opt1, &opt3));
692
693 remove_session("session-test-1");
695 remove_session("session-test-2");
696 }
697
698 #[test]
701 fn optimizer_decisions_are_deterministic() {
702 let mut opt = SessionOptimizer::new(default_config());
703 let key = compute_cache_key("m", None, &["q"]);
704 opt.record_response(key, "answer", 5);
705
706 let h1 = opt.try_cache_hit(key).map(str::to_string);
708 let h2 = opt.try_cache_hit(key).map(str::to_string);
709 assert_eq!(h1, h2);
710 }
711
712 #[tokio::test]
713 async fn ocla_registry_path_measures_response_tokens() {
714 let _isolated = crate::core::data_dir::isolated_data_dir();
715 let registry = crate::core::ocla::registry::OclaRegistry::with_builtins();
716 let request = ResponseOptimizationRequest {
717 context: crate::core::ocla::types::OclaRequestContext {
718 request_id: "response-optimizer-test".into(),
719 session_id: "response-optimizer-test".into(),
720 agent_id: "agent-test".into(),
721 content_ref: "response:test".into(),
722 tenant_id: None,
723 trace_id: "tr-unit".into(),
724 },
725 response_ref: "blake3:response-optimizer-test".into(),
726 original_tokens: 1_000,
727 target_tokens: 400,
728 };
729
730 let result = registry
731 .response_optimizer
732 .optimize_response(request)
733 .await
734 .expect("registry response optimizer must succeed");
735 assert_eq!(result.delivered_tokens, 400);
736
737 let event = savings_ledger::all_events()
738 .into_iter()
739 .find(|event| event.tool == "proxy_response_optimizer")
740 .expect("response optimization must create a ledger event");
741 assert_eq!(event.response_original_tokens, Some(1_000));
742 assert_eq!(event.response_delivered_tokens, Some(400));
743 }
744}