openai_interface/files/create/
request.rs

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