fix: LLM providers use own HTTP client with 120s timeout (was sharing scraper's 15s)

The scraper client (build_scraper_client) has a 15s timeout appropriate for web
scraping, but LLM API calls — especially with web search — take 30-60s. LLM
providers now build their own reqwest client with 120s timeout via build_llm_client().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
master
oabrivard 3 months ago
parent 69e5f2257a
commit 3fe667591d

@ -122,7 +122,7 @@ pub async fn test_key(
encryption::decrypt(&master_key, &stored_key.encrypted_key, &stored_key.nonce)?;
// Create a provider instance
let llm_provider = factory::create_provider(&provider, decrypted_key, &state.http_client)?;
let llm_provider = factory::create_provider(&provider, decrypted_key)?;
// Make a minimal test call using the rewrite pass (no web search needed)
let test_schema = serde_json::json!({

@ -7,27 +7,42 @@ use super::openai::OpenAiProvider;
use super::LlmProvider;
use crate::errors::AppError;
/// LLM API timeout: 120 seconds.
/// LLM calls with web search can take 30-60s; 120s provides headroom.
const LLM_TIMEOUT_SECS: u64 = 120;
/// Build an HTTP client configured for LLM API calls.
///
/// Uses a longer timeout than the scraper client since LLM calls
/// (especially with web search) are significantly slower.
fn build_llm_client() -> Result<reqwest::Client, AppError> {
reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(10))
.timeout(std::time::Duration::from_secs(LLM_TIMEOUT_SECS))
.build()
.map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to build LLM HTTP client: {}", e)))
}
/// Create an LLM provider instance for the given provider name.
///
/// Each provider gets its own HTTP client with a 120s timeout,
/// separate from the scraper client (15s timeout).
///
/// # Arguments
/// * `provider_name` — One of "gemini", "openai", "anthropic"
/// * `api_key` — The decrypted API key for the provider
/// * `http_client` — Shared HTTP client for making API calls
///
/// # Errors
/// Returns `AppError::BadRequest` if the provider name is unknown.
pub fn create_provider(
provider_name: &str,
api_key: String,
http_client: &reqwest::Client,
) -> Result<Box<dyn LlmProvider>, AppError> {
let http_client = build_llm_client()?;
match provider_name {
"gemini" => Ok(Box::new(GeminiProvider::new(api_key, http_client.clone()))),
"openai" => Ok(Box::new(OpenAiProvider::new(api_key, http_client.clone()))),
"anthropic" => Ok(Box::new(AnthropicProvider::new(
api_key,
http_client.clone(),
))),
"gemini" => Ok(Box::new(GeminiProvider::new(api_key, http_client))),
"openai" => Ok(Box::new(OpenAiProvider::new(api_key, http_client))),
"anthropic" => Ok(Box::new(AnthropicProvider::new(api_key, http_client))),
_ => Err(AppError::BadRequest(format!(
"Unknown provider: '{}'",
provider_name
@ -41,32 +56,28 @@ mod tests {
#[test]
fn factory_creates_gemini_provider() {
let client = reqwest::Client::new();
let provider = create_provider("gemini", "test-key".into(), &client).unwrap();
let provider = create_provider("gemini", "test-key".into()).unwrap();
assert_eq!(provider.provider_id(), "gemini");
assert!(provider.supports_web_search());
}
#[test]
fn factory_creates_openai_provider() {
let client = reqwest::Client::new();
let provider = create_provider("openai", "test-key".into(), &client).unwrap();
let provider = create_provider("openai", "test-key".into()).unwrap();
assert_eq!(provider.provider_id(), "openai");
assert!(provider.supports_web_search());
}
#[test]
fn factory_creates_anthropic_provider() {
let client = reqwest::Client::new();
let provider = create_provider("anthropic", "test-key".into(), &client).unwrap();
let provider = create_provider("anthropic", "test-key".into()).unwrap();
assert_eq!(provider.provider_id(), "anthropic");
assert!(provider.supports_web_search());
}
#[test]
fn factory_rejects_unknown_provider() {
let client = reqwest::Client::new();
let result = create_provider("mistral", "test-key".into(), &client);
let result = create_provider("mistral", "test-key".into());
match result {
Err(AppError::BadRequest(msg)) => assert!(msg.contains("Unknown provider")),
Err(_) => panic!("Expected BadRequest variant"),
@ -76,8 +87,7 @@ mod tests {
#[test]
fn factory_rejects_empty_provider() {
let client = reqwest::Client::new();
let result = create_provider("", "test-key".into(), &client);
let result = create_provider("", "test-key".into());
match result {
Err(AppError::BadRequest(msg)) => assert!(msg.contains("Unknown provider")),
Err(_) => panic!("Expected BadRequest variant"),

@ -272,7 +272,7 @@ async fn run_generation_inner(
emit_progress(tx, "provider", "Configuration du fournisseur IA...", 15);
let (provider_name, api_key) = resolve_provider_and_key(state, user_id, &settings).await?;
let provider = create_provider(&provider_name, api_key, &state.http_client)?;
let provider = create_provider(&provider_name, api_key)?;
// Step 4: Build schema from categories
let schema = build_category_schema(&settings.categories);

Loading…
Cancel
Save