openai_interface/files/create/
request.rs

1//! Create a `CreateFileRequest` object for uploading a file.
2
3use serde::Serialize;
4use std::future::Future;
5use std::path::PathBuf;
6
7use crate::errors::OapiError;
8use crate::rest::post::{Post, PostNoStream};
9
10/// Upload a file that can be used across various endpoints.
11///
12/// Individual files can be up to 512 MB, and the size of all files uploaded by one
13/// organization can be up to 1 TB.
14///
15/// The Assistants API supports files up to 2 million tokens and of specific file
16/// types. See the
17/// [OpenAI Assistants Tools guide](https://platform.openai.com/docs/assistants/tools) for
18/// details.
19///
20/// The Fine-tuning API only supports `.jsonl` files. The input also has certain
21/// required formats for fine-tuning
22/// [chat](https://platform.openai.com/docs/api-reference/fine-tuning/chat-input) or
23/// [completions](https://platform.openai.com/docs/api-reference/fine-tuning/completions-input)
24/// models.
25///
26/// The Batch API only supports `.jsonl` files up to 200 MB in size. The input also
27/// has a specific required
28/// [format](https://platform.openai.com/docs/api-reference/batch/request-input).
29///
30/// Please [contact OpenAI](https://help.openai.com/) if you need to increase these
31/// storage limits.
32#[derive(Debug, Serialize, Clone, Default)]
33pub struct CreateFileRequest {
34    /// The File object (not file name) to be uploaded.
35    #[serde(skip_serializing)]
36    pub file: PathBuf,
37    /// The intended purpose of the uploaded file. One of: - `assistants`: Used in the
38    /// Assistants API - `batch`: Used in the Batch API - `fine-tune`: Used for
39    /// fine-tuning - `vision`: Images used for vision fine-tuning - `user_data`:
40    /// Flexible file type for any purpose - `evals`: Used for eval data sets
41    pub purpose: FilePurpose,
42    /// The expiration policy for a file. By default, files with `purpose=batch` expire
43    /// after 30 days and all other files are persisted until they are manually deleted.
44    ///
45    /// This parameter is not supported by Qwen is not tested.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub expires_after: Option<ExpiresAfter>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub extra_body: Option<serde_json::Map<String, serde_json::Value>>,
50}
51
52#[derive(Debug, Serialize, Clone, Default)]
53pub enum FilePurpose {
54    #[serde(rename = "assistant")]
55    Assistant,
56    #[serde(rename = "batch")]
57    #[default]
58    Batch,
59    #[serde(rename = "fine-tune")]
60    FineTune,
61    #[serde(rename = "vision")]
62    Vision,
63    #[serde(rename = "user_data")]
64    UserData,
65    #[serde(rename = "evals")]
66    Evals,
67    #[serde(untagged)]
68    Other(String),
69}
70
71// #[derive(Debug, Serialize, Clone)]
72// pub enum FileTypes {
73//     /// file (or bytes)
74//     FileContent(Vec<u8>),
75//     /// (filename, file (or bytes))
76//     FileNameAndContent(String, Vec<u8>),
77//     /// (filename, file (or bytes), content_type)
78//     FileNameAndContentAndType(String, Vec<u8>, String),
79//     /// (filename, file (or bytes), content_type, headers)
80//     FileNameAndContentAndTypeAndHeaders(String, Vec<u8>, String, HashMap<String, String>),
81// }
82
83#[derive(Debug, Serialize, Clone)]
84#[serde(tag = "anchor", rename = "snake_case")]
85/// The expiration policy for a file.
86///
87/// By default, files with `purpose=batch` expire after 30 days and all other files
88/// are persisted until they are manually deleted.
89pub enum ExpiresAfter {
90    /// Anchor timestamp after which the expiration policy applies.
91    /// Supported anchors: `created_at`.
92    CreatedAt {
93        /// The number of seconds after the anchor time that the file will expire.
94        /// Must be between 3600 (1 hour) and 2592000 (30 days).
95        seconds: usize,
96    },
97}
98
99impl Post for CreateFileRequest {
100    #[inline]
101    fn is_streaming(&self) -> bool {
102        false
103    }
104}
105
106impl PostNoStream for CreateFileRequest {
107    type Response = crate::files::FileObject;
108
109    /// Sends a file upload POST request using multipart/form-data format.
110    /// This implementation handles the actual file upload with proper file handling.
111    fn get_response_string(
112        &self,
113        url: &str,
114        key: &str,
115    ) -> impl Future<Output = Result<String, OapiError>> + Send + Sync {
116        async move {
117            if self.is_streaming() {
118                return Err(OapiError::NonStreamingViolation);
119            }
120
121            let client = reqwest::Client::new();
122
123            // Check if file exists
124            if !self.file.exists() {
125                return Err(OapiError::FileNotFoundError(self.file.clone()));
126            }
127
128            // Read file content
129            let file_content = tokio::fs::read(&self.file).await.map_err(|e| {
130                OapiError::ResponseError(format!(
131                    "Failed to read file {}: {}",
132                    self.file.display(),
133                    e
134                ))
135            })?;
136
137            // Get file name from path
138            let file_name = self
139                .file
140                .file_name()
141                .and_then(|name| name.to_str())
142                .ok_or_else(|| OapiError::ResponseError("Invalid file name".to_string()))?
143                .to_string();
144
145            // Create multipart form with file and purpose
146            let file_part =
147                reqwest::multipart::Part::bytes(file_content).file_name(file_name.clone());
148
149            let mut form = reqwest::multipart::Form::new().part("file", file_part);
150
151            // Add purpose field
152            let purpose_str = serde_json::to_string(&self.purpose).map_err(|e| {
153                OapiError::ResponseError(format!("Failed to serialize purpose: {}", e))
154            })?;
155            let trimmed_purpose = purpose_str.trim_matches('"').to_string();
156            form = form.text("purpose", trimmed_purpose);
157
158            // Add expires_after if present
159            if let Some(expires_after) = &self.expires_after {
160                let expires_str = serde_json::to_string(expires_after).map_err(|e| {
161                    OapiError::ResponseError(format!("Failed to serialize expires_after: {}", e))
162                })?;
163                form = form.text("expires_after", expires_str);
164            }
165
166            let response = client
167                .post(url)
168                .headers({
169                    let mut headers = reqwest::header::HeaderMap::new();
170                    headers.insert("Accept", "application/json".parse().unwrap());
171                    headers
172                })
173                .bearer_auth(key)
174                .multipart(form)
175                .send()
176                .await
177                .map_err(|e| OapiError::SendError(format!("Failed to send request: {:#?}", e)))?;
178
179            if response.status() != reqwest::StatusCode::OK {
180                return Err(OapiError::ResponseStatus(response.status().as_u16()).into());
181            }
182
183            let text = response.text().await.map_err(|e| {
184                OapiError::ResponseError(format!("Failed to get response text: {:#?}", e))
185            })?;
186
187            Ok(text)
188        }
189    }
190}