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::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}