Skip to main content

openai_interface/files/create/
request.rs

1//! Create a `CreateFileRequest` object for uploading a file.
2
3use serde::Serialize;
4use std::path::PathBuf;
5use url::Url;
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    #[serde(skip_serializing_if = "Option::is_none")]
46    pub expires_after: Option<ExpiresAfter>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub extra_body: Option<serde_json::Map<String, serde_json::Value>>,
49}
50
51#[derive(Debug, Serialize, Clone, Default)]
52pub enum FilePurpose {
53    #[serde(rename = "assistants")]
54    Assistants,
55    #[serde(rename = "batch")]
56    #[default]
57    Batch,
58    #[serde(rename = "fine-tune")]
59    FineTune,
60    #[serde(rename = "vision")]
61    Vision,
62    #[serde(rename = "user_data")]
63    UserData,
64    #[serde(rename = "evals")]
65    Evals,
66    #[serde(untagged)]
67    Other(String),
68}
69
70// #[derive(Debug, Serialize, Clone)]
71// pub enum FileTypes {
72//     /// file (or bytes)
73//     FileContent(Vec<u8>),
74//     /// (filename, file (or bytes))
75//     FileNameAndContent(String, Vec<u8>),
76//     /// (filename, file (or bytes), content_type)
77//     FileNameAndContentAndType(String, Vec<u8>, String),
78//     /// (filename, file (or bytes), content_type, headers)
79//     FileNameAndContentAndTypeAndHeaders(String, Vec<u8>, String, HashMap<String, String>),
80// }
81
82#[derive(Debug, Serialize, Clone)]
83#[serde(tag = "anchor", rename = "snake_case")]
84/// The expiration policy for a file.
85///
86/// By default, files with `purpose=batch` expire after 30 days and all other files
87/// are persisted until they are manually deleted.
88///
89/// Matches the official multipart form fields `expires_after[anchor]` and
90/// `expires_after[seconds]`.
91pub enum ExpiresAfter {
92    /// Anchor timestamp after which the expiration policy applies.
93    /// Supported anchors: `created_at`.
94    CreatedAt {
95        /// The number of seconds after the anchor time that the file will expire.
96        /// Must be between 3600 (1 hour) and 2592000 (30 days).
97        seconds: usize,
98    },
99}
100
101impl Post for CreateFileRequest {
102    #[inline]
103    fn is_streaming(&self) -> bool {
104        false
105    }
106
107    /// Builds the URL for the request.
108    ///
109    /// `base_url` should be like <https://api.openai.com/v1>
110    fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
111        let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
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    async fn get_response_string(
126        &self,
127        client: &reqwest::Client,
128        url: &str,
129        key: &str,
130    ) -> Result<String, OapiError> {
131        if self.is_streaming() {
132            return Err(OapiError::NonStreamingViolation);
133        }
134
135        // Check if file exists
136        if !self.file.exists() {
137            return Err(OapiError::FileNotFoundError(self.file.clone()));
138        }
139
140        // Read file content
141        let file_content = tokio::fs::read(&self.file).await?;
142
143        // Get file name from path
144        let file_name = self
145            .file
146            .file_name()
147            .and_then(|name| name.to_str())
148            .ok_or_else(|| OapiError::ResponseError("Invalid file name".to_string()))?
149            .to_string();
150
151        // Create multipart form with file and purpose
152        let file_part = reqwest::multipart::Part::bytes(file_content).file_name(file_name.clone());
153
154        let mut form = reqwest::multipart::Form::new().part("file", file_part);
155
156        // Add purpose field
157        let purpose_str = serde_json::to_string(&self.purpose)
158            .map_err(|e| OapiError::ResponseError(format!("Failed to serialize purpose: {}", e)))?;
159        let trimmed_purpose = purpose_str.trim_matches('"').to_string();
160        form = form.text("purpose", trimmed_purpose);
161
162        // Add expires_after if present, using the official multipart form
163        // fields `expires_after[anchor]` and `expires_after[seconds]`.
164        if let Some(expires_after) = &self.expires_after {
165            let (anchor, seconds) = match expires_after {
166                ExpiresAfter::CreatedAt { seconds } => ("created_at", *seconds),
167            };
168            form = form
169                .text("expires_after[anchor]", anchor)
170                .text("expires_after[seconds]", seconds.to_string());
171        }
172
173        let response = client
174            .post(url)
175            .header("Accept", "application/json")
176            .bearer_auth(key)
177            .multipart(form)
178            .send()
179            .await?;
180
181        crate::rest::response_text_checked(response).await
182    }
183}