From d7cb70ed267ee798a3988812a1a3c788fb006f65 Mon Sep 17 00:00:00 2001 From: xl-openai Date: Thu, 12 Feb 2026 14:10:30 -0800 Subject: [PATCH] Better error message for model limit hit. (#11636) image --- codex-rs/core/src/api_bridge.rs | 46 +++++++++++++++++++++++++++++++-- codex-rs/core/src/error.rs | 34 ++++++++++-------------- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/codex-rs/core/src/api_bridge.rs b/codex-rs/core/src/api_bridge.rs index b461ac4ad..3b2024f58 100644 --- a/codex-rs/core/src/api_bridge.rs +++ b/codex-rs/core/src/api_bridge.rs @@ -81,7 +81,6 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr { resets_at, rate_limits: rate_limits.map(Box::new), promo_message, - limit_name: limit_id, }); } else if err.error.error_type.as_deref() == Some("usage_not_included") { return CodexErr::UsageNotIncluded; @@ -156,6 +155,10 @@ mod tests { ACTIVE_LIMIT_HEADER, http::HeaderValue::from_static("codex_other"), ); + headers.insert( + "x-codex-other-limit-name", + http::HeaderValue::from_static("codex_other"), + ); let body = serde_json::json!({ "error": { "type": "usage_limit_reached", @@ -173,7 +176,46 @@ mod tests { let CodexErr::UsageLimitReached(usage_limit) = err else { panic!("expected CodexErr::UsageLimitReached, got {err:?}"); }; - assert_eq!(usage_limit.limit_name.as_deref(), Some("codex_other")); + assert_eq!( + usage_limit + .rate_limits + .as_ref() + .and_then(|snapshot| snapshot.limit_name.as_deref()), + Some("codex_other") + ); + } + + #[test] + fn map_api_error_does_not_fallback_limit_name_to_limit_id() { + let mut headers = HeaderMap::new(); + headers.insert( + ACTIVE_LIMIT_HEADER, + http::HeaderValue::from_static("codex_other"), + ); + let body = serde_json::json!({ + "error": { + "type": "usage_limit_reached", + "plan_type": "pro", + } + }) + .to_string(); + let err = map_api_error(ApiError::Transport(TransportError::Http { + status: http::StatusCode::TOO_MANY_REQUESTS, + url: Some("http://example.com/v1/responses".to_string()), + headers: Some(headers), + body: Some(body), + })); + + let CodexErr::UsageLimitReached(usage_limit) = err else { + panic!("expected CodexErr::UsageLimitReached, got {err:?}"); + }; + assert_eq!( + usage_limit + .rate_limits + .as_ref() + .and_then(|snapshot| snapshot.limit_name.as_deref()), + None + ); } } diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index c7d9ffb82..404fbc5a7 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -411,18 +411,22 @@ pub struct UsageLimitReachedError { pub(crate) resets_at: Option>, pub(crate) rate_limits: Option>, pub(crate) promo_message: Option, - pub(crate) limit_name: Option, } impl std::fmt::Display for UsageLimitReachedError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(limit_name) = self.limit_name.as_deref() + if let Some(limit_name) = self + .rate_limits + .as_ref() + .and_then(|snapshot| snapshot.limit_name.as_deref()) + .map(str::trim) + .filter(|name| !name.is_empty()) && !limit_name.eq_ignore_ascii_case("codex") { return write!( f, - "You've hit your usage limit for {limit_name}.{}", - retry_suffix(self.resets_at.as_ref()) + "You've hit your usage limit for {limit_name}. Switch to another model now,{}", + retry_suffix_after_or(self.resets_at.as_ref()) ); } @@ -704,7 +708,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -822,7 +825,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -837,7 +839,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -852,7 +853,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -871,7 +871,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: 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}." @@ -887,7 +886,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -902,7 +900,6 @@ mod tests { resets_at: None, rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; assert_eq!( err.to_string(), @@ -921,7 +918,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: 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}." @@ -939,15 +935,18 @@ mod tests { let err = UsageLimitReachedError { plan_type: Some(PlanType::Known(KnownPlan::Plus)), resets_at: Some(resets_at), - rate_limits: Some(Box::new(rate_limit_snapshot())), + rate_limits: Some(Box::new(RateLimitSnapshot { + limit_id: Some("codex_other".to_string()), + limit_name: Some("codex_other".to_string()), + ..rate_limit_snapshot() + })), promo_message: Some( "Visit https://chatgpt.com/codex/settings/usage to purchase more credits" .to_string(), ), - limit_name: Some("codex_other".to_string()), }; let expected = format!( - "You've hit your usage limit for codex_other. Try again at {expected_time}." + "You've hit your usage limit for codex_other. Switch to another model now, or try again at {expected_time}." ); assert_eq!(err.to_string(), expected); }); @@ -964,7 +963,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; let expected = format!("You've hit your usage limit. Try again at {expected_time}."); assert_eq!(err.to_string(), expected); @@ -1074,7 +1072,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; let expected = format!( "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}." @@ -1095,7 +1092,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; let expected = format!("You've hit your usage limit. Try again at {expected_time}."); assert_eq!(err.to_string(), expected); @@ -1113,7 +1109,6 @@ mod tests { resets_at: Some(resets_at), rate_limits: Some(Box::new(rate_limit_snapshot())), promo_message: None, - limit_name: None, }; let expected = format!("You've hit your usage limit. Try again at {expected_time}."); assert_eq!(err.to_string(), expected); @@ -1133,7 +1128,6 @@ mod tests { promo_message: Some( "To continue using Codex, start a free trial of today".to_string(), ), - limit_name: None, }; 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}."