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