use futures::Stream;
use serde_json::Value;
use std::collections::HashMap;
use std::pin::Pin;
use std::sync::Arc;
use crate::core::audio::types::{
SpeechRequest, SpeechResponse, TranscriptionRequest, TranscriptionResponse, TranslationRequest,
TranslationResponse,
};
use crate::core::providers::base::{
GlobalPoolManager, HeaderPair, HttpMethod, header, header_owned, read_streaming_error_body,
};
use crate::core::providers::unified_provider::ProviderError;
use crate::core::traits::provider::llm_provider::trait_definition::LLMProvider;
use crate::core::types::{
chat::ChatRequest,
context::RequestContext,
embedding::EmbeddingRequest,
health::HealthStatus,
image::{ImageEditRequest, ImageGenerationRequest},
model::ModelInfo,
model::ProviderCapability,
responses::{ChatChunk, ChatResponse, EmbeddingResponse, ImageGenerationResponse},
};
use super::{
config::OpenAIConfig,
models::{OpenAIModelRegistry, get_openai_registry},
};
use crate::core::traits::error_mapper::trait_def::ErrorMapper;
#[derive(Debug, Clone)]
pub struct OpenAIProvider {
pub(crate) pool_manager: Arc<GlobalPoolManager>,
pub(crate) config: OpenAIConfig,
pub(crate) model_registry: &'static OpenAIModelRegistry,
pub(crate) model_identity:
Option<crate::core::providers::model_identity::DeploymentProviderBinding>,
}
impl OpenAIProvider {
#[cfg(feature = "gateway")]
pub(crate) async fn native_response(
&self,
body: Value,
) -> Result<reqwest::Response, ProviderError> {
self.native_response_operation(body, false).await
}
#[cfg(feature = "gateway")]
pub(crate) async fn compact_response(
&self,
body: Value,
) -> Result<reqwest::Response, ProviderError> {
self.native_response_operation(body, true).await
}
#[cfg(feature = "gateway")]
async fn native_response_operation(
&self,
mut body: Value,
compact: bool,
) -> Result<reqwest::Response, ProviderError> {
let model = body
.get("model")
.and_then(Value::as_str)
.ok_or_else(|| ProviderError::invalid_request("openai", "model must be a string"))?;
let mapped_model = self.config.get_model_mapping(model);
let capability_model = self
.model_identity
.as_ref()
.and_then(|binding| binding.identity().capability_catalog_model())
.unwrap_or(&mapped_model);
if !self.model_supports_capability(capability_model, &ProviderCapability::Responses) {
return Err(ProviderError::invalid_request(
"openai",
format!("Model {capability_model} does not support native Responses"),
));
}
let wire_model = self
.model_identity
.as_ref()
.map(|binding| binding.identity().wire_model())
.or_else(|| self.config.model_mappings.get(model).map(String::as_str))
.unwrap_or(model)
.to_string();
body["model"] = Value::String(wire_model);
body["service_tier"] = Value::String("default".into());
if compact {
return super::super::responses_native::compact(
&self.pool_manager,
&self.config.get_api_base(),
self.get_request_headers(),
body,
)
.await;
}
super::super::responses_native::send(
&self.pool_manager,
&self.config.get_api_base(),
self.get_request_headers(),
self.config.base.timeout,
body,
"openai",
)
.await
}
#[cfg(feature = "gateway")]
pub(crate) fn native_response_binding(&self) -> String {
use sha2::{Digest, Sha256};
let mut hash = Sha256::new();
let base = self.config.get_api_base();
hash.update((base.len() as u64).to_be_bytes());
hash.update(base.as_bytes());
let mut headers = self.get_request_headers();
headers.sort_unstable();
for (name, value) in headers {
for part in [name.as_ref(), value.as_ref()] {
hash.update((part.len() as u64).to_be_bytes());
hash.update(part.as_bytes());
}
}
format!("{:x}", hash.finalize())
}
#[cfg(feature = "gateway")]
pub(crate) async fn native_response_lifecycle(
&self,
id: &str,
method: HttpMethod,
suffix: Option<&str>,
query: &str,
) -> Result<reqwest::Response, ProviderError> {
super::super::responses_native::lifecycle(
&self.pool_manager,
&self.config.get_api_base(),
self.get_request_headers(),
id,
method,
suffix,
query,
)
.await
}
#[cfg(feature = "gateway")]
pub(crate) async fn native_response_input_tokens(
&self,
mut body: Value,
) -> Result<u32, ProviderError> {
let model = body
.get("model")
.and_then(Value::as_str)
.ok_or_else(|| ProviderError::invalid_request("openai", "model must be a string"))?;
body["model"] = self
.model_identity
.as_ref()
.map(|binding| binding.identity().wire_model())
.or_else(|| self.config.model_mappings.get(model).map(String::as_str))
.unwrap_or(model)
.to_string()
.into();
let url = format!(
"{}/responses/input_tokens",
self.config.get_api_base().trim_end_matches('/')
);
let response = self
.pool_manager
.execute_request_preserving_endpoint_policy(
&url,
HttpMethod::POST,
self.get_request_headers(),
Some(body),
)
.await?;
let status = response.status();
if !status.is_success() {
let retry_after = response
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse().ok());
let message = read_streaming_error_body(response)
.await
.unwrap_or_else(|_| "Failed to read Responses token count error".into());
if status.as_u16() == 429 {
return Err(ProviderError::rate_limit_with_retry(
"openai",
message,
retry_after,
));
}
return Err(super::super::unified_provider::default_http_error_mapper(
"openai",
status.as_u16(),
&message,
));
}
let text = read_streaming_error_body(response)
.await
.map_err(|error| error.into_provider_error("openai"))?;
let value: Value = serde_json::from_str(&text).map_err(|_| {
ProviderError::response_parsing("openai", "Invalid Responses token count JSON")
})?;
if value.get("object").and_then(Value::as_str) != Some("response.input_tokens") {
return Err(ProviderError::response_parsing(
"openai",
"Invalid Responses token count object",
));
}
value
.get("input_tokens")
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok())
.ok_or_else(|| {
ProviderError::response_parsing("openai", "Invalid Responses input token count")
})
}
pub(crate) fn get_request_headers(&self) -> Vec<HeaderPair> {
let mut headers = Vec::with_capacity(4);
if let Some(api_key) = &self.config.base.api_key {
headers.push(header("Authorization", format!("Bearer {}", api_key)));
}
if let Some(org) = &self.config.organization {
headers.push(header("OpenAI-Organization", org.clone()));
}
if let Some(project) = &self.config.project {
headers.push(header("OpenAI-Project", project.clone()));
}
for (key, value) in &self.config.base.headers {
headers.push(header_owned(key.clone(), value.clone()));
}
headers
}
pub async fn new(config: OpenAIConfig) -> Result<Self, ProviderError> {
config
.validate()
.map_err(|e| ProviderError::Configuration {
provider: "openai",
message: e.to_string(),
})?;
let pool_manager = Arc::new(GlobalPoolManager::new_for_provider(
"openai",
config.base.clone(),
)?);
let model_registry = get_openai_registry();
Ok(Self {
pool_manager,
config,
model_registry,
model_identity: None,
})
}
pub async fn with_api_key(api_key: impl Into<String>) -> Result<Self, ProviderError> {
let mut config = OpenAIConfig::default();
config.base.api_key = Some(api_key.into());
Self::new(config).await
}
async fn execute_chat_completion(
&self,
request: ChatRequest,
) -> Result<ChatResponse, ProviderError> {
let openai_request = self.transform_chat_request(request)?;
let url = format!("{}/chat/completions", self.config.get_api_base());
let headers = self.get_request_headers();
let body = Some(openai_request);
let response = self
.pool_manager
.execute_request(&url, HttpMethod::POST, headers, body)
.await
.map_err(|e| ProviderError::Network {
provider: "openai",
message: e.to_string(),
})?;
let response_bytes = super::api_methods::read_success_response_bytes(response).await?;
let response_json: Value = serde_json::from_slice(&response_bytes).map_err(|e| {
ProviderError::ResponseParsing {
provider: "openai",
message: e.to_string(),
}
})?;
self.transform_chat_response(response_json)
}
async fn execute_chat_completion_stream(
&self,
request: ChatRequest,
) -> Result<Pin<Box<dyn Stream<Item = Result<ChatChunk, ProviderError>> + Send>>, ProviderError>
{
let mut openai_request = self.transform_chat_request(request)?;
openai_request["stream"] = Value::Bool(true);
if self.config.base.api_key.is_none() {
return Err(ProviderError::Authentication {
provider: "openai",
message: "API key is required".to_string(),
});
}
let url = format!("{}/chat/completions", self.config.get_api_base());
let headers = self.get_request_headers();
let response = self
.pool_manager
.execute_streaming_request(&url, headers, openai_request, "openai")
.await?;
let status = response.status();
if !status.is_success() {
let body = read_streaming_error_body(response)
.await
.unwrap_or_else(|_| "failed to read upstream error body".to_string());
let mapper = super::error_mapper::OpenAIErrorMapper;
return Err(mapper.map_http_error(status.as_u16(), &body));
}
let stream = response.bytes_stream();
Ok(Box::pin(super::streaming::create_openai_stream(stream)))
}
pub(crate) fn transform_chat_request(
&self,
request: ChatRequest,
) -> Result<Value, ProviderError> {
let mapped_model = self.config.get_model_mapping(&request.model);
let capability_model = self
.model_identity
.as_ref()
.and_then(|binding| binding.identity().capability_catalog_model())
.unwrap_or(&mapped_model);
if let Some(spec) = self.model_registry.get_model_spec(capability_model)
&& !spec
.model_info
.capabilities
.contains(&ProviderCapability::ChatCompletion)
{
return Err(ProviderError::invalid_request(
"openai",
format!("Model {capability_model} does not support Chat Completions"),
));
}
let mut openai_request = serde_json::json!({
"model": self.config.get_model_mapping(&request.model),
"messages": request.messages
});
if let Some(temp) = request.temperature {
let temp_f64 = temp as f64;
openai_request["temperature"] = Value::Number(
serde_json::Number::from_f64(temp_f64).ok_or_else(|| {
ProviderError::invalid_request(
"openai",
format!("invalid temperature value: {temp_f64} (NaN and Infinity are not allowed)"),
)
})?,
);
}
if let Some(max_tokens) = request.max_tokens {
openai_request["max_tokens"] = Value::Number(serde_json::Number::from(max_tokens));
}
if let Some(max_completion_tokens) = request.max_completion_tokens {
openai_request["max_completion_tokens"] =
Value::Number(serde_json::Number::from(max_completion_tokens));
}
if let Some(top_p) = request.top_p {
let top_p_f64 = top_p as f64;
openai_request["top_p"] =
Value::Number(serde_json::Number::from_f64(top_p_f64).ok_or_else(|| {
ProviderError::invalid_request(
"openai",
format!(
"invalid top_p value: {top_p_f64} (NaN and Infinity are not allowed)"
),
)
})?);
}
if let Some(tools) = request.tools {
openai_request["tools"] = serde_json::to_value(tools)?;
}
if let Some(tool_choice) = request.tool_choice {
openai_request["tool_choice"] = serde_json::to_value(tool_choice)?;
}
if let Some(response_format) = request.response_format {
openai_request["response_format"] = serde_json::to_value(response_format)?;
}
if let Some(stop) = request.stop {
openai_request["stop"] = serde_json::to_value(stop)?;
}
if let Some(user) = request.user {
openai_request["user"] = Value::String(user);
}
if let Some(seed) = request.seed {
openai_request["seed"] = Value::Number(serde_json::Number::from(seed));
}
if let Some(n) = request.n {
openai_request["n"] = Value::Number(serde_json::Number::from(n));
}
if let Some(stream_options) = request.stream_options {
openai_request["stream_options"] = serde_json::to_value(stream_options)?;
}
macro_rules! insert_optional_param {
($field:ident) => {
if let Some(value) = request.$field {
openai_request[stringify!($field)] = serde_json::to_value(value)?;
}
};
}
insert_optional_param!(frequency_penalty);
insert_optional_param!(presence_penalty);
insert_optional_param!(logit_bias);
insert_optional_param!(logprobs);
insert_optional_param!(top_logprobs);
insert_optional_param!(reasoning_effort);
insert_optional_param!(store);
insert_optional_param!(metadata);
insert_optional_param!(service_tier);
insert_optional_param!(parallel_tool_calls);
insert_optional_param!(functions);
insert_optional_param!(function_call);
if let Some(obj) = openai_request.as_object_mut() {
for (key, value) in request.extra_params {
obj.entry(key).or_insert(value);
}
}
Ok(openai_request)
}
fn transform_chat_response(&self, response: Value) -> Result<ChatResponse, ProviderError> {
let response: crate::core::providers::openai::models::OpenAIChatResponse =
serde_json::from_value(response)?;
use crate::core::providers::openai::transformer::OpenAIResponseTransformer;
OpenAIResponseTransformer::transform(response).map_err(|e| ProviderError::Other {
provider: "openai",
message: e.to_string(),
})
}
pub fn get_model_info(
&self,
model_id: &str,
) -> Result<crate::core::types::model::ModelInfo, ProviderError> {
if let Some(catalog_model) = self.model_identity.as_ref().and_then(|binding| {
let identity = binding.identity();
(identity.capability_catalog_provider() == Some("openai"))
.then(|| identity.capability_catalog_model())
.flatten()
}) {
return self
.model_registry
.get_model_spec(catalog_model)
.map(|spec| spec.model_info.clone())
.ok_or_else(|| ProviderError::model_not_found("openai", catalog_model));
}
use crate::core::types::model::ModelInfo;
Ok(ModelInfo {
id: model_id.to_string(),
name: model_id.to_string(),
provider: "openai".to_string(),
max_context_length: 128000, max_output_length: Some(4096),
supports_streaming: true,
supports_tools: true,
supports_multimodal: false,
capabilities: vec![], input_cost_per_1k_tokens: None,
output_cost_per_1k_tokens: None,
currency: "USD".to_string(),
created_at: None,
updated_at: None,
metadata: std::collections::HashMap::new(),
})
}
pub fn model_supports_capability(
&self,
model_id: &str,
capability: &ProviderCapability,
) -> bool {
if let Some(model_spec) = self.model_registry.get_model_spec(model_id) {
model_spec.model_info.capabilities.contains(capability)
} else {
false
}
}
pub fn get_model_config(&self, model_id: &str) -> Option<&super::models::OpenAIModelConfig> {
self.model_registry
.get_model_spec(model_id)
.map(|spec| &spec.config)
}
}
impl LLMProvider for OpenAIProvider {
fn name(&self) -> &str {
&self.config.provider_name
}
fn error_provider_name(&self) -> &'static str {
"openai"
}
fn capabilities(&self) -> &'static [ProviderCapability] {
static CAPABILITIES: &[ProviderCapability] = &[
ProviderCapability::ChatCompletion,
ProviderCapability::ChatCompletionStream,
ProviderCapability::Embeddings,
ProviderCapability::ImageGeneration,
ProviderCapability::AudioTranscription,
ProviderCapability::AudioTranslation,
ProviderCapability::TextToSpeech,
ProviderCapability::Moderation,
ProviderCapability::ToolCalling,
ProviderCapability::FunctionCalling,
ProviderCapability::FineTuning,
ProviderCapability::ImageEdit,
ProviderCapability::ImageVariation,
ProviderCapability::RealtimeApi,
ProviderCapability::Responses,
];
CAPABILITIES
}
fn models(&self) -> &[ModelInfo] {
static MODELS: std::sync::LazyLock<Vec<ModelInfo>> =
std::sync::LazyLock::new(|| get_openai_registry().get_all_models());
&MODELS
}
async fn chat_completion(
&self,
request: ChatRequest,
_context: RequestContext,
) -> Result<ChatResponse, ProviderError> {
self.execute_chat_completion(request).await
}
async fn chat_completion_stream(
&self,
request: ChatRequest,
_context: RequestContext,
) -> Result<Pin<Box<dyn Stream<Item = Result<ChatChunk, ProviderError>> + Send>>, ProviderError>
{
self.execute_chat_completion_stream(request).await
}
async fn embeddings(
&self,
request: EmbeddingRequest,
_context: RequestContext,
) -> Result<EmbeddingResponse, ProviderError> {
self.embeddings(request).await
}
async fn image_generation(
&self,
request: ImageGenerationRequest,
_context: RequestContext,
) -> Result<ImageGenerationResponse, ProviderError> {
let response = self
.generate_images(
request.prompt,
Some(request.model.unwrap_or_else(|| "gpt-image-2".to_string())),
request.n,
request.size,
request.quality,
request.style,
)
.await?;
serde_json::from_value(response).map_err(|e| ProviderError::ResponseParsing {
provider: "openai",
message: e.to_string(),
})
}
async fn image_edit(
&self,
request: ImageEditRequest,
_context: RequestContext,
) -> Result<ImageGenerationResponse, ProviderError> {
super::execute_image_edit(
self.config.base.clone(),
&self.config.get_api_base(),
self.get_request_headers(),
request,
"openai",
)
.await
}
async fn audio_transcription(
&self,
request: TranscriptionRequest,
_context: RequestContext,
) -> Result<TranscriptionResponse, ProviderError> {
self.audio_transcription(request).await
}
async fn audio_translation(
&self,
request: TranslationRequest,
_context: RequestContext,
) -> Result<TranslationResponse, ProviderError> {
self.audio_translation(request).await
}
async fn text_to_speech(
&self,
request: SpeechRequest,
_context: RequestContext,
) -> Result<SpeechResponse, ProviderError> {
self.text_to_speech(request).await
}
async fn health_check(&self) -> HealthStatus {
let url = format!("{}/models?limit=1", self.config.get_api_base());
match self
.pool_manager
.execute_request(&url, HttpMethod::GET, self.get_request_headers(), None)
.await
{
Ok(response) if response.status().is_success() => HealthStatus::Healthy,
_ => HealthStatus::Unhealthy,
}
}
async fn calculate_cost(
&self,
model: &str,
input_tokens: u32,
output_tokens: u32,
) -> Result<f64, ProviderError> {
crate::core::providers::model_identity::calculate_managed_provider_cost(
&crate::core::providers::Provider::OpenAI(self.clone()),
model,
input_tokens,
output_tokens,
)
.unwrap_or_else(|| {
Err(ProviderError::configuration(
"pricing",
"OpenAI pricing authority dispatch is unavailable",
))
})
}
fn get_supported_openai_params(&self, model: &str) -> &'static [&'static str] {
let model = self
.model_identity
.as_ref()
.and_then(|binding| binding.identity().capability_catalog_model())
.unwrap_or_else(|| model.strip_prefix("openai/").unwrap_or(model));
if let Some(model_spec) = self.model_registry.get_model_spec(model) {
match model_spec.family {
super::models::OpenAIModelFamily::GPT6Astra
| super::models::OpenAIModelFamily::GPT61Sol => &[
"messages",
"model",
"max_completion_tokens",
"stream",
"response_format",
"reasoning_effort",
"store",
"metadata",
"service_tier",
"user",
],
super::models::OpenAIModelFamily::GPT5
| super::models::OpenAIModelFamily::GPT5Mini
| super::models::OpenAIModelFamily::GPT5Nano
| super::models::OpenAIModelFamily::GPT51
| super::models::OpenAIModelFamily::GPT51Thinking
| super::models::OpenAIModelFamily::GPT52
| super::models::OpenAIModelFamily::GPT52Codex
| super::models::OpenAIModelFamily::GPT54
| super::models::OpenAIModelFamily::GPT54Mini
| super::models::OpenAIModelFamily::GPT54Nano
| super::models::OpenAIModelFamily::GPT55
| super::models::OpenAIModelFamily::GPT56Sol
| super::models::OpenAIModelFamily::GPT56Terra
| super::models::OpenAIModelFamily::GPT56Luna
| super::models::OpenAIModelFamily::GPT56Cyber
| super::models::OpenAIModelFamily::GPT6Sol
| super::models::OpenAIModelFamily::GPT6Luna => &[
"messages",
"model",
"temperature",
"max_tokens",
"max_completion_tokens",
"top_p",
"frequency_penalty",
"presence_penalty",
"stop",
"stream",
"tools",
"tool_choice",
"parallel_tool_calls",
"response_format",
"user",
"seed",
"n",
"logit_bias",
"logprobs",
"top_logprobs",
"reasoning_effort",
"store",
"metadata",
"service_tier",
],
super::models::OpenAIModelFamily::GPT52Pro
| super::models::OpenAIModelFamily::GPT54Pro
| super::models::OpenAIModelFamily::GPT55Pro => &[
"messages",
"model",
"temperature",
"max_tokens",
"max_completion_tokens",
"top_p",
"frequency_penalty",
"presence_penalty",
"stop",
"tools",
"tool_choice",
"parallel_tool_calls",
"response_format",
"user",
"seed",
"n",
"logit_bias",
"logprobs",
"top_logprobs",
"reasoning_effort",
"store",
"metadata",
"service_tier",
],
super::models::OpenAIModelFamily::GPT4
| super::models::OpenAIModelFamily::GPT4Turbo
| super::models::OpenAIModelFamily::GPT4O
| super::models::OpenAIModelFamily::GPT4OMini
| super::models::OpenAIModelFamily::GPT41
| super::models::OpenAIModelFamily::GPT41Mini
| super::models::OpenAIModelFamily::GPT41Nano
| super::models::OpenAIModelFamily::GPT4OAudio
| super::models::OpenAIModelFamily::GPTAudio => &[
"messages",
"model",
"temperature",
"max_tokens",
"max_completion_tokens",
"top_p",
"frequency_penalty",
"presence_penalty",
"stop",
"stream",
"tools",
"tool_choice",
"parallel_tool_calls",
"response_format",
"user",
"seed",
"n",
"logit_bias",
"logprobs",
"top_logprobs",
"store",
"metadata",
"service_tier",
],
super::models::OpenAIModelFamily::GPT35 => &[
"messages",
"model",
"temperature",
"max_tokens",
"top_p",
"frequency_penalty",
"presence_penalty",
"stop",
"stream",
"tools",
"tool_choice",
"response_format",
"user",
"n",
"logit_bias",
],
super::models::OpenAIModelFamily::O1
| super::models::OpenAIModelFamily::O1Pro
| super::models::OpenAIModelFamily::O3
| super::models::OpenAIModelFamily::O3Pro
| super::models::OpenAIModelFamily::O3Mini
| super::models::OpenAIModelFamily::O4Mini => &[
"messages",
"model",
"max_completion_tokens",
"stream",
"user",
"reasoning_effort",
"store",
"metadata",
"service_tier",
],
_ => &[
"messages",
"model",
"temperature",
"max_tokens",
"top_p",
"stream",
"user",
],
}
} else {
&[
"messages",
"model",
"temperature",
"max_tokens",
"top_p",
"stream",
"user",
]
}
}
async fn map_openai_params(
&self,
params: HashMap<String, Value>,
_model: &str,
) -> Result<HashMap<String, Value>, ProviderError> {
Ok(params)
}
async fn transform_request(
&self,
request: ChatRequest,
_context: RequestContext,
) -> Result<Value, ProviderError> {
self.transform_chat_request(request)
}
async fn transform_response(
&self,
raw_response: &[u8],
_model: &str,
_request_id: &str,
) -> Result<ChatResponse, ProviderError> {
let response_value: Value = serde_json::from_slice(raw_response)?;
self.transform_chat_response(response_value)
}
fn get_error_mapper(&self) -> Box<dyn ErrorMapper<ProviderError>> {
Box::new(crate::core::traits::error_mapper::implementations::OpenAIErrorMapper)
}
}
pub use super::client_convenience::OpenAITask;
pub use super::error_mapper::OpenAIErrorMapper;