rate-limit-error

This commit is contained in:
Ahmed Ibrahim
2025-09-23 21:35:02 -07:00
parent a071e72a6c
commit 25c16af3e9
2 changed files with 83 additions and 4 deletions

View File

@@ -332,6 +332,8 @@ impl ModelClient {
if status == StatusCode::TOO_MANY_REQUESTS {
let rate_limit_snapshot = parse_rate_limit_snapshot(res.headers());
let header_reset_hint =
rate_limit_snapshot.as_ref().and_then(rate_limit_reset_hint);
let body = res.json::<ErrorResponse>().await.ok();
if let Some(ErrorResponse { error }) = body {
if error.r#type.as_deref() == Some("usage_limit_reached") {
@@ -341,10 +343,9 @@ impl ModelClient {
let plan_type = error
.plan_type
.or_else(|| auth.as_ref().and_then(CodexAuth::get_plan_type));
let resets_in_seconds = error.resets_in_seconds;
return Err(CodexErr::UsageLimitReached(UsageLimitReachedError {
plan_type,
resets_in_seconds,
resets_in_seconds: header_reset_hint,
rate_limits: rate_limit_snapshot,
}));
} else if error.r#type.as_deref() == Some("usage_not_included") {
@@ -518,6 +519,21 @@ fn parse_rate_limit_snapshot(headers: &HeaderMap) -> Option<RateLimitSnapshot> {
Some(RateLimitSnapshot { primary, secondary })
}
fn rate_limit_reset_hint(snapshot: &RateLimitSnapshot) -> Option<u64> {
[snapshot.primary.as_ref(), snapshot.secondary.as_ref()]
.into_iter()
.flatten()
.filter(|window| window.used_percent >= 100.0)
.filter_map(|window| {
window.resets_in_seconds.or_else(|| {
window
.window_minutes
.map(|minutes| minutes.saturating_mul(60))
})
})
.max()
}
fn parse_header_f64(headers: &HeaderMap, name: &str) -> Option<f64> {
parse_header_str(headers, name)?
.parse::<f64>()

View File

@@ -907,6 +907,8 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
.insert_header("x-codex-primary-over-secondary-limit-percent", "95.0")
.insert_header("x-codex-primary-window-minutes", "15")
.insert_header("x-codex-secondary-window-minutes", "60")
.insert_header("x-codex-primary-reset-after-seconds", "900")
.insert_header("x-codex-secondary-reset-after-seconds", "3600")
.set_body_json(json!({
"error": {
"type": "usage_limit_reached",
@@ -931,12 +933,12 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
"primary": {
"used_percent": 100.0,
"window_minutes": 15,
"resets_in_seconds": null
"resets_in_seconds": 900
},
"secondary": {
"used_percent": 87.5,
"window_minutes": 60,
"resets_in_seconds": null
"resets_in_seconds": 3600
}
});
@@ -972,6 +974,67 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
"unexpected error message for submission {submission_id}: {}",
error_event.message
);
assert!(
error_event.message.contains("15 minutes"),
"expected reset hint in error message for submission {submission_id}: {}",
error_event.message
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn usage_limit_error_prefers_longer_reset_window() -> anyhow::Result<()> {
let server = MockServer::start().await;
let response = ResponseTemplate::new(429)
.insert_header("x-codex-primary-used-percent", "100.0")
.insert_header("x-codex-secondary-used-percent", "100.0")
.insert_header("x-codex-primary-window-minutes", "10")
.insert_header("x-codex-secondary-window-minutes", "60")
.insert_header("x-codex-primary-reset-after-seconds", "600")
.insert_header("x-codex-secondary-reset-after-seconds", "7200")
.set_body_json(json!({
"error": {
"type": "usage_limit_reached",
"message": "limit reached",
"resets_in_seconds": 5,
"plan_type": "pro"
}
}));
Mock::given(method("POST"))
.and(path("/v1/responses"))
.respond_with(response)
.expect(1)
.mount(&server)
.await;
let mut builder = test_codex();
let codex_fixture = builder.build(&server).await?;
let codex = codex_fixture.codex.clone();
codex
.submit(Op::UserInput {
items: vec![InputItem::Text {
text: "hello".into(),
}],
})
.await
.expect("submission should succeed while emitting usage limit error events");
wait_for_event(&codex, |msg| matches!(msg, EventMsg::TokenCount(_))).await;
let error_event = wait_for_event(&codex, |msg| matches!(msg, EventMsg::Error(_))).await;
let EventMsg::Error(error_event) = error_event else {
unreachable!();
};
assert!(
error_event.message.contains("2 hours"),
"expected longer reset hint in error message: {}",
error_event.message
);
Ok(())
}