use serde_json::Value;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CostBasis {
ProviderReported,
TokenizerEstimated,
Interpolated,
}
impl CostBasis {
pub fn as_str(&self) -> &'static str {
match self {
CostBasis::ProviderReported => "provider_reported",
CostBasis::TokenizerEstimated => "tokenizer_estimated",
CostBasis::Interpolated => "interpolated",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CaptureGap {
UnknownWireFormat,
UnknownModel,
ProviderError,
StreamInterrupted,
}
impl CaptureGap {
pub fn as_str(&self) -> &'static str {
match self {
CaptureGap::UnknownWireFormat => "unknown_wire_format",
CaptureGap::UnknownModel => "unknown_model",
CaptureGap::ProviderError => "provider_error",
CaptureGap::StreamInterrupted => "stream_interrupted",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Usage {
pub input_tokens: u64,
pub cache_read: u64,
pub cache_write: u64,
pub eph_5m: u64,
pub eph_1h: u64,
pub output_tokens: u64,
}
impl Usage {
fn merged_max(self, other: Usage) -> Usage {
Usage {
input_tokens: self.input_tokens.max(other.input_tokens),
cache_read: self.cache_read.max(other.cache_read),
cache_write: self.cache_write.max(other.cache_write),
eph_5m: self.eph_5m.max(other.eph_5m),
eph_1h: self.eph_1h.max(other.eph_1h),
output_tokens: self.output_tokens.max(other.output_tokens),
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct UsageAccumulator {
usage: Usage,
seen: bool,
terminal: bool,
}
impl UsageAccumulator {
pub fn scan_chunk(&mut self, chunk: &[u8]) -> bool {
match scan_usage(chunk) {
Some(found) => {
self.merge(found.usage);
self.seen = true;
if found.terminal {
self.terminal = true;
}
true
}
None => false,
}
}
fn merge(&mut self, u: Usage) {
self.usage = self.usage.merged_max(u);
}
pub fn has_usage(&self) -> bool {
self.seen
}
pub fn is_terminal(&self) -> bool {
self.terminal
}
pub fn usage(&self) -> Usage {
self.usage
}
}
struct ScanResult {
usage: Usage,
terminal: bool,
}
fn scan_usage(chunk: &[u8]) -> Option<ScanResult> {
let text = std::str::from_utf8(chunk).ok()?;
let mut best: Option<Usage> = None;
let mut terminal = false;
for line in text.lines() {
let line = line.trim_start();
let payload = line.strip_prefix("data:").map(str::trim).unwrap_or(line);
if !payload.starts_with('{') {
continue;
}
if !payload.contains("usage") {
continue;
}
if let Ok(v) = serde_json::from_str::<Value>(payload) {
if let Some((u, term)) = usage_and_terminal(&v) {
best = Some(merge_pick(best, u));
terminal |= term;
}
}
}
if best.is_none() && text.trim_start().starts_with('{') {
if let Ok(v) = serde_json::from_str::<Value>(text.trim()) {
if let Some((u, term)) = usage_and_terminal(&v) {
best = Some(u);
terminal |= term;
}
}
}
best.map(|usage| ScanResult { usage, terminal })
}
fn merge_pick(prev: Option<Usage>, cur: Usage) -> Usage {
match prev {
None => cur,
Some(p) => p.merged_max(cur),
}
}
fn usage_and_terminal(v: &Value) -> Option<(Usage, bool)> {
if v.get("type").and_then(Value::as_str) == Some("message_start") {
let u = v.get("message").and_then(|m| m.get("usage"))?;
return Some((usage_fields(u), false));
}
let u = v.get("usage")?;
Some((usage_fields(u), true))
}
fn usage_fields(u: &Value) -> Usage {
let cache_creation = u.get("cache_creation");
let eph_5m = cache_creation
.and_then(|c| c.get("ephemeral_5m_input_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0);
let eph_1h = cache_creation
.and_then(|c| c.get("ephemeral_1h_input_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0);
Usage {
input_tokens: u.get("input_tokens").and_then(Value::as_u64).unwrap_or(0),
cache_read: u
.get("cache_read_input_tokens")
.and_then(Value::as_u64)
.unwrap_or(0),
cache_write: u
.get("cache_creation_input_tokens")
.and_then(Value::as_u64)
.unwrap_or(0),
eph_5m,
eph_1h,
output_tokens: u.get("output_tokens").and_then(Value::as_u64).unwrap_or(0),
}
}
#[derive(Clone, Debug, Default)]
pub struct PricingInputs {
pub batch: bool,
pub fast_mode: bool,
pub inference_geo: Option<String>,
}
pub fn derive_pricing_inputs(body: &Value, headers: &axum::http::HeaderMap) -> PricingInputs {
let batch = body.get("batch").and_then(Value::as_bool).unwrap_or(false);
let fast_mode = body
.get("fast_mode")
.and_then(Value::as_bool)
.unwrap_or(false);
let inference_geo = headers
.get("x-openlatch-inference-geo")
.and_then(|v| v.to_str().ok())
.map(|s| s.trim().to_ascii_lowercase())
.filter(|s| !s.is_empty());
PricingInputs {
batch,
fast_mode,
inference_geo,
}
}
pub fn model_of(body: &Value) -> Option<String> {
body.get("model")
.and_then(Value::as_str)
.map(str::to_string)
}
pub fn has_cache_breakpoint(raw_body: &[u8]) -> bool {
memmem(raw_body, b"\"cache_control\"")
}
pub fn infer_cache_preserved(usage: &Usage) -> bool {
usage.cache_read > 0
}
fn memmem(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || haystack.len() < needle.len() {
return false;
}
haystack.windows(needle.len()).any(|w| w == needle)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn c3_input_is_not_the_total() {
let chunk = br#"data: {"type":"message_start","message":{"usage":{"input_tokens":50,"cache_read_input_tokens":100000,"cache_creation_input_tokens":0,"output_tokens":1}}}"#;
let mut acc = UsageAccumulator::default();
assert!(acc.scan_chunk(chunk));
let u = acc.usage();
assert_eq!(u.input_tokens, 50);
assert_eq!(u.cache_read, 100_000);
let total_input = u.input_tokens + u.cache_write + u.cache_read;
assert_eq!(
total_input, 100_050,
"total input must be input + cache_creation + cache_read (C-3)"
);
}
#[test]
fn merges_message_start_and_message_delta() {
let start = br#"data: {"type":"message_start","message":{"usage":{"input_tokens":10,"cache_read_input_tokens":5,"cache_creation_input_tokens":8,"cache_creation":{"ephemeral_5m_input_tokens":6,"ephemeral_1h_input_tokens":2},"output_tokens":1}}}"#;
let delta = br#"data: {"type":"message_delta","usage":{"output_tokens":321}}"#;
let mut acc = UsageAccumulator::default();
acc.scan_chunk(start);
acc.scan_chunk(delta);
let u = acc.usage();
assert_eq!(u.input_tokens, 10);
assert_eq!(u.cache_read, 5);
assert_eq!(u.cache_write, 8);
assert_eq!(u.eph_5m, 6);
assert_eq!(u.eph_1h, 2);
assert_eq!(u.output_tokens, 321);
assert!(acc.has_usage());
}
#[test]
fn message_start_is_not_terminal_until_message_delta() {
let start = br#"data: {"type":"message_start","message":{"usage":{"input_tokens":10,"cache_read_input_tokens":5,"output_tokens":1}}}"#;
let mut acc = UsageAccumulator::default();
assert!(
acc.scan_chunk(start),
"message_start contributes input/cache usage"
);
assert!(acc.has_usage(), "usage WAS observed");
assert!(
!acc.is_terminal(),
"but message_start is NOT terminal — output is preliminary (=1)"
);
let delta = br#"data: {"type":"message_delta","usage":{"output_tokens":321}}"#;
acc.scan_chunk(delta);
assert!(acc.is_terminal(), "message_delta IS terminal");
assert_eq!(
acc.usage().output_tokens,
321,
"the terminal output overrides the preliminary 1"
);
}
#[test]
fn non_streaming_body_is_terminal() {
let body =
br#"{"id":"msg_1","type":"message","usage":{"input_tokens":42,"output_tokens":7}}"#;
let mut acc = UsageAccumulator::default();
assert!(acc.scan_chunk(body));
assert!(acc.is_terminal());
}
#[test]
fn ephemeral_5m_1h_split_captured() {
let chunk = br#"data: {"usage":{"input_tokens":0,"cache_creation_input_tokens":100,"cache_creation":{"ephemeral_5m_input_tokens":80,"ephemeral_1h_input_tokens":20},"output_tokens":0}}"#;
let mut acc = UsageAccumulator::default();
acc.scan_chunk(chunk);
let u = acc.usage();
assert_eq!(u.eph_5m, 80);
assert_eq!(u.eph_1h, 20);
assert_eq!(u.eph_5m + u.eph_1h, u.cache_write);
}
#[test]
fn non_streaming_body_usage() {
let body = br#"{"id":"msg_1","usage":{"input_tokens":42,"output_tokens":7}}"#;
let mut acc = UsageAccumulator::default();
assert!(acc.scan_chunk(body));
assert_eq!(acc.usage().input_tokens, 42);
assert_eq!(acc.usage().output_tokens, 7);
}
#[test]
fn non_usage_chunk_is_ignored() {
let mut acc = UsageAccumulator::default();
assert!(!acc.scan_chunk(b"data: {\"type\":\"content_block_delta\"}\n\n"));
assert!(!acc.has_usage());
}
#[test]
fn cache_preserved_is_read_gated() {
assert!(infer_cache_preserved(&Usage {
cache_read: 1,
..Default::default()
}));
assert!(!infer_cache_preserved(&Usage::default()));
}
#[test]
fn pricing_inputs_default_conservative() {
let body = serde_json::json!({"model":"claude-opus-4-8","messages":[]});
let p = derive_pricing_inputs(&body, &axum::http::HeaderMap::new());
assert!(!p.batch);
assert!(!p.fast_mode);
assert!(p.inference_geo.is_none());
}
#[test]
fn breakpoint_detection() {
assert!(has_cache_breakpoint(
br#"{"system":[{"type":"text","cache_control":{"type":"ephemeral"}}]}"#
));
assert!(!has_cache_breakpoint(br#"{"messages":[]}"#));
}
}