codexusage 0.3.0

Fast CLI reports for OpenAI Codex session usage and cost
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
//! Shared app data models and presentation options.

use clap::ValueEnum;
use serde::Serialize;
use std::collections::BTreeMap;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::time::Duration;

/// Environment variable that overrides the default Codex home directory.
pub(in crate::app) const DEFAULT_CODEX_HOME_ENV: &str = "CODEX_HOME";
/// Model used when legacy logs do not expose model metadata.
pub(in crate::app) const DEFAULT_FALLBACK_MODEL: &str = "gpt-5";
/// Number of tokens in one million-token pricing unit.
pub(in crate::app) const MILLION: f64 = 1_000_000.0;

/// Cached input token pricing mode.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CachedInputCostMode {
    /// Use the cached-input price from the pricing catalog.
    #[default]
    Priced,
    /// Treat cached input tokens as free.
    Free,
}

/// Cache-read token reporting mode.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CacheReadMode {
    /// Include cache-read input tokens in the reported token counters.
    #[default]
    Include,
    /// Exclude cache-read input tokens from input and total counters.
    Exclude,
}

/// Supported report kinds.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReportKind {
    /// Group usage by calendar day.
    Daily,
    /// Group usage by calendar month.
    Monthly,
    /// Group usage by session file.
    Session,
}

/// Numeric table display mode.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum NumberFormat {
    /// Shorten token counts using integer K/M/B/T suffixes.
    #[default]
    Short,
    /// Show full token counts with separators.
    Full,
}

/// Scanner worker configuration.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ScannerParallelism {
    /// Use the host's available parallelism.
    Auto,
    /// Use an explicit worker count.
    Fixed(NonZeroUsize),
}

/// CLI-free options passed into report generation.
#[derive(Clone, Debug)]
pub struct ReportOptions {
    /// Inclusive lower bound.
    pub since: Option<String>,
    /// Inclusive upper bound.
    pub until: Option<String>,
    /// Trailing calendar days ending today in the selected timezone. Daily report only.
    pub last_days: Option<NonZeroUsize>,
    /// IANA timezone name.
    pub timezone: String,
    /// Output locale hint.
    pub locale: String,
    /// Human-readable number formatting mode.
    pub number_format: NumberFormat,
    /// Emit JSON instead of table output.
    pub json: bool,
    /// Disable network pricing refreshes.
    pub offline: bool,
    /// Force pricing refresh even when cache is fresh.
    pub refresh_pricing: bool,
    /// Cached input token pricing mode.
    pub cached_input_cost_mode: CachedInputCostMode,
    /// Cache-read token reporting mode.
    pub cache_read_mode: CacheReadMode,
    /// Session directories to scan.
    pub session_dirs: Vec<PathBuf>,
    /// Project directory used to filter sessions by logged working directory.
    pub project_dir: Option<PathBuf>,
    /// Scanner worker configuration.
    pub parallelism: ScannerParallelism,
}

/// Aggregated token usage for a single model.
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct ModelBreakdown {
    /// Total input tokens.
    pub input_tokens: u64,
    /// Total cached input tokens.
    pub cached_input_tokens: u64,
    /// Total output tokens.
    pub output_tokens: u64,
    /// Total reasoning tokens.
    pub reasoning_output_tokens: u64,
    /// Total billable tokens.
    pub total_tokens: u64,
    /// Precomputed cost in USD for text rendering.
    #[serde(skip_serializing)]
    pub cost_usd: f64,
    /// Fallback-only usage kept for human-readable rendering.
    #[serde(skip_serializing)]
    pub fallback_usage: UsageTotals,
    /// Fallback-only cost kept for human-readable rendering.
    #[serde(skip_serializing)]
    pub fallback_cost_usd: f64,
    /// Whether fallback model inference was used.
    #[serde(skip_serializing_if = "is_false")]
    pub is_fallback: bool,
}

/// Daily row shape.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct DailyRow {
    /// Calendar day in the requested timezone.
    pub date: String,
    /// Total input tokens.
    pub input_tokens: u64,
    /// Total cached input tokens.
    pub cached_input_tokens: u64,
    /// Total output tokens.
    pub output_tokens: u64,
    /// Total reasoning output tokens.
    pub reasoning_output_tokens: u64,
    /// Total billable tokens.
    pub total_tokens: u64,
    /// Cost in USD.
    pub cost_usd: f64,
    /// Per-model breakdown.
    pub models: BTreeMap<String, ModelBreakdown>,
}

/// Monthly row shape.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct MonthlyRow {
    /// Calendar month in the requested timezone.
    pub month: String,
    /// Total input tokens.
    pub input_tokens: u64,
    /// Total cached input tokens.
    pub cached_input_tokens: u64,
    /// Total output tokens.
    pub output_tokens: u64,
    /// Total reasoning output tokens.
    pub reasoning_output_tokens: u64,
    /// Total billable tokens.
    pub total_tokens: u64,
    /// Cost in USD.
    pub cost_usd: f64,
    /// Per-model breakdown.
    pub models: BTreeMap<String, ModelBreakdown>,
}

/// Session row shape.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct SessionRow {
    /// Relative session identifier.
    pub session_id: String,
    /// Relative directory.
    pub directory: String,
    /// Session file name without extension.
    pub session_file: String,
    /// Last activity timestamp in RFC 3339.
    pub last_activity: String,
    /// Total input tokens.
    pub input_tokens: u64,
    /// Total cached input tokens.
    pub cached_input_tokens: u64,
    /// Total output tokens.
    pub output_tokens: u64,
    /// Total reasoning output tokens.
    pub reasoning_output_tokens: u64,
    /// Total billable tokens.
    pub total_tokens: u64,
    /// Cost in USD.
    pub cost_usd: f64,
    /// Per-model breakdown.
    pub models: BTreeMap<String, ModelBreakdown>,
}

/// Grand totals emitted with every report.
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Totals {
    /// Total input tokens.
    pub input_tokens: u64,
    /// Total cached input tokens.
    pub cached_input_tokens: u64,
    /// Total output tokens.
    pub output_tokens: u64,
    /// Total reasoning output tokens.
    pub reasoning_output_tokens: u64,
    /// Total billable tokens.
    pub total_tokens: u64,
    /// Cost in USD.
    pub cost_usd: f64,
}

/// Watch-only CLI-free options.
#[derive(Clone, Debug)]
pub(in crate::app) struct WatchOptions {
    /// IANA timezone name.
    pub(in crate::app) timezone: String,
    /// Output locale hint.
    pub(in crate::app) locale: String,
    /// Human-readable number formatting mode.
    pub(in crate::app) number_format: NumberFormat,
    /// Disable network pricing refreshes.
    pub(in crate::app) offline: bool,
    /// Force pricing refresh even when cache is fresh.
    pub(in crate::app) refresh_pricing: bool,
    /// Cached input token pricing mode.
    pub(in crate::app) cached_input_cost_mode: CachedInputCostMode,
    /// Cache-read token reporting mode.
    pub(in crate::app) cache_read_mode: CacheReadMode,
    /// Session directories to scan.
    pub(in crate::app) session_dirs: Vec<PathBuf>,
    /// Project directory used to filter sessions by logged working directory.
    pub(in crate::app) project_dir: Option<PathBuf>,
    /// Scanner worker configuration.
    pub(in crate::app) parallelism: ScannerParallelism,
    /// Refresh interval for the live screen.
    pub(in crate::app) interval: Duration,
    /// Show per-model detail rows in the watch table.
    pub(in crate::app) show_model_burn_rate: bool,
    /// Debug-only runtime options forwarded from the CLI.
    #[cfg(debug_assertions)]
    pub(in crate::app) debug: DebugRuntimeOptions,
}

/// Debug-only runtime behavior selected by the CLI and consumed by app runtimes.
#[cfg(debug_assertions)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(in crate::app) struct DebugRuntimeOptions {
    /// Simulate variable disk latency before opening each parsed file.
    pub(in crate::app) simulate_slow_disk: bool,
}

/// Rolling usage rate computed for one watch snapshot.
#[derive(Clone, Debug, PartialEq)]
pub(in crate::app) struct BurnRateSnapshot {
    /// Exact rolling window duration.
    pub(in crate::app) window_duration: Duration,
    /// Effective rolling window width after current-day clamping.
    pub(in crate::app) window_minutes: u64,
    /// Input tokens per hour.
    pub(in crate::app) input_tokens_per_hour: u64,
    /// Cached input tokens per hour.
    pub(in crate::app) cached_input_tokens_per_hour: u64,
    /// Output tokens per hour.
    pub(in crate::app) output_tokens_per_hour: u64,
    /// Reasoning output tokens per hour.
    pub(in crate::app) reasoning_output_tokens_per_hour: u64,
    /// Total billable tokens per hour.
    pub(in crate::app) total_tokens_per_hour: u64,
    /// Cost in USD per hour.
    pub(in crate::app) cost_usd_per_hour: f64,
}

/// One cost burn-rate sample rendered by the live watch graph.
#[derive(Clone, Debug, PartialEq)]
pub(in crate::app) struct BurnRateHistoryPoint {
    /// Local sample end time shown on the graph axis.
    pub(in crate::app) end_time: String,
    /// Cost in USD per hour for the sample's trailing window.
    pub(in crate::app) cost_usd_per_hour: f64,
}

/// One rendered watch snapshot.
#[derive(Clone, Debug, PartialEq)]
pub(in crate::app) struct WatchSnapshot {
    /// Current day in the selected timezone.
    pub(in crate::app) date: String,
    /// Current-day cumulative totals.
    pub(in crate::app) totals: Totals,
    /// Rolling burn-rate summary.
    pub(in crate::app) burn_rate: BurnRateSnapshot,
    /// Cost burn-rate samples for the compact watch graph.
    pub(in crate::app) burn_history: Vec<BurnRateHistoryPoint>,
    /// Per-model rolling burn-window detail.
    pub(in crate::app) per_model: BTreeMap<String, ModelBreakdown>,
    /// Missing directories encountered during scan.
    pub(in crate::app) missing_directories: Vec<String>,
    /// Last refresh time in the selected timezone.
    pub(in crate::app) updated_time: String,
}

/// Result of a report command.
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ReportOutput {
    /// Daily report output.
    Daily {
        /// Rows in report order.
        rows: Vec<DailyRow>,
        /// Grand totals.
        totals: Totals,
        /// Missing directories encountered during scan.
        missing_directories: Vec<String>,
    },
    /// Monthly report output.
    Monthly {
        /// Rows in report order.
        rows: Vec<MonthlyRow>,
        /// Grand totals.
        totals: Totals,
        /// Missing directories encountered during scan.
        missing_directories: Vec<String>,
    },
    /// Session report output.
    Session {
        /// Rows in report order.
        rows: Vec<SessionRow>,
        /// Grand totals.
        totals: Totals,
        /// Missing directories encountered during scan.
        missing_directories: Vec<String>,
    },
}

/// Usage and cost presentation behavior shared by reports and watch snapshots.
#[derive(Clone, Copy, Debug)]
pub(in crate::app) struct UsagePresentation {
    /// Cached input token pricing behavior.
    pub(in crate::app) cached_input_cost_mode: CachedInputCostMode,
    /// Cache-read token reporting behavior.
    pub(in crate::app) cache_read_mode: CacheReadMode,
}

impl UsagePresentation {
    /// Create presentation behavior from the CLI-free option modes.
    pub(in crate::app) const fn new(
        cached_input_cost_mode: CachedInputCostMode,
        cache_read_mode: CacheReadMode,
    ) -> Self {
        Self {
            cached_input_cost_mode,
            cache_read_mode,
        }
    }
}

/// Whether a bool is false.
#[allow(
    clippy::trivially_copy_pass_by_ref,
    reason = "serde skip_serializing_if passes field values by reference"
)]
pub(in crate::app) fn is_false(value: &bool) -> bool {
    !*value
}

#[derive(Clone, Debug, Default, PartialEq)]
/// Internal usage accumulator.
pub struct UsageTotals {
    /// Total input tokens.
    pub input: u64,
    /// Total cached input tokens.
    pub cached_input: u64,
    /// Total output tokens.
    pub output: u64,
    /// Total reasoning output tokens.
    pub reasoning_output: u64,
    /// Total billable tokens.
    pub total: u64,
}

impl UsageTotals {
    /// Add one event.
    pub(in crate::app) fn add(&mut self, other: &UsageTotals) {
        self.input += other.input;
        self.cached_input += other.cached_input;
        self.output += other.output;
        self.reasoning_output += other.reasoning_output;
        self.total += other.total;
    }

    /// Remove one event.
    pub(in crate::app) fn subtract(&mut self, other: &UsageTotals) {
        self.input = self.input.saturating_sub(other.input);
        self.cached_input = self.cached_input.saturating_sub(other.cached_input);
        self.output = self.output.saturating_sub(other.output);
        self.reasoning_output = self.reasoning_output.saturating_sub(other.reasoning_output);
        self.total = self.total.saturating_sub(other.total);
    }

    /// Return whether this usage bucket contains any billable activity.
    pub(in crate::app) fn has_usage(&self) -> bool {
        self.input > 0
            || self.cached_input > 0
            || self.output > 0
            || self.reasoning_output > 0
            || self.total > 0
    }

    /// Return usage counters with the selected cache-read reporting mode applied.
    pub(in crate::app) fn with_cache_read_mode(&self, cache_read_mode: CacheReadMode) -> Self {
        match cache_read_mode {
            CacheReadMode::Include => self.clone(),
            CacheReadMode::Exclude => {
                let cached_input = self.cached_input.min(self.input);
                Self {
                    input: self.input.saturating_sub(cached_input),
                    cached_input: 0,
                    output: self.output,
                    reasoning_output: self.reasoning_output,
                    total: self.total.saturating_sub(cached_input),
                }
            }
        }
    }
}

/// Return the non-fallback portion of a mixed model breakdown.
pub(in crate::app) fn explicit_usage(breakdown: &ModelBreakdown) -> UsageTotals {
    UsageTotals {
        input: breakdown
            .input_tokens
            .saturating_sub(breakdown.fallback_usage.input),
        cached_input: breakdown
            .cached_input_tokens
            .saturating_sub(breakdown.fallback_usage.cached_input),
        output: breakdown
            .output_tokens
            .saturating_sub(breakdown.fallback_usage.output),
        reasoning_output: breakdown
            .reasoning_output_tokens
            .saturating_sub(breakdown.fallback_usage.reasoning_output),
        total: breakdown
            .total_tokens
            .saturating_sub(breakdown.fallback_usage.total),
    }
}