Something went wrong. Try again.
npx ccusage
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373use std::collections::{BTreeMap, BTreeSet};
use serde_json::{Value, json};
use crate::{ Align, CodexGroup, CodexModelUsage, CodexServiceTier, CodexUsageBucket, Color, PricingMap, Result, SimpleTable, cli::{AgentReportKind, SharedArgs}, color, format_currency, format_models_multiline, format_number, json_float, missing_pricing_model_for_token_total, print_box_title, print_missing_pricing_warnings_for_models,};
use super::speed::CodexSpeedPolicy;
pub(super) fn report_from_groups( groups: &BTreeMap<String, CodexGroup>, kind: AgentReportKind, pricing: &PricingMap, speed: CodexSpeedPolicy,) -> Value { let rows = groups .iter() .map(|(period, group)| group_json(period, group, kind, pricing, speed)) .collect::<Vec<_>>(); let totals = totals_json(groups.values(), pricing, speed); json!({ rows_key(kind): rows, "totals": totals, })}
fn rows_key(kind: AgentReportKind) -> &'static str { match kind { AgentReportKind::Daily => "daily", AgentReportKind::Weekly => "weekly", AgentReportKind::Monthly => "monthly", AgentReportKind::Session => "sessions", }}
fn period_key(kind: AgentReportKind) -> &'static str { match kind { AgentReportKind::Daily => "date", AgentReportKind::Weekly => "week", AgentReportKind::Monthly => "month", AgentReportKind::Session => "sessionId", }}
fn group_json( period: &str, group: &CodexGroup, kind: AgentReportKind, pricing: &PricingMap, speed: CodexSpeedPolicy,) -> Value { let cost = calculate_group_cost(group, pricing, speed); let input_tokens = non_cached_input_tokens(group.input_tokens, group.cached_input_tokens); let models = group .models .iter() .map(|(model, usage)| (model.clone(), model_usage_json(usage))) .collect::<BTreeMap<_, _>>(); let mut row = json!({ period_key(kind): period, "inputTokens": input_tokens, "cacheCreationTokens": 0, "cacheReadTokens": group.cached_input_tokens, "outputTokens": group.output_tokens, "reasoningOutputTokens": group.reasoning_output_tokens, "totalTokens": group.total_tokens, "costUSD": json_float(cost), "models": models, }); if kind == AgentReportKind::Session { row["lastActivity"] = json!(group.last_activity); let separator = period.rfind('/'); row["sessionFile"] = json!(separator.map_or(period, |index| &period[index + 1..])); row["directory"] = json!(separator.map_or("", |index| &period[..index])); } row}
pub fn non_cached_input_tokens(input_tokens: u64, cached_input_tokens: u64) -> u64 { input_tokens.saturating_sub(cached_input_tokens)}
fn model_usage_json(usage: &CodexModelUsage) -> Value { json!({ "inputTokens": non_cached_input_tokens(usage.input_tokens, usage.cached_input_tokens), "cacheCreationTokens": 0, "cacheReadTokens": usage.cached_input_tokens, "outputTokens": usage.output_tokens, "reasoningOutputTokens": usage.reasoning_output_tokens, "totalTokens": usage.total_tokens, "isFallback": usage.is_fallback, })}
fn totals_json<'a>( groups: impl Iterator<Item = &'a CodexGroup>, pricing: &PricingMap, speed: CodexSpeedPolicy,) -> Value { let mut input = 0; let mut cached = 0; let mut output = 0; let mut reasoning = 0; let mut total = 0; let mut cost = 0.0; for group in groups { input += non_cached_input_tokens(group.input_tokens, group.cached_input_tokens); cached += group.cached_input_tokens; output += group.output_tokens; reasoning += group.reasoning_output_tokens; total += group.total_tokens; cost += calculate_group_cost(group, pricing, speed); } json!({ "inputTokens": input, "cacheCreationTokens": 0, "cacheReadTokens": cached, "outputTokens": output, "reasoningOutputTokens": reasoning, "totalTokens": total, "costUSD": json_float(cost), })}
pub fn calculate_codex_model_cost( model: &str, usage: &CodexModelUsage, pricing: &PricingMap, speed: impl Into<CodexSpeedPolicy>,) -> f64 { let Some(pricing) = pricing.find(model) else { return 0.0; }; let total_usage = model_usage_bucket(usage); let standard_cost = calculate_codex_bucket_cost(&total_usage, &pricing); let fast_usage = match speed.into() { CodexSpeedPolicy::Forced(CodexServiceTier::Standard) => return standard_cost, CodexSpeedPolicy::Forced(CodexServiceTier::Fast) => total_usage, CodexSpeedPolicy::Auto(CodexServiceTier::Standard) => usage.recorded_fast_usage, CodexSpeedPolicy::Auto(CodexServiceTier::Fast) => { subtract_codex_usage_bucket(total_usage, usage.recorded_standard_usage) } }; standard_cost + calculate_codex_bucket_cost(&fast_usage, &pricing) * (pricing.fast_multiplier - 1.0)}
fn model_usage_bucket(usage: &CodexModelUsage) -> CodexUsageBucket { CodexUsageBucket { input_tokens: usage.input_tokens, cached_input_tokens: usage.cached_input_tokens, output_tokens: usage.output_tokens, long_context_input_tokens: usage.long_context_input_tokens, long_context_cached_input_tokens: usage.long_context_cached_input_tokens, long_context_output_tokens: usage.long_context_output_tokens, }}
fn subtract_codex_usage_bucket( total: CodexUsageBucket, excluded: CodexUsageBucket,) -> CodexUsageBucket { CodexUsageBucket { input_tokens: total.input_tokens.saturating_sub(excluded.input_tokens), cached_input_tokens: total .cached_input_tokens .saturating_sub(excluded.cached_input_tokens), output_tokens: total.output_tokens.saturating_sub(excluded.output_tokens), long_context_input_tokens: total .long_context_input_tokens .saturating_sub(excluded.long_context_input_tokens), long_context_cached_input_tokens: total .long_context_cached_input_tokens .saturating_sub(excluded.long_context_cached_input_tokens), long_context_output_tokens: total .long_context_output_tokens .saturating_sub(excluded.long_context_output_tokens), }}
fn calculate_codex_bucket_cost(usage: &CodexUsageBucket, pricing: &crate::Pricing) -> f64 { let cache_read = if pricing.cache_read_explicit { pricing.cache_read } else { pricing.input }; // OpenAI bills every token of a long-context request (input above 272K // tokens) at the long-context rates, so the aggregated usage is priced as // two independent buckets. Models without long-context rates fall back to // the flat rates, which keeps both buckets at the same price. let long_input_rate = pricing.input_above_200k.unwrap_or(pricing.input); let long_output_rate = pricing.output_above_200k.unwrap_or(pricing.output); let long_cache_read = if pricing.cache_read_explicit { pricing.cache_read_above_200k.unwrap_or(cache_read) } else { long_input_rate }; let long_input = usage.long_context_input_tokens.min(usage.input_tokens); let long_cached = usage .long_context_cached_input_tokens .min(usage.cached_input_tokens) .min(long_input); let long_output = usage.long_context_output_tokens.min(usage.output_tokens); let short_non_cached = (usage.input_tokens - long_input).saturating_sub(usage.cached_input_tokens - long_cached); let long_non_cached = long_input - long_cached; short_non_cached as f64 * pricing.input + (usage.cached_input_tokens - long_cached) as f64 * cache_read + (usage.output_tokens - long_output) as f64 * pricing.output + long_non_cached as f64 * long_input_rate + long_cached as f64 * long_cache_read + long_output as f64 * long_output_rate}
pub fn calculate_group_cost<S>(group: &CodexGroup, pricing: &PricingMap, speed: S) -> f64where S: Into<CodexSpeedPolicy> + Copy,{ let speed = speed.into(); group .models .iter() .map(|(model, usage)| calculate_codex_model_cost(model, usage, pricing, speed)) .sum()}
pub fn codex_model_missing_pricing( model: &str, usage: &CodexModelUsage, pricing: &PricingMap,) -> bool { missing_pricing_model_for_token_total( Some(model), usage .total_tokens .max(usage.input_tokens.saturating_add(usage.output_tokens)), Some(pricing), ) .is_some()}
pub fn codex_missing_pricing_models( groups: &BTreeMap<String, CodexGroup>, pricing: &PricingMap,) -> Vec<String> { let mut models = BTreeSet::new(); for group in groups.values() { for (model, usage) in &group.models { if codex_model_missing_pricing(model, usage, pricing) { models.insert(model.clone()); } } } models.into_iter().collect()}
pub(super) fn print_table_from_groups( groups: &BTreeMap<String, CodexGroup>, kind: AgentReportKind, pricing: &PricingMap, speed: CodexSpeedPolicy, shared: &SharedArgs,) -> Result<()> { if groups.is_empty() { eprintln!("No Codex usage data found."); return Ok(()); } let first_column = match kind { AgentReportKind::Daily => "Date", AgentReportKind::Weekly => "Week", AgentReportKind::Monthly => "Month", AgentReportKind::Session => "Session", }; print_box_title( &format!( "Codex Token Usage Report - {}", match kind { AgentReportKind::Daily => "Daily", AgentReportKind::Weekly => "Weekly", AgentReportKind::Monthly => "Monthly", AgentReportKind::Session => "Session", } ), shared, ); let mut headers = vec![ first_column, "Models", "Input", "Output", "Reasoning", "Cache Read", "Total Tokens", "Cost (USD)", ]; let mut aligns = vec![ Align::Left, Align::Left, Align::Right, Align::Right, Align::Right, Align::Right, Align::Right, Align::Right, ]; if shared.no_cost { headers.pop(); aligns.pop(); } let mut table = SimpleTable::new(headers, aligns, crate::terminal_style(shared)) .with_terminal_width(crate::terminal_width()) .with_date_compaction(true); let mut total_input = 0; let mut total_cached = 0; let mut total_output = 0; let mut total_reasoning = 0; let mut total_tokens = 0; let mut total_cost = 0.0; for (label, group) in groups { let input_tokens = non_cached_input_tokens(group.input_tokens, group.cached_input_tokens); let cost = calculate_group_cost(group, pricing, speed); total_input += input_tokens; total_cached += group.cached_input_tokens; total_output += group.output_tokens; total_reasoning += group.reasoning_output_tokens; total_tokens += group.total_tokens; total_cost += cost; let models = format_models_multiline(&group.models.keys().cloned().collect::<Vec<_>>()); let mut row = vec![ label.clone(), models, format_number(input_tokens), format_number(group.output_tokens), format_number(group.reasoning_output_tokens), format_number(group.cached_input_tokens), format_number(group.total_tokens), format_currency(cost), ]; if shared.no_cost { row.pop(); } table.push(row); } table.separator(); let mut total_row = vec![ color(shared, "Total", Color::Yellow), String::new(), color(shared, format_number(total_input), Color::Yellow), color(shared, format_number(total_output), Color::Yellow), color(shared, format_number(total_reasoning), Color::Yellow), color(shared, format_number(total_cached), Color::Yellow), color(shared, format_number(total_tokens), Color::Yellow), color(shared, format_currency(total_cost), Color::Yellow), ]; if shared.no_cost { total_row.pop(); } table.push(total_row); table.print()?; let missing_models = codex_missing_pricing_models(groups, pricing); print_missing_pricing_warnings_for_models( missing_models.iter().map(String::as_str), shared.offline, ); Ok(())}