use serde_json::Value;
use tokn_core::db::{Usage, UsageDetails, UsageType};
pub fn parse_usage_any_value(v: &Value) -> Usage {
if let Some(usage) = parse_anthropic_usage(v.pointer("/message/usage")) {
return usage;
}
if let Some(usage) = parse_openai_responses_usage(v.pointer("/response/usage")) {
return usage;
}
match detect_usage_type(v.pointer("/usage")) {
Some(UsageType::Chat) => {
if let Some(usage) = parse_openai_chat_usage(v.pointer("/usage")) {
return usage;
}
}
Some(UsageType::Responses) => {
if let Some(usage) = parse_openai_responses_usage(v.pointer("/usage")) {
return usage;
}
}
Some(UsageType::Messages) => {
if let Some(usage) = parse_anthropic_usage(v.pointer("/usage")) {
return usage;
}
}
None => {}
}
if let Some(usage) = parse_openai_chat_usage(v.pointer("/usage")) {
return usage;
}
if let Some(usage) = parse_openai_responses_usage(v.pointer("/usage")) {
return usage;
}
if let Some(usage) = parse_anthropic_usage(v.pointer("/usage")) {
return usage;
}
Usage::default()
}
pub fn usage_has_any(usage: &Usage) -> bool {
usage.input_tokens.is_some()
|| usage.output_tokens.is_some()
|| usage.total_tokens.is_some()
|| usage.details.cache_read.is_some()
|| usage.details.cache_write.is_some()
|| usage.details.reasoning.is_some()
}
fn ptr_u64(v: Option<&Value>, path: &str) -> Option<u64> {
v.and_then(|value| value.pointer(path)).and_then(Value::as_u64)
}
fn detect_usage_type(u: Option<&Value>) -> Option<UsageType> {
let u = u?;
if has_anthropic_usage_markers(u) {
return Some(UsageType::Messages);
}
if ptr_u64(Some(u), "/prompt_tokens").is_some() || ptr_u64(Some(u), "/completion_tokens").is_some() {
return Some(UsageType::Chat);
}
if ptr_u64(Some(u), "/input_tokens").is_some() || ptr_u64(Some(u), "/output_tokens").is_some() {
return Some(UsageType::Responses);
}
None
}
fn has_anthropic_usage_markers(u: &Value) -> bool {
ptr_u64(Some(u), "/cache_creation_input_tokens").is_some() || ptr_u64(Some(u), "/cache_read_input_tokens").is_some()
}
fn parse_openai_chat_usage(u: Option<&Value>) -> Option<Usage> {
let input_tokens = ptr_u64(u, "/prompt_tokens");
let output_tokens = ptr_u64(u, "/completion_tokens");
let total_tokens = ptr_u64(u, "/total_tokens");
if input_tokens.is_none() && output_tokens.is_none() {
return None;
}
let cache_read = ptr_u64(u, "/prompt_tokens_details/cached_tokens");
let reasoning = ptr_u64(u, "/completion_tokens_details/reasoning_tokens");
Some(Usage {
input_tokens,
output_tokens,
total_tokens,
usage_type: Some(UsageType::Chat),
details: UsageDetails {
cache_read,
cache_write: None,
reasoning,
},
})
}
fn parse_openai_responses_usage(u: Option<&Value>) -> Option<Usage> {
let input_tokens = ptr_u64(u, "/input_tokens");
let output_tokens = ptr_u64(u, "/output_tokens");
let total_tokens = ptr_u64(u, "/total_tokens");
if input_tokens.is_none() && output_tokens.is_none() {
return None;
}
let cache_read = ptr_u64(u, "/input_tokens_details/cached_tokens");
let reasoning = ptr_u64(u, "/output_tokens_details/reasoning_tokens");
Some(Usage {
input_tokens,
output_tokens,
total_tokens,
usage_type: Some(UsageType::Responses),
details: UsageDetails {
cache_read,
cache_write: None,
reasoning,
},
})
}
fn parse_anthropic_usage(u: Option<&Value>) -> Option<Usage> {
let raw_input = ptr_u64(u, "/input_tokens");
let cache_write = ptr_u64(u, "/cache_creation_input_tokens");
let cache_read = ptr_u64(u, "/cache_read_input_tokens");
if !has_anthropic_usage_markers(u?) {
return None;
}
let output_tokens = ptr_u64(u, "/output_tokens");
let total_input = match (raw_input, cache_write, cache_read) {
(None, None, None) => None,
(a, b, c) => Some(a.unwrap_or(0) + b.unwrap_or(0) + c.unwrap_or(0)),
};
Some(Usage {
input_tokens: total_input,
output_tokens,
total_tokens: ptr_u64(u, "/total_tokens"),
usage_type: Some(UsageType::Messages),
details: UsageDetails {
cache_read,
cache_write,
reasoning: None,
},
})
}
pub fn parse_usage_any_json(bytes: &[u8]) -> Usage {
let v: Value = match serde_json::from_slice(bytes) {
Ok(v) => v,
Err(_) => return Usage::default(),
};
parse_usage_any_value(&v)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parses_openai_chat_usage() {
let v = json!({ "usage": { "prompt_tokens": 11, "completion_tokens": 22 }});
let u = parse_usage_any_value(&v);
assert_eq!(u.input_tokens, Some(11));
assert_eq!(u.output_tokens, Some(22));
assert_eq!(u.total_tokens, None);
assert_eq!(u.usage_type, Some(UsageType::Chat));
assert_eq!(u.details.cache_read, None);
assert_eq!(u.details.cache_write, None);
assert_eq!(u.details.reasoning, None);
}
#[test]
fn parses_responses_usage_shape() {
let v = json!({ "usage": { "input_tokens": 5, "output_tokens": 7 }});
let u = parse_usage_any_value(&v);
assert_eq!(u.input_tokens, Some(5));
assert_eq!(u.output_tokens, Some(7));
assert_eq!(u.total_tokens, None);
assert_eq!(u.usage_type, Some(UsageType::Responses));
}
#[test]
fn parses_anthropic_message_start_nested_usage() {
let v = json!({
"type": "message_start",
"message": { "usage": {
"input_tokens": 9,
"output_tokens": 1,
"cache_creation_input_tokens": 4,
"cache_read_input_tokens": 2
}}
});
let u = parse_usage_any_value(&v);
assert_eq!(u.input_tokens, Some(15));
assert_eq!(u.output_tokens, Some(1));
assert_eq!(u.total_tokens, None);
assert_eq!(u.usage_type, Some(UsageType::Messages));
assert_eq!(u.details.cache_read, Some(2));
assert_eq!(u.details.cache_write, Some(4));
}
#[test]
fn parses_responses_response_completed_nested_usage() {
let v = json!({
"type": "response.completed",
"response": { "usage": { "input_tokens": 3, "output_tokens": 4 }}
});
let u = parse_usage_any_value(&v);
assert_eq!(u.input_tokens, Some(3));
assert_eq!(u.output_tokens, Some(4));
assert_eq!(u.total_tokens, None);
assert_eq!(u.usage_type, Some(UsageType::Responses));
}
#[test]
fn parses_openai_cached_and_reasoning_tokens() {
let v = json!({ "usage": {
"prompt_tokens": 100,
"completion_tokens": 50,
"prompt_tokens_details": { "cached_tokens": 30 },
"completion_tokens_details": { "reasoning_tokens": 20 }
}});
let u = parse_usage_any_value(&v);
assert_eq!(u.input_tokens, Some(100));
assert_eq!(u.output_tokens, Some(50));
assert_eq!(u.total_tokens, None);
assert_eq!(u.usage_type, Some(UsageType::Chat));
assert_eq!(u.details.cache_read, Some(30));
assert_eq!(u.details.cache_write, None);
assert_eq!(u.details.reasoning, Some(20));
}
#[test]
fn parses_codex_usage_details_shape() {
let v = json!({ "usage": {
"input_tokens": 35973,
"input_tokens_details": { "cached_tokens": 34176 },
"output_tokens": 989,
"output_tokens_details": { "reasoning_tokens": 11 },
"total_tokens": 36962
}});
let u = parse_usage_any_value(&v);
assert_eq!(u.input_tokens, Some(35973));
assert_eq!(u.output_tokens, Some(989));
assert_eq!(u.total_tokens, Some(36962));
assert_eq!(u.usage_type, Some(UsageType::Responses));
assert_eq!(u.details.cache_read, Some(34176));
assert_eq!(u.details.cache_write, None);
assert_eq!(u.details.reasoning, Some(11));
}
#[test]
fn top_level_anthropic_usage_is_detected_before_openai_responses() {
let v = json!({
"type": "message_delta",
"usage": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input_tokens": 8,
"output_tokens": 23
}
});
let u = parse_usage_any_value(&v);
assert_eq!(u.input_tokens, Some(8));
assert_eq!(u.output_tokens, Some(23));
assert_eq!(u.usage_type, Some(UsageType::Messages));
assert_eq!(u.details.cache_read, Some(0));
assert_eq!(u.details.cache_write, Some(0));
}
#[test]
fn returns_default_on_unknown_shape() {
let v = json!({ "foo": "bar" });
let u = parse_usage_any_value(&v);
assert!(!usage_has_any(&u));
}
#[test]
fn json_helper_handles_invalid_input() {
let u = parse_usage_any_json(b"not json");
assert!(!usage_has_any(&u));
}
}