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}