use serde_json::Value;
#[derive(Debug, Default)]
pub struct TokenCounts {
pub input_tokens: i64,
pub output_tokens: i64,
pub reasoning_tokens: i64,
pub cache_read: i64,
pub cache_creation: i64,
pub cache_creation_5m: i64,
pub cache_creation_1h: i64,
pub web_search_requests: i64,
pub total: i64,
pub above_input: i64,
pub above_output: i64,
pub above_reasoning: i64,
pub above_cache_read: i64,
pub above_cache_creation_5m: i64,
pub above_cache_creation_1h: i64,
}
impl TokenCounts {
pub fn has_activity(&self) -> bool {
self.total != 0
|| self.input_tokens != 0
|| self.output_tokens != 0
|| self.reasoning_tokens != 0
|| self.cache_read != 0
|| self.cache_creation != 0
|| self.cache_creation_5m != 0
|| self.cache_creation_1h != 0
|| self.web_search_requests != 0
}
}
pub fn extract_token_counts(usage: &Value) -> TokenCounts {
let mut counts = TokenCounts::default();
if let Some(usage_obj) = usage.as_object() {
if let Some(input) = usage_obj.get("input_tokens").and_then(|v| v.as_i64()) {
counts.input_tokens = input;
}
if let Some(output) = usage_obj.get("output_tokens").and_then(|v| v.as_i64()) {
counts.output_tokens = output;
}
if let Some(cache_read) = usage_obj
.get("cache_read_input_tokens")
.and_then(|v| v.as_i64())
{
counts.cache_read = cache_read;
}
if let Some(cache_creation) = usage_obj
.get("cache_creation_input_tokens")
.and_then(|v| v.as_i64())
{
counts.cache_creation = cache_creation;
}
if let Some(server_tool_use) = usage_obj.get("server_tool_use").and_then(|v| v.as_object())
{
counts.web_search_requests = server_tool_use
.get("web_search_requests")
.and_then(|v| v.as_i64())
.unwrap_or(0);
}
if let Some(above) = usage_obj.get("above_tier").and_then(|v| v.as_object()) {
let field = |key: &str| above.get(key).and_then(|v| v.as_i64()).unwrap_or(0);
counts.above_input = field("input_tokens");
counts.above_output = field("output_tokens");
counts.above_reasoning = field("reasoning_tokens");
counts.above_cache_read = field("cache_read_tokens");
counts.above_cache_creation_5m = field("cache_creation_5m_tokens");
counts.above_cache_creation_1h = field("cache_creation_1h_tokens");
}
let thoughts = usage_obj
.get("thoughts_tokens")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let reasoning_output = usage_obj
.get("reasoning_output_tokens")
.and_then(|v| v.as_i64())
.unwrap_or(0);
counts.reasoning_tokens = thoughts + reasoning_output;
if let Some(cc_split) = usage_obj.get("cache_creation").and_then(|v| v.as_object()) {
let ephemeral_5m = cc_split
.get("ephemeral_5m_input_tokens")
.and_then(|v| v.as_i64())
.unwrap_or(0);
counts.cache_creation_1h = cc_split
.get("ephemeral_1h_input_tokens")
.and_then(|v| v.as_i64())
.unwrap_or(0);
counts.cache_creation = counts
.cache_creation
.max(ephemeral_5m + counts.cache_creation_1h);
counts.cache_creation_5m = counts.cache_creation - counts.cache_creation_1h;
} else {
counts.cache_creation_5m = counts.cache_creation;
}
if let Some(total_usage) = usage_obj
.get("total_token_usage")
.and_then(|v| v.as_object())
{
let raw_input = total_usage
.get("input_tokens")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let cached = total_usage
.get("cached_input_tokens")
.and_then(|v| v.as_i64())
.unwrap_or(0);
counts.input_tokens = (raw_input - cached).max(0);
counts.cache_read = cached;
if let Some(output) = total_usage.get("output_tokens").and_then(|v| v.as_i64()) {
counts.output_tokens = output;
}
if let Some(reasoning) = total_usage
.get("reasoning_output_tokens")
.and_then(|v| v.as_i64())
{
counts.reasoning_tokens = reasoning;
}
counts.output_tokens = (counts.output_tokens - counts.reasoning_tokens).max(0);
if let Some(total) = total_usage.get("total_tokens").and_then(|v| v.as_i64()) {
counts.total = total;
return counts;
}
}
let tool_tokens = usage_obj
.get("tool_tokens")
.and_then(|value| value.as_i64())
.unwrap_or(0);
let derived_total = counts.input_tokens
+ counts.output_tokens
+ counts.reasoning_tokens
+ counts.cache_read
+ counts.cache_creation
+ tool_tokens;
counts.total = usage_obj
.get("total_tokens")
.and_then(|value| value.as_i64())
.map_or(derived_total, |published| published.max(derived_total));
}
counts
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn claude_format_without_ttl_split_defaults_to_5m() {
let usage = json!({
"input_tokens": 100,
"output_tokens": 50,
"cache_read_input_tokens": 200,
"cache_creation_input_tokens": 10_000
});
let c = extract_token_counts(&usage);
assert_eq!(c.cache_creation, 10_000);
assert_eq!(c.cache_creation_5m, 10_000);
assert_eq!(c.cache_creation_1h, 0);
}
#[test]
fn claude_code_ttl_split_is_preserved() {
let usage = json!({
"input_tokens": 6,
"output_tokens": 866,
"cache_read_input_tokens": 16_651,
"cache_creation_input_tokens": 10_338,
"cache_creation": {
"ephemeral_5m_input_tokens": 0,
"ephemeral_1h_input_tokens": 10_338
}
});
let c = extract_token_counts(&usage);
assert_eq!(c.cache_creation, 10_338);
assert_eq!(c.cache_creation_5m, 0);
assert_eq!(c.cache_creation_1h, 10_338);
}
#[test]
fn merged_thoughts_and_reasoning_output_are_summed() {
let gemini_only = json!({ "input_tokens": 10, "thoughts_tokens": 30 });
assert_eq!(extract_token_counts(&gemini_only).reasoning_tokens, 30);
let flat_only = json!({ "input_tokens": 10, "reasoning_output_tokens": 7 });
assert_eq!(extract_token_counts(&flat_only).reasoning_tokens, 7);
let merged = json!({
"input_tokens": 100,
"output_tokens": 50,
"thoughts_tokens": 30,
"reasoning_output_tokens": 7,
});
assert_eq!(extract_token_counts(&merged).reasoning_tokens, 37);
}
#[test]
fn ttl_split_mixed_5m_and_1h() {
let usage = json!({
"cache_creation_input_tokens": 1_500,
"cache_creation": {
"ephemeral_5m_input_tokens": 500,
"ephemeral_1h_input_tokens": 1_000
}
});
let c = extract_token_counts(&usage);
assert_eq!(c.cache_creation, 1_500);
assert_eq!(c.cache_creation_5m, 500);
assert_eq!(c.cache_creation_1h, 1_000);
}
#[test]
fn ttl_split_smaller_than_scalar_bills_the_remainder_at_5m() {
let usage = json!({
"cache_creation_input_tokens": 397_602_459_i64,
"cache_creation": {
"ephemeral_5m_input_tokens": 183_962_961_i64,
"ephemeral_1h_input_tokens": 213_022_705_i64
}
});
let c = extract_token_counts(&usage);
assert_eq!(c.cache_creation, 397_602_459);
assert_eq!(c.cache_creation_1h, 213_022_705);
assert_eq!(c.cache_creation_5m, 397_602_459 - 213_022_705);
assert_eq!(c.cache_creation_5m + c.cache_creation_1h, c.cache_creation);
}
#[test]
fn ttl_split_derives_total_when_scalar_missing() {
let usage = json!({
"cache_creation": {
"ephemeral_5m_input_tokens": 200,
"ephemeral_1h_input_tokens": 300
}
});
let c = extract_token_counts(&usage);
assert_eq!(c.cache_creation, 500);
assert_eq!(c.cache_creation_5m, 200);
assert_eq!(c.cache_creation_1h, 300);
}
#[test]
fn codex_format_no_cache_creation() {
let usage = json!({
"total_token_usage": {
"input_tokens": 1000,
"output_tokens": 500,
"cached_input_tokens": 200,
"total_tokens": 1500
}
});
let c = extract_token_counts(&usage);
assert_eq!(c.cache_creation, 0);
assert_eq!(c.cache_creation_5m, 0);
assert_eq!(c.cache_creation_1h, 0);
assert_eq!(c.cache_read, 200);
assert_eq!(c.input_tokens, 800);
}
#[test]
fn codex_input_tokens_excludes_cached_bucket() {
let usage = json!({
"total_token_usage": {
"input_tokens": 576_145,
"cached_input_tokens": 408_832,
"output_tokens": 13_156,
"reasoning_output_tokens": 8_591,
"total_tokens": 589_301
}
});
let c = extract_token_counts(&usage);
assert_eq!(c.input_tokens, 576_145 - 408_832);
assert_eq!(c.cache_read, 408_832);
assert_eq!(c.output_tokens, 13_156 - 8_591);
assert_eq!(c.reasoning_tokens, 8_591);
assert_eq!(c.total, 589_301);
}
#[test]
fn codex_cached_exceeding_input_clamps_to_zero() {
let usage = json!({
"total_token_usage": {
"input_tokens": 100,
"cached_input_tokens": 500,
"output_tokens": 50,
"total_tokens": 150
}
});
let c = extract_token_counts(&usage);
assert_eq!(c.input_tokens, 0);
assert_eq!(c.cache_read, 500);
}
#[test]
fn gemini_thoughts_tokens_populate_reasoning_bucket() {
let usage = json!({
"input_tokens": 13_906,
"output_tokens": 185,
"cache_read_input_tokens": 0,
"thoughts_tokens": 306,
"tool_tokens": 0,
"total_tokens": 14_397
});
let c = extract_token_counts(&usage);
assert_eq!(c.input_tokens, 13_906);
assert_eq!(c.output_tokens, 185);
assert_eq!(c.reasoning_tokens, 306);
assert_eq!(c.total, 14_397);
}
#[test]
fn gemini_tool_only_usage_remains_active() {
let usage = json!({
"input_tokens": 0,
"output_tokens": 0,
"thoughts_tokens": 0,
"tool_tokens": 5,
"total_tokens": 5
});
let counts = extract_token_counts(&usage);
assert_eq!(counts.total, 5);
assert_eq!(counts.input_tokens, 0);
assert_eq!(counts.output_tokens, 0);
}
#[test]
fn codex_reasoning_is_split_out_of_output() {
let usage = json!({
"total_token_usage": {
"input_tokens": 5_645,
"cached_input_tokens": 5_504,
"output_tokens": 810,
"reasoning_output_tokens": 640,
"total_tokens": 6_455
}
});
let c = extract_token_counts(&usage);
assert_eq!(c.input_tokens, 5_645 - 5_504);
assert_eq!(c.cache_read, 5_504);
assert_eq!(
c.output_tokens,
810 - 640,
"reasoning must be subtracted out of output, not double-counted"
);
assert_eq!(c.reasoning_tokens, 640);
assert_eq!(c.total, 6_455);
}
#[test]
fn codex_real_world_reasoning_subset_of_output() {
let usage = json!({
"total_token_usage": {
"input_tokens": 73_353,
"cached_input_tokens": 31_744,
"output_tokens": 508,
"reasoning_output_tokens": 255,
"total_tokens": 73_861
}
});
let c = extract_token_counts(&usage);
assert_eq!(c.input_tokens, 73_353 - 31_744);
assert_eq!(c.cache_read, 31_744);
assert_eq!(c.output_tokens, 508 - 255);
assert_eq!(c.reasoning_tokens, 255);
assert_eq!(c.total, 73_861);
}
#[test]
fn claude_server_tool_use_web_search_is_extracted() {
let with_search = json!({
"input_tokens": 100,
"output_tokens": 50,
"server_tool_use": {
"web_search_requests": 3,
"web_fetch_requests": 1
}
});
assert_eq!(extract_token_counts(&with_search).web_search_requests, 3);
let without = json!({ "input_tokens": 100, "output_tokens": 50 });
assert_eq!(extract_token_counts(&without).web_search_requests, 0);
}
#[test]
fn above_tier_slices_are_extracted_for_both_shapes() {
let flat = json!({
"input_tokens": 300_000,
"output_tokens": 1_000,
"cache_read_input_tokens": 50_000,
"above_tier": {
"input_tokens": 280_000,
"output_tokens": 600,
"cache_read_tokens": 50_000
}
});
let c = extract_token_counts(&flat);
assert_eq!(c.input_tokens, 300_000);
assert_eq!(c.above_input, 280_000);
assert_eq!(c.above_output, 600);
assert_eq!(c.above_cache_read, 50_000);
assert_eq!(c.above_cache_creation_5m, 0);
let codex = json!({
"total_token_usage": {
"input_tokens": 400_000,
"cached_input_tokens": 100_000,
"output_tokens": 2_000,
"reasoning_output_tokens": 500,
"total_tokens": 402_000
},
"above_tier": {
"input_tokens": 250_000,
"cache_read_tokens": 80_000,
"output_tokens": 900,
"reasoning_tokens": 300
}
});
let c = extract_token_counts(&codex);
assert_eq!(c.total, 402_000);
assert_eq!(c.above_input, 250_000);
assert_eq!(c.above_cache_read, 80_000);
assert_eq!(c.above_output, 900);
assert_eq!(c.above_reasoning, 300);
}
#[test]
fn copilot_reasoning_output_tokens_populate_reasoning_bucket() {
let usage = json!({
"input_tokens": 2_000,
"output_tokens": 300,
"cache_read_input_tokens": 100,
"cache_creation_input_tokens": 0,
"reasoning_output_tokens": 150
});
let c = extract_token_counts(&usage);
assert_eq!(c.output_tokens, 300);
assert_eq!(c.reasoning_tokens, 150);
assert_eq!(c.total, 2_000 + 300 + 150 + 100);
}
}