diff --git a/src/providers/mod.rs b/src/providers/mod.rs index a0f1e7b2a..bfc788cb8 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -1119,10 +1119,13 @@ fn create_provider_with_url_and_options( )?)) } // ── Primary providers (custom implementations) ─────── - "openrouter" => Ok(Box::new(openrouter::OpenRouterProvider::new( - key, - options.provider_timeout_secs, - ))), + "openrouter" => { + let mut p = openrouter::OpenRouterProvider::new(key); + if let Some(t) = options.provider_timeout_secs { + p = p.with_timeout_secs(t); + } + Ok(Box::new(p)) + } "anthropic" => Ok(Box::new(anthropic::AnthropicProvider::new(key))), "openai" => Ok(Box::new(openai::OpenAiProvider::with_base_url(api_url, key))), // Ollama uses api_url for custom base URL (e.g. remote Ollama instance) diff --git a/src/providers/openrouter.rs b/src/providers/openrouter.rs index f545f9e13..b1e1ea4ca 100644 --- a/src/providers/openrouter.rs +++ b/src/providers/openrouter.rs @@ -4,6 +4,7 @@ use crate::providers::traits::{ Provider, ProviderCapabilities, TokenUsage, ToolCall as ProviderToolCall, }; use crate::tools::ToolSpec; +use anyhow::Context as _; use async_trait::async_trait; use reqwest::Client; use serde::de::DeserializeOwned; @@ -154,12 +155,16 @@ impl OpenRouterProvider { pub fn new(credential: Option<&str>, timeout_secs: Option) -> Self { Self { credential: credential.map(ToString::to_string), - timeout_secs: timeout_secs - .filter(|secs| *secs > 0) - .unwrap_or(DEFAULT_OPENROUTER_TIMEOUT_SECS), + timeout_secs: 120, } } + /// Override the HTTP request timeout for LLM API calls. + pub fn with_timeout_secs(mut self, secs: u64) -> Self { + self.timeout_secs = secs; + self + } + fn convert_tools(tools: Option<&[ToolSpec]>) -> Option> { let items = tools?; if items.is_empty() { @@ -339,7 +344,7 @@ impl OpenRouterProvider { crate::config::build_runtime_proxy_client_with_timeouts( "provider.openrouter", self.timeout_secs, - OPENROUTER_CONNECT_TIMEOUT_SECS, + 10, ) } } @@ -412,9 +417,13 @@ impl Provider for OpenRouterProvider { return Err(super::api_error("OpenRouter", response).await); } - let body = Self::read_response_body("OpenRouter", response).await?; - let chat_response = - Self::parse_response_body::("OpenRouter", &body, "chat-completions")?; + let text = response.text().await?; + let chat_response: ApiChatResponse = serde_json::from_str(&text).with_context(|| { + format!( + "OpenRouter: failed to decode response body: {}", + &text[..text.len().min(500)] + ) + })?; chat_response .choices @@ -461,9 +470,13 @@ impl Provider for OpenRouterProvider { return Err(super::api_error("OpenRouter", response).await); } - let body = Self::read_response_body("OpenRouter", response).await?; - let chat_response = - Self::parse_response_body::("OpenRouter", &body, "chat-completions")?; + let text = response.text().await?; + let chat_response: ApiChatResponse = serde_json::from_str(&text).with_context(|| { + format!( + "OpenRouter: failed to decode response body: {}", + &text[..text.len().min(500)] + ) + })?; chat_response .choices @@ -508,9 +521,14 @@ impl Provider for OpenRouterProvider { return Err(super::api_error("OpenRouter", response).await); } - let body = Self::read_response_body("OpenRouter", response).await?; - let native_response = - Self::parse_response_body::("OpenRouter", &body, "native chat")?; + let text = response.text().await?; + let native_response: NativeChatResponse = + serde_json::from_str(&text).with_context(|| { + format!( + "OpenRouter: failed to decode response body: {}", + &text[..text.len().min(500)] + ) + })?; let usage = native_response.usage.map(|u| TokenUsage { input_tokens: u.prompt_tokens, output_tokens: u.completion_tokens, @@ -602,9 +620,14 @@ impl Provider for OpenRouterProvider { return Err(super::api_error("OpenRouter", response).await); } - let body = Self::read_response_body("OpenRouter", response).await?; - let native_response = - Self::parse_response_body::("OpenRouter", &body, "native chat")?; + let text = response.text().await?; + let native_response: NativeChatResponse = + serde_json::from_str(&text).with_context(|| { + format!( + "OpenRouter: failed to decode response body: {}", + &text[..text.len().min(500)] + ) + })?; let usage = native_response.usage.map(|u| TokenUsage { input_tokens: u.prompt_tokens, output_tokens: u.completion_tokens, @@ -1115,4 +1138,20 @@ mod tests { assert!(json.contains("reasoning_content")); assert!(json.contains("thinking...")); } + + // ═══════════════════════════════════════════════════════════════════════ + // timeout_secs configuration tests + // ═══════════════════════════════════════════════════════════════════════ + + #[test] + fn default_timeout_is_120() { + let provider = OpenRouterProvider::new(Some("key")); + assert_eq!(provider.timeout_secs, 120); + } + + #[test] + fn with_timeout_secs_overrides_default() { + let provider = OpenRouterProvider::new(Some("key")).with_timeout_secs(300); + assert_eq!(provider.timeout_secs, 300); + } }