#![deny(missing_docs)]
use std::collections::HashMap;
use std::time::Instant;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Usage {
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_creation_input_tokens: u64,
pub cache_read_input_tokens: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Block {
pub text: String,
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheControl {
Ephemeral,
}
impl From<String> for Block {
fn from(text: String) -> Self {
Block {
text,
cache_control: None,
}
}
}
impl From<&str> for Block {
fn from(text: &str) -> Self {
Block {
text: text.to_string(),
cache_control: None,
}
}
}
#[derive(Debug, Clone)]
pub struct WarmRequest {
pub model: String,
pub system_blocks: Vec<Block>,
pub messages: Vec<Message>,
pub tools: Vec<Tool>,
pub max_tokens: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub role: String,
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tool {
pub name: String,
}
#[derive(Debug, Clone)]
pub struct WarmResponse {
pub usage: Usage,
}
pub trait WarmCall {
type Error: std::error::Error;
fn call(&self, req: &WarmRequest) -> Result<WarmResponse, Self::Error>;
}
pub fn to_system_blocks(system: &str) -> Vec<Block> {
vec![Block::from(system)]
}
pub fn add_cache_breakpoints(blocks: &[Block], n: usize) -> Vec<Block> {
if n == 0 || blocks.is_empty() {
return blocks.to_vec();
}
let capped = n.min(4);
let len = blocks.len();
let positions: std::collections::HashSet<usize> = if capped >= len {
(0..len).collect()
} else {
let step = len as f64 / capped as f64;
(0..capped)
.map(|i| (((i + 1) as f64 * step) as usize).saturating_sub(1))
.collect()
};
blocks
.iter()
.enumerate()
.map(|(i, b)| {
let mut nb = b.clone();
if positions.contains(&i) && nb.cache_control.is_none() {
nb.cache_control = Some(CacheControl::Ephemeral);
}
nb
})
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ModelPrice {
pub input: f64,
pub output: f64,
}
pub type PriceTable = HashMap<&'static str, ModelPrice>;
pub const CACHE_WRITE_MULTIPLIER: f64 = 1.25;
pub const CACHE_READ_MULTIPLIER: f64 = 0.10;
pub fn default_prices() -> PriceTable {
let mut t: PriceTable = HashMap::new();
t.insert(
"claude-opus-4-7",
ModelPrice {
input: 15.0,
output: 75.0,
},
);
t.insert(
"claude-opus-4-6",
ModelPrice {
input: 15.0,
output: 75.0,
},
);
t.insert(
"claude-sonnet-4-6",
ModelPrice {
input: 3.0,
output: 15.0,
},
);
t.insert(
"claude-haiku-4-5",
ModelPrice {
input: 0.80,
output: 4.0,
},
);
t
}
#[derive(Debug, Clone, PartialEq)]
pub struct WarmResult {
pub model: String,
pub cache_creation_input_tokens: u64,
pub cache_read_input_tokens: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub latency_ms_warm: u64,
pub verified_hit_tokens: Option<u64>,
pub latency_ms_verify: Option<u64>,
pub cost_usd: Option<f64>,
}
pub struct Warmer<C: WarmCall> {
client: C,
prices: PriceTable,
}
#[derive(Debug, Clone)]
pub struct WarmInput {
pub model: String,
pub system_blocks: Vec<Block>,
pub messages: Vec<Message>,
pub tools: Vec<Tool>,
pub max_tokens: u32,
pub breakpoints: usize,
pub ping_text: String,
}
impl WarmInput {
pub fn new(model: impl Into<String>, system: impl AsRef<str>) -> Self {
WarmInput {
model: model.into(),
system_blocks: to_system_blocks(system.as_ref()),
messages: Vec::new(),
tools: Vec::new(),
max_tokens: 8,
breakpoints: 1,
ping_text: "ok".to_string(),
}
}
}
impl<C: WarmCall> Warmer<C> {
pub fn new(client: C) -> Self {
Warmer {
client,
prices: default_prices(),
}
}
pub fn with_prices(client: C, prices: PriceTable) -> Self {
Warmer { client, prices }
}
pub fn client(&self) -> &C {
&self.client
}
pub fn warm(
&self,
model: impl Into<String>,
system: impl AsRef<str>,
) -> Result<WarmResult, C::Error> {
self.warm_with(WarmInput::new(model, system))
}
pub fn warm_verified(
&self,
model: impl Into<String>,
system: impl AsRef<str>,
) -> Result<WarmResult, C::Error> {
self.warm_with_verified(WarmInput::new(model, system))
}
pub fn warm_with(&self, input: WarmInput) -> Result<WarmResult, C::Error> {
self.warm_inner(input, false)
}
pub fn warm_with_verified(&self, input: WarmInput) -> Result<WarmResult, C::Error> {
self.warm_inner(input, true)
}
fn warm_inner(&self, input: WarmInput, verify: bool) -> Result<WarmResult, C::Error> {
let WarmInput {
model,
system_blocks,
messages,
tools,
max_tokens,
breakpoints,
ping_text,
} = input;
let system_blocks = add_cache_breakpoints(&system_blocks, breakpoints);
let messages = if messages.is_empty() {
vec![Message {
role: "user".to_string(),
content: ping_text,
}]
} else {
messages
};
let request = WarmRequest {
model: model.clone(),
system_blocks,
messages,
tools,
max_tokens,
};
let t0 = Instant::now();
let resp1 = self.client.call(&request)?;
let warm_ms = t0.elapsed().as_millis() as u64;
let usage1 = resp1.usage;
let (verified, verify_ms) = if verify {
let t1 = Instant::now();
let resp2 = self.client.call(&request)?;
let verify_ms = t1.elapsed().as_millis() as u64;
(Some(resp2.usage.cache_read_input_tokens), Some(verify_ms))
} else {
(None, None)
};
Ok(WarmResult {
cost_usd: self.estimate_cost(&model, &usage1),
model,
cache_creation_input_tokens: usage1.cache_creation_input_tokens,
cache_read_input_tokens: usage1.cache_read_input_tokens,
input_tokens: usage1.input_tokens,
output_tokens: usage1.output_tokens,
latency_ms_warm: warm_ms,
verified_hit_tokens: verified,
latency_ms_verify: verify_ms,
})
}
fn estimate_cost(&self, model: &str, usage: &Usage) -> Option<f64> {
let price = self.prices.get(model)?;
let per_in = price.input / 1_000_000.0;
let per_out = price.output / 1_000_000.0;
let regular_in = usage.input_tokens as f64;
let write_in = usage.cache_creation_input_tokens as f64;
let read_in = usage.cache_read_input_tokens as f64;
let out = usage.output_tokens as f64;
Some(
regular_in * per_in
+ write_in * per_in * CACHE_WRITE_MULTIPLIER
+ read_in * per_in * CACHE_READ_MULTIPLIER
+ out * per_out,
)
}
}