Classify streaming rate-limit errors (#40931)

## What changed

- Classify `response.failed` events with the `rate_limit_exceeded` code as a distinct retryable error while preserving any parsed retry delay.
- Expose the error as `rateLimitExceeded` through the core protocol and app-server schemas after stream retries are exhausted.
- Preserve the upstream message for TUI display while keeping it out of telemetry summaries.

## Testing

- Cover SSE classification, retry metadata, protocol conversion and serialization, exhausted stream retries, telemetry redaction, and TUI rendering.

GitOrigin-RevId: 02dab4d3477dcd7653a58c49c4bd38687a616579
This commit is contained in:
Steve Coffey
2026-08-26 17:45:42 +00:00
committed by copyberry
parent 7625bd5665
commit e0c727de04
38 changed files with 198 additions and 26 deletions
+37 -10
View File
@@ -1,5 +1,6 @@
use super::*;
use base64::Engine;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::RateLimitReachedType;
use pretty_assertions::assert_eq;
@@ -12,16 +13,42 @@ fn map_api_error_maps_server_overloaded() {
#[test]
fn map_api_error_preserves_retry_delay() {
let retry_delay = std::time::Duration::from_secs(17);
let err = map_api_error(ApiError::Retryable {
message: "retry later".to_string(),
delay: Some(retry_delay),
});
assert!(matches!(
err.details(),
CodexErrorDetails::Stream(message) if message == "retry later"
));
assert_eq!(err.retry_delay(), Some(retry_delay));
for (error, expected_code, expected_message) in [
(
ApiError::Retryable {
message: "retry later".to_string(),
delay: Some(retry_delay),
},
CodexErrorInfo::Other,
"stream disconnected before completion: retry later",
),
(
ApiError::RateLimitExceeded {
message: "retry later".to_string(),
delay: Some(retry_delay),
},
CodexErrorInfo::RateLimitExceeded,
"rate limit exceeded: retry later",
),
] {
let err = map_api_error(error);
assert_eq!(
(
err.to_codex_protocol_error(),
err.retry_delay(),
err.is_retryable(),
err.http_status_code_value(),
err.to_string(),
),
(
expected_code,
Some(retry_delay),
true,
None,
expected_message.to_string(),
)
);
}
}
#[test]