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