rate limit errors now provide absolute time (#6000)
This commit is contained in:
@@ -4,6 +4,8 @@ use crate::token_data::KnownPlan;
|
|||||||
use crate::token_data::PlanType;
|
use crate::token_data::PlanType;
|
||||||
use crate::truncate::truncate_middle;
|
use crate::truncate::truncate_middle;
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
|
use chrono::Datelike;
|
||||||
|
use chrono::Local;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use codex_async_utils::CancelErr;
|
use codex_async_utils::CancelErr;
|
||||||
use codex_protocol::ConversationId;
|
use codex_protocol::ConversationId;
|
||||||
@@ -286,28 +288,46 @@ impl std::fmt::Display for UsageLimitReachedError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn retry_suffix(resets_at: Option<&DateTime<Utc>>) -> String {
|
fn retry_suffix(resets_at: Option<&DateTime<Utc>>) -> String {
|
||||||
if let Some(secs) = remaining_seconds(resets_at) {
|
if let Some(resets_at) = resets_at {
|
||||||
let reset_duration = format_reset_duration(secs);
|
let formatted = format_retry_timestamp(resets_at);
|
||||||
format!(" Try again in {reset_duration}.")
|
format!(" Try again at {formatted}.")
|
||||||
} else {
|
} else {
|
||||||
" Try again later.".to_string()
|
" Try again later.".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn retry_suffix_after_or(resets_at: Option<&DateTime<Utc>>) -> String {
|
fn retry_suffix_after_or(resets_at: Option<&DateTime<Utc>>) -> String {
|
||||||
if let Some(secs) = remaining_seconds(resets_at) {
|
if let Some(resets_at) = resets_at {
|
||||||
let reset_duration = format_reset_duration(secs);
|
let formatted = format_retry_timestamp(resets_at);
|
||||||
format!(" or try again in {reset_duration}.")
|
format!(" or try again at {formatted}.")
|
||||||
} else {
|
} else {
|
||||||
" or try again later.".to_string()
|
" or try again later.".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remaining_seconds(resets_at: Option<&DateTime<Utc>>) -> Option<u64> {
|
fn format_retry_timestamp(resets_at: &DateTime<Utc>) -> String {
|
||||||
let resets_at = resets_at.cloned()?;
|
let local_reset = resets_at.with_timezone(&Local);
|
||||||
let now = now_for_retry();
|
let local_now = now_for_retry().with_timezone(&Local);
|
||||||
let secs = resets_at.signed_duration_since(now).num_seconds();
|
if local_reset.date_naive() == local_now.date_naive() {
|
||||||
Some(if secs <= 0 { 0 } else { secs as u64 })
|
local_reset.format("%-I:%M %p").to_string()
|
||||||
|
} else {
|
||||||
|
let suffix = day_suffix(local_reset.day());
|
||||||
|
local_reset
|
||||||
|
.format(&format!("%b %-d{suffix}, %Y %-I:%M %p"))
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn day_suffix(day: u32) -> &'static str {
|
||||||
|
match day {
|
||||||
|
11..=13 => "th",
|
||||||
|
_ => match day % 10 {
|
||||||
|
1 => "st",
|
||||||
|
2 => "nd", // codespell:ignore
|
||||||
|
3 => "rd",
|
||||||
|
_ => "th",
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -326,36 +346,6 @@ fn now_for_retry() -> DateTime<Utc> {
|
|||||||
Utc::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;
|
|
||||||
let minutes = (total_secs % 3_600) / 60;
|
|
||||||
|
|
||||||
let mut parts: Vec<String> = Vec::new();
|
|
||||||
if days > 0 {
|
|
||||||
let unit = if days == 1 { "day" } else { "days" };
|
|
||||||
parts.push(format!("{days} {unit}"));
|
|
||||||
}
|
|
||||||
if hours > 0 {
|
|
||||||
let unit = if hours == 1 { "hour" } else { "hours" };
|
|
||||||
parts.push(format!("{hours} {unit}"));
|
|
||||||
}
|
|
||||||
if minutes > 0 {
|
|
||||||
let unit = if minutes == 1 { "minute" } else { "minutes" };
|
|
||||||
parts.push(format!("{minutes} {unit}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if parts.is_empty() {
|
|
||||||
return "less than a minute".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
match parts.len() {
|
|
||||||
1 => parts[0].clone(),
|
|
||||||
2 => format!("{} {}", parts[0], parts[1]),
|
|
||||||
_ => format!("{} {} {}", parts[0], parts[1], parts[2]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EnvVarError {
|
pub struct EnvVarError {
|
||||||
/// Name of the environment variable that is missing.
|
/// Name of the environment variable that is missing.
|
||||||
@@ -572,15 +562,16 @@ mod tests {
|
|||||||
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
||||||
let resets_at = base + ChronoDuration::hours(1);
|
let resets_at = base + ChronoDuration::hours(1);
|
||||||
with_now_override(base, move || {
|
with_now_override(base, move || {
|
||||||
|
let expected_time = format_retry_timestamp(&resets_at);
|
||||||
let err = UsageLimitReachedError {
|
let err = UsageLimitReachedError {
|
||||||
plan_type: Some(PlanType::Known(KnownPlan::Team)),
|
plan_type: Some(PlanType::Known(KnownPlan::Team)),
|
||||||
resets_at: Some(resets_at),
|
resets_at: Some(resets_at),
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
let expected = format!(
|
||||||
err.to_string(),
|
"You've hit your usage limit. To get more access now, send a request to your admin or try again at {expected_time}."
|
||||||
"You've hit your usage limit. To get more access now, send a request to your admin or try again in 1 hour."
|
|
||||||
);
|
);
|
||||||
|
assert_eq!(err.to_string(), expected);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,15 +606,16 @@ mod tests {
|
|||||||
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
||||||
let resets_at = base + ChronoDuration::hours(1);
|
let resets_at = base + ChronoDuration::hours(1);
|
||||||
with_now_override(base, move || {
|
with_now_override(base, move || {
|
||||||
|
let expected_time = format_retry_timestamp(&resets_at);
|
||||||
let err = UsageLimitReachedError {
|
let err = UsageLimitReachedError {
|
||||||
plan_type: Some(PlanType::Known(KnownPlan::Pro)),
|
plan_type: Some(PlanType::Known(KnownPlan::Pro)),
|
||||||
resets_at: Some(resets_at),
|
resets_at: Some(resets_at),
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
let expected = format!(
|
||||||
err.to_string(),
|
"You've hit your usage limit. Visit chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}."
|
||||||
"You've hit your usage limit. Visit chatgpt.com/codex/settings/usage to purchase more credits or try again in 1 hour."
|
|
||||||
);
|
);
|
||||||
|
assert_eq!(err.to_string(), expected);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,15 +624,14 @@ mod tests {
|
|||||||
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
||||||
let resets_at = base + ChronoDuration::minutes(5);
|
let resets_at = base + ChronoDuration::minutes(5);
|
||||||
with_now_override(base, move || {
|
with_now_override(base, move || {
|
||||||
|
let expected_time = format_retry_timestamp(&resets_at);
|
||||||
let err = UsageLimitReachedError {
|
let err = UsageLimitReachedError {
|
||||||
plan_type: None,
|
plan_type: None,
|
||||||
resets_at: Some(resets_at),
|
resets_at: Some(resets_at),
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
|
||||||
err.to_string(),
|
assert_eq!(err.to_string(), expected);
|
||||||
"You've hit your usage limit. Try again in 5 minutes."
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,15 +640,16 @@ mod tests {
|
|||||||
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
||||||
let resets_at = base + ChronoDuration::hours(3) + ChronoDuration::minutes(32);
|
let resets_at = base + ChronoDuration::hours(3) + ChronoDuration::minutes(32);
|
||||||
with_now_override(base, move || {
|
with_now_override(base, move || {
|
||||||
|
let expected_time = format_retry_timestamp(&resets_at);
|
||||||
let err = UsageLimitReachedError {
|
let err = UsageLimitReachedError {
|
||||||
plan_type: Some(PlanType::Known(KnownPlan::Plus)),
|
plan_type: Some(PlanType::Known(KnownPlan::Plus)),
|
||||||
resets_at: Some(resets_at),
|
resets_at: Some(resets_at),
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
let expected = format!(
|
||||||
err.to_string(),
|
"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}."
|
||||||
"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."
|
|
||||||
);
|
);
|
||||||
|
assert_eq!(err.to_string(), expected);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,15 +659,14 @@ mod tests {
|
|||||||
let resets_at =
|
let resets_at =
|
||||||
base + ChronoDuration::days(2) + ChronoDuration::hours(3) + ChronoDuration::minutes(5);
|
base + ChronoDuration::days(2) + ChronoDuration::hours(3) + ChronoDuration::minutes(5);
|
||||||
with_now_override(base, move || {
|
with_now_override(base, move || {
|
||||||
|
let expected_time = format_retry_timestamp(&resets_at);
|
||||||
let err = UsageLimitReachedError {
|
let err = UsageLimitReachedError {
|
||||||
plan_type: None,
|
plan_type: None,
|
||||||
resets_at: Some(resets_at),
|
resets_at: Some(resets_at),
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
|
||||||
err.to_string(),
|
assert_eq!(err.to_string(), expected);
|
||||||
"You've hit your usage limit. Try again in 2 days 3 hours 5 minutes."
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,15 +675,14 @@ mod tests {
|
|||||||
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
||||||
let resets_at = base + ChronoDuration::seconds(30);
|
let resets_at = base + ChronoDuration::seconds(30);
|
||||||
with_now_override(base, move || {
|
with_now_override(base, move || {
|
||||||
|
let expected_time = format_retry_timestamp(&resets_at);
|
||||||
let err = UsageLimitReachedError {
|
let err = UsageLimitReachedError {
|
||||||
plan_type: None,
|
plan_type: None,
|
||||||
resets_at: Some(resets_at),
|
resets_at: Some(resets_at),
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
|
||||||
err.to_string(),
|
assert_eq!(err.to_string(), expected);
|
||||||
"You've hit your usage limit. Try again in less than a minute."
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user