Better error message for model limit hit. (#11636)

<img width="553" height="147" alt="image"
src="https://github.com/user-attachments/assets/f04cdebd-608a-4055-a413-fae92aaf04e5"
/>
This commit is contained in:
xl-openai
2026-02-12 14:10:30 -08:00
committed by GitHub
parent 4668feb43a
commit d7cb70ed26
2 changed files with 58 additions and 22 deletions

View File

@@ -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
);
}
}

View File

@@ -411,18 +411,22 @@ pub struct UsageLimitReachedError {
pub(crate) resets_at: Option<DateTime<Utc>>,
pub(crate) rate_limits: Option<Box<RateLimitSnapshot>>,
pub(crate) promo_message: Option<String>,
pub(crate) limit_name: Option<String>,
}
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 <PLAN> today".to_string(),
),
limit_name: None,
};
let expected = format!(
"You've hit your usage limit. To continue using Codex, start a free trial of <PLAN> today, or try again at {expected_time}."