Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.direnv
!.envrc
.pre-commit-config.yaml
.npmrc

# generated local agent skills
.claude/skills
10 changes: 8 additions & 2 deletions docs/guide/cost-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ When calculating costs from tokens, ccusage uses:
type TokenCosts = {
input: number; // Input tokens
output: number; // Output tokens
cacheCreate: number; // Cache creation tokens
cacheCreate5m: number; // 5-minute cache creation tokens
cacheCreate1h: number; // 1-hour cache creation tokens
cacheRead: number; // Cache read tokens
};
```
Expand All @@ -197,10 +198,15 @@ type TokenCosts = {
totalCost =
inputTokens * inputPrice +
outputTokens * outputPrice +
cacheCreateTokens * cacheCreatePrice +
cacheCreate5mTokens * cacheCreatePrice +
cacheCreate1hTokens * inputPrice * 2 +
cacheReadTokens * cacheReadPrice;
```

When Claude Code records do not include the `cache_creation` duration
breakdown, ccusage falls back to pricing `cache_creation_input_tokens` at the
standard cache creation rate.

### Pre-calculated Costs

Claude Code provides `costUSD` values in JSONL files:
Expand Down
4 changes: 4 additions & 0 deletions rust/crates/ccusage/src/adapter/amp/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ fn parse_ledger_events(
cache_creation_input_tokens: cache.0,
cache_read_input_tokens: cache.1,
speed: None,
cache_creation: None,
};
let total_tokens = json_value_u64(tokens.get("total"));
let (usage, extra_total_tokens) = apply_total_token_fallback(usage, 0, total_tokens);
Expand Down Expand Up @@ -109,6 +110,7 @@ fn parse_ledger_events(
.usage
.output_tokens
.saturating_add(extra_total_tokens),
cache_creation: None,
..data.message.usage
},
..data.message.clone()
Expand Down Expand Up @@ -179,6 +181,7 @@ fn parse_message_usage(
cache_creation_input_tokens: json_value_u64(usage.get("cacheCreationInputTokens")),
cache_read_input_tokens: json_value_u64(usage.get("cacheReadInputTokens")),
speed: None,
cache_creation: None,
};
let total_tokens = json_value_u64(usage.get("totalTokens"));
let (usage_raw, extra_total_tokens) =
Expand Down Expand Up @@ -218,6 +221,7 @@ fn parse_message_usage(
.usage
.output_tokens
.saturating_add(extra_total_tokens),
cache_creation: None,
..data.message.usage
},
..data.message.clone()
Expand Down
5 changes: 3 additions & 2 deletions rust/crates/ccusage/src/adapter/claude/daily.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ fn is_valid_daily_usage_entry(data: &DailyUsageEntry) -> bool {
fn daily_usage_token_total(entry: &DailyLoadedEntry) -> u64 {
entry.usage.input_tokens
+ entry.usage.output_tokens
+ entry.usage.cache_creation_input_tokens
+ entry.usage.cache_creation_token_count()
+ entry.usage.cache_read_input_tokens
}

Expand Down Expand Up @@ -470,7 +470,7 @@ impl DailyAccumulator {
let breakdown = &mut self.breakdowns[index];
breakdown.input_tokens += entry.usage.input_tokens;
breakdown.output_tokens += entry.usage.output_tokens;
breakdown.cache_creation_tokens += entry.usage.cache_creation_input_tokens;
breakdown.cache_creation_tokens += entry.usage.cache_creation_token_count();
breakdown.cache_read_tokens += entry.usage.cache_read_input_tokens;
breakdown.cost += entry.cost;
if entry.missing_pricing_model.is_some() {
Expand Down Expand Up @@ -590,6 +590,7 @@ mod tests {
cache_creation_input_tokens: 0,
cache_read_input_tokens: fixture.cache_read_tokens,
speed: None,
cache_creation: None,
},
cost: 0.0,
model: Some("claude-sonnet-4-20250514".to_string()),
Expand Down
3 changes: 2 additions & 1 deletion rust/crates/ccusage/src/adapter/claude/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ fn usage_token_total(data: &UsageEntry) -> u64 {
let usage = data.message.usage;
usage.input_tokens
+ usage.output_tokens
+ usage.cache_creation_input_tokens
+ usage.cache_creation_token_count()
+ usage.cache_read_input_tokens
}

Expand Down Expand Up @@ -761,6 +761,7 @@ mod tests {
cache_creation_input_tokens: 0,
cache_read_input_tokens: fixture.cache_read_tokens,
speed: None,
cache_creation: None,
},
model: Some("claude-sonnet-4-20250514".to_string()),
id: Some(fixture.message_id.to_string()),
Expand Down
1 change: 1 addition & 0 deletions rust/crates/ccusage/src/adapter/codebuff/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ mod tests {
cache_creation_input_tokens: 20,
cache_read_input_tokens: 10,
speed: None,
cache_creation: None,
},
model: Some("claude-sonnet-4-20250514".to_string()),
id: Some("message-a".to_string()),
Expand Down
4 changes: 4 additions & 0 deletions rust/crates/ccusage/src/adapter/codebuff/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pub(super) fn load_chat_file(path: &Path) -> Result<Vec<CodebuffEntry>> {
cache_creation_input_tokens: usage.cache_creation_input_tokens,
cache_read_input_tokens: usage.cache_read_input_tokens,
speed: None,
cache_creation: None,
},
extra_total_tokens: usage.extra_total_tokens,
dedup_key,
Expand Down Expand Up @@ -251,6 +252,7 @@ pub(super) fn parse_usage_object(value: Option<&Value>) -> AssistantUsage {
cache_creation_input_tokens: usage.cache_creation_input_tokens,
cache_read_input_tokens: usage.cache_read_input_tokens,
speed: None,
cache_creation: None,
};
let (raw_usage, extra_total_tokens) =
apply_total_token_fallback(raw_usage, usage.extra_total_tokens, total_tokens);
Expand Down Expand Up @@ -396,6 +398,7 @@ pub(super) fn calculate_codebuff_cost(entry: &CodebuffEntry, pricing: &PricingMa
.usage
.output_tokens
.saturating_add(entry.extra_total_tokens),
cache_creation: None,
..entry.usage
};
let raw = calculate_cost_for_usage(
Expand Down Expand Up @@ -429,6 +432,7 @@ pub(super) fn missing_codebuff_pricing(
.usage
.output_tokens
.saturating_add(entry.extra_total_tokens),
cache_creation: None,
..entry.usage
};
let mut candidates = vec![entry.model.clone()];
Expand Down
2 changes: 2 additions & 0 deletions rust/crates/ccusage/src/adapter/copilot/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ fn usage_entry_to_loaded(
cache_creation_input_tokens: entry.cache_creation_tokens,
cache_read_input_tokens: entry.cache_read_tokens,
speed: None,
cache_creation: None,
};
let cost_usage = TokenUsageRaw {
output_tokens: entry.output_tokens + entry.reasoning_output_tokens,
cache_creation: None,
..usage
};
let data = UsageEntry {
Expand Down
1 change: 1 addition & 0 deletions rust/crates/ccusage/src/adapter/copilot/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ fn to_candidate(
cache_creation_input_tokens: cache_creation,
cache_read_input_tokens: cache_read,
speed: None,
cache_creation: None,
};
let (usage, reasoning) = apply_total_token_fallback(usage, reasoning, total);
if crate::total_usage_tokens(usage) + reasoning == 0 {
Expand Down
1 change: 1 addition & 0 deletions rust/crates/ccusage/src/adapter/droid/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ mod tests {
cache_creation_input_tokens: 20,
cache_read_input_tokens: 10,
speed: None,
cache_creation: None,
},
model: Some("claude-sonnet-4".to_string()),
id: Some("droid:session-a".to_string()),
Expand Down
4 changes: 4 additions & 0 deletions rust/crates/ccusage/src/adapter/droid/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pub(super) fn load_settings_file(path: &Path) -> Result<Option<DroidEntry>> {
cache_creation_input_tokens: usage.cache_creation_tokens,
cache_read_input_tokens: usage.cache_read_tokens,
speed: None,
cache_creation: None,
},
reasoning_tokens: usage.thinking_tokens,
}))
Expand All @@ -97,6 +98,7 @@ pub(super) fn parse_token_usage(value: Option<&Value>) -> Option<DroidTokenUsage
cache_creation_input_tokens: json_value_u64(usage.get("cacheCreationTokens")),
cache_read_input_tokens: json_value_u64(usage.get("cacheReadTokens")),
speed: None,
cache_creation: None,
};
let thinking_tokens = json_value_u64(usage.get("thinkingTokens"));
let total_tokens = json_value_u64(usage.get("totalTokens"));
Expand Down Expand Up @@ -140,6 +142,7 @@ fn settings_timestamp(
pub(super) fn calculate_droid_cost(entry: &DroidEntry, pricing: &PricingMap) -> f64 {
let usage = TokenUsageRaw {
output_tokens: entry.usage.output_tokens + entry.reasoning_tokens,
cache_creation: None,
..entry.usage
};
for candidate in droid_model_candidates(entry) {
Expand All @@ -160,6 +163,7 @@ pub(super) fn calculate_droid_cost(entry: &DroidEntry, pricing: &PricingMap) ->
pub(super) fn missing_droid_pricing(entry: &DroidEntry, pricing: &PricingMap) -> Option<String> {
let usage = TokenUsageRaw {
output_tokens: entry.usage.output_tokens + entry.reasoning_tokens,
cache_creation: None,
..entry.usage
};
missing_pricing_model_for_candidates(
Expand Down
3 changes: 3 additions & 0 deletions rust/crates/ccusage/src/adapter/gemini/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ fn build_event(
cache_creation_input_tokens: 0,
cache_read_input_tokens: cache_read_tokens,
speed: None,
cache_creation: None,
};
let (display_usage, extra_total_tokens) =
apply_total_token_fallback(display_usage, tokens.thoughts, total_tokens);
Expand Down Expand Up @@ -344,9 +345,11 @@ pub(super) fn event_to_loaded(
cache_creation_input_tokens: 0,
cache_read_input_tokens: event.cache_read_tokens,
speed: None,
cache_creation: None,
};
let cost_usage = TokenUsageRaw {
output_tokens: event.output_tokens + event.reasoning_tokens,
cache_creation: None,
..usage
};
let extra_total_tokens = event
Expand Down
3 changes: 3 additions & 0 deletions rust/crates/ccusage/src/adapter/goose/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub(super) fn row_to_entry(
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
speed: None,
cache_creation: None,
};
let timestamp_text = crate::format_rfc3339_millis(timestamp);
let data = UsageEntry {
Expand Down Expand Up @@ -165,6 +166,7 @@ fn calculate_goose_cost(
) -> f64 {
let cost_usage = TokenUsageRaw {
output_tokens: usage.output_tokens.saturating_add(reasoning_tokens),
cache_creation: None,
..usage
};
let raw = calculate_cost_for_usage(
Expand Down Expand Up @@ -196,6 +198,7 @@ fn missing_goose_pricing(
) -> Option<String> {
let cost_usage = TokenUsageRaw {
output_tokens: usage.output_tokens.saturating_add(reasoning_tokens),
cache_creation: None,
..usage
};
let mut candidates = vec![model.to_string()];
Expand Down
1 change: 1 addition & 0 deletions rust/crates/ccusage/src/adapter/goose/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ mod tests {
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
speed: None,
cache_creation: None,
},
model: Some("claude-sonnet-4-20250514".to_string()),
id: Some("session-a".to_string()),
Expand Down
6 changes: 6 additions & 0 deletions rust/crates/ccusage/src/adapter/hermes/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub(super) fn read_session_row(statement: &sqlite::Statement<'_>) -> Option<Herm
cache_creation_input_tokens: cache_creation_tokens,
cache_read_input_tokens: cache_read_tokens,
speed: None,
cache_creation: None,
},
reasoning_tokens,
message_count,
Expand Down Expand Up @@ -182,6 +183,7 @@ fn calculate_hermes_cost(entry: &HermesEntry, pricing: &PricingMap) -> f64 {
}
let usage = TokenUsageRaw {
output_tokens: entry.usage.output_tokens + entry.reasoning_tokens,
cache_creation: None,
..entry.usage
};
for candidate in model_candidates(entry) {
Expand All @@ -205,6 +207,7 @@ fn missing_hermes_pricing(entry: &HermesEntry, pricing: &PricingMap) -> Option<S
}
let usage = TokenUsageRaw {
output_tokens: entry.usage.output_tokens + entry.reasoning_tokens,
cache_creation: None,
..entry.usage
};
missing_pricing_model_for_candidates(
Expand Down Expand Up @@ -248,6 +251,7 @@ mod tests {
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
speed: None,
cache_creation: None,
},
reasoning_tokens: 50,
message_count: 1,
Expand Down Expand Up @@ -276,6 +280,7 @@ mod tests {
cache_creation_input_tokens: 0,
cache_read_input_tokens: 3_339_776,
speed: None,
cache_creation: None,
},
reasoning_tokens: 3_216,
message_count: 72,
Expand Down Expand Up @@ -303,6 +308,7 @@ mod tests {
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
speed: None,
cache_creation: None,
},
reasoning_tokens: 0,
message_count: 1,
Expand Down
1 change: 1 addition & 0 deletions rust/crates/ccusage/src/adapter/hermes/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ mod tests {
cache_creation_input_tokens: 20,
cache_read_input_tokens: 50,
speed: None,
cache_creation: None,
},
model: Some("claude-sonnet-4-20250514".to_string()),
id: Some("hermes:session-1".to_string()),
Expand Down
2 changes: 2 additions & 0 deletions rust/crates/ccusage/src/adapter/kilo/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub(super) fn message_value_to_entry(
.get("cache")
.map_or(0, |cache| json_value_u64(cache.get("read"))),
speed: None,
cache_creation: None,
};
let reasoning_tokens = json_value_u64(tokens.get("reasoning"));
let total_tokens = json_value_u64(tokens.get("total"));
Expand Down Expand Up @@ -80,6 +81,7 @@ pub(super) fn message_value_to_entry(
.usage
.output_tokens
.saturating_add(extra_total_tokens),
cache_creation: None,
..data.message.usage
},
..data.message.clone()
Expand Down
3 changes: 3 additions & 0 deletions rust/crates/ccusage/src/adapter/kimi/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ fn wire_line_to_entry(
cache_creation_input_tokens: cache_creation_tokens,
cache_read_input_tokens: cache_read_tokens,
speed: None,
cache_creation: None,
};
let (usage, extra_total_tokens) = apply_total_token_fallback(usage, 0, total_tokens);
if crate::total_usage_tokens(usage) + extra_total_tokens == 0 {
Expand Down Expand Up @@ -173,6 +174,7 @@ pub(super) fn kimi_entry_to_loaded(
cache_creation_input_tokens: entry.cache_creation_tokens,
cache_read_input_tokens: entry.cache_read_tokens,
speed: None,
cache_creation: None,
};
let cost = calculate_kimi_cost(&entry, mode, pricing, usage);
let missing_pricing_model = missing_kimi_pricing(&entry, mode, pricing, usage);
Expand Down Expand Up @@ -310,6 +312,7 @@ mod tests {
cache_creation_input_tokens: 20,
cache_read_input_tokens: 10,
speed: None,
cache_creation: None,
};
let before_cutoff = KimiUsageEntry {
timestamp: TimestampMs::from_millis(1_776_698_890_071),
Expand Down
2 changes: 2 additions & 0 deletions rust/crates/ccusage/src/adapter/openclaw/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ fn parse_message_entry(
cache_creation_input_tokens: cache_creation_tokens,
cache_read_input_tokens: cache_read_tokens,
speed: None,
cache_creation: None,
};
let (raw_usage, extra_total_tokens) = apply_total_token_fallback(raw_usage, 0, total_tokens);
if crate::total_usage_tokens(raw_usage) + extra_total_tokens == 0 {
Expand Down Expand Up @@ -156,6 +157,7 @@ fn openclaw_entry_to_loaded(entry: OpenClawEntry, tz: Option<&JiffTimeZone>) ->
cache_creation_input_tokens: entry.cache_creation_tokens,
cache_read_input_tokens: entry.cache_read_tokens,
speed: None,
cache_creation: None,
};
let data = UsageEntry {
session_id: Some(entry.session_id.clone()),
Expand Down
2 changes: 2 additions & 0 deletions rust/crates/ccusage/src/adapter/opencode/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub(crate) fn message_value_to_entry(
.get("cache")
.map_or(0, |cache| json_value_u64(cache.get("read"))),
speed: None,
cache_creation: None,
};
let total_tokens = json_value_u64(tokens.get("total"));
let (usage, extra_total_tokens) = apply_total_token_fallback(usage, 0, total_tokens);
Expand Down Expand Up @@ -66,6 +67,7 @@ pub(crate) fn message_value_to_entry(
};
let cost_usage = TokenUsageRaw {
output_tokens: usage.output_tokens.saturating_add(extra_total_tokens),
cache_creation: None,
..usage
};
let cost =
Expand Down
1 change: 1 addition & 0 deletions rust/crates/ccusage/src/adapter/pi/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub(crate) fn read_session_file(
cache_creation_input_tokens: cache_create,
cache_read_input_tokens: cache_read,
speed: None,
cache_creation: None,
};
let (usage, extra_total_tokens) = apply_total_token_fallback(usage, 0, total);
if crate::total_usage_tokens(usage) + extra_total_tokens == 0 {
Expand Down
Loading
Loading