use clap::ValueEnum;
use serde::Serialize;
use std::collections::BTreeMap;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::time::Duration;
pub(in crate::app) const DEFAULT_CODEX_HOME_ENV: &str = "CODEX_HOME";
pub(in crate::app) const DEFAULT_FALLBACK_MODEL: &str = "gpt-5";
pub(in crate::app) const MILLION: f64 = 1_000_000.0;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CachedInputCostMode {
#[default]
Priced,
Free,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CacheReadMode {
#[default]
Include,
Exclude,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReportKind {
Daily,
Monthly,
Session,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum NumberFormat {
#[default]
Short,
Full,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ScannerParallelism {
Auto,
Fixed(NonZeroUsize),
}
#[derive(Clone, Debug)]
pub struct ReportOptions {
pub since: Option<String>,
pub until: Option<String>,
pub last_days: Option<NonZeroUsize>,
pub timezone: String,
pub locale: String,
pub number_format: NumberFormat,
pub json: bool,
pub offline: bool,
pub refresh_pricing: bool,
pub cached_input_cost_mode: CachedInputCostMode,
pub cache_read_mode: CacheReadMode,
pub session_dirs: Vec<PathBuf>,
pub project_dir: Option<PathBuf>,
pub parallelism: ScannerParallelism,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct ModelBreakdown {
pub input_tokens: u64,
pub cached_input_tokens: u64,
pub output_tokens: u64,
pub reasoning_output_tokens: u64,
pub total_tokens: u64,
#[serde(skip_serializing)]
pub cost_usd: f64,
#[serde(skip_serializing)]
pub fallback_usage: UsageTotals,
#[serde(skip_serializing)]
pub fallback_cost_usd: f64,
#[serde(skip_serializing_if = "is_false")]
pub is_fallback: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct DailyRow {
pub date: String,
pub input_tokens: u64,
pub cached_input_tokens: u64,
pub output_tokens: u64,
pub reasoning_output_tokens: u64,
pub total_tokens: u64,
pub cost_usd: f64,
pub models: BTreeMap<String, ModelBreakdown>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct MonthlyRow {
pub month: String,
pub input_tokens: u64,
pub cached_input_tokens: u64,
pub output_tokens: u64,
pub reasoning_output_tokens: u64,
pub total_tokens: u64,
pub cost_usd: f64,
pub models: BTreeMap<String, ModelBreakdown>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct SessionRow {
pub session_id: String,
pub directory: String,
pub session_file: String,
pub last_activity: String,
pub input_tokens: u64,
pub cached_input_tokens: u64,
pub output_tokens: u64,
pub reasoning_output_tokens: u64,
pub total_tokens: u64,
pub cost_usd: f64,
pub models: BTreeMap<String, ModelBreakdown>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Totals {
pub input_tokens: u64,
pub cached_input_tokens: u64,
pub output_tokens: u64,
pub reasoning_output_tokens: u64,
pub total_tokens: u64,
pub cost_usd: f64,
}
#[derive(Clone, Debug)]
pub(in crate::app) struct WatchOptions {
pub(in crate::app) timezone: String,
pub(in crate::app) locale: String,
pub(in crate::app) number_format: NumberFormat,
pub(in crate::app) offline: bool,
pub(in crate::app) refresh_pricing: bool,
pub(in crate::app) cached_input_cost_mode: CachedInputCostMode,
pub(in crate::app) cache_read_mode: CacheReadMode,
pub(in crate::app) session_dirs: Vec<PathBuf>,
pub(in crate::app) project_dir: Option<PathBuf>,
pub(in crate::app) parallelism: ScannerParallelism,
pub(in crate::app) interval: Duration,
pub(in crate::app) show_model_burn_rate: bool,
#[cfg(debug_assertions)]
pub(in crate::app) debug: DebugRuntimeOptions,
}
#[cfg(debug_assertions)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(in crate::app) struct DebugRuntimeOptions {
pub(in crate::app) simulate_slow_disk: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub(in crate::app) struct BurnRateSnapshot {
pub(in crate::app) window_duration: Duration,
pub(in crate::app) window_minutes: u64,
pub(in crate::app) input_tokens_per_hour: u64,
pub(in crate::app) cached_input_tokens_per_hour: u64,
pub(in crate::app) output_tokens_per_hour: u64,
pub(in crate::app) reasoning_output_tokens_per_hour: u64,
pub(in crate::app) total_tokens_per_hour: u64,
pub(in crate::app) cost_usd_per_hour: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub(in crate::app) struct BurnRateHistoryPoint {
pub(in crate::app) end_time: String,
pub(in crate::app) cost_usd_per_hour: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub(in crate::app) struct WatchSnapshot {
pub(in crate::app) date: String,
pub(in crate::app) totals: Totals,
pub(in crate::app) burn_rate: BurnRateSnapshot,
pub(in crate::app) burn_history: Vec<BurnRateHistoryPoint>,
pub(in crate::app) per_model: BTreeMap<String, ModelBreakdown>,
pub(in crate::app) missing_directories: Vec<String>,
pub(in crate::app) updated_time: String,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ReportOutput {
Daily {
rows: Vec<DailyRow>,
totals: Totals,
missing_directories: Vec<String>,
},
Monthly {
rows: Vec<MonthlyRow>,
totals: Totals,
missing_directories: Vec<String>,
},
Session {
rows: Vec<SessionRow>,
totals: Totals,
missing_directories: Vec<String>,
},
}
#[derive(Clone, Copy, Debug)]
pub(in crate::app) struct UsagePresentation {
pub(in crate::app) cached_input_cost_mode: CachedInputCostMode,
pub(in crate::app) cache_read_mode: CacheReadMode,
}
impl UsagePresentation {
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,
}
}
}
#[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)]
pub struct UsageTotals {
pub input: u64,
pub cached_input: u64,
pub output: u64,
pub reasoning_output: u64,
pub total: u64,
}
impl UsageTotals {
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;
}
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);
}
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
}
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),
}
}
}
}
}
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),
}
}