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
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MutationDecision {
Mutate { break_even: u32 },
Preserve { break_even: u32 },
}
#[must_use]
pub fn model_cost_for(model: &str) -> ModelCost {
let m = model.to_ascii_lowercase();
if m.contains("opus") {
ModelCost {
input_per_m: 15.0,
output_per_m: 75.0,
cache_write_per_m: 18.75,
cache_read_per_m: 1.5,
}
} else if m.contains("haiku") {
ModelCost {
input_per_m: 0.25,
output_per_m: 1.25,
cache_write_per_m: 0.30,
cache_read_per_m: 0.03,
}
} else {
ModelCost {
input_per_m: 3.0,
output_per_m: 15.0,
cache_write_per_m: 3.75,
cache_read_per_m: 0.30,
}
}
}
pub fn should_mutate_frozen(
before_tokens: u64,
after_tokens: u64,
estimated_reuse_count: u32,
cost: &ModelCost,
) -> MutationDecision {
if before_tokens < MIN_CACHEABLE_TOKENS || after_tokens >= before_tokens {
return MutationDecision::Preserve {
break_even: u32::MAX,
};
}
let saved_tokens = before_tokens - after_tokens;
let bust_cost = (after_tokens as f64 / 1_000_000.0 * cost.cache_write_per_m)
+ (before_tokens as f64 / 1_000_000.0 * cost.cache_read_per_m);
let per_call_saving = saved_tokens as f64 / 1_000_000.0 * cost.input_per_m;
if per_call_saving <= 0.0 {
return MutationDecision::Preserve {
break_even: u32::MAX,
};
}
let break_even = (bust_cost / per_call_saving).ceil().max(1.0) as u32;
if estimated_reuse_count >= break_even {
MutationDecision::Mutate { break_even }
} else {
MutationDecision::Preserve { break_even }
}
}
#[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);
}
#[test]
fn should_mutate_frozen_accepts_with_enough_reuse() {
let d = should_mutate_frozen(4000, 2000, 10, &opus());
match d {
MutationDecision::Mutate { break_even } => assert!(break_even <= 10),
MutationDecision::Preserve { .. } => panic!("expected Mutate"),
}
}
#[test]
fn should_mutate_frozen_rejects_with_low_reuse() {
let d = should_mutate_frozen(4000, 3900, 1, &opus());
assert!(matches!(d, MutationDecision::Preserve { .. }));
}
#[test]
fn should_mutate_frozen_rejects_subcacheable() {
let d = should_mutate_frozen(500, 200, 100, &opus());
assert!(matches!(d, MutationDecision::Preserve { .. }));
}
#[test]
fn should_mutate_frozen_rejects_inflation() {
let d = should_mutate_frozen(3000, 4000, 100, &opus());
assert!(matches!(d, MutationDecision::Preserve { .. }));
}
}