#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct UsageReport {
pub(crate) generated_at: Option<String>,
pub(crate) providers: Vec<UsageProviderRow>,
pub(crate) help: Vec<String>,
pub(crate) warning: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct UsageProviderRow {
pub(crate) provider: String,
pub(crate) label: String,
pub(crate) source: String,
pub(crate) plan: Option<String>,
pub(crate) status: String,
pub(crate) windows: Vec<UsageWindowRow>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct UsageWindowRow {
pub(crate) label: String,
pub(crate) kind: String,
pub(crate) percent_remaining_basis_points: u16,
pub(crate) percent_used_basis_points: Option<u16>,
pub(crate) reset_text: Option<String>,
pub(crate) resets_at: Option<String>,
pub(crate) spent_usd: Option<String>,
pub(crate) limit_usd: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum UsageLoadResult {
Loaded(UsageReport),
Error(String),
}
pub(crate) fn usage_report(usage: crate::providers::codex_usage::CodexUsage) -> UsageReport {
let windows = usage
.windows
.into_iter()
.map(|window| {
let resets_at = window
.reset_at
.and_then(|timestamp| chrono::DateTime::from_timestamp(timestamp, 0))
.map(|time| time.to_rfc3339());
UsageWindowRow {
label: window.label,
kind: "rolling".to_string(),
percent_remaining_basis_points: percent_to_basis_points(
100.0 - window.used_percent,
),
percent_used_basis_points: Some(percent_to_basis_points(window.used_percent)),
reset_text: None,
resets_at,
spent_usd: None,
limit_usd: None,
}
})
.collect();
UsageReport {
generated_at: Some(chrono::Utc::now().to_rfc3339()),
providers: vec![UsageProviderRow {
provider: "openai-codex".to_string(),
label: "OpenAI Codex".to_string(),
source: "API".to_string(),
plan: usage.plan,
status: "fresh".to_string(),
windows,
}],
help: Vec::new(),
warning: None,
}
}
fn percent_to_basis_points(value: f64) -> u16 {
(value.clamp(0.0, 100.0) * 100.0).round() as u16
}
pub(crate) fn format_percent_basis_points(basis_points: u16) -> String {
let whole = basis_points / 100;
let tenth = (basis_points % 100) / 10;
if tenth == 0 {
format!("{whole}%")
} else {
format!("{whole}.{tenth}%")
}
}
pub(crate) fn usage_text_content(
loading: bool,
result: Option<&UsageLoadResult>,
) -> Option<String> {
if loading || result.is_none() {
return Some("Loading provider usageā¦".to_string());
}
match result.unwrap() {
UsageLoadResult::Error(message) => Some(format!(
"Could not load usage: {message}\n\nRerun /usage to retry."
)),
UsageLoadResult::Loaded(report) if report.providers.is_empty() => {
let mut text = "No Codex usage windows available.".to_string();
if !report.help.is_empty() {
text.push_str("\n\n");
text.push_str(&report.help.join("\n"));
}
Some(text)
}
UsageLoadResult::Loaded(_) => None,
}
}
pub(crate) fn usage_visual_rows(
loading: bool,
result: Option<&UsageLoadResult>,
wrap_width: u16,
) -> usize {
if let Some(text) = usage_text_content(loading, result) {
return wrapped_rows(&text, wrap_width);
}
let Some(UsageLoadResult::Loaded(report)) = result else {
return 0;
};
usize::from(report.generated_at.is_some())
+ report
.providers
.iter()
.map(|provider| 1 + provider.windows.len() * 2 + 1)
.sum::<usize>()
}
fn wrapped_rows(text: &str, wrap_width: u16) -> usize {
crate::tui::transcript::ratatui_wrapped_visual_rows(text, wrap_width.max(1))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::providers::codex_usage::CodexUsage;
use crate::providers::codex_usage::CodexUsageWindow;
#[test]
fn shows_live_codex_remaining_usage_and_reset() {
let report = usage_report(CodexUsage {
plan: Some("plus".to_string()),
windows: vec![CodexUsageWindow {
label: "5h".to_string(),
limit_window_seconds: Some(18_000),
used_percent: 42.5,
reset_at: Some(1_700_000_000),
}],
});
let provider = &report.providers[0];
assert_eq!(provider.provider, "openai-codex");
assert_eq!(provider.status, "fresh");
assert_eq!(provider.windows[0].percent_remaining_basis_points, 5750);
assert_eq!(provider.windows[0].percent_used_basis_points, Some(4250));
assert_eq!(
provider.windows[0].resets_at.as_deref(),
Some("2023-11-14T22:13:20+00:00")
);
}
#[test]
fn usage_errors_show_retry_without_external_cli_instructions() {
let result = UsageLoadResult::Error("Codex usage request timed out".to_string());
let text = usage_text_content(false, Some(&result)).unwrap();
assert!(text.contains("timed out"));
assert!(text.contains("Rerun /usage"));
assert!(usage_visual_rows(false, Some(&result), 20) > 3);
}
}