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