openai_tools/batch/request.rs
1//! OpenAI Batch API Request Module
2//!
3//! This module provides the functionality to interact with the OpenAI Batch API.
4//! It allows you to create, list, retrieve, and cancel batch jobs.
5//!
6//! # Key Features
7//!
8//! - **Create Batch**: Submit a batch of requests for asynchronous processing
9//! - **Retrieve Batch**: Get the status and details of a batch job
10//! - **List Batches**: List all batch jobs
11//! - **Cancel Batch**: Cancel an in-progress batch job
12//!
13//! # Quick Start
14//!
15//! ```rust,no_run
16//! use openai_tools::batch::request::{Batches, CreateBatchRequest, BatchEndpoint, CompletionWindow};
17//!
18//! #[tokio::main]
19//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
20//! let batches = Batches::new()?;
21//!
22//! // List all batches
23//! let response = batches.list(None, None).await?;
24//! for batch in &response.data {
25//! println!("{}: {:?}", batch.id, batch.status);
26//! }
27//!
28//! Ok(())
29//! }
30//! ```
31
32use crate::batch::response::{BatchListResponse, BatchObject};
33use crate::common::auth::AuthProvider;
34use crate::common::client::create_http_client;
35use crate::common::errors::{OpenAIToolError, Result};
36use serde::Serialize;
37use std::collections::HashMap;
38use std::time::Duration;
39
40/// Default API path for Batches
41const BATCHES_PATH: &str = "batches";
42
43/// The API endpoint to use for batch requests.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
45#[non_exhaustive]
46pub enum BatchEndpoint {
47 /// Chat Completions API (/v1/chat/completions)
48 #[serde(rename = "/v1/chat/completions")]
49 ChatCompletions,
50 /// Embeddings API (/v1/embeddings)
51 #[serde(rename = "/v1/embeddings")]
52 Embeddings,
53 /// Completions API (/v1/completions)
54 #[serde(rename = "/v1/completions")]
55 Completions,
56 /// Responses API (/v1/responses)
57 #[serde(rename = "/v1/responses")]
58 Responses,
59 /// Moderations API (/v1/moderations)
60 #[serde(rename = "/v1/moderations")]
61 Moderations,
62}
63
64impl BatchEndpoint {
65 /// Returns the string representation of the endpoint.
66 pub fn as_str(&self) -> &'static str {
67 match self {
68 BatchEndpoint::ChatCompletions => "/v1/chat/completions",
69 BatchEndpoint::Embeddings => "/v1/embeddings",
70 BatchEndpoint::Completions => "/v1/completions",
71 BatchEndpoint::Responses => "/v1/responses",
72 BatchEndpoint::Moderations => "/v1/moderations",
73 }
74 }
75}
76
77/// The time window in which the batch must be completed.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
79#[non_exhaustive]
80pub enum CompletionWindow {
81 /// 24 hours
82 #[serde(rename = "24h")]
83 #[default]
84 Hours24,
85}
86
87impl CompletionWindow {
88 /// Returns the string representation of the completion window.
89 pub fn as_str(&self) -> &'static str {
90 match self {
91 CompletionWindow::Hours24 => "24h",
92 }
93 }
94}
95
96/// Request to create a new batch job.
97#[derive(Debug, Clone, Serialize)]
98pub struct CreateBatchRequest {
99 /// The ID of an uploaded file that contains requests for the batch.
100 /// The file must be uploaded with purpose "batch".
101 pub input_file_id: String,
102
103 /// The endpoint to use for all requests in the batch.
104 pub endpoint: BatchEndpoint,
105
106 /// The time window in which the batch must be completed.
107 pub completion_window: CompletionWindow,
108
109 /// Optional metadata to attach to the batch.
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub metadata: Option<HashMap<String, String>>,
112}
113
114impl CreateBatchRequest {
115 /// Creates a new batch request with the given input file ID and endpoint.
116 ///
117 /// # Arguments
118 ///
119 /// * `input_file_id` - The ID of the uploaded input file
120 /// * `endpoint` - The API endpoint to use for the batch
121 ///
122 /// # Example
123 ///
124 /// ```rust
125 /// use openai_tools::batch::request::{CreateBatchRequest, BatchEndpoint};
126 ///
127 /// let request = CreateBatchRequest::new("file-abc123", BatchEndpoint::ChatCompletions);
128 /// ```
129 pub fn new(input_file_id: impl Into<String>, endpoint: BatchEndpoint) -> Self {
130 Self { input_file_id: input_file_id.into(), endpoint, completion_window: CompletionWindow::default(), metadata: None }
131 }
132
133 /// Sets the metadata for the batch.
134 ///
135 /// # Arguments
136 ///
137 /// * `metadata` - Key-value pairs to attach to the batch
138 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
139 self.metadata = Some(metadata);
140 self
141 }
142}
143
144/// Client for interacting with the OpenAI Batch API.
145///
146/// This struct provides methods to create, list, retrieve, and cancel batch jobs.
147/// Use [`Batches::new()`] to create a new instance.
148///
149/// # Example
150///
151/// ```rust,no_run
152/// use openai_tools::batch::request::{Batches, CreateBatchRequest, BatchEndpoint};
153///
154/// #[tokio::main]
155/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
156/// let batches = Batches::new()?;
157///
158/// // Create a batch job
159/// let request = CreateBatchRequest::new("file-abc123", BatchEndpoint::ChatCompletions);
160/// let batch = batches.create(request).await?;
161/// println!("Created batch: {} ({:?})", batch.id, batch.status);
162///
163/// Ok(())
164/// }
165/// ```
166pub struct Batches {
167 /// Authentication provider (OpenAI or Azure)
168 auth: AuthProvider,
169 /// Optional request timeout duration
170 timeout: Option<Duration>,
171}
172
173impl Batches {
174 /// Creates a new Batches client for OpenAI API.
175 ///
176 /// Initializes the client by loading the OpenAI API key from
177 /// the environment variable `OPENAI_API_KEY`. Supports `.env` file loading
178 /// via dotenvy.
179 ///
180 /// # Returns
181 ///
182 /// * `Ok(Batches)` - A new Batches client ready for use
183 /// * `Err(OpenAIToolError)` - If the API key is not found in the environment
184 ///
185 /// # Example
186 ///
187 /// ```rust,no_run
188 /// use openai_tools::batch::request::Batches;
189 ///
190 /// let batches = Batches::new().expect("API key should be set");
191 /// ```
192 pub fn new() -> Result<Self> {
193 let auth = AuthProvider::openai_from_env()?;
194 Ok(Self { auth, timeout: None })
195 }
196
197 /// Creates a new Batches client with a custom authentication provider
198 pub fn with_auth(auth: AuthProvider) -> Self {
199 Self { auth, timeout: None }
200 }
201
202 /// Creates a new Batches client for Azure OpenAI API
203 pub fn azure() -> Result<Self> {
204 let auth = AuthProvider::azure_from_env()?;
205 Ok(Self { auth, timeout: None })
206 }
207
208 /// Creates a new Batches client by auto-detecting the provider
209 pub fn detect_provider() -> Result<Self> {
210 let auth = AuthProvider::from_env()?;
211 Ok(Self { auth, timeout: None })
212 }
213
214 /// Creates a new Batches client with URL-based provider detection
215 pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
216 let auth = AuthProvider::from_url_with_key(base_url, api_key);
217 Self { auth, timeout: None }
218 }
219
220 /// Creates a new Batches client from URL using environment variables
221 pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
222 let auth = AuthProvider::from_url(url)?;
223 Ok(Self { auth, timeout: None })
224 }
225
226 /// Returns the authentication provider
227 pub fn auth(&self) -> &AuthProvider {
228 &self.auth
229 }
230
231 /// Sets the request timeout duration.
232 ///
233 /// # Arguments
234 ///
235 /// * `timeout` - The maximum time to wait for a response
236 ///
237 /// # Returns
238 ///
239 /// A mutable reference to self for method chaining
240 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
241 self.timeout = Some(timeout);
242 self
243 }
244
245 /// Creates the HTTP client with default headers.
246 fn create_client(&self) -> Result<(request::Client, request::header::HeaderMap)> {
247 let client = create_http_client(self.timeout)?;
248 let mut headers = request::header::HeaderMap::new();
249 self.auth.apply_headers(&mut headers)?;
250 headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
251 headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
252 Ok((client, headers))
253 }
254
255 /// Creates a new batch job.
256 ///
257 /// # Arguments
258 ///
259 /// * `request` - The batch creation request
260 ///
261 /// # Returns
262 ///
263 /// * `Ok(BatchObject)` - The created batch object
264 /// * `Err(OpenAIToolError)` - If the request fails
265 ///
266 /// # Example
267 ///
268 /// ```rust,no_run
269 /// use openai_tools::batch::request::{Batches, CreateBatchRequest, BatchEndpoint};
270 /// use std::collections::HashMap;
271 ///
272 /// #[tokio::main]
273 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
274 /// let batches = Batches::new()?;
275 ///
276 /// let mut metadata = HashMap::new();
277 /// metadata.insert("customer_id".to_string(), "user_123".to_string());
278 ///
279 /// let request = CreateBatchRequest::new("file-abc123", BatchEndpoint::ChatCompletions)
280 /// .with_metadata(metadata);
281 ///
282 /// let batch = batches.create(request).await?;
283 /// println!("Created batch: {}", batch.id);
284 /// Ok(())
285 /// }
286 /// ```
287 pub async fn create(&self, request: CreateBatchRequest) -> Result<BatchObject> {
288 let (client, headers) = self.create_client()?;
289
290 let body = serde_json::to_string(&request).map_err(OpenAIToolError::SerdeJsonError)?;
291
292 let url = self.auth.endpoint(BATCHES_PATH);
293 let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
294
295 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
296
297 if cfg!(test) {
298 tracing::info!("Response content: {}", content);
299 }
300
301 serde_json::from_str::<BatchObject>(&content).map_err(OpenAIToolError::SerdeJsonError)
302 }
303
304 /// Retrieves details of a specific batch job.
305 ///
306 /// # Arguments
307 ///
308 /// * `batch_id` - The ID of the batch to retrieve
309 ///
310 /// # Returns
311 ///
312 /// * `Ok(BatchObject)` - The batch details
313 /// * `Err(OpenAIToolError)` - If the batch is not found or the request fails
314 ///
315 /// # Example
316 ///
317 /// ```rust,no_run
318 /// use openai_tools::batch::request::Batches;
319 ///
320 /// #[tokio::main]
321 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
322 /// let batches = Batches::new()?;
323 /// let batch = batches.retrieve("batch_abc123").await?;
324 ///
325 /// println!("Status: {:?}", batch.status);
326 /// if let Some(counts) = &batch.request_counts {
327 /// println!("Completed: {}/{}", counts.completed, counts.total);
328 /// }
329 /// Ok(())
330 /// }
331 /// ```
332 pub async fn retrieve(&self, batch_id: &str) -> Result<BatchObject> {
333 let (client, headers) = self.create_client()?;
334 let url = format!("{}/{}", self.auth.endpoint(BATCHES_PATH), batch_id);
335
336 let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
337
338 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
339
340 if cfg!(test) {
341 tracing::info!("Response content: {}", content);
342 }
343
344 serde_json::from_str::<BatchObject>(&content).map_err(OpenAIToolError::SerdeJsonError)
345 }
346
347 /// Cancels an in-progress batch job.
348 ///
349 /// The batch will transition to "cancelling" and eventually "cancelled".
350 ///
351 /// # Arguments
352 ///
353 /// * `batch_id` - The ID of the batch to cancel
354 ///
355 /// # Returns
356 ///
357 /// * `Ok(BatchObject)` - The updated batch object
358 /// * `Err(OpenAIToolError)` - If the batch cannot be cancelled or the request fails
359 ///
360 /// # Example
361 ///
362 /// ```rust,no_run
363 /// use openai_tools::batch::request::Batches;
364 ///
365 /// #[tokio::main]
366 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
367 /// let batches = Batches::new()?;
368 /// let batch = batches.cancel("batch_abc123").await?;
369 ///
370 /// println!("Batch status: {:?}", batch.status);
371 /// Ok(())
372 /// }
373 /// ```
374 pub async fn cancel(&self, batch_id: &str) -> Result<BatchObject> {
375 let (client, headers) = self.create_client()?;
376 let url = format!("{}/{}/cancel", self.auth.endpoint(BATCHES_PATH), batch_id);
377
378 let response = client.post(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
379
380 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
381
382 if cfg!(test) {
383 tracing::info!("Response content: {}", content);
384 }
385
386 serde_json::from_str::<BatchObject>(&content).map_err(OpenAIToolError::SerdeJsonError)
387 }
388
389 /// Lists all batch jobs.
390 ///
391 /// Supports pagination through `limit` and `after` parameters.
392 ///
393 /// # Arguments
394 ///
395 /// * `limit` - Maximum number of batches to return (default: 20)
396 /// * `after` - Cursor for pagination (batch ID to start after)
397 ///
398 /// # Returns
399 ///
400 /// * `Ok(BatchListResponse)` - The list of batch jobs
401 /// * `Err(OpenAIToolError)` - If the request fails
402 ///
403 /// # Example
404 ///
405 /// ```rust,no_run
406 /// use openai_tools::batch::request::Batches;
407 ///
408 /// #[tokio::main]
409 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
410 /// let batches = Batches::new()?;
411 ///
412 /// // Get first page
413 /// let response = batches.list(Some(10), None).await?;
414 /// for batch in &response.data {
415 /// println!("{}: {:?}", batch.id, batch.status);
416 /// }
417 ///
418 /// // Get next page if available
419 /// if response.has_more {
420 /// if let Some(last_id) = &response.last_id {
421 /// let next_page = batches.list(Some(10), Some(last_id)).await?;
422 /// // ...
423 /// }
424 /// }
425 ///
426 /// Ok(())
427 /// }
428 /// ```
429 pub async fn list(&self, limit: Option<u32>, after: Option<&str>) -> Result<BatchListResponse> {
430 let (client, headers) = self.create_client()?;
431
432 let mut url = self.auth.endpoint(BATCHES_PATH);
433 let mut params = Vec::new();
434
435 if let Some(l) = limit {
436 params.push(format!("limit={}", l));
437 }
438 if let Some(a) = after {
439 params.push(format!("after={}", a));
440 }
441
442 if !params.is_empty() {
443 url.push('?');
444 url.push_str(¶ms.join("&"));
445 }
446
447 let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
448
449 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
450
451 if cfg!(test) {
452 tracing::info!("Response content: {}", content);
453 }
454
455 serde_json::from_str::<BatchListResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
456 }
457}