Display workspace usage limit error copy from response header (#24114)

## Why

`openai/openai#947613` adds `X-Codex-Rate-Limit-Reached-Type` for Codex
workspace credit-depletion and spend-cap responses. The CLI currently
reads the adjacent promo header but otherwise renders generic
usage-limit copy, so those responses do not explain the
workspace-specific action the user needs to take.

Backend dependency: https://github.com/openai/openai/pull/947613

## What Changed

- Parse `X-Codex-Rate-Limit-Reached-Type` in the usage-limit error
handling path alongside `x-codex-promo-message`.
- Keep the header value parsing with the shared `RateLimitReachedType`
enum.
- Carry the parsed type on `UsageLimitReachedError` and render
client-owned copy for the four workspace owner/member credit and
spend-cap values.
- Preserve existing promo and plan-based text for absent, generic, or
unknown header values.
- Keep the existing TUI workspace-owner nudge state path unchanged; the
response header only selects the displayed error string.
- Add focused display coverage for all specific type values and the
generic fallback case.

## Test Plan

- Added `usage_limit_reached_error_formats_rate_limit_reached_types`
coverage.
- Not run manually, per request; CI runs validation on the pushed
commit.
This commit is contained in:
dhruvgupta-oai
2026-05-22 19:58:49 -04:00
committed by GitHub
Unverified
parent 6ad3a83509
commit 4bcabbfbec
6 changed files with 146 additions and 0 deletions
+54
View File
@@ -56,6 +56,7 @@ fn usage_limit_reached_error_formats_plus_plan() {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: None,
};
assert_eq!(
err.to_string(),
@@ -63,6 +64,44 @@ fn usage_limit_reached_error_formats_plus_plan() {
);
}
#[test]
fn usage_limit_reached_error_formats_rate_limit_reached_types() {
let cases = [
(
RateLimitReachedType::RateLimitReached,
"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.",
),
(
RateLimitReachedType::WorkspaceOwnerCreditsDepleted,
"Your workspace is out of credits. Add credits to continue.",
),
(
RateLimitReachedType::WorkspaceMemberCreditsDepleted,
"Your workspace is out of credits. Ask your workspace owner to refill in order to continue.",
),
(
RateLimitReachedType::WorkspaceOwnerUsageLimitReached,
"You hit your spend cap set in your workspace. Increase your spend cap to continue.",
),
(
RateLimitReachedType::WorkspaceMemberUsageLimitReached,
"You hit your spend cap set by the owner of your workspace. Ask an owner to increase your spend cap to continue.",
),
];
for (rate_limit_reached_type, expected) in cases {
let err = UsageLimitReachedError {
plan_type: Some(PlanType::Known(KnownPlan::Plus)),
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: Some(rate_limit_reached_type),
};
assert_eq!(err.to_string(), expected);
}
}
#[test]
fn server_overloaded_maps_to_protocol() {
let err = CodexErr::ServerOverloaded;
@@ -177,6 +216,7 @@ fn usage_limit_reached_error_formats_free_plan() {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: None,
};
assert_eq!(
err.to_string(),
@@ -191,6 +231,7 @@ fn usage_limit_reached_error_formats_go_plan() {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: None,
};
assert_eq!(
err.to_string(),
@@ -205,6 +246,7 @@ fn usage_limit_reached_error_formats_default_when_none() {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: None,
};
assert_eq!(
err.to_string(),
@@ -223,6 +265,7 @@ fn usage_limit_reached_error_formats_team_plan() {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: 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}."
@@ -238,6 +281,7 @@ fn usage_limit_reached_error_formats_business_plan_without_reset() {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: None,
};
assert_eq!(
err.to_string(),
@@ -252,6 +296,7 @@ fn usage_limit_reached_error_formats_self_serve_business_usage_based_plan() {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: None,
};
assert_eq!(
err.to_string(),
@@ -266,6 +311,7 @@ fn usage_limit_reached_error_formats_enterprise_cbp_usage_based_plan() {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: None,
};
assert_eq!(
err.to_string(),
@@ -280,6 +326,7 @@ fn usage_limit_reached_error_formats_default_for_other_plans() {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: None,
};
assert_eq!(
err.to_string(),
@@ -298,6 +345,7 @@ fn usage_limit_reached_error_formats_pro_plan_with_reset() {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: 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}."
@@ -324,6 +372,7 @@ fn usage_limit_reached_error_hides_upsell_for_non_codex_limit_name() {
"Visit https://chatgpt.com/codex/settings/usage to purchase more credits"
.to_string(),
),
rate_limit_reached_type: None,
};
let expected = format!(
"You've hit your usage limit for codex_other. Switch to another model now, or try again at {expected_time}."
@@ -343,6 +392,7 @@ fn usage_limit_reached_includes_minutes_when_available() {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: None,
};
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
assert_eq!(err.to_string(), expected);
@@ -482,6 +532,7 @@ fn usage_limit_reached_includes_hours_and_minutes() {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: 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}."
@@ -502,6 +553,7 @@ fn usage_limit_reached_includes_days_hours_minutes() {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: None,
};
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
assert_eq!(err.to_string(), expected);
@@ -519,6 +571,7 @@ fn usage_limit_reached_less_than_minute() {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
rate_limit_reached_type: None,
};
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
assert_eq!(err.to_string(), expected);
@@ -538,6 +591,7 @@ fn usage_limit_reached_with_promo_message() {
promo_message: Some(
"To continue using Codex, start a free trial of <PLAN> today".to_string(),
),
rate_limit_reached_type: 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}."