feat: API timeout config, Retry-After header support, and configurable retry
- Add TimeoutConfig to HTTP client builder with connect_timeout (30s) and request_timeout (5min) defaults, configurable via CLAW_API_CONNECT_TIMEOUT and CLAW_API_REQUEST_TIMEOUT env vars - Add with_timeout() builder to both AnthropicClient and OpenAiCompatClient for per-client timeout configuration - Parse Retry-After header on 429 responses and use it to override exponential backoff delay when present - Add ApiTimeoutConfig to runtime config with apiTimeout settings in ~/.claw/settings.json (connectTimeout, requestTimeout, maxRetries) - Add retry_after field to ApiError::Api for propagating rate limit backoff hints through the retry pipeline
This commit is contained in:
parent
8b120a322d
commit
763179877a
|
|
@ -135,6 +135,10 @@ impl ApiError {
|
|||
/// Return the `Retry-After` delay if this error came from a 429 response
|
||||
/// that included a `retry-after` header. Callers should prefer this value
|
||||
/// over the computed backoff delay when it exists.
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
#[must_use]
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
pub fn retry_after(&self) -> Option<Duration> {
|
||||
match self {
|
||||
Self::Api { retry_after, .. } => *retry_after,
|
||||
|
|
|
|||
|
|
@ -132,20 +132,20 @@ pub fn build_http_client() -> Result<reqwest::Client, ApiError> {
|
|||
#[must_use]
|
||||
pub fn build_http_client_or_default() -> reqwest::Client {
|
||||
build_http_client_with_opts(&ProxyConfig::from_env(), &TimeoutConfig::from_env())
|
||||
<<<<<<< HEAD
|
||||
.unwrap_or_else(|_| {
|
||||
reqwest::Client::builder()
|
||||
.user_agent("clawd-rust-tools/0.1")
|
||||
.build()
|
||||
.expect("default client with user_agent should always succeed")
|
||||
})
|
||||
=======
|
||||
.unwrap_or_else(|_| reqwest::Client::new())
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
}
|
||||
|
||||
/// Build a `reqwest::Client` from an explicit [`ProxyConfig`]. Used by tests
|
||||
/// and by callers that want to override process-level environment lookups.
|
||||
///
|
||||
/// When `config.proxy_url` is set it overrides the per-scheme `http_proxy`
|
||||
/// and `https_proxy` fields and is registered as both an HTTP and HTTPS
|
||||
/// proxy so a single value can route every outbound request.
|
||||
pub fn build_http_client_with(config: &ProxyConfig) -> Result<reqwest::Client, ApiError> {
|
||||
build_http_client_with_opts(config, &TimeoutConfig::from_env())
|
||||
}
|
||||
|
|
@ -158,7 +158,10 @@ pub fn build_http_client_with_opts(
|
|||
) -> Result<reqwest::Client, ApiError> {
|
||||
let mut builder = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
<<<<<<< HEAD
|
||||
.user_agent("clawd-rust-tools/0.1")
|
||||
=======
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
.connect_timeout(timeout.connect_timeout)
|
||||
.timeout(timeout.request_timeout);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,14 @@ pub use client::{
|
|||
};
|
||||
pub use error::ApiError;
|
||||
pub use http_client::{
|
||||
<<<<<<< HEAD
|
||||
build_http_client, build_http_client_or_default, build_http_client_with,
|
||||
build_http_client_with_opts, ProxyConfig, TimeoutConfig,
|
||||
=======
|
||||
TimeoutConfig,
|
||||
build_http_client, build_http_client_or_default, build_http_client_with,
|
||||
build_http_client_with_opts, ProxyConfig,
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
};
|
||||
pub use prompt_cache::{
|
||||
CacheBreakEvent, PromptCache, PromptCacheConfig, PromptCachePaths, PromptCacheRecord,
|
||||
|
|
|
|||
|
|
@ -467,8 +467,12 @@ impl AnthropicClient {
|
|||
break;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
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()) {
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
retry_after
|
||||
} else {
|
||||
self.jittered_backoff_for_attempt(attempts)?
|
||||
|
|
@ -907,10 +911,14 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
|
|||
})
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
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> {
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
if status != reqwest::StatusCode::TOO_MANY_REQUESTS {
|
||||
return None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1764,10 +1764,14 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
|
|||
})
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
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> {
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
if status != reqwest::StatusCode::TOO_MANY_REQUESTS {
|
||||
return None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,7 +152,29 @@ pub struct LspServerConfig {
|
|||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
pub enabled: bool,
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
/// API timeout and retry configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ApiTimeoutConfig {
|
||||
/// Connect timeout in seconds. Defaults to 30.
|
||||
pub connect_timeout_secs: u64,
|
||||
/// Request timeout in seconds. Defaults to 300 (5 minutes).
|
||||
pub request_timeout_secs: u64,
|
||||
/// Maximum retry attempts on transient failures. Defaults to 8.
|
||||
pub max_retries: u32,
|
||||
}
|
||||
|
||||
impl Default for ApiTimeoutConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
connect_timeout_secs: 30,
|
||||
request_timeout_secs: 300,
|
||||
max_retries: 8,
|
||||
}
|
||||
}
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
}
|
||||
|
||||
/// Structured feature configuration consumed by runtime subsystems.
|
||||
|
|
@ -233,6 +255,7 @@ impl RuntimeProviderConfig {
|
|||
pub fn model(&self) -> Option<&str> {
|
||||
self.model.as_deref()
|
||||
}
|
||||
api_timeout: ApiTimeoutConfig,
|
||||
}
|
||||
|
||||
/// Ordered chain of fallback model identifiers used when the primary
|
||||
|
|
@ -680,7 +703,16 @@ impl ConfigLoader {
|
|||
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
|
||||
provider: parse_optional_provider_config(&merged_value)?,
|
||||
lsp: parse_optional_lsp_config(&merged_value)?,
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
lsp_auto_start: merged_value
|
||||
.as_object()
|
||||
.and_then(|o| o.get("lspAutoStart"))
|
||||
.and_then(JsonValue::as_bool)
|
||||
.unwrap_or(true),
|
||||
api_timeout: parse_optional_api_timeout_config(&merged_value)?,
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
};
|
||||
|
||||
ConfigInspection {
|
||||
|
|
@ -2142,8 +2174,15 @@ fn parse_optional_api_timeout_config(root: &JsonValue) -> Result<ApiTimeoutConfi
|
|||
return Ok(ApiTimeoutConfig::default());
|
||||
};
|
||||
let context = "merged settings.apiTimeout";
|
||||
<<<<<<< HEAD
|
||||
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);
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
let max_retries = optional_u64(obj, "maxRetries", context)?
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(8);
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ pub use config::{
|
|||
ConfigLoader, ConfigSource, LspServerConfig, McpConfigCollection, McpManagedProxyServerConfig,
|
||||
McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig, McpServerConfig,
|
||||
McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
|
||||
ApiTimeoutConfig, ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpConfigCollection,
|
||||
McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
|
||||
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
|
||||
ProviderFallbackConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig,
|
||||
RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig, RuntimeProviderConfig,
|
||||
ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
|
||||
|
|
|
|||
|
|
@ -14648,7 +14648,11 @@ mod tests {
|
|||
retryable: false,
|
||||
suggested_action: None,
|
||||
retry_after: None,
|
||||
<<<<<<< HEAD
|
||||
}),
|
||||
=======
|
||||
}),
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
};
|
||||
|
||||
let rendered = format_user_visible_api_error("session-issue-32", &error);
|
||||
|
|
|
|||
Loading…
Reference in New Issue