use crate::message::{CompletionRequest, Usage};
use std::hash::{DefaultHasher, Hash, Hasher};
const DROP_FLOOR_TOKENS: u64 = 1_024;
const DROP_FRACTION: f64 = 0.25;
#[derive(Debug, Clone, PartialEq)]
pub enum Verdict {
Baseline,
Stable { uncached: u64, read: u64 },
SurfaceChanged { system: bool, tools: bool },
TranscriptRewritten,
Unobservable,
Drop { uncached: u64, prev_total: u64 },
}
struct Prev {
system: u64,
tools: u64,
messages: Vec<u64>,
total_input: u64,
}
#[derive(Default)]
pub struct CacheLens {
prev: Option<Prev>,
reporting_seen: bool,
}
impl CacheLens {
pub fn new() -> Self {
Self::default()
}
pub fn observe(&mut self, request: &CompletionRequest, usage: &Usage) -> Verdict {
let current = Prev {
system: hash_of(&request.system),
tools: hash_of(&request.tools),
messages: request.messages.iter().map(hash_of).collect(),
total_input: usage.total_input(),
};
let cached_reported =
usage.cache_read_input_tokens > 0 || usage.cache_creation_input_tokens > 0;
let verdict = match &self.prev {
None => Verdict::Baseline,
Some(prev) => {
let system = prev.system != current.system;
let tools = prev.tools != current.tools;
if system || tools {
Verdict::SurfaceChanged { system, tools }
} else if !is_prefix(&prev.messages, ¤t.messages) {
Verdict::TranscriptRewritten
} else if !self.reporting_seen && !cached_reported {
Verdict::Unobservable
} else {
let uncached = usage.input_tokens;
let repaid_share = uncached as f64 / prev.total_input.max(1) as f64;
if uncached > DROP_FLOOR_TOKENS && repaid_share > DROP_FRACTION {
Verdict::Drop {
uncached,
prev_total: prev.total_input,
}
} else {
Verdict::Stable {
uncached,
read: usage.cache_read_input_tokens,
}
}
}
}
};
self.reporting_seen |= cached_reported;
self.prev = Some(current);
verdict
}
}
fn hash_of<T: serde::Serialize>(value: &T) -> u64 {
let mut h = DefaultHasher::new();
serde_json::to_string(value)
.unwrap_or_default()
.hash(&mut h);
h.finish()
}
fn is_prefix(prev: &[u64], current: &[u64]) -> bool {
current.len() >= prev.len() && current[..prev.len()] == *prev
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::Message;
fn request(messages: Vec<Message>) -> CompletionRequest {
CompletionRequest {
model: "m".into(),
system: Some("system".into()),
messages,
tools: vec![],
max_tokens: 512,
effort: None,
thinking: false,
cache_prompt: true,
}
}
fn usage(uncached: u64, creation: u64, read: u64) -> Usage {
Usage {
input_tokens: uncached,
output_tokens: 10,
cache_creation_input_tokens: creation,
cache_read_input_tokens: read,
}
}
fn convo(n: usize) -> Vec<Message> {
(0..n).map(|i| Message::user(format!("turn {i}"))).collect()
}
#[test]
fn an_appended_turn_with_reuse_is_stable() {
let mut lens = CacheLens::new();
assert_eq!(
lens.observe(&request(convo(1)), &usage(8, 18_000, 0)),
Verdict::Baseline
);
assert_eq!(
lens.observe(&request(convo(2)), &usage(40, 200, 18_000)),
Verdict::Stable {
uncached: 40,
read: 18_000
}
);
}
#[test]
fn a_changed_tool_surface_is_an_expected_break_not_a_drop() {
let mut lens = CacheLens::new();
lens.observe(&request(convo(1)), &usage(8, 18_000, 0));
let mut second = request(convo(2));
second.tools = vec![crate::message::ToolSpec {
name: "new_tool".into(),
description: "appeared mid-run".into(),
input_schema: serde_json::json!({}),
}];
assert_eq!(
lens.observe(&second, &usage(18_000, 500, 0)),
Verdict::SurfaceChanged {
system: false,
tools: true
}
);
}
#[test]
fn a_rewritten_transcript_is_an_expected_break_not_a_drop() {
let mut lens = CacheLens::new();
lens.observe(&request(convo(3)), &usage(8, 18_000, 0));
let compacted = vec![
Message::user("[summary of turns 0-1]"),
Message::user("turn 2"),
];
assert_eq!(
lens.observe(&request(compacted), &usage(9_000, 400, 0)),
Verdict::TranscriptRewritten
);
}
#[test]
fn an_unexplained_repayment_is_a_drop() {
let mut lens = CacheLens::new();
lens.observe(&request(convo(1)), &usage(8, 18_000, 0));
assert_eq!(
lens.observe(&request(convo(2)), &usage(17_500, 600, 0)),
Verdict::Drop {
uncached: 17_500,
prev_total: 18_008
}
);
}
#[test]
fn no_reporting_means_unobservable_never_a_drop() {
let mut lens = CacheLens::new();
lens.observe(&request(convo(1)), &usage(18_000, 0, 0));
assert_eq!(
lens.observe(&request(convo(2)), &usage(18_100, 0, 0)),
Verdict::Unobservable
);
lens.observe(&request(convo(3)), &usage(50, 0, 18_100));
assert!(matches!(
lens.observe(&request(convo(4)), &usage(18_200, 0, 0)),
Verdict::Drop { .. }
));
}
#[test]
fn the_ordinary_uncached_tail_stays_stable() {
let mut lens = CacheLens::new();
lens.observe(&request(convo(1)), &usage(8, 18_000, 0));
assert!(matches!(
lens.observe(&request(convo(2)), &usage(900, 100, 17_000)),
Verdict::Stable { .. }
));
}
}