Skip to main content

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(&params.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}