use crate::llm::message::Usage;
#[derive(Debug, Default)]
pub struct CacheTracker {
pub total_cache_writes: u64,
pub total_cache_reads: u64,
pub call_count: u64,
pub hit_count: u64,
pub break_count: u64,
last_write: u64,
last_read: u64,
last_fingerprint: u64,
}
impl CacheTracker {
pub fn new() -> Self {
Self::default()
}
pub fn update_fingerprint(&mut self, system_prompt: &str, tool_count: usize) -> bool {
let mut hasher = std::hash::DefaultHasher::new();
std::hash::Hash::hash(&system_prompt.len(), &mut hasher);
let prefix = &system_prompt[..system_prompt.len().min(200)];
std::hash::Hash::hash(prefix, &mut hasher);
std::hash::Hash::hash(&tool_count, &mut hasher);
let fp = std::hash::Hasher::finish(&hasher);
let changed = self.last_fingerprint != 0 && self.last_fingerprint != fp;
self.last_fingerprint = fp;
changed
}
pub fn record(&mut self, usage: &Usage) -> CacheEvent {
self.call_count += 1;
self.total_cache_writes += usage.cache_creation_input_tokens;
self.total_cache_reads += usage.cache_read_input_tokens;
let had_reads = usage.cache_read_input_tokens > 0;
let had_writes = usage.cache_creation_input_tokens > 0;
if had_reads {
self.hit_count += 1;
}
let event = if !had_reads && had_writes && self.call_count > 1 {
self.break_count += 1;
CacheEvent::Break {
write_tokens: usage.cache_creation_input_tokens,
reason: if self.last_read > 0 {
"Cache invalidated since last call".to_string()
} else {
"No cache hits — content may have changed".to_string()
},
}
} else if had_reads && !had_writes {
CacheEvent::Hit {
read_tokens: usage.cache_read_input_tokens,
}
} else if had_reads && had_writes {
CacheEvent::Partial {
read_tokens: usage.cache_read_input_tokens,
write_tokens: usage.cache_creation_input_tokens,
}
} else {
CacheEvent::Miss
};
self.last_write = usage.cache_creation_input_tokens;
self.last_read = usage.cache_read_input_tokens;
event
}
pub fn hit_rate(&self) -> f64 {
if self.call_count == 0 {
return 0.0;
}
(self.hit_count as f64 / self.call_count as f64) * 100.0
}
pub fn estimated_savings(&self) -> f64 {
self.total_cache_reads as f64 * 0.9
}
}
#[derive(Debug)]
pub enum CacheEvent {
Hit { read_tokens: u64 },
Break { write_tokens: u64, reason: String },
Partial { read_tokens: u64, write_tokens: u64 },
Miss,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_tracker() {
let t = CacheTracker::new();
assert_eq!(t.call_count, 0);
assert_eq!(t.hit_rate(), 0.0);
}
#[test]
fn test_first_call_miss() {
let mut t = CacheTracker::new();
let event = t.record(&Usage {
cache_creation_input_tokens: 1000,
..Default::default()
});
assert!(matches!(event, CacheEvent::Miss));
assert_eq!(t.call_count, 1);
}
#[test]
fn test_cache_hit() {
let mut t = CacheTracker::new();
t.record(&Usage {
cache_creation_input_tokens: 1000,
..Default::default()
});
let event = t.record(&Usage {
cache_read_input_tokens: 900,
..Default::default()
});
assert!(matches!(event, CacheEvent::Hit { .. }));
assert_eq!(t.hit_count, 1);
}
#[test]
fn test_cache_break() {
let mut t = CacheTracker::new();
t.record(&Usage {
cache_read_input_tokens: 500,
..Default::default()
});
let event = t.record(&Usage {
cache_creation_input_tokens: 1000,
..Default::default()
});
assert!(matches!(event, CacheEvent::Break { .. }));
assert_eq!(t.break_count, 1);
}
#[test]
fn test_partial_hit() {
let mut t = CacheTracker::new();
t.record(&Usage::default()); let event = t.record(&Usage {
cache_read_input_tokens: 500,
cache_creation_input_tokens: 200,
..Default::default()
});
assert!(matches!(event, CacheEvent::Partial { .. }));
}
#[test]
fn test_hit_rate() {
let mut t = CacheTracker::new();
t.record(&Usage::default()); t.record(&Usage {
cache_read_input_tokens: 100,
..Default::default()
}); assert!((t.hit_rate() - 50.0).abs() < 0.01);
}
#[test]
fn test_fingerprint_change_detection() {
let mut t = CacheTracker::new();
let changed = t.update_fingerprint("system prompt v1", 10);
assert!(!changed);
let changed = t.update_fingerprint("system prompt v1", 10);
assert!(!changed);
let changed = t.update_fingerprint("system prompt v2", 10);
assert!(changed); }
#[test]
fn test_fingerprint_tool_count_change() {
let mut t = CacheTracker::new();
t.update_fingerprint("prompt", 10);
let changed = t.update_fingerprint("prompt", 15);
assert!(changed); }
}