use codex_client::Request; use codex_client::RequestCompression; use codex_client::RetryOn; use codex_client::RetryPolicy; use http::Method; use http::header::HeaderMap; use std::collections::HashMap; use std::time::Duration; use url::Url; /// Wire-level APIs supported by a `Provider`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum WireApi { Responses, Chat, Compact, } /// High-level retry configuration for a provider. /// /// This is converted into a `RetryPolicy` used by `codex-client` to drive /// transport-level retries for both unary and streaming calls. #[derive(Debug, Clone)] pub struct RetryConfig { pub max_attempts: u64, pub base_delay: Duration, pub retry_429: bool, pub retry_5xx: bool, pub retry_transport: bool, } impl RetryConfig { pub fn to_policy(&self) -> RetryPolicy { RetryPolicy { max_attempts: self.max_attempts, base_delay: self.base_delay, retry_on: RetryOn { retry_429: self.retry_429, retry_5xx: self.retry_5xx, retry_transport: self.retry_transport, }, } } } /// HTTP endpoint configuration used to talk to a concrete API deployment. /// /// Encapsulates base URL, default headers, query params, retry policy, and /// stream idle timeout, plus helper methods for building requests. #[derive(Debug, Clone)] pub struct Provider { pub name: String, pub base_url: String, pub query_params: Option>, pub wire: WireApi, pub headers: HeaderMap, pub retry: RetryConfig, pub stream_idle_timeout: Duration, } impl Provider { pub fn url_for_path(&self, path: &str) -> String { let base = self.base_url.trim_end_matches('/'); let path = path.trim_start_matches('/'); let mut url = if path.is_empty() { base.to_string() } else { format!("{base}/{path}") }; if let Some(params) = &self.query_params && !params.is_empty() { let qs = params .iter() .map(|(k, v)| format!("{k}={v}")) .collect::>() .join("&"); url.push('?'); url.push_str(&qs); } url } pub fn build_request(&self, method: Method, path: &str) -> Request { Request { method, url: self.url_for_path(path), headers: self.headers.clone(), body: None, compression: RequestCompression::None, timeout: None, } } pub fn is_azure_responses_endpoint(&self) -> bool { if self.wire != WireApi::Responses { return false; } if self.name.eq_ignore_ascii_case("azure") { return true; } self.base_url.to_ascii_lowercase().contains("openai.azure.") || matches_azure_responses_base_url(&self.base_url) } pub fn websocket_url_for_path(&self, path: &str) -> Result { let mut url = Url::parse(&self.url_for_path(path))?; let scheme = match url.scheme() { "http" => "ws", "https" => "wss", "ws" | "wss" => return Ok(url), _ => return Ok(url), }; let _ = url.set_scheme(scheme); Ok(url) } } fn matches_azure_responses_base_url(base_url: &str) -> bool { const AZURE_MARKERS: [&str; 5] = [ "cognitiveservices.azure.", "aoai.azure.", "azure-api.", "azurefd.", "windows.net/openai", ]; let base = base_url.to_ascii_lowercase(); AZURE_MARKERS.iter().any(|marker| base.contains(marker)) }