diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 2085c39f02..9dfa3a1316 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -931,8 +931,10 @@ async fn stream_from_fixture( fn rate_limit_regex() -> &'static Regex { static RE: OnceLock = OnceLock::new(); + // Match both OpenAI-style messages like "Please try again in 1.898s" + // and Azure OpenAI-style messages like "Try again in 35 seconds". #[expect(clippy::unwrap_used)] - RE.get_or_init(|| Regex::new(r"Please try again in (\d+(?:\.\d+)?)(s|ms)").unwrap()) + RE.get_or_init(|| Regex::new(r"(?i)try again in\s*(\d+(?:\.\d+)?)\s*(s|ms|seconds?)").unwrap()) } fn try_parse_retry_after(err: &Error) -> Option { @@ -940,7 +942,8 @@ fn try_parse_retry_after(err: &Error) -> Option { return None; } - // parse the Please try again in 1.898s format using regex + // parse retry hints like "try again in 1.898s" or + // "Try again in 35 seconds" using regex let re = rate_limit_regex(); if let Some(message) = &err.message && let Some(captures) = re.captures(message) @@ -950,9 +953,9 @@ fn try_parse_retry_after(err: &Error) -> Option { if let (Some(value), Some(unit)) = (seconds, unit) { let value = value.as_str().parse::().ok()?; - let unit = unit.as_str(); + let unit = unit.as_str().to_ascii_lowercase(); - if unit == "s" { + if unit == "s" || unit.starts_with("second") { return Some(Duration::from_secs_f64(value)); } else if unit == "ms" { return Some(Duration::from_millis(value as u64)); @@ -1427,6 +1430,19 @@ mod tests { assert_eq!(delay, Some(Duration::from_secs_f64(1.898))); } + #[test] + fn test_try_parse_retry_after_azure() { + let err = Error { + r#type: None, + message: Some("Rate limit exceeded. Try again in 35 seconds.".to_string()), + code: Some("rate_limit_exceeded".to_string()), + plan_type: None, + resets_at: None, + }; + let delay = try_parse_retry_after(&err); + assert_eq!(delay, Some(Duration::from_secs(35))); + } + #[test] fn error_response_deserializes_schema_known_plan_type_and_serializes_back() { use crate::token_data::KnownPlan;