diff --git a/crates/goose-providers/src/anthropic.rs b/crates/goose-providers/src/anthropic.rs index f5248fa79..d505cadd2 100644 --- a/crates/goose-providers/src/anthropic.rs +++ b/crates/goose-providers/src/anthropic.rs @@ -52,6 +52,12 @@ const ANTHROPIC_KNOWN_MODELS: &[&str] = &[ const ANTHROPIC_DOC_URL: &str = "https://docs.anthropic.com/en/docs/about-claude/models"; pub const ANTHROPIC_API_VERSION: &str = "2023-06-01"; +// Total-request timeout applied when a declarative provider does not set +// `timeout_seconds`. Matches the OpenAI engine's default (`openai.rs`) and the +// shared `ApiClient` default so behavior is unchanged for providers that leave +// the field unset. +const DEFAULT_ANTHROPIC_TIMEOUT_SECONDS: u64 = 600; + #[derive(serde::Serialize)] pub struct AnthropicProvider { #[serde(skip)] @@ -390,7 +396,15 @@ pub fn from_declarative_config( let format_options = format_options_for_provider(config.preserves_thinking); - let mut api_client = ApiClient::new_with_tls(config.base_url, auth, tls_config)?; + let timeout_secs = config + .timeout_seconds + .unwrap_or(DEFAULT_ANTHROPIC_TIMEOUT_SECONDS); + let mut api_client = ApiClient::with_timeout_and_tls( + config.base_url, + auth, + std::time::Duration::from_secs(timeout_secs), + tls_config, + )?; if let Some(headers) = &config.headers { let mut header_map = reqwest::header::HeaderMap::new(); diff --git a/crates/goose-providers/src/api_client.rs b/crates/goose-providers/src/api_client.rs index 5ba3b2a30..b41b35594 100644 --- a/crates/goose-providers/src/api_client.rs +++ b/crates/goose-providers/src/api_client.rs @@ -279,6 +279,10 @@ impl ApiClient { &self.host } + pub fn timeout(&self) -> Duration { + self.timeout + } + fn rebuild_client(&mut self) -> Result<()> { let mut client_builder = Client::builder() .timeout(self.timeout) diff --git a/crates/goose/src/providers/anthropic_def.rs b/crates/goose/src/providers/anthropic_def.rs index cadbb9a85..6388511a4 100644 --- a/crates/goose/src/providers/anthropic_def.rs +++ b/crates/goose/src/providers/anthropic_def.rs @@ -152,4 +152,35 @@ mod tests { "error message should mention dynamic_models: false; got: {msg}" ); } + + // Capture the built client's request timeout without exposing a builder + // getter: map_api_client sees the ApiClient produced by + // from_declarative_config before it is replaced/decorated. + fn built_timeout(config: DeclarativeProviderConfig) -> std::time::Duration { + let captured = std::cell::Cell::new(None); + anthropic::from_declarative_config(config, None, ConfigKeyResolver::new(Config::global())) + .expect("provider construction should succeed") + .map_api_client(|api_client| { + captured.set(Some(api_client.timeout())); + api_client + }); + captured.get().expect("map_api_client should have run") + } + + #[test] + fn from_custom_config_honors_explicit_timeout_seconds() { + let mut config = + base_declarative_config(vec![ModelInfo::new("m1".to_string(), 200000)], Some(false)); + config.timeout_seconds = Some(120); + assert_eq!(built_timeout(config), std::time::Duration::from_secs(120)); + } + + #[test] + fn from_custom_config_defaults_timeout_when_unset() { + // timeout_seconds: None in base config → 600s default, unchanged + // behavior for providers that don't set the field. + let config = + base_declarative_config(vec![ModelInfo::new("m1".to_string(), 200000)], Some(false)); + assert_eq!(built_timeout(config), std::time::Duration::from_secs(600)); + } }