Skip to main content

openai_oxide/types/
file.rs

1// File types — mirrors openai-python types/file_object.py
2
3use serde::{Deserialize, Serialize};
4
5use super::common::SortOrder;
6
7/// The intended purpose of an uploaded file.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[non_exhaustive]
10pub enum FilePurpose {
11    #[serde(rename = "assistants")]
12    Assistants,
13    #[serde(rename = "assistants_output")]
14    AssistantsOutput,
15    #[serde(rename = "batch")]
16    Batch,
17    #[serde(rename = "batch_output")]
18    BatchOutput,
19    #[serde(rename = "fine-tune")]
20    FineTune,
21    #[serde(rename = "fine-tune-results")]
22    FineTuneResults,
23    #[serde(rename = "vision")]
24    Vision,
25    #[serde(rename = "user_data")]
26    UserData,
27}
28
29/// Processing status of an uploaded file.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32#[non_exhaustive]
33pub enum FileStatus {
34    Uploaded,
35    Processed,
36    Error,
37}
38
39/// A file object from the API.
40#[derive(Debug, Clone, Deserialize)]
41pub struct FileObject {
42    pub id: String,
43    pub bytes: i64,
44    pub created_at: i64,
45    pub filename: String,
46    pub object: String,
47    pub purpose: FilePurpose,
48    pub status: FileStatus,
49    #[serde(default)]
50    pub status_details: Option<String>,
51    #[serde(default)]
52    pub expires_at: Option<i64>,
53}
54
55/// Response from listing files.
56#[derive(Debug, Clone, Deserialize)]
57pub struct FileList {
58    pub object: String,
59    pub data: Vec<FileObject>,
60    /// Whether there are more results available.
61    #[serde(default)]
62    pub has_more: Option<bool>,
63    /// ID of the first object in the list.
64    #[serde(default)]
65    pub first_id: Option<String>,
66    /// ID of the last object in the list.
67    #[serde(default)]
68    pub last_id: Option<String>,
69}
70
71/// Response from deleting a file.
72#[derive(Debug, Clone, Deserialize)]
73pub struct FileDeleted {
74    pub id: String,
75    pub deleted: bool,
76    pub object: String,
77}
78
79/// Parameters for file upload (multipart).
80#[derive(Debug)]
81pub struct FileUploadParams {
82    pub file: Vec<u8>,
83    pub filename: String,
84    pub purpose: FilePurpose,
85}
86
87impl FileUploadParams {
88    pub fn new(file: Vec<u8>, filename: impl Into<String>, purpose: FilePurpose) -> Self {
89        Self {
90            file,
91            filename: filename.into(),
92            purpose,
93        }
94    }
95}
96
97/// Parameters for listing files with pagination.
98#[derive(Debug, Clone, Default)]
99pub struct FileListParams {
100    /// Cursor for pagination — fetch results after this file ID.
101    pub after: Option<String>,
102    /// Maximum number of results per page (1–10000).
103    pub limit: Option<i64>,
104    /// Sort order by `created_at`.
105    pub order: Option<SortOrder>,
106    /// Filter by file purpose.
107    pub purpose: Option<FilePurpose>,
108}
109
110impl FileListParams {
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    pub fn after(mut self, after: impl Into<String>) -> Self {
116        self.after = Some(after.into());
117        self
118    }
119
120    pub fn limit(mut self, limit: i64) -> Self {
121        self.limit = Some(limit);
122        self
123    }
124
125    pub fn order(mut self, order: SortOrder) -> Self {
126        self.order = Some(order);
127        self
128    }
129
130    pub fn purpose(mut self, purpose: FilePurpose) -> Self {
131        self.purpose = Some(purpose);
132        self
133    }
134
135    /// Convert to query parameter pairs for the HTTP request.
136    pub fn to_query(&self) -> Vec<(String, String)> {
137        let mut q = Vec::new();
138        if let Some(ref after) = self.after {
139            q.push(("after".into(), after.clone()));
140        }
141        if let Some(limit) = self.limit {
142            q.push(("limit".into(), limit.to_string()));
143        }
144        if let Some(ref order) = self.order {
145            q.push((
146                "order".into(),
147                serde_json::to_value(order)
148                    .unwrap()
149                    .as_str()
150                    .unwrap()
151                    .to_string(),
152            ));
153        }
154        if let Some(ref purpose) = self.purpose {
155            q.push((
156                "purpose".into(),
157                serde_json::to_value(purpose)
158                    .unwrap()
159                    .as_str()
160                    .unwrap()
161                    .to_string(),
162            ));
163        }
164        q
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_deserialize_file_object() {
174        let json = r#"{
175            "id": "file-abc123",
176            "object": "file",
177            "bytes": 120000,
178            "created_at": 1677610602,
179            "filename": "data.jsonl",
180            "purpose": "fine-tune",
181            "status": "processed"
182        }"#;
183        let file: FileObject = serde_json::from_str(json).unwrap();
184        assert_eq!(file.id, "file-abc123");
185        assert_eq!(file.bytes, 120000);
186        assert_eq!(file.purpose, FilePurpose::FineTune);
187        assert_eq!(file.status, FileStatus::Processed);
188    }
189
190    #[test]
191    fn test_deserialize_file_list_with_pagination() {
192        let json = r#"{
193            "object": "list",
194            "data": [{
195                "id": "file-abc123",
196                "object": "file",
197                "bytes": 120000,
198                "created_at": 1677610602,
199                "filename": "data.jsonl",
200                "purpose": "fine-tune",
201                "status": "processed"
202            }],
203            "has_more": true,
204            "first_id": "file-abc123",
205            "last_id": "file-abc123"
206        }"#;
207        let list: FileList = serde_json::from_str(json).unwrap();
208        assert_eq!(list.data.len(), 1);
209        assert_eq!(list.has_more, Some(true));
210        assert_eq!(list.first_id.as_deref(), Some("file-abc123"));
211        assert_eq!(list.last_id.as_deref(), Some("file-abc123"));
212    }
213
214    #[test]
215    fn test_file_list_params_to_query() {
216        let params = FileListParams::new()
217            .after("file-cursor")
218            .limit(10)
219            .order(crate::types::common::SortOrder::Desc)
220            .purpose(FilePurpose::FineTune);
221        let query = params.to_query();
222        assert!(query.contains(&("after".into(), "file-cursor".into())));
223        assert!(query.contains(&("limit".into(), "10".into())));
224        assert!(query.contains(&("order".into(), "desc".into())));
225        assert!(query.contains(&("purpose".into(), "fine-tune".into())));
226    }
227
228    #[test]
229    fn test_deserialize_file_deleted() {
230        let json = r#"{"id": "file-abc123", "object": "file", "deleted": true}"#;
231        let deleted: FileDeleted = serde_json::from_str(json).unwrap();
232        assert!(deleted.deleted);
233    }
234}