Skip to main content

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}