pub mod build;
pub mod parse;
pub mod wire;
use std::sync::Arc;
use async_trait::async_trait;
pub use build::{build_request, freeform_fallback_parameters, freeform_tool_names};
pub use parse::response_to_completion;
use crate::completion::Completion;
use crate::http::{self, HttpFailure, RetryPolicy};
use crate::openai::{OpenAiModelConfig, classify};
use crate::provider::{Provider, ProviderError};
use crate::repair::repair_pairing;
use crate::request::ConversationRequest;
pub struct OpenAiResponsesProvider {
http: reqwest::Client,
config: OpenAiModelConfig,
retry: RetryPolicy,
}
impl OpenAiResponsesProvider {
pub fn new(config: OpenAiModelConfig) -> Result<Self, ProviderError> {
Ok(Self {
http: http::build_http_client()?,
config,
retry: RetryPolicy::default(),
})
}
pub fn from_env() -> Result<Self, ProviderError> {
Self::new(OpenAiModelConfig::from_env()?)
}
#[must_use]
pub fn with_retry_policy(mut self, retry: RetryPolicy) -> Self {
self.retry = retry;
self
}
#[must_use]
pub fn config(&self) -> &OpenAiModelConfig {
&self.config
}
pub fn config_mut(&mut self) -> &mut OpenAiModelConfig {
&mut self.config
}
async fn send_once(
&self,
request: &wire::ResponsesRequest,
freeform_names: &std::collections::HashSet<String>,
) -> Result<Completion, HttpFailure> {
let url = format!("{}/v1/responses", self.config.base_url);
let mut builder = self
.http
.post(&url)
.bearer_auth(&self.config.bearer)
.json(request);
for (name, value) in &self.config.extra_headers {
builder = builder.header(name, value);
}
let response = builder
.send()
.await
.map_err(|e| HttpFailure::transport(e.to_string()))?;
let status = response.status();
let retry_after = response
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|v| v.to_str().ok())
.and_then(http::parse_retry_after);
if status.is_success() {
let parsed: wire::ResponsesResponse = response
.json()
.await
.map_err(|e| HttpFailure::decode(format!("response body: {e}")))?;
return response_to_completion(parsed, freeform_names).map_err(|error| HttpFailure {
force_terminal: false,
retry_after,
error,
});
}
let text = response.text().await.unwrap_or_default();
let body: crate::openai::OpenAiErrorBody = match serde_json::from_str(&text) {
Ok(body) => body,
Err(_) => crate::openai::OpenAiErrorBody {
error: crate::openai::OpenAiErrorDetail {
code: None,
r#type: None,
message: text,
},
},
};
Err(classify(status.as_u16(), retry_after, &body))
}
}
#[async_trait]
impl Provider for OpenAiResponsesProvider {
#[allow(clippy::unnecessary_literal_bound)] fn api_schema(&self) -> &str {
"openai-responses"
}
async fn complete(&self, request: &ConversationRequest) -> Result<Completion, ProviderError> {
let mut repaired = request.clone();
let _ = repair_pairing(&mut repaired.messages);
let wire_request = build_request(&repaired, &self.config);
let freeform_names = freeform_tool_names(&repaired.tools);
let freeform_ref = &freeform_names;
http::run_with_retry(&self.retry, |_attempt| {
self.send_once(&wire_request, freeform_ref)
})
.await
}
}
#[must_use]
pub fn into_provider(provider: OpenAiResponsesProvider) -> Arc<dyn Provider> {
Arc::new(provider)
}