Skip to main content

codex_ops/stats/
reports.rs

1use super::StatSort;
2use crate::limits::RateLimitDiagnostics;
3use crate::pricing::TokenUsage as PricingTokenUsage;
4use crate::session_scan::SessionScanDiagnostics;
5use crate::time::{local_to_utc, StatGroupBy};
6use chrono::{DateTime, Datelike, Local, SecondsFormat, Timelike, Utc};
7use serde::Serialize;
8
9#[derive(Debug, Clone)]
10pub struct UsageRecordsReadOptions {
11    pub start: DateTime<Utc>,
12    pub end: DateTime<Utc>,
13    pub sessions_dir: std::path::PathBuf,
14    pub scan_all_files: bool,
15    pub account_history_file: Option<std::path::PathBuf>,
16    pub account_id: Option<String>,
17}
18
19#[derive(Clone, Debug)]
20pub struct UsageRecordsReport {
21    pub start: DateTime<Utc>,
22    pub end: DateTime<Utc>,
23    pub sessions_dir: String,
24    pub records: Vec<UsageRecord>,
25    pub diagnostics: UsageDiagnostics,
26}
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub(super) enum LimitUsageGroupBy {
30    Window,
31    Model,
32    Cwd,
33    Account,
34}
35
36impl LimitUsageGroupBy {
37    pub(super) fn from_stat(value: Option<StatGroupBy>) -> Self {
38        match value {
39            Some(StatGroupBy::Model) => Self::Model,
40            Some(StatGroupBy::Cwd) => Self::Cwd,
41            Some(StatGroupBy::Account) => Self::Account,
42            _ => Self::Window,
43        }
44    }
45
46    pub(super) fn as_str(self) -> &'static str {
47        match self {
48            Self::Window => "window",
49            Self::Model => "model",
50            Self::Cwd => "cwd",
51            Self::Account => "account",
52        }
53    }
54
55    pub(super) fn as_stat(self) -> Option<StatGroupBy> {
56        match self {
57            Self::Window => None,
58            Self::Model => Some(StatGroupBy::Model),
59            Self::Cwd => Some(StatGroupBy::Cwd),
60            Self::Account => Some(StatGroupBy::Account),
61        }
62    }
63}
64
65#[derive(Clone, Debug, Default, Serialize, PartialEq)]
66#[serde(rename_all = "camelCase")]
67pub struct TokenUsage {
68    pub input_tokens: i64,
69    pub cached_input_tokens: i64,
70    pub output_tokens: i64,
71    pub reasoning_output_tokens: i64,
72    pub total_tokens: i64,
73}
74
75impl TokenUsage {
76    pub(super) fn add(&mut self, other: &TokenUsage) {
77        self.input_tokens += other.input_tokens;
78        self.cached_input_tokens += other.cached_input_tokens;
79        self.output_tokens += other.output_tokens;
80        self.reasoning_output_tokens += other.reasoning_output_tokens;
81        self.total_tokens += other.total_tokens;
82    }
83
84    pub(super) fn is_empty(&self) -> bool {
85        self.input_tokens == 0
86            && self.cached_input_tokens == 0
87            && self.output_tokens == 0
88            && self.reasoning_output_tokens == 0
89            && self.total_tokens == 0
90    }
91
92    pub(super) fn pricing_usage(&self) -> PricingTokenUsage {
93        PricingTokenUsage {
94            input_tokens: self.input_tokens.max(0) as u64,
95            cached_input_tokens: self.cached_input_tokens.max(0) as u64,
96            output_tokens: self.output_tokens.max(0) as u64,
97        }
98    }
99}
100
101#[derive(Clone, Debug)]
102pub struct UsageRecord {
103    pub timestamp: DateTime<Utc>,
104    pub session_id: String,
105    pub model: String,
106    pub reasoning_effort: Option<String>,
107    pub cwd: String,
108    pub account_id: Option<String>,
109    pub file_path: String,
110    pub usage: TokenUsage,
111}
112
113#[derive(Clone, Copy)]
114pub(super) struct UsageRecordView<'a> {
115    pub(super) timestamp: DateTime<Utc>,
116    pub(super) session_id: &'a str,
117    pub(super) model: &'a str,
118    pub(super) reasoning_effort: Option<&'a str>,
119    pub(super) cwd: &'a str,
120    pub(super) account_id: Option<&'a str>,
121    pub(super) file_path: &'a str,
122    pub(super) usage: &'a TokenUsage,
123}
124
125impl UsageRecordView<'_> {
126    pub(super) fn to_owned_record(self) -> UsageRecord {
127        UsageRecord {
128            timestamp: self.timestamp,
129            session_id: self.session_id.to_string(),
130            model: self.model.to_string(),
131            reasoning_effort: self.reasoning_effort.map(str::to_string),
132            cwd: self.cwd.to_string(),
133            account_id: self.account_id.map(str::to_string),
134            file_path: self.file_path.to_string(),
135            usage: self.usage.clone(),
136        }
137    }
138}
139
140#[derive(Clone, Debug, Serialize, PartialEq)]
141#[serde(rename_all = "camelCase")]
142pub struct UsageStatRow {
143    pub(super) key: String,
144    pub(super) sessions: usize,
145    pub(super) calls: i64,
146    pub(super) usage: TokenUsage,
147    pub(super) credits: f64,
148    pub(super) usd: f64,
149    pub(super) priced_calls: i64,
150    pub(super) unpriced_calls: i64,
151}
152
153#[derive(Clone, Debug, Serialize, PartialEq)]
154#[serde(rename_all = "camelCase")]
155pub struct UsageUnpricedModelRow {
156    pub(super) model: String,
157    pub(super) pricing_key: String,
158    pub(super) calls: i64,
159    pub(super) total_tokens: i64,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub(super) note: Option<String>,
162    pub(super) pricing_stub: String,
163}
164
165#[derive(Clone, Debug, Serialize, PartialEq)]
166#[serde(rename_all = "camelCase")]
167pub struct UsageDiagnostics {
168    pub scan_all_files: bool,
169    pub scanned_directories: i64,
170    pub skipped_directories: i64,
171    pub read_files: i64,
172    pub skipped_files: i64,
173    pub prefiltered_files: i64,
174    pub tail_read_files: i64,
175    pub tail_read_hits: i64,
176    pub mtime_read_files: i64,
177    pub mtime_tail_hits: i64,
178    pub mtime_read_hits: i64,
179    pub fork_files: i64,
180    pub fork_parent_missing: i64,
181    pub fork_replay_lines: i64,
182    pub read_lines: i64,
183    pub invalid_json_lines: i64,
184    pub token_count_events: i64,
185    pub included_usage_events: i64,
186    pub skipped_events: SkippedEvents,
187    pub file_read_concurrency: i64,
188}
189
190#[derive(Clone, Debug, Default, Serialize, PartialEq)]
191#[serde(rename_all = "camelCase")]
192pub struct SkippedEvents {
193    pub missing_metadata: i64,
194    pub missing_usage: i64,
195    pub empty_usage: i64,
196    pub out_of_range: i64,
197    pub account_mismatch: i64,
198    pub fork_replay: i64,
199}
200
201impl UsageDiagnostics {
202    pub(super) fn new(file_read_concurrency: i64, scan_all_files: bool) -> Self {
203        Self {
204            scan_all_files,
205            scanned_directories: 0,
206            skipped_directories: 0,
207            read_files: 0,
208            skipped_files: 0,
209            prefiltered_files: 0,
210            tail_read_files: 0,
211            tail_read_hits: 0,
212            mtime_read_files: 0,
213            mtime_tail_hits: 0,
214            mtime_read_hits: 0,
215            fork_files: 0,
216            fork_parent_missing: 0,
217            fork_replay_lines: 0,
218            read_lines: 0,
219            invalid_json_lines: 0,
220            token_count_events: 0,
221            included_usage_events: 0,
222            skipped_events: SkippedEvents::default(),
223            file_read_concurrency,
224        }
225    }
226
227    pub(super) fn merge_file_scan(&mut self, other: &UsageDiagnostics) {
228        self.prefiltered_files += other.prefiltered_files;
229        self.tail_read_files += other.tail_read_files;
230        self.tail_read_hits += other.tail_read_hits;
231        self.mtime_read_files += other.mtime_read_files;
232        self.mtime_tail_hits += other.mtime_tail_hits;
233        self.mtime_read_hits += other.mtime_read_hits;
234        self.fork_files += other.fork_files;
235        self.fork_parent_missing += other.fork_parent_missing;
236        self.fork_replay_lines += other.fork_replay_lines;
237        self.read_lines += other.read_lines;
238        self.invalid_json_lines += other.invalid_json_lines;
239        self.token_count_events += other.token_count_events;
240        self.included_usage_events += other.included_usage_events;
241        self.skipped_events.missing_metadata += other.skipped_events.missing_metadata;
242        self.skipped_events.missing_usage += other.skipped_events.missing_usage;
243        self.skipped_events.empty_usage += other.skipped_events.empty_usage;
244        self.skipped_events.out_of_range += other.skipped_events.out_of_range;
245        self.skipped_events.account_mismatch += other.skipped_events.account_mismatch;
246        self.skipped_events.fork_replay += other.skipped_events.fork_replay;
247    }
248
249    pub(crate) fn merge_session_scan(&mut self, other: &SessionScanDiagnostics) {
250        self.scanned_directories += other.scanned_directories;
251        self.skipped_directories += other.skipped_directories;
252        self.read_files += other.read_files;
253        self.skipped_files += other.skipped_files;
254        self.prefiltered_files += other.prefiltered_files;
255        self.tail_read_files += other.tail_read_files;
256        self.tail_read_hits += other.tail_read_hits;
257        self.mtime_read_files += other.mtime_read_files;
258        self.mtime_tail_hits += other.mtime_tail_hits;
259        self.mtime_read_hits += other.mtime_read_hits;
260        self.fork_files += other.fork_files;
261        self.fork_parent_missing += other.fork_parent_missing;
262        self.fork_replay_lines += other.fork_replay_lines;
263    }
264}
265
266#[derive(Clone, Debug)]
267pub(super) struct UsageStatsReport {
268    pub(super) start: DateTime<Utc>,
269    pub(super) end: DateTime<Utc>,
270    pub(super) group_by: StatGroupBy,
271    pub(super) include_reasoning_effort: bool,
272    pub(super) sort_by: Option<StatSort>,
273    pub(super) limit: Option<usize>,
274    pub(super) sessions_dir: String,
275    pub(super) rows: Vec<UsageStatRow>,
276    pub(super) totals: UsageStatRow,
277    pub(super) unpriced_models: Vec<UsageUnpricedModelRow>,
278    pub(super) diagnostics: Option<UsageDiagnostics>,
279}
280
281#[derive(Clone, Debug, Serialize, PartialEq)]
282#[serde(rename_all = "camelCase")]
283pub(super) struct LimitUsageRow {
284    pub(super) window_id: String,
285    pub(super) window: String,
286    pub(super) window_minutes: i64,
287    pub(super) window_start: Option<DateTime<Utc>>,
288    pub(super) reset_at: Option<DateTime<Utc>>,
289    pub(super) observed: bool,
290    pub(super) group_by: &'static str,
291    pub(super) group_key: String,
292    pub(super) sessions: usize,
293    pub(super) calls: i64,
294    pub(super) usage: TokenUsage,
295    pub(super) credits: f64,
296    pub(super) usd: f64,
297    pub(super) priced_calls: i64,
298    pub(super) unpriced_calls: i64,
299}
300
301#[derive(Clone, Debug, Serialize, PartialEq)]
302#[serde(rename_all = "camelCase")]
303pub(super) struct LimitUsageDiagnostics {
304    pub(super) observed_windows: i64,
305    pub(super) unobserved_usage_events: i64,
306    pub(super) usage: UsageDiagnostics,
307    pub(super) rate_limits: RateLimitDiagnostics,
308}
309
310#[derive(Clone, Debug)]
311pub(super) struct LimitUsageReport {
312    pub(super) start: DateTime<Utc>,
313    pub(super) end: DateTime<Utc>,
314    pub(super) limit_window: &'static str,
315    pub(super) window_minutes: i64,
316    pub(super) group_by: LimitUsageGroupBy,
317    pub(super) include_reasoning_effort: bool,
318    pub(super) sort_by: Option<StatSort>,
319    pub(super) limit: Option<usize>,
320    pub(super) sessions_dir: String,
321    pub(super) rows: Vec<LimitUsageRow>,
322    pub(super) totals: UsageStatRow,
323    pub(super) unpriced_models: Vec<UsageUnpricedModelRow>,
324    pub(super) diagnostics: Option<LimitUsageDiagnostics>,
325}
326
327#[derive(Clone, Debug)]
328pub(super) struct UsageSessionRow {
329    pub(super) session_id: String,
330    pub(super) model: String,
331    pub(super) cwd: String,
332    pub(super) first_seen: DateTime<Utc>,
333    pub(super) last_seen: DateTime<Utc>,
334    pub(super) calls: i64,
335    pub(super) usage: TokenUsage,
336    pub(super) credits: f64,
337    pub(super) usd: f64,
338    pub(super) priced_calls: i64,
339    pub(super) unpriced_calls: i64,
340    pub(super) file_path: String,
341}
342
343#[derive(Clone, Debug)]
344pub(super) struct UsageSessionEventRow {
345    pub(super) timestamp: DateTime<Utc>,
346    pub(super) model: String,
347    pub(super) reasoning_effort: Option<String>,
348    pub(super) cwd: String,
349    pub(super) usage: TokenUsage,
350    pub(super) credits: f64,
351    pub(super) usd: f64,
352    pub(super) priced: bool,
353    pub(super) file_path: String,
354}
355
356#[derive(Clone, Debug)]
357pub(super) struct UsageSessionCompactRow {
358    pub(super) start: DateTime<Utc>,
359    pub(super) end: DateTime<Utc>,
360    pub(super) events: usize,
361    pub(super) model: String,
362    pub(super) reasoning_effort: Option<String>,
363    pub(super) usage: TokenUsage,
364    pub(super) credits: f64,
365    pub(super) usd: f64,
366    pub(super) unpriced_calls: i64,
367}
368
369#[derive(Clone, Debug)]
370pub(super) struct UsageSessionsReport {
371    pub(super) start: DateTime<Utc>,
372    pub(super) end: DateTime<Utc>,
373    pub(super) sort_by: Option<StatSort>,
374    pub(super) limit: usize,
375    pub(super) sessions_dir: String,
376    pub(super) rows: Vec<UsageSessionRow>,
377    pub(super) totals: UsageStatRow,
378    pub(super) unpriced_models: Vec<UsageUnpricedModelRow>,
379    pub(super) diagnostics: Option<UsageDiagnostics>,
380}
381
382#[derive(Clone, Debug)]
383pub(super) struct UsageSessionDetailReport {
384    pub(super) start: DateTime<Utc>,
385    pub(super) end: DateTime<Utc>,
386    pub(super) session_id: String,
387    pub(super) limit: Option<usize>,
388    pub(super) sessions_dir: String,
389    pub(super) summary: Option<UsageSessionRow>,
390    pub(super) rows: Vec<UsageSessionEventRow>,
391    pub(super) by_model: Vec<UsageStatRow>,
392    pub(super) by_cwd: Vec<UsageStatRow>,
393    pub(super) by_reasoning_effort: Vec<UsageStatRow>,
394    pub(super) model_switches: i64,
395    pub(super) cwd_switches: i64,
396    pub(super) reasoning_effort_switches: i64,
397    pub(super) totals: UsageStatRow,
398    pub(super) unpriced_models: Vec<UsageUnpricedModelRow>,
399    pub(super) diagnostics: Option<UsageDiagnostics>,
400}
401
402#[derive(Serialize)]
403#[serde(rename_all = "camelCase")]
404pub(super) struct UsageStatsJson<'a> {
405    start: String,
406    end: String,
407    group_by: &'static str,
408    include_reasoning_effort: bool,
409    #[serde(skip_serializing_if = "Option::is_none")]
410    sort_by: Option<&'static str>,
411    #[serde(skip_serializing_if = "Option::is_none")]
412    limit: Option<usize>,
413    sessions_dir: &'a str,
414    rows: &'a [UsageStatRow],
415    totals: &'a UsageStatRow,
416    unpriced_models: &'a [UsageUnpricedModelRow],
417    warnings: Vec<String>,
418    #[serde(skip_serializing_if = "Option::is_none")]
419    diagnostics: Option<&'a UsageDiagnostics>,
420}
421
422#[derive(Serialize)]
423#[serde(rename_all = "camelCase")]
424pub(super) struct LimitUsageJson<'a> {
425    start: String,
426    end: String,
427    limit_window: &'static str,
428    window_minutes: i64,
429    group_by: &'static str,
430    include_reasoning_effort: bool,
431    #[serde(skip_serializing_if = "Option::is_none")]
432    sort_by: Option<&'static str>,
433    #[serde(skip_serializing_if = "Option::is_none")]
434    limit: Option<usize>,
435    sessions_dir: &'a str,
436    rows: &'a [LimitUsageRow],
437    totals: &'a UsageStatRow,
438    unpriced_models: &'a [UsageUnpricedModelRow],
439    warnings: Vec<String>,
440    #[serde(skip_serializing_if = "Option::is_none")]
441    diagnostics: Option<&'a LimitUsageDiagnostics>,
442}
443
444#[derive(Serialize)]
445#[serde(rename_all = "camelCase")]
446pub(super) struct UsageSessionsJson<'a> {
447    start: String,
448    end: String,
449    #[serde(skip_serializing_if = "Option::is_none")]
450    sort_by: Option<&'static str>,
451    limit: usize,
452    sessions_dir: &'a str,
453    rows: Vec<UsageSessionRowJson<'a>>,
454    totals: &'a UsageStatRow,
455    unpriced_models: &'a [UsageUnpricedModelRow],
456    warnings: Vec<String>,
457    #[serde(skip_serializing_if = "Option::is_none")]
458    diagnostics: Option<&'a UsageDiagnostics>,
459}
460
461#[derive(Serialize)]
462#[serde(rename_all = "camelCase")]
463pub(super) struct UsageSessionRowJson<'a> {
464    session_id: &'a str,
465    model: &'a str,
466    cwd: &'a str,
467    first_seen: String,
468    last_seen: String,
469    calls: i64,
470    usage: &'a TokenUsage,
471    credits: f64,
472    usd: f64,
473    priced_calls: i64,
474    unpriced_calls: i64,
475    file_path: &'a str,
476}
477
478#[derive(Serialize)]
479#[serde(rename_all = "camelCase")]
480pub(super) struct UsageSessionDetailJson<'a> {
481    start: String,
482    end: String,
483    session_id: &'a str,
484    #[serde(skip_serializing_if = "Option::is_none")]
485    limit: Option<usize>,
486    sessions_dir: &'a str,
487    #[serde(skip_serializing_if = "Option::is_none")]
488    summary: Option<UsageSessionRowJson<'a>>,
489    rows: Vec<UsageSessionEventRowJson<'a>>,
490    by_model: &'a [UsageStatRow],
491    by_cwd: &'a [UsageStatRow],
492    by_reasoning_effort: &'a [UsageStatRow],
493    model_switches: i64,
494    cwd_switches: i64,
495    reasoning_effort_switches: i64,
496    totals: &'a UsageStatRow,
497    unpriced_models: &'a [UsageUnpricedModelRow],
498    warnings: Vec<String>,
499    #[serde(skip_serializing_if = "Option::is_none")]
500    diagnostics: Option<&'a UsageDiagnostics>,
501}
502
503#[derive(Serialize)]
504#[serde(rename_all = "camelCase")]
505pub(super) struct UsageSessionEventRowJson<'a> {
506    timestamp: String,
507    model: &'a str,
508    #[serde(skip_serializing_if = "Option::is_none")]
509    reasoning_effort: Option<&'a str>,
510    cwd: &'a str,
511    usage: &'a TokenUsage,
512    credits: f64,
513    usd: f64,
514    priced: bool,
515    file_path: &'a str,
516}
517
518pub(super) fn to_usage_stats_json(report: &UsageStatsReport) -> UsageStatsJson<'_> {
519    UsageStatsJson {
520        start: iso_string(report.start),
521        end: iso_string(report.end),
522        group_by: report.group_by.as_str(),
523        include_reasoning_effort: report.include_reasoning_effort,
524        sort_by: report.sort_by.map(StatSort::as_str),
525        limit: report.limit,
526        sessions_dir: &report.sessions_dir,
527        rows: &report.rows,
528        totals: &report.totals,
529        unpriced_models: &report.unpriced_models,
530        warnings: usage_warnings(report.start, report.end, report.diagnostics.as_ref()),
531        diagnostics: report.diagnostics.as_ref(),
532    }
533}
534
535pub(super) fn to_limit_usage_json(report: &LimitUsageReport) -> LimitUsageJson<'_> {
536    LimitUsageJson {
537        start: iso_string(report.start),
538        end: iso_string(report.end),
539        limit_window: report.limit_window,
540        window_minutes: report.window_minutes,
541        group_by: report.group_by.as_str(),
542        include_reasoning_effort: report.include_reasoning_effort,
543        sort_by: report.sort_by.map(StatSort::as_str),
544        limit: report.limit,
545        sessions_dir: &report.sessions_dir,
546        rows: &report.rows,
547        totals: &report.totals,
548        unpriced_models: &report.unpriced_models,
549        warnings: limit_usage_warnings(report),
550        diagnostics: report.diagnostics.as_ref(),
551    }
552}
553
554pub(super) fn to_usage_sessions_json(report: &UsageSessionsReport) -> UsageSessionsJson<'_> {
555    UsageSessionsJson {
556        start: iso_string(report.start),
557        end: iso_string(report.end),
558        sort_by: report.sort_by.map(StatSort::as_str),
559        limit: report.limit,
560        sessions_dir: &report.sessions_dir,
561        rows: report.rows.iter().map(to_session_row_json).collect(),
562        totals: &report.totals,
563        unpriced_models: &report.unpriced_models,
564        warnings: usage_warnings(report.start, report.end, report.diagnostics.as_ref()),
565        diagnostics: report.diagnostics.as_ref(),
566    }
567}
568
569pub(super) fn to_usage_session_detail_json(
570    report: &UsageSessionDetailReport,
571) -> UsageSessionDetailJson<'_> {
572    UsageSessionDetailJson {
573        start: iso_string(report.start),
574        end: iso_string(report.end),
575        session_id: &report.session_id,
576        limit: report.limit,
577        sessions_dir: &report.sessions_dir,
578        summary: report.summary.as_ref().map(to_session_row_json),
579        rows: report.rows.iter().map(to_session_event_row_json).collect(),
580        by_model: &report.by_model,
581        by_cwd: &report.by_cwd,
582        by_reasoning_effort: &report.by_reasoning_effort,
583        model_switches: report.model_switches,
584        cwd_switches: report.cwd_switches,
585        reasoning_effort_switches: report.reasoning_effort_switches,
586        totals: &report.totals,
587        unpriced_models: &report.unpriced_models,
588        warnings: usage_warnings(report.start, report.end, report.diagnostics.as_ref()),
589        diagnostics: report.diagnostics.as_ref(),
590    }
591}
592
593fn to_session_row_json(row: &UsageSessionRow) -> UsageSessionRowJson<'_> {
594    UsageSessionRowJson {
595        session_id: &row.session_id,
596        model: &row.model,
597        cwd: &row.cwd,
598        first_seen: iso_string(row.first_seen),
599        last_seen: iso_string(row.last_seen),
600        calls: row.calls,
601        usage: &row.usage,
602        credits: row.credits,
603        usd: row.usd,
604        priced_calls: row.priced_calls,
605        unpriced_calls: row.unpriced_calls,
606        file_path: &row.file_path,
607    }
608}
609
610fn to_session_event_row_json(row: &UsageSessionEventRow) -> UsageSessionEventRowJson<'_> {
611    UsageSessionEventRowJson {
612        timestamp: iso_string(row.timestamp),
613        model: &row.model,
614        reasoning_effort: row.reasoning_effort.as_deref(),
615        cwd: &row.cwd,
616        usage: &row.usage,
617        credits: row.credits,
618        usd: row.usd,
619        priced: row.priced,
620        file_path: &row.file_path,
621    }
622}
623
624pub(super) fn usage_warnings(
625    _start: DateTime<Utc>,
626    _end: DateTime<Utc>,
627    _diagnostics: Option<&UsageDiagnostics>,
628) -> Vec<String> {
629    Vec::new()
630}
631
632pub(super) fn limit_usage_warnings(report: &LimitUsageReport) -> Vec<String> {
633    let mut warnings = Vec::new();
634    if report
635        .diagnostics
636        .as_ref()
637        .is_some_and(|diagnostics| diagnostics.unobserved_usage_events > 0)
638    {
639        warnings.push(
640            "Some usage events were not inside an observed rate-limit window and are grouped as unobserved.".to_string(),
641        );
642    }
643    warnings
644}
645
646pub(super) fn is_all_usage_range(start: DateTime<Utc>, end: DateTime<Utc>) -> bool {
647    start == local_to_utc(1900, 1, 1, 0, 0, 0, 0)
648        && end == local_to_utc(9999, 12, 31, 23, 59, 59, 999)
649}
650
651pub(super) fn format_report_range(start: DateTime<Utc>, end: DateTime<Utc>) -> String {
652    if is_all_usage_range(start, end) {
653        "all".to_string()
654    } else {
655        format!("{} to {}", format_date_time(start), format_date_time(end))
656    }
657}
658
659pub(super) fn format_group_by(report: &UsageStatsReport) -> String {
660    if report.group_by == StatGroupBy::Model && report.include_reasoning_effort {
661        "model + reasoning_effort".to_string()
662    } else {
663        report.group_by.as_str().to_string()
664    }
665}
666
667pub(super) fn format_date_time(date: DateTime<Utc>) -> String {
668    let local = date.with_timezone(&Local);
669    format!(
670        "{}-{:02}-{:02} {:02}:{:02}:{:02}",
671        local.year(),
672        local.month(),
673        local.day(),
674        local.hour(),
675        local.minute(),
676        local.second()
677    )
678}
679
680fn iso_string(value: DateTime<Utc>) -> String {
681    value.to_rfc3339_opts(SecondsFormat::Millis, true)
682}