use anyhow::Result; use async_trait::async_trait; use futures::stream::BoxStream; use super::helpers; use super::{ProviderResponse, ProviderStreamChunk}; use crate::{config::AppConfig, errors::AppError, models::UnifiedRequest}; pub struct OpenAIProvider { client: reqwest::Client, config: crate::config::OpenAIConfig, api_key: String, pricing: Vec, } impl OpenAIProvider { pub fn new(config: &crate::config::OpenAIConfig, app_config: &AppConfig) -> Result { let api_key = app_config.get_api_key("openai")?; Self::new_with_key(config, app_config, api_key) } pub fn new_with_key(config: &crate::config::OpenAIConfig, app_config: &AppConfig, api_key: String) -> Result { let client = reqwest::Client::builder() .connect_timeout(std::time::Duration::from_secs(5)) .timeout(std::time::Duration::from_secs(300)) .pool_idle_timeout(std::time::Duration::from_secs(90)) .pool_max_idle_per_host(4) .tcp_keepalive(std::time::Duration::from_secs(30)) .build()?; Ok(Self { client, config: config.clone(), api_key, pricing: app_config.pricing.openai.clone(), }) } } #[async_trait] impl super::Provider for OpenAIProvider { fn name(&self) -> &str { "openai" } fn supports_model(&self, model: &str) -> bool { model.starts_with("gpt-") || model.starts_with("o1-") || model.starts_with("o3-") || model.starts_with("o4-") } fn supports_multimodal(&self) -> bool { true } async fn chat_completion(&self, request: UnifiedRequest) -> Result { let messages_json = helpers::messages_to_openai_json(&request.messages).await?; let body = helpers::build_openai_body(&request, messages_json, false); let response = self .client .post(format!("{}/chat/completions", self.config.base_url)) .header("Authorization", format!("Bearer {}", self.api_key)) .json(&body) .send() .await .map_err(|e| AppError::ProviderError(e.to_string()))?; if !response.status().is_success() { let error_text = response.text().await.unwrap_or_default(); return Err(AppError::ProviderError(format!("OpenAI API error: {}", error_text))); } let resp_json: serde_json::Value = response .json() .await .map_err(|e| AppError::ProviderError(e.to_string()))?; helpers::parse_openai_response(&resp_json, request.model) } fn estimate_tokens(&self, request: &UnifiedRequest) -> Result { Ok(crate::utils::tokens::estimate_request_tokens(&request.model, request)) } fn calculate_cost( &self, model: &str, prompt_tokens: u32, completion_tokens: u32, cache_read_tokens: u32, cache_write_tokens: u32, registry: &crate::models::registry::ModelRegistry, ) -> f64 { helpers::calculate_cost_with_registry( model, prompt_tokens, completion_tokens, cache_read_tokens, cache_write_tokens, registry, &self.pricing, 0.15, 0.60, ) } async fn chat_completion_stream( &self, request: UnifiedRequest, ) -> Result>, AppError> { let messages_json = helpers::messages_to_openai_json(&request.messages).await?; let body = helpers::build_openai_body(&request, messages_json, true); let es = reqwest_eventsource::EventSource::new( self.client .post(format!("{}/chat/completions", self.config.base_url)) .header("Authorization", format!("Bearer {}", self.api_key)) .json(&body), ) .map_err(|e| AppError::ProviderError(format!("Failed to create EventSource: {}", e)))?; Ok(helpers::create_openai_stream(es, request.model, None)) } }