openai_tools/files/request.rs
1//! OpenAI Files API Request Module
2//!
3//! This module provides the functionality to interact with the OpenAI Files API.
4//! It allows you to upload, list, retrieve, delete files, and get file content.
5//!
6//! # Key Features
7//!
8//! - **Upload Files**: Upload files for fine-tuning, batch processing, assistants, etc.
9//! - **List Files**: Retrieve all uploaded files
10//! - **Retrieve File**: Get details of a specific file
11//! - **Delete File**: Remove an uploaded file
12//! - **Get Content**: Retrieve the content of a file
13//!
14//! # Quick Start
15//!
16//! ```rust,no_run
17//! use openai_tools::files::request::{Files, FilePurpose};
18//!
19//! #[tokio::main]
20//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
21//! let files = Files::new()?;
22//!
23//! // List all files
24//! let response = files.list(None).await?;
25//! for file in &response.data {
26//! println!("{}: {} bytes", file.filename, file.bytes);
27//! }
28//!
29//! Ok(())
30//! }
31//! ```
32
33use crate::common::auth::{AuthProvider, OpenAIAuth};
34use crate::common::client::create_http_client;
35use crate::common::errors::{ErrorResponse, OpenAIToolError, Result};
36use crate::files::response::{DeleteResponse, File, FileListResponse};
37use request::multipart::{Form, Part};
38use serde::{Deserialize, Serialize};
39use std::path::Path;
40use std::time::Duration;
41
42/// Default API path for Files
43const FILES_PATH: &str = "files";
44
45/// The intended purpose of the uploaded file.
46///
47/// Different purposes have different processing requirements and usage patterns.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50#[non_exhaustive]
51pub enum FilePurpose {
52 /// For use with Assistants and Message files
53 Assistants,
54 /// For files generated by Assistants
55 AssistantsOutput,
56 /// For use with Batch API
57 Batch,
58 /// For files generated by Batch API
59 BatchOutput,
60 /// For use with Fine-tuning
61 FineTune,
62 /// For files generated by Fine-tuning
63 FineTuneResults,
64 /// For use with Vision features
65 Vision,
66 /// For user-uploaded data
67 UserData,
68}
69
70impl FilePurpose {
71 /// Returns the string representation of the purpose.
72 pub fn as_str(&self) -> &'static str {
73 match self {
74 FilePurpose::Assistants => "assistants",
75 FilePurpose::AssistantsOutput => "assistants_output",
76 FilePurpose::Batch => "batch",
77 FilePurpose::BatchOutput => "batch_output",
78 FilePurpose::FineTune => "fine-tune",
79 FilePurpose::FineTuneResults => "fine-tune-results",
80 FilePurpose::Vision => "vision",
81 FilePurpose::UserData => "user_data",
82 }
83 }
84}
85
86impl std::fmt::Display for FilePurpose {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(f, "{}", self.as_str())
89 }
90}
91
92/// Client for interacting with the OpenAI Files API.
93///
94/// This struct provides methods to upload, list, retrieve, delete files,
95/// and get file content. Use [`Files::new()`] to create a new instance.
96///
97/// # Providers
98///
99/// The client supports two providers:
100/// - **OpenAI**: Standard OpenAI API (default)
101/// - **Azure**: Azure OpenAI Service
102///
103/// # Example
104///
105/// ```rust,no_run
106/// use openai_tools::files::request::{Files, FilePurpose};
107///
108/// #[tokio::main]
109/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
110/// let files = Files::new()?;
111///
112/// // Upload a file for fine-tuning
113/// let file = files.upload_path("training_data.jsonl", FilePurpose::FineTune).await?;
114/// println!("Uploaded: {} ({})", file.filename, file.id);
115///
116/// Ok(())
117/// }
118/// ```
119pub struct Files {
120 /// Authentication provider (OpenAI or Azure)
121 auth: AuthProvider,
122 /// Optional request timeout duration
123 timeout: Option<Duration>,
124}
125
126impl Files {
127 /// Creates a new Files client for OpenAI API.
128 ///
129 /// Initializes the client by loading the OpenAI API key from
130 /// the environment variable `OPENAI_API_KEY`. Supports `.env` file loading
131 /// via dotenvy.
132 ///
133 /// # Returns
134 ///
135 /// * `Ok(Files)` - A new Files client ready for use
136 /// * `Err(OpenAIToolError)` - If the API key is not found in the environment
137 ///
138 /// # Example
139 ///
140 /// ```rust,no_run
141 /// use openai_tools::files::request::Files;
142 ///
143 /// let files = Files::new().expect("API key should be set");
144 /// ```
145 pub fn new() -> Result<Self> {
146 let auth = AuthProvider::openai_from_env()?;
147 Ok(Self { auth, timeout: None })
148 }
149
150 /// Creates a new Files client with a custom authentication provider
151 pub fn with_auth(auth: AuthProvider) -> Self {
152 Self { auth, timeout: None }
153 }
154
155 /// Creates a new Files client for Azure OpenAI API
156 pub fn azure() -> Result<Self> {
157 let auth = AuthProvider::azure_from_env()?;
158 Ok(Self { auth, timeout: None })
159 }
160
161 /// Creates a new Files client by auto-detecting the provider
162 pub fn detect_provider() -> Result<Self> {
163 let auth = AuthProvider::from_env()?;
164 Ok(Self { auth, timeout: None })
165 }
166
167 /// Creates a new Files client with URL-based provider detection
168 pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
169 let auth = AuthProvider::from_url_with_key(base_url, api_key);
170 Self { auth, timeout: None }
171 }
172
173 /// Creates a new Files client from URL using environment variables
174 pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
175 let auth = AuthProvider::from_url(url)?;
176 Ok(Self { auth, timeout: None })
177 }
178
179 /// Returns the authentication provider
180 pub fn auth(&self) -> &AuthProvider {
181 &self.auth
182 }
183
184 /// Sets a custom API endpoint URL (OpenAI only)
185 ///
186 /// Use this to point to alternative OpenAI-compatible APIs.
187 ///
188 /// # Arguments
189 ///
190 /// * `url` - The base URL (e.g., "https://my-proxy.example.com/v1")
191 ///
192 /// # Returns
193 ///
194 /// A mutable reference to self for method chaining
195 pub fn base_url<T: AsRef<str>>(&mut self, url: T) -> &mut Self {
196 if let AuthProvider::OpenAI(ref openai_auth) = self.auth {
197 let new_auth = OpenAIAuth::new(openai_auth.api_key()).with_base_url(url.as_ref());
198 self.auth = AuthProvider::OpenAI(new_auth);
199 } else {
200 tracing::warn!("base_url() is only supported for OpenAI provider. Use azure() or with_auth() for Azure.");
201 }
202 self
203 }
204
205 /// Sets the request timeout duration.
206 ///
207 /// # Arguments
208 ///
209 /// * `timeout` - The maximum time to wait for a response
210 ///
211 /// # Returns
212 ///
213 /// A mutable reference to self for method chaining
214 ///
215 /// # Example
216 ///
217 /// ```rust,no_run
218 /// use std::time::Duration;
219 /// use openai_tools::files::request::Files;
220 ///
221 /// let mut files = Files::new().unwrap();
222 /// files.timeout(Duration::from_secs(120)); // Longer timeout for file uploads
223 /// ```
224 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
225 self.timeout = Some(timeout);
226 self
227 }
228
229 /// Creates the HTTP client with default headers.
230 fn create_client(&self) -> Result<(request::Client, request::header::HeaderMap)> {
231 let client = create_http_client(self.timeout)?;
232 let mut headers = request::header::HeaderMap::new();
233 self.auth.apply_headers(&mut headers)?;
234 headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
235 Ok((client, headers))
236 }
237
238 /// Uploads a file from a file path.
239 ///
240 /// The file will be uploaded with the specified purpose.
241 /// Individual files can be up to 512 MB, and the total size of all files
242 /// uploaded by one organization can be up to 100 GB.
243 ///
244 /// # Arguments
245 ///
246 /// * `file_path` - Path to the file to upload
247 /// * `purpose` - The intended purpose of the uploaded file
248 ///
249 /// # Returns
250 ///
251 /// * `Ok(File)` - The uploaded file object
252 /// * `Err(OpenAIToolError)` - If the file cannot be read or the upload fails
253 ///
254 /// # Example
255 ///
256 /// ```rust,no_run
257 /// use openai_tools::files::request::{Files, FilePurpose};
258 ///
259 /// #[tokio::main]
260 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
261 /// let files = Files::new()?;
262 /// let file = files.upload_path("data.jsonl", FilePurpose::FineTune).await?;
263 /// println!("Uploaded: {}", file.id);
264 /// Ok(())
265 /// }
266 /// ```
267 pub async fn upload_path(&self, file_path: &str, purpose: FilePurpose) -> Result<File> {
268 let path = Path::new(file_path);
269 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file").to_string();
270
271 let content = tokio::fs::read(file_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read file: {}", e)))?;
272
273 self.upload_bytes(&content, &filename, purpose).await
274 }
275
276 /// Uploads a file from bytes.
277 ///
278 /// The file will be uploaded with the specified filename and purpose.
279 ///
280 /// # Arguments
281 ///
282 /// * `content` - The file content as bytes
283 /// * `filename` - The name to give the file
284 /// * `purpose` - The intended purpose of the uploaded file
285 ///
286 /// # Returns
287 ///
288 /// * `Ok(File)` - The uploaded file object
289 /// * `Err(OpenAIToolError)` - If the upload fails
290 ///
291 /// # Example
292 ///
293 /// ```rust,no_run
294 /// use openai_tools::files::request::{Files, FilePurpose};
295 ///
296 /// #[tokio::main]
297 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
298 /// let files = Files::new()?;
299 ///
300 /// let content = b"{\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}";
301 /// let file = files.upload_bytes(content, "training.jsonl", FilePurpose::FineTune).await?;
302 ///
303 /// println!("Uploaded: {}", file.id);
304 /// Ok(())
305 /// }
306 /// ```
307 pub async fn upload_bytes(&self, content: &[u8], filename: &str, purpose: FilePurpose) -> Result<File> {
308 let (client, headers) = self.create_client()?;
309
310 let file_part = Part::bytes(content.to_vec())
311 .file_name(filename.to_string())
312 .mime_str("application/octet-stream")
313 .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
314
315 let form = Form::new().part("file", file_part).text("purpose", purpose.as_str().to_string());
316
317 let endpoint = self.auth.endpoint(FILES_PATH);
318 let response = client.post(&endpoint).headers(headers).multipart(form).send().await.map_err(OpenAIToolError::RequestError)?;
319
320 let status = response.status();
321 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
322
323 if cfg!(test) {
324 tracing::info!("Response content: {}", content);
325 }
326
327 if !status.is_success() {
328 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
329 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
330 }
331 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
332 }
333
334 serde_json::from_str::<File>(&content).map_err(OpenAIToolError::SerdeJsonError)
335 }
336
337 /// Lists all files that belong to the user's organization.
338 ///
339 /// Optionally filter by purpose.
340 ///
341 /// # Arguments
342 ///
343 /// * `purpose` - Optional filter by file purpose
344 ///
345 /// # Returns
346 ///
347 /// * `Ok(FileListResponse)` - The list of files
348 /// * `Err(OpenAIToolError)` - If the request fails
349 ///
350 /// # Example
351 ///
352 /// ```rust,no_run
353 /// use openai_tools::files::request::{Files, FilePurpose};
354 ///
355 /// #[tokio::main]
356 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
357 /// let files = Files::new()?;
358 ///
359 /// // List all files
360 /// let all_files = files.list(None).await?;
361 /// println!("Total files: {}", all_files.data.len());
362 ///
363 /// // List only fine-tuning files
364 /// let ft_files = files.list(Some(FilePurpose::FineTune)).await?;
365 /// println!("Fine-tuning files: {}", ft_files.data.len());
366 ///
367 /// Ok(())
368 /// }
369 /// ```
370 pub async fn list(&self, purpose: Option<FilePurpose>) -> Result<FileListResponse> {
371 let (client, headers) = self.create_client()?;
372
373 let endpoint = self.auth.endpoint(FILES_PATH);
374 let url = match purpose {
375 Some(p) => format!("{}?purpose={}", endpoint, p.as_str()),
376 None => endpoint,
377 };
378
379 let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
380
381 let status = response.status();
382 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
383
384 if cfg!(test) {
385 tracing::info!("Response content: {}", content);
386 }
387
388 if !status.is_success() {
389 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
390 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
391 }
392 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
393 }
394
395 serde_json::from_str::<FileListResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
396 }
397
398 /// Retrieves details of a specific file.
399 ///
400 /// # Arguments
401 ///
402 /// * `file_id` - The ID of the file to retrieve
403 ///
404 /// # Returns
405 ///
406 /// * `Ok(File)` - The file details
407 /// * `Err(OpenAIToolError)` - If the file is not found or the request fails
408 ///
409 /// # Example
410 ///
411 /// ```rust,no_run
412 /// use openai_tools::files::request::Files;
413 ///
414 /// #[tokio::main]
415 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
416 /// let files = Files::new()?;
417 /// let file = files.retrieve("file-abc123").await?;
418 ///
419 /// println!("File: {}", file.filename);
420 /// println!("Size: {} bytes", file.bytes);
421 /// println!("Purpose: {}", file.purpose);
422 /// Ok(())
423 /// }
424 /// ```
425 pub async fn retrieve(&self, file_id: &str) -> Result<File> {
426 let (client, headers) = self.create_client()?;
427 let url = format!("{}/{}", self.auth.endpoint(FILES_PATH), file_id);
428
429 let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
430
431 let status = response.status();
432 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
433
434 if cfg!(test) {
435 tracing::info!("Response content: {}", content);
436 }
437
438 if !status.is_success() {
439 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
440 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
441 }
442 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
443 }
444
445 serde_json::from_str::<File>(&content).map_err(OpenAIToolError::SerdeJsonError)
446 }
447
448 /// Deletes a file.
449 ///
450 /// # Arguments
451 ///
452 /// * `file_id` - The ID of the file to delete
453 ///
454 /// # Returns
455 ///
456 /// * `Ok(DeleteResponse)` - Confirmation of deletion
457 /// * `Err(OpenAIToolError)` - If the file cannot be deleted or the request fails
458 ///
459 /// # Example
460 ///
461 /// ```rust,no_run
462 /// use openai_tools::files::request::Files;
463 ///
464 /// #[tokio::main]
465 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
466 /// let files = Files::new()?;
467 /// let result = files.delete("file-abc123").await?;
468 ///
469 /// if result.deleted {
470 /// println!("File {} was deleted", result.id);
471 /// }
472 /// Ok(())
473 /// }
474 /// ```
475 pub async fn delete(&self, file_id: &str) -> Result<DeleteResponse> {
476 let (client, headers) = self.create_client()?;
477 let url = format!("{}/{}", self.auth.endpoint(FILES_PATH), file_id);
478
479 let response = client.delete(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
480
481 let status = response.status();
482 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
483
484 if cfg!(test) {
485 tracing::info!("Response content: {}", content);
486 }
487
488 if !status.is_success() {
489 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
490 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
491 }
492 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
493 }
494
495 serde_json::from_str::<DeleteResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
496 }
497
498 /// Retrieves the content of a file.
499 ///
500 /// # Arguments
501 ///
502 /// * `file_id` - The ID of the file to retrieve content from
503 ///
504 /// # Returns
505 ///
506 /// * `Ok(Vec<u8>)` - The file content as bytes
507 /// * `Err(OpenAIToolError)` - If the file cannot be retrieved or the request fails
508 ///
509 /// # Example
510 ///
511 /// ```rust,no_run
512 /// use openai_tools::files::request::Files;
513 ///
514 /// #[tokio::main]
515 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
516 /// let files = Files::new()?;
517 /// let content = files.content("file-abc123").await?;
518 ///
519 /// // Convert to string if it's text content
520 /// let text = String::from_utf8(content)?;
521 /// println!("Content: {}", text);
522 /// Ok(())
523 /// }
524 /// ```
525 pub async fn content(&self, file_id: &str) -> Result<Vec<u8>> {
526 let (client, headers) = self.create_client()?;
527 let url = format!("{}/{}/content", self.auth.endpoint(FILES_PATH), file_id);
528
529 let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
530
531 let bytes = response.bytes().await.map_err(OpenAIToolError::RequestError)?;
532
533 Ok(bytes.to_vec())
534 }
535}