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::RequestOptions;
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 #[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_map: Option<serde_json::Map<String, serde_json::Value>>,
50}
51
52#[derive(Debug, Serialize, Clone, Default)]
53pub enum FilePurpose {
54 #[serde(rename = "assistants")]
55 Assistants,
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/// 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.
87///
88/// Matches the official multipart form fields `expires_after[anchor]` and
89/// `expires_after[seconds]`.
90#[derive(Debug, Serialize, Clone)]
91#[serde(tag = "anchor", rename_all = "snake_case")]
92pub enum ExpiresAfter {
93 /// Anchor timestamp after which the expiration policy applies.
94 /// Supported anchors: `created_at`.
95 CreatedAt {
96 /// The number of seconds after the anchor time that the file will expire.
97 /// Must be between 3600 (1 hour) and 2592000 (30 days).
98 seconds: usize,
99 },
100}
101
102impl Post for CreateFileRequest {
103 #[inline]
104 fn is_streaming(&self) -> bool {
105 false
106 }
107
108 /// Builds the URL for the request.
109 ///
110 /// `base_url` should be like <https://api.openai.com/v1>
111 fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
112 let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
113 url.path_segments_mut()
114 .map_err(|_| OapiError::UrlCannotBeBase(base_url.to_string()))?
115 .push("files");
116
117 Ok(url.to_string())
118 }
119}
120
121impl PostNoStream for CreateFileRequest {
122 type Response = crate::files::FileObject;
123
124 /// Sends a file upload POST request using multipart/form-data format.
125 /// This implementation handles the actual file upload with proper file handling.
126 async fn get_response_string(
127 &self,
128 client: &reqwest::Client,
129 base_url: &str,
130 options: &RequestOptions,
131 ) -> Result<String, OapiError> {
132 // Read file content
133 let file_content = tokio::fs::read(&self.file).await?;
134
135 // Get file name from path
136 let file_name = self
137 .file
138 .file_name()
139 .and_then(|name| name.to_str())
140 .ok_or_else(|| OapiError::ResponseError("Invalid file name".to_string()))?
141 .to_string();
142
143 // Create multipart form with file and purpose
144 let file_part = reqwest::multipart::Part::bytes(file_content).file_name(file_name);
145
146 let mut form = reqwest::multipart::Form::new().part("file", file_part);
147
148 // Add purpose field
149 let purpose_str = serde_json::to_string(&self.purpose)
150 .map_err(|e| OapiError::ResponseError(format!("Failed to serialize purpose: {}", e)))?;
151 let trimmed_purpose = purpose_str.trim_matches('"').to_string();
152 form = form.text("purpose", trimmed_purpose);
153
154 // Add expires_after if present, using the official multipart form
155 // fields `expires_after[anchor]` and `expires_after[seconds]`.
156 if let Some(expires_after) = &self.expires_after {
157 let (anchor, seconds) = match expires_after {
158 ExpiresAfter::CreatedAt { seconds } => ("created_at", *seconds),
159 };
160 form = form
161 .text("expires_after[anchor]", anchor)
162 .text("expires_after[seconds]", seconds.to_string());
163 }
164
165 // Extra body properties are sent as additional multipart text
166 // fields, matching the official SDK (strings verbatim, other JSON
167 // values as their serialized form).
168 form = crate::rest::post::append_extra_body_map(form, &self.extra_body_map);
169
170 let url = self.build_url(base_url)?;
171 crate::rest::post::post_multipart_json(client, url, form, options).await
172 }
173}