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 quality_signal: None,
470 attribution_group: None,
471 attribution_id: Some(request.response_ref.clone()),
472 baseline_ref: None,
473 price_version: None,
474 customer_approval: None,
475 settlement_status: None,
476 is_first_inject: None,
477 cache_read_per_m_usd: Some(quote.cost.cache_read_per_m),
478 cache_write_per_m_usd: Some(quote.cost.cache_write_per_m),
479 };
480 let _ = savings_ledger::store::append(&path, event);
481}
482
483pub fn remove_session(session_id: &str) {
485 let mut reg = registry()
486 .lock()
487 .unwrap_or_else(std::sync::PoisonError::into_inner);
488 reg.remove(session_id);
489}
490
491pub fn global_stats() -> OptimizerStats {
493 let reg = registry()
494 .lock()
495 .unwrap_or_else(std::sync::PoisonError::into_inner);
496 let mut total = OptimizerStats::default();
497 for opt in reg.values() {
498 let guard = opt
499 .lock()
500 .unwrap_or_else(std::sync::PoisonError::into_inner);
501 total.cache_hits += guard.stats.cache_hits;
502 total.cache_misses += guard.stats.cache_misses;
503 total.dedup_detections += guard.stats.dedup_detections;
504 total.total_tokens_saved += guard.stats.total_tokens_saved;
505 }
506 total
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 fn default_config() -> ResponseOptimizerConfig {
514 ResponseOptimizerConfig {
515 enabled: true,
516 ..Default::default()
517 }
518 }
519
520 #[test]
523 fn cache_stores_and_retrieves() {
524 let mut cache = ResponseCache::new(8, Duration::from_mins(1));
525 cache.put(42, "hello world".to_string(), 5);
526 assert_eq!(cache.get(42), Some("hello world"));
527 }
528
529 #[test]
530 fn cache_miss_returns_none() {
531 let mut cache = ResponseCache::new(8, Duration::from_mins(1));
532 assert_eq!(cache.get(99), None);
533 }
534
535 #[test]
536 fn cache_respects_capacity() {
537 let mut cache = ResponseCache::new(3, Duration::from_mins(1));
538 cache.put(1, "a".into(), 1);
539 cache.put(2, "b".into(), 1);
540 cache.put(3, "c".into(), 1);
541 cache.put(4, "d".into(), 1);
542 assert_eq!(cache.get(1), None);
544 assert_eq!(cache.get(2), Some("b"));
545 assert_eq!(cache.get(4), Some("d"));
546 assert_eq!(cache.len(), 3);
547 }
548
549 #[test]
550 fn cache_updates_existing_key() {
551 let mut cache = ResponseCache::new(8, Duration::from_mins(1));
552 cache.put(1, "old".into(), 5);
553 cache.put(1, "new".into(), 5);
554 assert_eq!(cache.get(1), Some("new"));
555 assert_eq!(cache.len(), 1);
556 }
557
558 #[test]
561 fn dedup_detects_repeated_fingerprint() {
562 let mut dedup = DedupTracker::new(8);
563 assert!(!dedup.record(100), "first occurrence");
564 assert!(!dedup.record(200), "different fingerprint");
565 assert!(dedup.record(100), "repeated");
566 }
567
568 #[test]
569 fn dedup_window_evicts_old_entries() {
570 let mut dedup = DedupTracker::new(3);
571 dedup.record(1);
572 dedup.record(2);
573 dedup.record(3);
574 dedup.record(4);
576 assert!(!dedup.record(1), "1 was evicted from window");
577 assert!(dedup.record(3), "3 still in window");
579 assert!(!dedup.record(2), "2 was evicted when 1 was added");
580 }
581
582 #[test]
585 fn cache_key_is_deterministic() {
586 let k1 = compute_cache_key("gpt-4o", Some("sys"), &["hello", "world"]);
587 let k2 = compute_cache_key("gpt-4o", Some("sys"), &["hello", "world"]);
588 assert_eq!(k1, k2);
589 }
590
591 #[test]
592 fn cache_key_differs_for_different_inputs() {
593 let k1 = compute_cache_key("gpt-4o", Some("sys"), &["hello"]);
594 let k2 = compute_cache_key("gpt-4o", Some("sys"), &["world"]);
595 assert_ne!(k1, k2);
596
597 let k3 = compute_cache_key("gpt-4o", None, &["hello"]);
598 let k4 = compute_cache_key("claude-sonnet-4", None, &["hello"]);
599 assert_ne!(k3, k4);
600 }
601
602 #[test]
603 fn cache_key_order_matters() {
604 let k1 = compute_cache_key("m", None, &["a", "b"]);
605 let k2 = compute_cache_key("m", None, &["b", "a"]);
606 assert_ne!(k1, k2, "message order must affect key");
607 }
608
609 #[test]
612 fn fingerprint_uses_prefix() {
613 let short = "hello";
614 let long = format!("{}{}", "x".repeat(200), "DIFFERENT_TAIL");
615 let long2 = format!("{}{}", "x".repeat(200), "OTHER_TAIL");
616 assert_eq!(fingerprint_response(&long), fingerprint_response(&long2));
618 assert_ne!(fingerprint_response(short), fingerprint_response(&long));
620 }
621
622 #[test]
625 fn session_optimizer_cache_flow() {
626 let mut opt = SessionOptimizer::new(default_config());
627 let key = compute_cache_key("gpt-4o", None, &["what is rust?"]);
628
629 assert!(opt.try_cache_hit(key).is_none());
631 assert_eq!(opt.stats.cache_misses, 1);
632
633 let decision = opt.record_response(key, "Rust is a systems programming language.", 12);
635 assert!(!decision.cache_hit);
636 assert!(!decision.is_duplicate);
637
638 let hit = opt.try_cache_hit(key);
640 assert_eq!(hit, Some("Rust is a systems programming language."));
641 assert_eq!(opt.stats.cache_hits, 1);
642 }
643
644 #[test]
645 fn session_optimizer_dedup_flow() {
646 let mut opt = SessionOptimizer::new(default_config());
647 let key1 = 100;
648 let key2 = 200;
649
650 let response = "Rust is a systems programming language.";
652 let d1 = opt.record_response(key1, response, 12);
653 assert!(!d1.is_duplicate);
654
655 let d2 = opt.record_response(key2, response, 12);
656 assert!(d2.is_duplicate);
657 assert_eq!(d2.source, OptimizationSource::Dedup);
658 assert_eq!(opt.stats.dedup_detections, 1);
659 }
660
661 #[test]
662 fn disabled_optimizer_is_noop() {
663 let config = ResponseOptimizerConfig {
664 enabled: true,
665 cache_enabled: false,
666 dedup_enabled: false,
667 ..Default::default()
668 };
669 let mut opt = SessionOptimizer::new(config);
670 let key = 42;
671
672 assert!(opt.try_cache_hit(key).is_none());
673 let d = opt.record_response(key, "response", 10);
674 assert!(!d.is_duplicate);
675 assert!(opt.cache.is_empty());
677 }
678
679 #[test]
680 fn global_registry_creates_and_retrieves() {
681 let config = default_config();
682 let opt1 = get_or_create("session-test-1", &config);
683 let opt2 = get_or_create("session-test-1", &config);
684 assert!(Arc::ptr_eq(&opt1, &opt2));
686
687 let opt3 = get_or_create("session-test-2", &config);
688 assert!(!Arc::ptr_eq(&opt1, &opt3));
689
690 remove_session("session-test-1");
692 remove_session("session-test-2");
693 }
694
695 #[test]
698 fn optimizer_decisions_are_deterministic() {
699 let mut opt = SessionOptimizer::new(default_config());
700 let key = compute_cache_key("m", None, &["q"]);
701 opt.record_response(key, "answer", 5);
702
703 let h1 = opt.try_cache_hit(key).map(str::to_string);
705 let h2 = opt.try_cache_hit(key).map(str::to_string);
706 assert_eq!(h1, h2);
707 }
708
709 #[test]
710 fn ocla_registry_path_measures_response_tokens() {
711 let _isolated = crate::core::data_dir::isolated_data_dir();
712 let registry = crate::core::ocla::registry::OclaRegistry::with_builtins();
713 let request = ResponseOptimizationRequest {
714 context: crate::core::ocla::types::OclaRequestContext {
715 request_id: "response-optimizer-test".into(),
716 session_id: "response-optimizer-test".into(),
717 agent_id: "agent-test".into(),
718 content_ref: "response:test".into(),
719 tenant_id: None,
720 trace_id: "tr-unit".into(),
721 },
722 response_ref: "blake3:response-optimizer-test".into(),
723 original_tokens: 1_000,
724 target_tokens: 400,
725 };
726
727 let result = registry
728 .response_optimizer
729 .optimize_response(request)
730 .expect("registry response optimizer must succeed");
731 assert_eq!(result.delivered_tokens, 400);
732
733 let event = savings_ledger::all_events()
734 .into_iter()
735 .find(|event| event.tool == "proxy_response_optimizer")
736 .expect("response optimization must create a ledger event");
737 assert_eq!(event.response_original_tokens, Some(1_000));
738 assert_eq!(event.response_delivered_tokens, Some(400));
739 }
740}