Skip to content

Commit dcf7397

Browse files
authored
rate limit errors now provide absolute time (#6000)
1 parent e761924 commit dcf7397

File tree

1 file changed

+52
-62
lines changed

1 file changed

+52
-62
lines changed

codex-rs/core/src/error.rs

Lines changed: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use crate::token_data::KnownPlan;
44
use crate::token_data::PlanType;
55
use crate::truncate::truncate_middle;
66
use chrono::DateTime;
7+
use chrono::Datelike;
8+
use chrono::Local;
79
use chrono::Utc;
810
use codex_async_utils::CancelErr;
911
use codex_protocol::ConversationId;
@@ -286,28 +288,46 @@ impl std::fmt::Display for UsageLimitReachedError {
286288
}
287289

288290
fn retry_suffix(resets_at: Option<&DateTime<Utc>>) -> String {
289-
if let Some(secs) = remaining_seconds(resets_at) {
290-
let reset_duration = format_reset_duration(secs);
291-
format!(" Try again in {reset_duration}.")
291+
if let Some(resets_at) = resets_at {
292+
let formatted = format_retry_timestamp(resets_at);
293+
format!(" Try again at {formatted}.")
292294
} else {
293295
" Try again later.".to_string()
294296
}
295297
}
296298

297299
fn retry_suffix_after_or(resets_at: Option<&DateTime<Utc>>) -> String {
298-
if let Some(secs) = remaining_seconds(resets_at) {
299-
let reset_duration = format_reset_duration(secs);
300-
format!(" or try again in {reset_duration}.")
300+
if let Some(resets_at) = resets_at {
301+
let formatted = format_retry_timestamp(resets_at);
302+
format!(" or try again at {formatted}.")
301303
} else {
302304
" or try again later.".to_string()
303305
}
304306
}
305307

306-
fn remaining_seconds(resets_at: Option<&DateTime<Utc>>) -> Option<u64> {
307-
let resets_at = resets_at.cloned()?;
308-
let now = now_for_retry();
309-
let secs = resets_at.signed_duration_since(now).num_seconds();
310-
Some(if secs <= 0 { 0 } else { secs as u64 })
308+
fn format_retry_timestamp(resets_at: &DateTime<Utc>) -> String {
309+
let local_reset = resets_at.with_timezone(&Local);
310+
let local_now = now_for_retry().with_timezone(&Local);
311+
if local_reset.date_naive() == local_now.date_naive() {
312+
local_reset.format("%-I:%M %p").to_string()
313+
} else {
314+
let suffix = day_suffix(local_reset.day());
315+
local_reset
316+
.format(&format!("%b %-d{suffix}, %Y %-I:%M %p"))
317+
.to_string()
318+
}
319+
}
320+
321+
fn day_suffix(day: u32) -> &'static str {
322+
match day {
323+
11..=13 => "th",
324+
_ => match day % 10 {
325+
1 => "st",
326+
2 => "nd", // codespell:ignore
327+
3 => "rd",
328+
_ => "th",
329+
},
330+
}
311331
}
312332

313333
#[cfg(test)]
@@ -326,36 +346,6 @@ fn now_for_retry() -> DateTime<Utc> {
326346
Utc::now()
327347
}
328348

329-
fn format_reset_duration(total_secs: u64) -> String {
330-
let days = total_secs / 86_400;
331-
let hours = (total_secs % 86_400) / 3_600;
332-
let minutes = (total_secs % 3_600) / 60;
333-
334-
let mut parts: Vec<String> = Vec::new();
335-
if days > 0 {
336-
let unit = if days == 1 { "day" } else { "days" };
337-
parts.push(format!("{days} {unit}"));
338-
}
339-
if hours > 0 {
340-
let unit = if hours == 1 { "hour" } else { "hours" };
341-
parts.push(format!("{hours} {unit}"));
342-
}
343-
if minutes > 0 {
344-
let unit = if minutes == 1 { "minute" } else { "minutes" };
345-
parts.push(format!("{minutes} {unit}"));
346-
}
347-
348-
if parts.is_empty() {
349-
return "less than a minute".to_string();
350-
}
351-
352-
match parts.len() {
353-
1 => parts[0].clone(),
354-
2 => format!("{} {}", parts[0], parts[1]),
355-
_ => format!("{} {} {}", parts[0], parts[1], parts[2]),
356-
}
357-
}
358-
359349
#[derive(Debug)]
360350
pub struct EnvVarError {
361351
/// Name of the environment variable that is missing.
@@ -572,15 +562,16 @@ mod tests {
572562
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
573563
let resets_at = base + ChronoDuration::hours(1);
574564
with_now_override(base, move || {
565+
let expected_time = format_retry_timestamp(&resets_at);
575566
let err = UsageLimitReachedError {
576567
plan_type: Some(PlanType::Known(KnownPlan::Team)),
577568
resets_at: Some(resets_at),
578569
rate_limits: Some(rate_limit_snapshot()),
579570
};
580-
assert_eq!(
581-
err.to_string(),
582-
"You've hit your usage limit. To get more access now, send a request to your admin or try again in 1 hour."
571+
let expected = format!(
572+
"You've hit your usage limit. To get more access now, send a request to your admin or try again at {expected_time}."
583573
);
574+
assert_eq!(err.to_string(), expected);
584575
});
585576
}
586577

@@ -615,15 +606,16 @@ mod tests {
615606
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
616607
let resets_at = base + ChronoDuration::hours(1);
617608
with_now_override(base, move || {
609+
let expected_time = format_retry_timestamp(&resets_at);
618610
let err = UsageLimitReachedError {
619611
plan_type: Some(PlanType::Known(KnownPlan::Pro)),
620612
resets_at: Some(resets_at),
621613
rate_limits: Some(rate_limit_snapshot()),
622614
};
623-
assert_eq!(
624-
err.to_string(),
625-
"You've hit your usage limit. Visit chatgpt.com/codex/settings/usage to purchase more credits or try again in 1 hour."
615+
let expected = format!(
616+
"You've hit your usage limit. Visit chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}."
626617
);
618+
assert_eq!(err.to_string(), expected);
627619
});
628620
}
629621

@@ -632,15 +624,14 @@ mod tests {
632624
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
633625
let resets_at = base + ChronoDuration::minutes(5);
634626
with_now_override(base, move || {
627+
let expected_time = format_retry_timestamp(&resets_at);
635628
let err = UsageLimitReachedError {
636629
plan_type: None,
637630
resets_at: Some(resets_at),
638631
rate_limits: Some(rate_limit_snapshot()),
639632
};
640-
assert_eq!(
641-
err.to_string(),
642-
"You've hit your usage limit. Try again in 5 minutes."
643-
);
633+
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
634+
assert_eq!(err.to_string(), expected);
644635
});
645636
}
646637

@@ -649,15 +640,16 @@ mod tests {
649640
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
650641
let resets_at = base + ChronoDuration::hours(3) + ChronoDuration::minutes(32);
651642
with_now_override(base, move || {
643+
let expected_time = format_retry_timestamp(&resets_at);
652644
let err = UsageLimitReachedError {
653645
plan_type: Some(PlanType::Known(KnownPlan::Plus)),
654646
resets_at: Some(resets_at),
655647
rate_limits: Some(rate_limit_snapshot()),
656648
};
657-
assert_eq!(
658-
err.to_string(),
659-
"You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit chatgpt.com/codex/settings/usage to purchase more credits or try again in 3 hours 32 minutes."
649+
let expected = format!(
650+
"You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}."
660651
);
652+
assert_eq!(err.to_string(), expected);
661653
});
662654
}
663655

@@ -667,15 +659,14 @@ mod tests {
667659
let resets_at =
668660
base + ChronoDuration::days(2) + ChronoDuration::hours(3) + ChronoDuration::minutes(5);
669661
with_now_override(base, move || {
662+
let expected_time = format_retry_timestamp(&resets_at);
670663
let err = UsageLimitReachedError {
671664
plan_type: None,
672665
resets_at: Some(resets_at),
673666
rate_limits: Some(rate_limit_snapshot()),
674667
};
675-
assert_eq!(
676-
err.to_string(),
677-
"You've hit your usage limit. Try again in 2 days 3 hours 5 minutes."
678-
);
668+
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
669+
assert_eq!(err.to_string(), expected);
679670
});
680671
}
681672

@@ -684,15 +675,14 @@ mod tests {
684675
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
685676
let resets_at = base + ChronoDuration::seconds(30);
686677
with_now_override(base, move || {
678+
let expected_time = format_retry_timestamp(&resets_at);
687679
let err = UsageLimitReachedError {
688680
plan_type: None,
689681
resets_at: Some(resets_at),
690682
rate_limits: Some(rate_limit_snapshot()),
691683
};
692-
assert_eq!(
693-
err.to_string(),
694-
"You've hit your usage limit. Try again in less than a minute."
695-
);
684+
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
685+
assert_eq!(err.to_string(), expected);
696686
});
697687
}
698688
}

0 commit comments

Comments
 (0)