diff --git a/codex-rs/codex-api/src/rate_limits.rs b/codex-rs/codex-api/src/rate_limits.rs index bb8ede2f57..c29aab21f8 100644 --- a/codex-rs/codex-api/src/rate_limits.rs +++ b/codex-rs/codex-api/src/rate_limits.rs @@ -41,6 +41,14 @@ pub fn parse_rate_limit(headers: &HeaderMap) -> Option { }) } +/// Parses the bespoke Codex rate-limit headers into a `RateLimitSnapshot`. +pub fn parse_promo_message(headers: &HeaderMap) -> Option { + parse_header_str(headers, "x-codex-promo-message") + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(std::string::ToString::to_string) +} + fn parse_rate_limit_window( headers: &HeaderMap, used_percent_header: &str, diff --git a/codex-rs/core/src/api_bridge.rs b/codex-rs/core/src/api_bridge.rs index ec21f1ec85..86aeaedda0 100644 --- a/codex-rs/core/src/api_bridge.rs +++ b/codex-rs/core/src/api_bridge.rs @@ -3,6 +3,7 @@ use chrono::Utc; use codex_api::AuthProvider as ApiAuthProvider; use codex_api::TransportError; use codex_api::error::ApiError; +use codex_api::rate_limits::parse_promo_message; use codex_api::rate_limits::parse_rate_limit; use http::HeaderMap; use serde::Deserialize; @@ -70,6 +71,7 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr { if let Ok(err) = serde_json::from_str::(&body_text) { if err.error.error_type.as_deref() == Some("usage_limit_reached") { let rate_limits = headers.as_ref().and_then(parse_rate_limit); + let promo_message = headers.as_ref().and_then(parse_promo_message); let resets_at = err .error .resets_at @@ -78,6 +80,7 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr { plan_type: err.error.plan_type, resets_at, rate_limits, + promo_message, }); } else if err.error.error_type.as_deref() == Some("usage_not_included") { return CodexErr::UsageNotIncluded; diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index f9f896c3b8..684d968162 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -126,7 +126,7 @@ pub enum CodexErr { QuotaExceeded, #[error( - "To use Codex with your ChatGPT plan, upgrade to Plus: https://openai.com/chatgpt/pricing." + "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus." )] UsageNotIncluded, @@ -360,13 +360,22 @@ pub struct UsageLimitReachedError { pub(crate) plan_type: Option, pub(crate) resets_at: Option>, pub(crate) rate_limits: Option, + pub(crate) promo_message: Option, } impl std::fmt::Display for UsageLimitReachedError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(promo_message) = &self.promo_message { + return write!( + f, + "You've hit your usage limit. {promo_message},{}", + retry_suffix_after_or(self.resets_at.as_ref()) + ); + } + 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), visit https://chatgpt.com/codex/settings/usage to purchase more credits{}", + "You've hit your usage limit. Upgrade to Pro (https://chatgpt.com/explore/pro), visit https://chatgpt.com/codex/settings/usage to purchase more credits{}", retry_suffix_after_or(self.resets_at.as_ref()) ), Some(PlanType::Known(KnownPlan::Team)) | Some(PlanType::Known(KnownPlan::Business)) => { @@ -377,7 +386,7 @@ impl std::fmt::Display for UsageLimitReachedError { } Some(PlanType::Known(KnownPlan::Free)) | Some(PlanType::Known(KnownPlan::Go)) => { format!( - "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://openai.com/chatgpt/pricing),{}", + "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://chatgpt.com/explore/plus),{}", retry_suffix_after_or(self.resets_at.as_ref()) ) } @@ -670,10 +679,11 @@ mod tests { plan_type: Some(PlanType::Known(KnownPlan::Plus)), resets_at: None, rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; assert_eq!( err.to_string(), - "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again later." + "You've hit your usage limit. Upgrade to Pro (https://chatgpt.com/explore/pro), visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again later." ); } @@ -816,10 +826,11 @@ mod tests { plan_type: Some(PlanType::Known(KnownPlan::Free)), resets_at: None, rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; assert_eq!( err.to_string(), - "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://openai.com/chatgpt/pricing), or try again later." + "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://chatgpt.com/explore/plus), or try again later." ); } @@ -829,10 +840,11 @@ mod tests { plan_type: Some(PlanType::Known(KnownPlan::Go)), resets_at: None, rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; assert_eq!( err.to_string(), - "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://openai.com/chatgpt/pricing), or try again later." + "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://chatgpt.com/explore/plus), or try again later." ); } @@ -842,6 +854,7 @@ mod tests { plan_type: None, resets_at: None, rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; assert_eq!( err.to_string(), @@ -859,6 +872,7 @@ mod tests { plan_type: Some(PlanType::Known(KnownPlan::Team)), resets_at: Some(resets_at), rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; let expected = format!( "You've hit your usage limit. To get more access now, send a request to your admin or try again at {expected_time}." @@ -873,6 +887,7 @@ mod tests { plan_type: Some(PlanType::Known(KnownPlan::Business)), resets_at: None, rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; assert_eq!( err.to_string(), @@ -886,6 +901,7 @@ mod tests { plan_type: Some(PlanType::Known(KnownPlan::Enterprise)), resets_at: None, rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; assert_eq!( err.to_string(), @@ -903,6 +919,7 @@ mod tests { plan_type: Some(PlanType::Known(KnownPlan::Pro)), resets_at: Some(resets_at), rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; let expected = format!( "You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}." @@ -921,6 +938,7 @@ mod tests { plan_type: None, resets_at: Some(resets_at), rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; let expected = format!("You've hit your usage limit. Try again at {expected_time}."); assert_eq!(err.to_string(), expected); @@ -972,9 +990,10 @@ mod tests { plan_type: Some(PlanType::Known(KnownPlan::Plus)), resets_at: Some(resets_at), rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; let expected = format!( - "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit https://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://chatgpt.com/explore/pro), visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}." ); assert_eq!(err.to_string(), expected); }); @@ -991,6 +1010,7 @@ mod tests { plan_type: None, resets_at: Some(resets_at), rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; let expected = format!("You've hit your usage limit. Try again at {expected_time}."); assert_eq!(err.to_string(), expected); @@ -1007,9 +1027,31 @@ mod tests { plan_type: None, resets_at: Some(resets_at), rate_limits: Some(rate_limit_snapshot()), + promo_message: None, }; let expected = format!("You've hit your usage limit. Try again at {expected_time}."); assert_eq!(err.to_string(), expected); }); } + + #[test] + fn usage_limit_reached_with_promo_message() { + 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 expected_time = format_retry_timestamp(&resets_at); + let err = UsageLimitReachedError { + plan_type: None, + resets_at: Some(resets_at), + rate_limits: Some(rate_limit_snapshot()), + promo_message: Some( + "To continue using Codex, start a free trial of today".to_string(), + ), + }; + let expected = format!( + "You've hit your usage limit. To continue using Codex, start a free trial of today, or try again at {expected_time}." + ); + assert_eq!(err.to_string(), expected); + }); + } }