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
This commit is contained in:
TheArchitectit
2026-06-02 15:35:29 -05:00
parent 571d3cdc0f
commit 04bc5f5788
6 changed files with 18 additions and 14 deletions

View File

@@ -582,6 +582,7 @@ mod tests {
body: String::new(),
retryable: false,
suggested_action: None,
retry_after: None,
};
assert!(error.is_context_window_failure());

View File

@@ -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,

View File

@@ -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<reqwest::Response
})
}
fn parse_retry_after(headers: &reqwest::header::HeaderMap, status: reqwest::StatusCode) -> Option<std::time::Duration> {
fn parse_retry_after(
headers: &reqwest::header::HeaderMap,
status: reqwest::StatusCode,
) -> Option<std::time::Duration> {
if status != reqwest::StatusCode::TOO_MANY_REQUESTS {
return None;
}

View File

@@ -1667,7 +1667,10 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
})
}
fn parse_retry_after(headers: &reqwest::header::HeaderMap, status: reqwest::StatusCode) -> Option<std::time::Duration> {
fn parse_retry_after(
headers: &reqwest::header::HeaderMap,
status: reqwest::StatusCode,
) -> Option<std::time::Duration> {
if status != reqwest::StatusCode::TOO_MANY_REQUESTS {
return None;
}

View File

@@ -1184,10 +1184,8 @@ fn parse_optional_api_timeout_config(root: &JsonValue) -> Result<ApiTimeoutConfi
return Ok(ApiTimeoutConfig::default());
};
let context = "merged settings.apiTimeout";
let connect_timeout_secs = optional_u64(obj, "connectTimeout", context)?
.unwrap_or(30);
let request_timeout_secs = optional_u64(obj, "requestTimeout", context)?
.unwrap_or(300);
let connect_timeout_secs = optional_u64(obj, "connectTimeout", context)?.unwrap_or(30);
let request_timeout_secs = optional_u64(obj, "requestTimeout", context)?.unwrap_or(300);
let max_retries = optional_u64(obj, "maxRetries", context)?
.map(|v| v as u32)
.unwrap_or(8);

View File

@@ -65,13 +65,12 @@ pub use compact::{
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
};
pub use config::{
ApiTimeoutConfig, ConfigEntry, ConfigError, ConfigLoader, ConfigSource,
McpConfigCollection, McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig,
McpSdkServerConfig, McpServerConfig, McpStdioServerConfig, McpTransport,
suppress_config_warnings_for_json_mode, ApiTimeoutConfig, ConfigEntry, ConfigError,
ConfigLoader, ConfigSource, McpConfigCollection, McpManagedProxyServerConfig, McpOAuthConfig,
McpRemoteServerConfig, McpSdkServerConfig, McpServerConfig, McpStdioServerConfig, McpTransport,
McpWebSocketServerConfig, OAuthConfig, ProviderFallbackConfig, ResolvedPermissionMode,
RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig, RuntimePermissionRuleConfig,
RuntimePluginConfig, ScopedMcpServerConfig, suppress_config_warnings_for_json_mode,
CLAW_SETTINGS_SCHEMA_NAME,
RuntimePluginConfig, ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
};
pub use config_validate::{
check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic,