mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
display promo message in usage error (#10285)
If a promo message is attached to a rate limit response, then display it in the error message.
This commit is contained in:
@@ -41,6 +41,14 @@ pub fn parse_rate_limit(headers: &HeaderMap) -> Option<RateLimitSnapshot> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses the bespoke Codex rate-limit headers into a `RateLimitSnapshot`.
|
||||||
|
pub fn parse_promo_message(headers: &HeaderMap) -> Option<String> {
|
||||||
|
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(
|
fn parse_rate_limit_window(
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
used_percent_header: &str,
|
used_percent_header: &str,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use chrono::Utc;
|
|||||||
use codex_api::AuthProvider as ApiAuthProvider;
|
use codex_api::AuthProvider as ApiAuthProvider;
|
||||||
use codex_api::TransportError;
|
use codex_api::TransportError;
|
||||||
use codex_api::error::ApiError;
|
use codex_api::error::ApiError;
|
||||||
|
use codex_api::rate_limits::parse_promo_message;
|
||||||
use codex_api::rate_limits::parse_rate_limit;
|
use codex_api::rate_limits::parse_rate_limit;
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -70,6 +71,7 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr {
|
|||||||
if let Ok(err) = serde_json::from_str::<UsageErrorResponse>(&body_text) {
|
if let Ok(err) = serde_json::from_str::<UsageErrorResponse>(&body_text) {
|
||||||
if err.error.error_type.as_deref() == Some("usage_limit_reached") {
|
if err.error.error_type.as_deref() == Some("usage_limit_reached") {
|
||||||
let rate_limits = headers.as_ref().and_then(parse_rate_limit);
|
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
|
let resets_at = err
|
||||||
.error
|
.error
|
||||||
.resets_at
|
.resets_at
|
||||||
@@ -78,6 +80,7 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr {
|
|||||||
plan_type: err.error.plan_type,
|
plan_type: err.error.plan_type,
|
||||||
resets_at,
|
resets_at,
|
||||||
rate_limits,
|
rate_limits,
|
||||||
|
promo_message,
|
||||||
});
|
});
|
||||||
} else if err.error.error_type.as_deref() == Some("usage_not_included") {
|
} else if err.error.error_type.as_deref() == Some("usage_not_included") {
|
||||||
return CodexErr::UsageNotIncluded;
|
return CodexErr::UsageNotIncluded;
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ pub enum CodexErr {
|
|||||||
QuotaExceeded,
|
QuotaExceeded,
|
||||||
|
|
||||||
#[error(
|
#[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,
|
UsageNotIncluded,
|
||||||
|
|
||||||
@@ -360,13 +360,22 @@ pub struct UsageLimitReachedError {
|
|||||||
pub(crate) plan_type: Option<PlanType>,
|
pub(crate) plan_type: Option<PlanType>,
|
||||||
pub(crate) resets_at: Option<DateTime<Utc>>,
|
pub(crate) resets_at: Option<DateTime<Utc>>,
|
||||||
pub(crate) rate_limits: Option<RateLimitSnapshot>,
|
pub(crate) rate_limits: Option<RateLimitSnapshot>,
|
||||||
|
pub(crate) promo_message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for UsageLimitReachedError {
|
impl std::fmt::Display for UsageLimitReachedError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
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() {
|
let message = match self.plan_type.as_ref() {
|
||||||
Some(PlanType::Known(KnownPlan::Plus)) => format!(
|
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())
|
retry_suffix_after_or(self.resets_at.as_ref())
|
||||||
),
|
),
|
||||||
Some(PlanType::Known(KnownPlan::Team)) | Some(PlanType::Known(KnownPlan::Business)) => {
|
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)) => {
|
Some(PlanType::Known(KnownPlan::Free)) | Some(PlanType::Known(KnownPlan::Go)) => {
|
||||||
format!(
|
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())
|
retry_suffix_after_or(self.resets_at.as_ref())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -670,10 +679,11 @@ mod tests {
|
|||||||
plan_type: Some(PlanType::Known(KnownPlan::Plus)),
|
plan_type: Some(PlanType::Known(KnownPlan::Plus)),
|
||||||
resets_at: None,
|
resets_at: None,
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.to_string(),
|
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)),
|
plan_type: Some(PlanType::Known(KnownPlan::Free)),
|
||||||
resets_at: None,
|
resets_at: None,
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.to_string(),
|
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)),
|
plan_type: Some(PlanType::Known(KnownPlan::Go)),
|
||||||
resets_at: None,
|
resets_at: None,
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.to_string(),
|
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,
|
plan_type: None,
|
||||||
resets_at: None,
|
resets_at: None,
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -859,6 +872,7 @@ mod tests {
|
|||||||
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()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
let expected = format!(
|
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}."
|
"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)),
|
plan_type: Some(PlanType::Known(KnownPlan::Business)),
|
||||||
resets_at: None,
|
resets_at: None,
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -886,6 +901,7 @@ mod tests {
|
|||||||
plan_type: Some(PlanType::Known(KnownPlan::Enterprise)),
|
plan_type: Some(PlanType::Known(KnownPlan::Enterprise)),
|
||||||
resets_at: None,
|
resets_at: None,
|
||||||
rate_limits: Some(rate_limit_snapshot()),
|
rate_limits: Some(rate_limit_snapshot()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -903,6 +919,7 @@ mod tests {
|
|||||||
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()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
let expected = format!(
|
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}."
|
"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,
|
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()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
|
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
|
||||||
assert_eq!(err.to_string(), expected);
|
assert_eq!(err.to_string(), expected);
|
||||||
@@ -972,9 +990,10 @@ mod tests {
|
|||||||
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()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
let expected = format!(
|
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);
|
assert_eq!(err.to_string(), expected);
|
||||||
});
|
});
|
||||||
@@ -991,6 +1010,7 @@ mod tests {
|
|||||||
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()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
|
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
|
||||||
assert_eq!(err.to_string(), expected);
|
assert_eq!(err.to_string(), expected);
|
||||||
@@ -1007,9 +1027,31 @@ mod tests {
|
|||||||
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()),
|
||||||
|
promo_message: None,
|
||||||
};
|
};
|
||||||
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
|
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
|
||||||
assert_eq!(err.to_string(), expected);
|
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 <PLAN> today".to_string(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
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}."
|
||||||
|
);
|
||||||
|
assert_eq!(err.to_string(), expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user