use serde_json::Value;
use crate::core::gain::model_pricing::ModelCost;
use crate::core::tokens::count_tokens;
pub const MIN_CACHEABLE_TOKENS: u64 = 1024;
fn system_tokens(system: Option<&Value>) -> u64 {
match system {
Some(v) if !v.is_null() => serde_json::to_string(v).map_or(0, |s| count_tokens(&s) as u64),
_ => 0,
}
}
#[must_use]
pub fn prefix_tokens(system: Option<&Value>, messages: &[Value], cached: usize) -> u64 {
let mut total = system_tokens(system);
let end = cached.min(messages.len());
if end > 0
&& let Ok(serialized) = serde_json::to_string(&messages[..end])
{
total += count_tokens(&serialized) as u64;
}
total
}
#[must_use]
pub fn worth_repacking(system: Option<&Value>, messages: &[Value], cached: usize) -> bool {
prefix_tokens(system, messages, cached) >= MIN_CACHEABLE_TOKENS
}
#[must_use]
pub fn repack_saving_usd(before_tokens: u64, after_tokens: u64, cost: &ModelCost) -> f64 {
let saved = before_tokens.saturating_sub(after_tokens);
saved as f64 / 1_000_000.0 * cost.cache_write_per_m
}
#[must_use]
pub fn net_cost_decision(before_tokens: u64, after_tokens: u64, cost: &ModelCost) -> bool {
before_tokens >= MIN_CACHEABLE_TOKENS
&& after_tokens < before_tokens
&& repack_saving_usd(before_tokens, after_tokens, cost) > 0.0
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn opus() -> ModelCost {
ModelCost {
input_per_m: 15.00,
output_per_m: 75.00,
cache_write_per_m: 18.75,
cache_read_per_m: 1.50,
}
}
#[test]
fn prefix_tokens_zero_when_nothing_cached() {
let msgs = vec![json!({"role": "user", "content": "hello"})];
assert_eq!(prefix_tokens(None, &msgs, 0), 0);
}
#[test]
fn prefix_tokens_counts_only_the_cached_span() {
let big = "lorem ipsum dolor sit amet ".repeat(50);
let msgs = vec![
json!({"role": "user", "content": big}),
json!({"role": "assistant", "content": "tail not counted"}),
];
let one = prefix_tokens(None, &msgs, 1);
let two = prefix_tokens(None, &msgs, 2);
assert!(one > 0);
assert!(two > one, "wider cached span counts more tokens");
}
#[test]
fn prefix_tokens_includes_the_system_field() {
let msgs = vec![json!({"role": "user", "content": "hi"})];
let big_system = json!("context engineering ".repeat(400));
let without = prefix_tokens(None, &msgs, 1);
let with = prefix_tokens(Some(&big_system), &msgs, 1);
assert!(with > without + 500, "system prose must dominate the count");
}
#[test]
fn worth_repacking_rejects_small_prefix() {
let msgs = vec![json!({"role": "user", "content": "tiny"})];
assert!(!worth_repacking(None, &msgs, 1));
}
#[test]
fn worth_repacking_accepts_large_prefix() {
let big = "context engineering ".repeat(1500);
let msgs = vec![json!({"role": "user", "content": big})];
assert!(worth_repacking(None, &msgs, 1));
}
#[test]
fn worth_repacking_counts_large_system_over_tiny_messages() {
let msgs = vec![json!({"role": "user", "content": "hi"})];
let big_system = json!("context engineering ".repeat(1500));
assert!(
!worth_repacking(None, &msgs, 1),
"tiny prefix alone is skipped"
);
assert!(
worth_repacking(Some(&big_system), &msgs, 1),
"a large system prompt makes the prefix worth re-seeding"
);
}
#[test]
fn net_cost_decision_rejects_subcacheable_even_if_smaller() {
assert!(!net_cost_decision(500, 200, &opus()));
}
#[test]
fn net_cost_decision_rejects_when_no_shrink() {
assert!(!net_cost_decision(4000, 4000, &opus()));
assert!(!net_cost_decision(4000, 5000, &opus()));
}
#[test]
fn net_cost_decision_accepts_real_saving() {
assert!(net_cost_decision(4000, 2500, &opus()));
let saved = repack_saving_usd(4000, 2500, &opus());
assert!((saved - (1500.0 / 1_000_000.0 * 18.75)).abs() < 1e-9);
}
#[test]
fn repack_saving_is_zero_when_inflated() {
assert_eq!(repack_saving_usd(1000, 2000, &opus()), 0.0);
}
}