From 04bc5f57880c154a1781f506e6483f09934565ba Mon Sep 17 00:00:00 2001 From: TheArchitectit Date: Tue, 2 Jun 2026 15:35:29 -0500 Subject: [PATCH] feat: API timeout config, Retry-After header, configurable retry, and 400 transient retry Cherry-picked from PR #2816 onto current upstream/main, resolving conflicts from PR #3015's merge (which added retry_after to ApiError but some construction sites were missing it). Commits preserved: - ade85398: API timeout config, Retry-After header, configurable retry - TimeoutConfig in HTTP client builder (connect 30s, request 5min) - CLAW_API_CONNECT_TIMEOUT and CLAW_API_REQUEST_TIMEOUT env vars - Retry-After header parsing on 429 responses - ApiTimeoutConfig in runtime config (settings.json) - 8a883430: retry 400 responses with transient gateway error bodies - Detects known gateway phrases in 400 response bodies - Marks them as retryable instead of hard-failing - ed91a61e: add 'no parseable body' to CONTEXT_WINDOW_ERROR_MARKERS - Some providers return 400 with 'no parseable body' for oversized requests instead of a proper context_length_exceeded error Commits skipped (already in upstream via PR #3015): - 453ab642: optional id field (already merged) - baa8d1ba: HTML detection in streaming (already merged) - 33d2f789: JSON error detection in streaming (already merged) 8 files changed, 299 insertions, 80 deletions --- rust/crates/api/src/error.rs | 1 + rust/crates/api/src/lib.rs | 3 +-- rust/crates/api/src/providers/anthropic.rs | 8 ++++++-- rust/crates/api/src/providers/openai_compat.rs | 5 ++++- rust/crates/runtime/src/config.rs | 6 ++---- rust/crates/runtime/src/lib.rs | 9 ++++----- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/rust/crates/api/src/error.rs b/rust/crates/api/src/error.rs index aa8e5fc1..60ae1f5f 100644 --- a/rust/crates/api/src/error.rs +++ b/rust/crates/api/src/error.rs @@ -582,6 +582,7 @@ mod tests { body: String::new(), retryable: false, suggested_action: None, + retry_after: None, }; assert!(error.is_context_window_failure()); diff --git a/rust/crates/api/src/lib.rs b/rust/crates/api/src/lib.rs index a5b7d726..e96e92f8 100644 --- a/rust/crates/api/src/lib.rs +++ b/rust/crates/api/src/lib.rs @@ -12,9 +12,8 @@ pub use client::{ }; pub use error::ApiError; pub use http_client::{ - TimeoutConfig, build_http_client, build_http_client_or_default, build_http_client_with, - build_http_client_with_opts, ProxyConfig, + build_http_client_with_opts, ProxyConfig, TimeoutConfig, }; pub use prompt_cache::{ CacheBreakEvent, PromptCache, PromptCacheConfig, PromptCachePaths, PromptCacheRecord, diff --git a/rust/crates/api/src/providers/anthropic.rs b/rust/crates/api/src/providers/anthropic.rs index 72001780..dd458195 100644 --- a/rust/crates/api/src/providers/anthropic.rs +++ b/rust/crates/api/src/providers/anthropic.rs @@ -467,7 +467,8 @@ impl AnthropicClient { break; } - let delay = if let Some(retry_after) = last_error.as_ref().and_then(|e| e.retry_after()) { + let delay = if let Some(retry_after) = last_error.as_ref().and_then(|e| e.retry_after()) + { retry_after } else { self.jittered_backoff_for_attempt(attempts)? @@ -909,7 +910,10 @@ async fn expect_success(response: reqwest::Response) -> Result Option { +fn parse_retry_after( + headers: &reqwest::header::HeaderMap, + status: reqwest::StatusCode, +) -> Option { if status != reqwest::StatusCode::TOO_MANY_REQUESTS { return None; } diff --git a/rust/crates/api/src/providers/openai_compat.rs b/rust/crates/api/src/providers/openai_compat.rs index 00326f52..cfeb4c3c 100644 --- a/rust/crates/api/src/providers/openai_compat.rs +++ b/rust/crates/api/src/providers/openai_compat.rs @@ -1667,7 +1667,10 @@ async fn expect_success(response: reqwest::Response) -> Result Option { +fn parse_retry_after( + headers: &reqwest::header::HeaderMap, + status: reqwest::StatusCode, +) -> Option { if status != reqwest::StatusCode::TOO_MANY_REQUESTS { return None; } diff --git a/rust/crates/runtime/src/config.rs b/rust/crates/runtime/src/config.rs index 46639b75..b31c0168 100644 --- a/rust/crates/runtime/src/config.rs +++ b/rust/crates/runtime/src/config.rs @@ -1184,10 +1184,8 @@ fn parse_optional_api_timeout_config(root: &JsonValue) -> Result