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