| 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; |
|
|
| |
| |
| |
| |
| #[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, |
| }, |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| #[derive(Debug, Clone)] |
| pub struct Provider { |
| pub name: String, |
| pub base_url: String, |
| pub query_params: Option<HashMap<String, String>>, |
| 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::<Vec<_>>() |
| .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, |
| response_body_limit_bytes: None, |
| } |
| } |
|
|
| pub fn websocket_url_for_path(&self, path: &str) -> Result<Url, url::ParseError> { |
| 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) |
| } |
| } |
|
|
| pub fn is_azure_responses_provider(name: &str, base_url: Option<&str>) -> bool { |
| if name.eq_ignore_ascii_case("azure") { |
| true |
| } else if let Some(base_url) = base_url { |
| matches_azure_responses_base_url(base_url) |
| } else { |
| false |
| } |
| } |
|
|
| fn matches_azure_responses_base_url(base_url: &str) -> bool { |
| let base_url = base_url.to_ascii_lowercase(); |
| const AZURE_MARKERS: [&str; 6] = [ |
| "openai.azure.", |
| "cognitiveservices.azure.", |
| "aoai.azure.", |
| "azure-api.", |
| "azurefd.", |
| "windows.net/openai", |
| ]; |
| AZURE_MARKERS.iter().any(|marker| base_url.contains(marker)) |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| #[test] |
| fn detects_azure_responses_base_urls() { |
| let positive_cases = [ |
| "https://foo.openai.azure.com/openai", |
| "https://foo.openai.azure.us/openai/deployments/bar", |
| "https://foo.cognitiveservices.azure.cn/openai", |
| "https://foo.aoai.azure.com/openai", |
| "https://foo.openai.azure-api.net/openai", |
| "https://foo.z01.azurefd.net/", |
| ]; |
|
|
| for base_url in positive_cases { |
| assert!( |
| is_azure_responses_provider("test", Some(base_url)), |
| "expected {base_url} to be detected as Azure" |
| ); |
| } |
|
|
| assert!(is_azure_responses_provider( |
| "Azure", |
| Some("https://example.com") |
| )); |
|
|
| let negative_cases = [ |
| "https://api.openai.com/v1", |
| "https://example.com/openai", |
| "https://myproxy.azurewebsites.net/openai", |
| ]; |
|
|
| for base_url in negative_cases { |
| assert!( |
| !is_azure_responses_provider("test", Some(base_url)), |
| "expected {base_url} not to be detected as Azure" |
| ); |
| } |
| } |
| } |
|
|