mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Preserve usage limit windows in turn and compaction analytics (#48174)
## Why Usage limit errors discarded the server-selected window responsible for the limit, leaving turn and compaction analytics unable to distinguish five-hour limits from weekly limits. ## What changed Preserve optional `limit_window_minutes` from usage limit error responses in `UsageLimitReachedError` and report it as `usage_limit_window_minutes` in turn and compaction events. Accept unsigned integers that fit in `u16`; treat missing, null, malformed, or out-of-range values as unknown. Events for other error kinds report `null`. ## Testing Add coverage for HTTP and wrapped WebSocket error mapping, analytics serialization and reduction, and app-server propagation to turn and local/remote compaction events. Cases include five-hour and weekly windows, missing or invalid values, and unrelated errors. GitOrigin-RevId: a32066a007559d91a3c868a756bfd0f976777176
This commit is contained in:
@@ -204,6 +204,12 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
|
||||
return CodexErr::UsageLimitReached(UsageLimitReachedError {
|
||||
plan_type: err.error.plan_type,
|
||||
resets_at,
|
||||
limit_window_minutes: err
|
||||
.error
|
||||
.limit_window_minutes
|
||||
.as_ref()
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|minutes| u16::try_from(minutes).ok()),
|
||||
rate_limits: rate_limits.map(Box::new),
|
||||
promo_message,
|
||||
rate_limit_reached_type,
|
||||
@@ -340,4 +346,5 @@ struct UsageErrorBody {
|
||||
error_type: Option<String>,
|
||||
plan_type: Option<PlanType>,
|
||||
resets_at: Option<i64>,
|
||||
limit_window_minutes: Option<Value>,
|
||||
}
|
||||
|
||||
@@ -457,6 +457,48 @@ fn map_api_error_distinguishes_http_quota_errors_from_rate_limits() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_api_error_preserves_optional_usage_limit_window() {
|
||||
let cases = [
|
||||
(None, None),
|
||||
(Some(serde_json::json!(null)), None),
|
||||
(Some(serde_json::json!(300)), Some(300)),
|
||||
(Some(serde_json::json!(10080)), Some(10080)),
|
||||
(Some(serde_json::json!(-1)), None),
|
||||
(Some(serde_json::json!(65536)), None),
|
||||
(Some(serde_json::json!("300")), None),
|
||||
(Some(serde_json::json!({"minutes": 300})), None),
|
||||
];
|
||||
|
||||
for (window, expected) in cases {
|
||||
let mut body = serde_json::json!({
|
||||
"error": {
|
||||
"type": "usage_limit_reached",
|
||||
"plan_type": "pro",
|
||||
}
|
||||
});
|
||||
if let Some(window) = window.as_ref() {
|
||||
body["error"]["limit_window_minutes"] = window.clone();
|
||||
}
|
||||
let err = map_api_error(ApiError::Transport(TransportError::Http {
|
||||
retry_after: None,
|
||||
status: http::StatusCode::TOO_MANY_REQUESTS,
|
||||
url: None,
|
||||
headers: None,
|
||||
body: Some(body.to_string()),
|
||||
}));
|
||||
|
||||
let CodexErrorDetails::UsageLimitReached(usage_limit) = err.details() else {
|
||||
panic!("expected usage-limit error for {window:?}, got {err:?}");
|
||||
};
|
||||
assert_eq!(usage_limit.limit_window_minutes, expected, "{window:?}");
|
||||
assert_eq!(
|
||||
err.to_codex_protocol_error(),
|
||||
CodexErrorInfo::UsageLimitExceeded
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_api_error_maps_usage_limit_limit_name_header() {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
@@ -1057,6 +1057,33 @@ mod tests {
|
||||
assert!(body.contains("The usage limit has been reached"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapped_websocket_usage_limit_preserves_optional_window() {
|
||||
for (window, expected) in [(Some(10080), Some(10080)), (None, None)] {
|
||||
let mut payload = json!({
|
||||
"type": "error",
|
||||
"status": 429,
|
||||
"error": {
|
||||
"type": "usage_limit_reached",
|
||||
"plan_type": "pro"
|
||||
}
|
||||
});
|
||||
if let Some(window) = window {
|
||||
payload["error"]["limit_window_minutes"] = json!(window);
|
||||
}
|
||||
let payload = payload.to_string();
|
||||
let wrapped = parse_wrapped_websocket_error_event(&payload).expect("websocket error");
|
||||
let api_error = map_wrapped_websocket_error_event(wrapped, payload).expect("API error");
|
||||
let err = crate::api_bridge::map_api_error(api_error);
|
||||
let codex_protocol::error::CodexErrorDetails::UsageLimitReached(usage_limit) =
|
||||
err.details()
|
||||
else {
|
||||
panic!("expected usage-limit error, got {err:?}");
|
||||
};
|
||||
assert_eq!(usage_limit.limit_window_minutes, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_wrapped_websocket_error_event_ignores_non_error_payloads() {
|
||||
let payload = json!({
|
||||
|
||||
Reference in New Issue
Block a user