locode_provider/openai/responses/
mod.rs1pub mod build;
11pub mod parse;
12mod stream;
13pub mod wire;
14
15use std::sync::Arc;
16
17use async_trait::async_trait;
18
19pub use build::{build_request, freeform_fallback_parameters, freeform_tool_names};
20pub use parse::response_to_completion;
21
22use crate::completion::{Completion, CompletionDelta};
23use crate::http::{self, HttpFailure, RetryPolicy};
24use crate::openai::{OpenAiModelConfig, classify};
25use crate::provider::{Provider, ProviderError};
26use crate::repair::repair_pairing;
27use crate::request::ConversationRequest;
28
29pub struct OpenAiResponsesProvider {
31 http: reqwest::Client,
32 config: OpenAiModelConfig,
33 retry: RetryPolicy,
34}
35
36impl OpenAiResponsesProvider {
37 pub fn new(config: OpenAiModelConfig) -> Result<Self, ProviderError> {
42 Ok(Self {
43 http: http::build_http_client()?,
44 config,
45 retry: RetryPolicy::default(),
46 })
47 }
48
49 pub fn from_env() -> Result<Self, ProviderError> {
56 Self::new(OpenAiModelConfig::from_env()?)
57 }
58
59 #[must_use]
61 pub fn with_retry_policy(mut self, retry: RetryPolicy) -> Self {
62 self.retry = retry;
63 self
64 }
65
66 #[must_use]
68 pub fn config(&self) -> &OpenAiModelConfig {
69 &self.config
70 }
71
72 pub fn config_mut(&mut self) -> &mut OpenAiModelConfig {
75 &mut self.config
76 }
77
78 async fn send_once(
79 &self,
80 request: &wire::ResponsesRequest,
81 freeform_names: &std::collections::HashSet<String>,
82 ) -> Result<Completion, HttpFailure> {
83 let url = format!("{}/v1/responses", self.config.base_url);
84 let mut builder = self
85 .http
86 .post(&url)
87 .bearer_auth(&self.config.bearer)
88 .json(request);
89 for (name, value) in &self.config.extra_headers {
90 builder = builder.header(name, value);
91 }
92 let response = builder
93 .send()
94 .await
95 .map_err(|e| HttpFailure::transport(e.to_string()))?;
96
97 let status = response.status();
98 let retry_after = response
99 .headers()
100 .get(reqwest::header::RETRY_AFTER)
101 .and_then(|v| v.to_str().ok())
102 .and_then(http::parse_retry_after);
103
104 if status.is_success() {
105 let parsed: wire::ResponsesResponse = response
106 .json()
107 .await
108 .map_err(|e| HttpFailure::decode(format!("response body: {e}")))?;
109 return response_to_completion(parsed, freeform_names).map_err(|error| HttpFailure {
110 force_terminal: false,
113 retry_after,
114 error,
115 });
116 }
117
118 let text = response.text().await.unwrap_or_default();
119 let body: crate::openai::OpenAiErrorBody = match serde_json::from_str(&text) {
120 Ok(body) => body,
121 Err(_) => crate::openai::OpenAiErrorBody {
122 error: crate::openai::OpenAiErrorDetail {
123 code: None,
124 r#type: None,
125 message: text,
126 },
127 },
128 };
129 Err(classify(status.as_u16(), retry_after, &body))
130 }
131}
132
133#[async_trait]
134impl Provider for OpenAiResponsesProvider {
135 #[allow(clippy::unnecessary_literal_bound)] fn api_schema(&self) -> &str {
137 "openai-responses"
138 }
139
140 async fn complete(&self, request: &ConversationRequest) -> Result<Completion, ProviderError> {
141 let mut repaired = request.clone();
144 let _ = repair_pairing(&mut repaired.messages);
145
146 let wire_request = build_request(&repaired, &self.config);
148 let freeform_names = freeform_tool_names(&repaired.tools);
149
150 let freeform_ref = &freeform_names;
152 http::run_with_retry(&self.retry, |_attempt| {
153 self.send_once(&wire_request, freeform_ref)
154 })
155 .await
156 }
157
158 async fn stream(
159 &self,
160 request: &ConversationRequest,
161 on_delta: &mut (dyn FnMut(CompletionDelta) + Send),
162 ) -> Result<Completion, ProviderError> {
163 let mut repaired = request.clone();
166 let _ = repair_pairing(&mut repaired.messages);
167 let mut wire_request = build_request(&repaired, &self.config);
168 wire_request.stream = true;
169 let freeform_names = freeform_tool_names(&repaired.tools);
170
171 stream::send_once_streaming(
173 &self.http,
174 &self.config,
175 &wire_request,
176 &freeform_names,
177 on_delta,
178 )
179 .await
180 .map_err(|f| f.error)
181 }
182}
183
184#[must_use]
186pub fn into_provider(provider: OpenAiResponsesProvider) -> Arc<dyn Provider> {
187 Arc::new(provider)
188}