From 0e08dd6055da1f1eb58a0ac0a15acfb038878029 Mon Sep 17 00:00:00 2001 From: Thibault Sottiaux Date: Fri, 17 Oct 2025 17:39:37 -0700 Subject: [PATCH] fix: switch rate limit reset handling to timestamps (#5304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change ensures that we store the absolute time instead of relative offsets of when the primary and secondary rate limits will reset. Previously these got recalculated relative to current time, which leads to the displayed reset times to change over time, including after doing a codex resume. For previously changed sessions, this will cause the reset times to not show due to this being a breaking change: Screenshot 2025-10-17 at 5 14 18 PM Fixes https://github.com/openai/codex/issues/4761 --- codex-rs/clippy.toml | 4 + codex-rs/core/src/client.rs | 44 +++--- codex-rs/core/src/error.rs | 181 ++++++++++++++++--------- codex-rs/core/tests/suite/client.rs | 22 +-- codex-rs/protocol/src/protocol.rs | 6 +- codex-rs/tui/src/status/rate_limits.rs | 9 +- codex-rs/tui/src/status/tests.rs | 64 +++++---- 7 files changed, 203 insertions(+), 127 deletions(-) diff --git a/codex-rs/clippy.toml b/codex-rs/clippy.toml index 5a6ff7f0..a2aae659 100644 --- a/codex-rs/clippy.toml +++ b/codex-rs/clippy.toml @@ -7,3 +7,7 @@ disallowed-methods = [ { path = "ratatui::style::Stylize::black", reason = "Avoid hardcoding black; prefer default fg or dim/bold. Exception: Disable this rule if rendering over a hardcoded ANSI background." }, { path = "ratatui::style::Stylize::yellow", reason = "Avoid yellow; prefer other colors in `tui/styles.md`." }, ] + +# Increase the size threshold for result_large_err to accommodate +# richer error variants. +large-error-threshold = 256 diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 3a259fce..b215106d 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -52,6 +52,8 @@ use crate::protocol::TokenUsage; use crate::state::TaskKind; use crate::token_data::PlanType; use crate::util::backoff; +use chrono::DateTime; +use chrono::Utc; use codex_otel::otel_event_manager::OtelEventManager; use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; @@ -71,7 +73,7 @@ struct Error { // Optional fields available on "usage_limit_reached" and "usage_not_included" errors plan_type: Option, - resets_in_seconds: Option, + resets_at: Option, } #[derive(Debug, Clone)] @@ -419,10 +421,14 @@ impl ModelClient { let plan_type = error .plan_type .or_else(|| auth.as_ref().and_then(CodexAuth::get_plan_type)); - let resets_in_seconds = error.resets_in_seconds; + let resets_at = error + .resets_at + .as_deref() + .and_then(|value| DateTime::parse_from_rfc3339(value).ok()) + .map(|dt| dt.with_timezone(&Utc)); let codex_err = CodexErr::UsageLimitReached(UsageLimitReachedError { plan_type, - resets_in_seconds, + resets_at, rate_limits: rate_limit_snapshot, }); return Err(StreamAttemptError::Fatal(codex_err)); @@ -605,14 +611,14 @@ fn parse_rate_limit_snapshot(headers: &HeaderMap) -> Option { headers, "x-codex-primary-used-percent", "x-codex-primary-window-minutes", - "x-codex-primary-reset-after-seconds", + "x-codex-primary-reset-at", ); let secondary = parse_rate_limit_window( headers, "x-codex-secondary-used-percent", "x-codex-secondary-window-minutes", - "x-codex-secondary-reset-after-seconds", + "x-codex-secondary-reset-at", ); Some(RateLimitSnapshot { primary, secondary }) @@ -628,16 +634,19 @@ fn parse_rate_limit_window( used_percent.and_then(|used_percent| { let window_minutes = parse_header_u64(headers, window_minutes_header); - let resets_in_seconds = parse_header_u64(headers, resets_header); + let resets_at = parse_header_str(headers, resets_header) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(std::string::ToString::to_string); let has_data = used_percent != 0.0 || window_minutes.is_some_and(|minutes| minutes != 0) - || resets_in_seconds.is_some_and(|seconds| seconds != 0); + || resets_at.is_some(); has_data.then_some(RateLimitWindow { used_percent, window_minutes, - resets_in_seconds, + resets_at, }) }) } @@ -1390,7 +1399,7 @@ mod tests { message: Some("Rate limit reached for gpt-5 in organization org- on tokens per min (TPM): Limit 1, Used 1, Requested 19304. Please try again in 28ms. Visit https://platform.openai.com/account/rate-limits to learn more.".to_string()), code: Some("rate_limit_exceeded".to_string()), plan_type: None, - resets_in_seconds: None + resets_at: None }; let delay = try_parse_retry_after(&err); @@ -1404,20 +1413,19 @@ mod tests { message: Some("Rate limit reached for gpt-5 in organization on tokens per min (TPM): Limit 30000, Used 6899, Requested 24050. Please try again in 1.898s. Visit https://platform.openai.com/account/rate-limits to learn more.".to_string()), code: Some("rate_limit_exceeded".to_string()), plan_type: None, - resets_in_seconds: None + resets_at: None }; let delay = try_parse_retry_after(&err); assert_eq!(delay, Some(Duration::from_secs_f64(1.898))); } #[test] - fn error_response_deserializes_old_schema_known_plan_type_and_serializes_back() { + fn error_response_deserializes_schema_known_plan_type_and_serializes_back() { use crate::token_data::KnownPlan; use crate::token_data::PlanType; - let json = r#"{"error":{"type":"usage_limit_reached","plan_type":"pro","resets_in_seconds":3600}}"#; - let resp: ErrorResponse = - serde_json::from_str(json).expect("should deserialize old schema"); + let json = r#"{"error":{"type":"usage_limit_reached","plan_type":"pro","resets_at":"2024-01-01T00:00:00Z"}}"#; + let resp: ErrorResponse = serde_json::from_str(json).expect("should deserialize schema"); assert_matches!(resp.error.plan_type, Some(PlanType::Known(KnownPlan::Pro))); @@ -1426,13 +1434,11 @@ mod tests { } #[test] - fn error_response_deserializes_old_schema_unknown_plan_type_and_serializes_back() { + fn error_response_deserializes_schema_unknown_plan_type_and_serializes_back() { use crate::token_data::PlanType; - let json = - r#"{"error":{"type":"usage_limit_reached","plan_type":"vip","resets_in_seconds":60}}"#; - let resp: ErrorResponse = - serde_json::from_str(json).expect("should deserialize old schema"); + let json = r#"{"error":{"type":"usage_limit_reached","plan_type":"vip","resets_at":"2024-01-01T00:01:00Z"}}"#; + let resp: ErrorResponse = serde_json::from_str(json).expect("should deserialize schema"); assert_matches!(resp.error.plan_type, Some(PlanType::Unknown(ref s)) if s == "vip"); diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 951ba395..5597ffd4 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -2,6 +2,8 @@ use crate::exec::ExecToolCallOutput; use crate::token_data::KnownPlan; use crate::token_data::PlanType; use crate::truncate::truncate_middle; +use chrono::DateTime; +use chrono::Utc; use codex_async_utils::CancelErr; use codex_protocol::ConversationId; use codex_protocol::protocol::RateLimitSnapshot; @@ -237,7 +239,7 @@ impl std::fmt::Display for RetryLimitReachedError { #[derive(Debug)] pub struct UsageLimitReachedError { pub(crate) plan_type: Option, - pub(crate) resets_in_seconds: Option, + pub(crate) resets_at: Option>, pub(crate) rate_limits: Option, } @@ -246,12 +248,12 @@ impl std::fmt::Display for UsageLimitReachedError { let message = match self.plan_type.as_ref() { Some(PlanType::Known(KnownPlan::Plus)) => format!( "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing){}", - retry_suffix_after_or(self.resets_in_seconds) + retry_suffix_after_or(self.resets_at.as_ref()) ), Some(PlanType::Known(KnownPlan::Team)) | Some(PlanType::Known(KnownPlan::Business)) => { format!( "You've hit your usage limit. To get more access now, send a request to your admin{}", - retry_suffix_after_or(self.resets_in_seconds) + retry_suffix_after_or(self.resets_at.as_ref()) ) } Some(PlanType::Known(KnownPlan::Free)) => { @@ -262,11 +264,11 @@ impl std::fmt::Display for UsageLimitReachedError { | Some(PlanType::Known(KnownPlan::Enterprise)) | Some(PlanType::Known(KnownPlan::Edu)) => format!( "You've hit your usage limit.{}", - retry_suffix(self.resets_in_seconds) + retry_suffix(self.resets_at.as_ref()) ), Some(PlanType::Unknown(_)) | None => format!( "You've hit your usage limit.{}", - retry_suffix(self.resets_in_seconds) + retry_suffix(self.resets_at.as_ref()) ), }; @@ -274,8 +276,8 @@ impl std::fmt::Display for UsageLimitReachedError { } } -fn retry_suffix(resets_in_seconds: Option) -> String { - if let Some(secs) = resets_in_seconds { +fn retry_suffix(resets_at: Option<&DateTime>) -> String { + if let Some(secs) = remaining_seconds(resets_at) { let reset_duration = format_reset_duration(secs); format!(" Try again in {reset_duration}.") } else { @@ -283,8 +285,8 @@ fn retry_suffix(resets_in_seconds: Option) -> String { } } -fn retry_suffix_after_or(resets_in_seconds: Option) -> String { - if let Some(secs) = resets_in_seconds { +fn retry_suffix_after_or(resets_at: Option<&DateTime>) -> String { + if let Some(secs) = remaining_seconds(resets_at) { let reset_duration = format_reset_duration(secs); format!(" or try again in {reset_duration}.") } else { @@ -292,6 +294,29 @@ fn retry_suffix_after_or(resets_in_seconds: Option) -> String { } } +fn remaining_seconds(resets_at: Option<&DateTime>) -> Option { + let resets_at = resets_at.cloned()?; + let now = now_for_retry(); + let secs = resets_at.signed_duration_since(now).num_seconds(); + Some(if secs <= 0 { 0 } else { secs as u64 }) +} + +#[cfg(test)] +thread_local! { + static NOW_OVERRIDE: std::cell::RefCell>> = + const { std::cell::RefCell::new(None) }; +} + +fn now_for_retry() -> DateTime { + #[cfg(test)] + { + if let Some(now) = NOW_OVERRIDE.with(|cell| *cell.borrow()) { + return now; + } + } + Utc::now() +} + fn format_reset_duration(total_secs: u64) -> String { let days = total_secs / 86_400; let hours = (total_secs % 86_400) / 3_600; @@ -388,6 +413,10 @@ pub fn get_error_message_ui(e: &CodexErr) -> String { mod tests { use super::*; use crate::exec::StreamOutput; + use chrono::DateTime; + use chrono::Duration as ChronoDuration; + use chrono::TimeZone; + use chrono::Utc; use codex_protocol::protocol::RateLimitWindow; use pretty_assertions::assert_eq; @@ -396,21 +425,30 @@ mod tests { primary: Some(RateLimitWindow { used_percent: 50.0, window_minutes: Some(60), - resets_in_seconds: Some(3600), + resets_at: Some("2024-01-01T01:00:00Z".to_string()), }), secondary: Some(RateLimitWindow { used_percent: 30.0, window_minutes: Some(120), - resets_in_seconds: Some(7200), + resets_at: Some("2024-01-01T02:00:00Z".to_string()), }), } } + fn with_now_override(now: DateTime, f: impl FnOnce() -> T) -> T { + NOW_OVERRIDE.with(|cell| { + *cell.borrow_mut() = Some(now); + let result = f(); + *cell.borrow_mut() = None; + result + }) + } + #[test] fn usage_limit_reached_error_formats_plus_plan() { let err = UsageLimitReachedError { plan_type: Some(PlanType::Known(KnownPlan::Plus)), - resets_in_seconds: None, + resets_at: None, rate_limits: Some(rate_limit_snapshot()), }; assert_eq!( @@ -490,7 +528,7 @@ mod tests { fn usage_limit_reached_error_formats_free_plan() { let err = UsageLimitReachedError { plan_type: Some(PlanType::Known(KnownPlan::Free)), - resets_in_seconds: Some(3600), + resets_at: None, rate_limits: Some(rate_limit_snapshot()), }; assert_eq!( @@ -503,7 +541,7 @@ mod tests { fn usage_limit_reached_error_formats_default_when_none() { let err = UsageLimitReachedError { plan_type: None, - resets_in_seconds: None, + resets_at: None, rate_limits: Some(rate_limit_snapshot()), }; assert_eq!( @@ -514,22 +552,26 @@ mod tests { #[test] fn usage_limit_reached_error_formats_team_plan() { - let err = UsageLimitReachedError { - plan_type: Some(PlanType::Known(KnownPlan::Team)), - resets_in_seconds: Some(3600), - rate_limits: Some(rate_limit_snapshot()), - }; - assert_eq!( - err.to_string(), - "You've hit your usage limit. To get more access now, send a request to your admin or try again in 1 hour." - ); + let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(); + let resets_at = base + ChronoDuration::hours(1); + with_now_override(base, move || { + let err = UsageLimitReachedError { + plan_type: Some(PlanType::Known(KnownPlan::Team)), + resets_at: Some(resets_at), + rate_limits: Some(rate_limit_snapshot()), + }; + assert_eq!( + err.to_string(), + "You've hit your usage limit. To get more access now, send a request to your admin or try again in 1 hour." + ); + }); } #[test] fn usage_limit_reached_error_formats_business_plan_without_reset() { let err = UsageLimitReachedError { plan_type: Some(PlanType::Known(KnownPlan::Business)), - resets_in_seconds: None, + resets_at: None, rate_limits: Some(rate_limit_snapshot()), }; assert_eq!( @@ -542,7 +584,7 @@ mod tests { fn usage_limit_reached_error_formats_default_for_other_plans() { let err = UsageLimitReachedError { plan_type: Some(PlanType::Known(KnownPlan::Pro)), - resets_in_seconds: None, + resets_at: None, rate_limits: Some(rate_limit_snapshot()), }; assert_eq!( @@ -553,53 +595,70 @@ mod tests { #[test] fn usage_limit_reached_includes_minutes_when_available() { - let err = UsageLimitReachedError { - plan_type: None, - resets_in_seconds: Some(5 * 60), - rate_limits: Some(rate_limit_snapshot()), - }; - assert_eq!( - err.to_string(), - "You've hit your usage limit. Try again in 5 minutes." - ); + let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(); + let resets_at = base + ChronoDuration::minutes(5); + with_now_override(base, move || { + let err = UsageLimitReachedError { + plan_type: None, + resets_at: Some(resets_at), + rate_limits: Some(rate_limit_snapshot()), + }; + assert_eq!( + err.to_string(), + "You've hit your usage limit. Try again in 5 minutes." + ); + }); } #[test] fn usage_limit_reached_includes_hours_and_minutes() { - let err = UsageLimitReachedError { - plan_type: Some(PlanType::Known(KnownPlan::Plus)), - resets_in_seconds: Some(3 * 3600 + 32 * 60), - rate_limits: Some(rate_limit_snapshot()), - }; - assert_eq!( - err.to_string(), - "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing) or try again in 3 hours 32 minutes." - ); + let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(); + let resets_at = base + ChronoDuration::hours(3) + ChronoDuration::minutes(32); + with_now_override(base, move || { + let err = UsageLimitReachedError { + plan_type: Some(PlanType::Known(KnownPlan::Plus)), + resets_at: Some(resets_at), + rate_limits: Some(rate_limit_snapshot()), + }; + assert_eq!( + err.to_string(), + "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing) or try again in 3 hours 32 minutes." + ); + }); } #[test] fn usage_limit_reached_includes_days_hours_minutes() { - let err = UsageLimitReachedError { - plan_type: None, - resets_in_seconds: Some(2 * 86_400 + 3 * 3600 + 5 * 60), - rate_limits: Some(rate_limit_snapshot()), - }; - assert_eq!( - err.to_string(), - "You've hit your usage limit. Try again in 2 days 3 hours 5 minutes." - ); + let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(); + let resets_at = + base + ChronoDuration::days(2) + ChronoDuration::hours(3) + ChronoDuration::minutes(5); + with_now_override(base, move || { + let err = UsageLimitReachedError { + plan_type: None, + resets_at: Some(resets_at), + rate_limits: Some(rate_limit_snapshot()), + }; + assert_eq!( + err.to_string(), + "You've hit your usage limit. Try again in 2 days 3 hours 5 minutes." + ); + }); } #[test] fn usage_limit_reached_less_than_minute() { - let err = UsageLimitReachedError { - plan_type: None, - resets_in_seconds: Some(30), - rate_limits: Some(rate_limit_snapshot()), - }; - assert_eq!( - err.to_string(), - "You've hit your usage limit. Try again in less than a minute." - ); + let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(); + let resets_at = base + ChronoDuration::seconds(30); + with_now_override(base, move || { + let err = UsageLimitReachedError { + plan_type: None, + resets_at: Some(resets_at), + rate_limits: Some(rate_limit_snapshot()), + }; + assert_eq!( + err.to_string(), + "You've hit your usage limit. Try again in less than a minute." + ); + }); } } diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index beaa87a8..817f2c2a 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -768,8 +768,8 @@ async fn token_count_includes_rate_limits_snapshot() { .insert_header("x-codex-secondary-used-percent", "40.0") .insert_header("x-codex-primary-window-minutes", "10") .insert_header("x-codex-secondary-window-minutes", "60") - .insert_header("x-codex-primary-reset-after-seconds", "1800") - .insert_header("x-codex-secondary-reset-after-seconds", "7200") + .insert_header("x-codex-primary-reset-at", "2024-01-01T00:30:00Z") + .insert_header("x-codex-secondary-reset-at", "2024-01-01T02:00:00Z") .set_body_raw(sse_body, "text/event-stream"); Mock::given(method("POST")) @@ -818,12 +818,12 @@ async fn token_count_includes_rate_limits_snapshot() { "primary": { "used_percent": 12.5, "window_minutes": 10, - "resets_in_seconds": 1800 + "resets_at": "2024-01-01T00:30:00Z" }, "secondary": { "used_percent": 40.0, "window_minutes": 60, - "resets_in_seconds": 7200 + "resets_at": "2024-01-01T02:00:00Z" } } }) @@ -865,12 +865,12 @@ async fn token_count_includes_rate_limits_snapshot() { "primary": { "used_percent": 12.5, "window_minutes": 10, - "resets_in_seconds": 1800 + "resets_at": "2024-01-01T00:30:00Z" }, "secondary": { "used_percent": 40.0, "window_minutes": 60, - "resets_in_seconds": 7200 + "resets_at": "2024-01-01T02:00:00Z" } } }) @@ -893,8 +893,8 @@ async fn token_count_includes_rate_limits_snapshot() { final_snapshot .primary .as_ref() - .and_then(|window| window.resets_in_seconds), - Some(1800) + .and_then(|window| window.resets_at.as_deref()), + Some("2024-01-01T00:30:00Z") ); wait_for_event(&codex, |msg| matches!(msg, EventMsg::TaskComplete(_))).await; @@ -915,7 +915,7 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> { "error": { "type": "usage_limit_reached", "message": "limit reached", - "resets_in_seconds": 42, + "resets_at": "2024-01-01T00:00:42Z", "plan_type": "pro" } })); @@ -935,12 +935,12 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> { "primary": { "used_percent": 100.0, "window_minutes": 15, - "resets_in_seconds": null + "resets_at": null }, "secondary": { "used_percent": 87.5, "window_minutes": 60, - "resets_in_seconds": null + "resets_at": null } }); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index d8881947..5429e4d4 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -644,9 +644,9 @@ pub struct RateLimitWindow { /// Rolling window duration, in minutes. #[ts(type = "number | null")] pub window_minutes: Option, - /// Seconds until the window resets. - #[ts(type = "number | null")] - pub resets_in_seconds: Option, + /// Timestamp (RFC3339) when the window resets. + #[ts(type = "string | null")] + pub resets_at: Option, } // Includes prompts, tools and space to call compact. diff --git a/codex-rs/tui/src/status/rate_limits.rs b/codex-rs/tui/src/status/rate_limits.rs index 875001ef..f6a9c06a 100644 --- a/codex-rs/tui/src/status/rate_limits.rs +++ b/codex-rs/tui/src/status/rate_limits.rs @@ -2,11 +2,9 @@ use crate::chatwidget::get_limits_duration; use super::helpers::format_reset_timestamp; use chrono::DateTime; -use chrono::Duration as ChronoDuration; use chrono::Local; use codex_core::protocol::RateLimitSnapshot; use codex_core::protocol::RateLimitWindow; -use std::convert::TryFrom; const STATUS_LIMIT_BAR_SEGMENTS: usize = 20; const STATUS_LIMIT_BAR_FILLED: &str = "█"; @@ -35,9 +33,10 @@ pub(crate) struct RateLimitWindowDisplay { impl RateLimitWindowDisplay { fn from_window(window: &RateLimitWindow, captured_at: DateTime) -> Self { let resets_at = window - .resets_in_seconds - .and_then(|seconds| i64::try_from(seconds).ok()) - .and_then(|secs| captured_at.checked_add_signed(ChronoDuration::seconds(secs))) + .resets_at + .as_deref() + .and_then(|value| DateTime::parse_from_rfc3339(value).ok()) + .map(|dt| dt.with_timezone(&Local)) .map(|dt| format_reset_timestamp(dt, captured_at)); Self { diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index 395d952f..5cb730c9 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -1,7 +1,9 @@ use super::new_status_output; use super::rate_limit_snapshot_display; use crate::history_cell::HistoryCell; +use chrono::Duration as ChronoDuration; use chrono::TimeZone; +use chrono::Utc; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config::ConfigToml; @@ -60,6 +62,12 @@ fn sanitize_directory(lines: Vec) -> Vec { .collect() } +fn reset_at_from(captured_at: &chrono::DateTime, seconds: i64) -> String { + (*captured_at + ChronoDuration::seconds(seconds)) + .with_timezone(&Utc) + .to_rfc3339() +} + #[test] fn status_snapshot_includes_reasoning_details() { let temp_home = TempDir::new().expect("temp home"); @@ -85,22 +93,22 @@ fn status_snapshot_includes_reasoning_details() { total_tokens: 2_250, }; - let snapshot = RateLimitSnapshot { - primary: Some(RateLimitWindow { - used_percent: 72.5, - window_minutes: Some(300), - resets_in_seconds: Some(600), - }), - secondary: Some(RateLimitWindow { - used_percent: 45.0, - window_minutes: Some(10080), - resets_in_seconds: Some(1_200), - }), - }; let captured_at = chrono::Local .with_ymd_and_hms(2024, 1, 2, 3, 4, 5) .single() .expect("timestamp"); + let snapshot = RateLimitSnapshot { + primary: Some(RateLimitWindow { + used_percent: 72.5, + window_minutes: Some(300), + resets_at: Some(reset_at_from(&captured_at, 600)), + }), + secondary: Some(RateLimitWindow { + used_percent: 45.0, + window_minutes: Some(10080), + resets_at: Some(reset_at_from(&captured_at, 1_200)), + }), + }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); let composite = new_status_output(&config, &usage, Some(&usage), &None, Some(&rate_display)); @@ -130,18 +138,18 @@ fn status_snapshot_includes_monthly_limit() { total_tokens: 1_200, }; - let snapshot = RateLimitSnapshot { - primary: Some(RateLimitWindow { - used_percent: 12.0, - window_minutes: Some(43_200), - resets_in_seconds: Some(86_400), - }), - secondary: None, - }; let captured_at = chrono::Local .with_ymd_and_hms(2024, 5, 6, 7, 8, 9) .single() .expect("timestamp"); + let snapshot = RateLimitSnapshot { + primary: Some(RateLimitWindow { + used_percent: 12.0, + window_minutes: Some(43_200), + resets_at: Some(reset_at_from(&captured_at, 86_400)), + }), + secondary: None, + }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); let composite = new_status_output(&config, &usage, Some(&usage), &None, Some(&rate_display)); @@ -197,18 +205,18 @@ fn status_snapshot_truncates_in_narrow_terminal() { total_tokens: 2_250, }; - let snapshot = RateLimitSnapshot { - primary: Some(RateLimitWindow { - used_percent: 72.5, - window_minutes: Some(300), - resets_in_seconds: Some(600), - }), - secondary: None, - }; let captured_at = chrono::Local .with_ymd_and_hms(2024, 1, 2, 3, 4, 5) .single() .expect("timestamp"); + let snapshot = RateLimitSnapshot { + primary: Some(RateLimitWindow { + used_percent: 72.5, + window_minutes: Some(300), + resets_at: Some(reset_at_from(&captured_at, 600)), + }), + secondary: None, + }; let rate_display = rate_limit_snapshot_display(&snapshot, captured_at); let composite = new_status_output(&config, &usage, Some(&usage), &None, Some(&rate_display));