Separate Codex error details from retry metadata (#34996)

## What changed

- Wrap `CodexErrorDetails` and an optional retry delay in `CodexErr`, allowing any mapped error to preserve server-provided retry timing.
- Generate the payload-free `CodexErrKind` classification alongside the error details and reuse it for analytics.
- Update error handling sites to inspect `CodexErr::details()` while preserving existing display, debug, protocol mapping, and retryability behavior.

## Testing

- Add coverage for legacy debug formatting, error-specific retryability, and retry-delay propagation through API error mapping.

GitOrigin-RevId: d3ab8a305f2a2ee21d0c0a8c8c388b06dda9c59a
This commit is contained in:
Adam Perry @ OpenAI authored and copyberry committed 2026-07-23 19:32:26 +00:00
1 parent 265cd2e100
commit 7bafdada8b
40 files changed
+741 -435

No files matched your search

+58
View File
@@ -11,6 +11,64 @@ use reqwest::Response;
use reqwest::ResponseBuilderExt;
use reqwest::StatusCode;
use reqwest::Url;
use std::time::Duration;
#[test]
fn codex_err_debug_preserves_legacy_shape() {
let actual = [
CodexErr::Timeout,
CodexErr::Stream("disconnected".to_string()),
CodexErr::Stream("retry later".to_string()).with_retry_delay(Duration::from_secs(2)),
CodexErr::InternalServerError.with_retry_delay(Duration::from_secs(3)),
]
.map(|err| format!("{err:?}"));
assert_eq!(
actual,
[
"Timeout".to_string(),
"Stream(\"disconnected\", None)".to_string(),
"Stream(\"retry later\", Some(2s))".to_string(),
"InternalServerError".to_string(),
]
);
}
#[test]
fn retryability_preserves_error_details_distinctions() {
let errors = [
(CodexErr::ServerOverloaded, false),
(
CodexErr::RetryLimit(RetryLimitReachedError {
status: StatusCode::TOO_MANY_REQUESTS,
request_id: None,
}),
false,
),
(
CodexErr::UnexpectedStatus(UnexpectedResponseError {
status: StatusCode::TOO_MANY_REQUESTS,
body: String::new(),
user_message: None,
url: None,
cf_ray: None,
request_id: None,
identity_authorization_error: None,
identity_error_code: None,
}),
true,
),
(CodexErr::InternalServerError, true),
];
for (err, expected) in errors {
assert_eq!(
err.is_retryable(),
expected,
"unexpected retryability for {err:?}"
);
}
}
fn rate_limit_snapshot() -> RateLimitSnapshot {
let primary_reset_at = Utc