Skip to main content

xai_rust/models/
file.rs

1//! File API types.
2
3use serde::{Deserialize, Serialize};
4
5/// A file object stored in the xAI API.
6#[derive(Debug, Clone, Deserialize)]
7pub struct FileObject {
8    /// Unique identifier for the file.
9    pub id: String,
10    /// The name of the file.
11    pub filename: String,
12    /// The size of the file in bytes.
13    pub bytes: u64,
14    /// Unix timestamp of when the file was created.
15    pub created_at: i64,
16    /// The purpose of the file.
17    pub purpose: String,
18    /// Object type (always "file").
19    #[serde(default)]
20    pub object: String,
21}
22
23/// Response from listing files.
24#[derive(Debug, Clone, Deserialize)]
25pub struct FileListResponse {
26    /// The list of files.
27    pub data: Vec<FileObject>,
28    /// Object type (always "list").
29    #[serde(default)]
30    pub object: String,
31}
32
33/// Purpose of an uploaded file.
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum FilePurpose {
37    /// For use with assistants/chat.
38    #[default]
39    Assistants,
40    /// For fine-tuning.
41    FineTune,
42    /// For batch operations.
43    Batch,
44}
45
46impl FilePurpose {
47    /// Get the string representation.
48    ///
49    /// Returns the same value as serde serialization (`snake_case`).
50    pub fn as_str(&self) -> &'static str {
51        match self {
52            FilePurpose::Assistants => "assistants",
53            FilePurpose::FineTune => "fine_tune",
54            FilePurpose::Batch => "batch",
55        }
56    }
57}
58
59impl std::fmt::Display for FilePurpose {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(f, "{}", self.as_str())
62    }
63}
64
65/// Delete file response.
66#[derive(Debug, Clone, Deserialize)]
67pub struct DeleteFileResponse {
68    /// The ID of the deleted file.
69    pub id: String,
70    /// Whether the deletion was successful.
71    pub deleted: bool,
72    /// Object type.
73    #[serde(default)]
74    pub object: String,
75}
76
77/// Request payload for downloading a file through the explicit upload/download flow.
78#[derive(Debug, Clone, Serialize)]
79pub struct FileDownloadRequest {
80    /// Identifier for the file to download.
81    pub file_id: String,
82}
83
84/// Request payload for initializing multipart-style uploads.
85#[derive(Debug, Clone, Serialize)]
86pub struct FileUploadInitializeRequest {
87    /// Name of the target file.
88    pub filename: String,
89    /// Target purpose.
90    pub purpose: FilePurpose,
91    /// Optional total size in bytes, if known.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub total_size_bytes: Option<u64>,
94}
95
96impl FileUploadInitializeRequest {
97    /// Create a new initialize request.
98    pub fn new(filename: impl Into<String>, purpose: FilePurpose) -> Self {
99        Self {
100            filename: filename.into(),
101            purpose,
102            total_size_bytes: None,
103        }
104    }
105
106    /// Set the total expected size.
107    pub fn total_size_bytes(mut self, size: u64) -> Self {
108        self.total_size_bytes = Some(size);
109        self
110    }
111}
112
113/// Response for upload initialization.
114#[derive(Debug, Clone, Deserialize)]
115pub struct FileUploadInitializeResponse {
116    /// Upload session identifier.
117    #[serde(default)]
118    pub upload_id: String,
119    /// Optional object type.
120    #[serde(default)]
121    pub object: String,
122    /// Optional chunk size that should be used.
123    #[serde(default)]
124    pub chunk_size: Option<u64>,
125    /// Optional upload session expiration timestamp.
126    #[serde(default)]
127    pub expires_at: Option<i64>,
128}
129
130/// Request payload for a file chunk upload.
131#[derive(Debug, Clone, Serialize)]
132pub struct FileUploadChunksRequest {
133    /// Upload session identifier.
134    pub upload_id: String,
135    /// Chunk sequence index (1-based).
136    pub part: u32,
137    /// Base64 encoded chunk payload.
138    pub chunk: String,
139    /// Optional chunk checksum.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub checksum: Option<String>,
142}
143
144impl FileUploadChunksRequest {
145    /// Create a new chunk upload request.
146    pub fn new(upload_id: impl Into<String>, part: u32, chunk: impl Into<String>) -> Self {
147        Self {
148            upload_id: upload_id.into(),
149            part,
150            chunk: chunk.into(),
151            checksum: None,
152        }
153    }
154
155    /// Set optional checksum for the chunk.
156    pub fn checksum(mut self, checksum: impl Into<String>) -> Self {
157        self.checksum = Some(checksum.into());
158        self
159    }
160}
161
162/// Response for chunk upload.
163#[derive(Debug, Clone, Deserialize)]
164pub struct FileUploadChunksResponse {
165    /// Upload session identifier.
166    #[serde(default)]
167    pub upload_id: String,
168    /// Whether all chunks for the upload session are uploaded.
169    #[serde(default)]
170    pub complete: bool,
171    /// Optional status message.
172    #[serde(default)]
173    pub status: Option<String>,
174    /// Optional current chunk index.
175    #[serde(default)]
176    pub uploaded_part: Option<u32>,
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    // ── FilePurpose serde roundtrips ───────────────────────────────────
184
185    #[test]
186    fn file_purpose_roundtrip_all_variants() {
187        for (variant, expected) in [
188            (FilePurpose::Assistants, "assistants"),
189            (FilePurpose::FineTune, "fine_tune"),
190            (FilePurpose::Batch, "batch"),
191        ] {
192            let json = serde_json::to_string(&variant).unwrap();
193            assert_eq!(json, format!("\"{expected}\""));
194
195            let back: FilePurpose = serde_json::from_str(&json).unwrap();
196            assert_eq!(back, variant);
197        }
198    }
199
200    #[test]
201    fn file_purpose_as_str_matches_serde() {
202        for variant in [
203            FilePurpose::Assistants,
204            FilePurpose::FineTune,
205            FilePurpose::Batch,
206        ] {
207            let serde_str = serde_json::to_value(variant)
208                .unwrap()
209                .as_str()
210                .unwrap()
211                .to_string();
212            assert_eq!(variant.as_str(), serde_str);
213        }
214    }
215
216    #[test]
217    fn file_purpose_display_matches_as_str() {
218        for variant in [
219            FilePurpose::Assistants,
220            FilePurpose::FineTune,
221            FilePurpose::Batch,
222        ] {
223            assert_eq!(format!("{variant}"), variant.as_str());
224        }
225    }
226
227    #[test]
228    fn file_purpose_default_is_assistants() {
229        assert_eq!(FilePurpose::default(), FilePurpose::Assistants);
230    }
231
232    #[test]
233    fn file_purpose_rejects_unknown() {
234        let result = serde_json::from_str::<FilePurpose>(r#""unknown_purpose""#);
235        assert!(result.is_err());
236    }
237
238    // ── FileObject deserialization ─────────────────────────────────────
239
240    #[test]
241    fn file_object_deserialize() {
242        let json = serde_json::json!({
243            "id": "file-abc",
244            "filename": "test.txt",
245            "bytes": 1024,
246            "created_at": 1700000000,
247            "purpose": "assistants",
248            "object": "file"
249        });
250        let fo: FileObject = serde_json::from_value(json).unwrap();
251        assert_eq!(fo.id, "file-abc");
252        assert_eq!(fo.filename, "test.txt");
253        assert_eq!(fo.bytes, 1024);
254        assert_eq!(fo.purpose, "assistants");
255    }
256
257    #[test]
258    fn file_object_object_field_defaults() {
259        let json = serde_json::json!({
260            "id": "file-abc",
261            "filename": "test.txt",
262            "bytes": 0,
263            "created_at": 0,
264            "purpose": "batch"
265        });
266        let fo: FileObject = serde_json::from_value(json).unwrap();
267        assert_eq!(fo.object, ""); // default for missing field
268    }
269
270    // ── DeleteFileResponse ─────────────────────────────────────────────
271
272    #[test]
273    fn delete_file_response_deserialize() {
274        let json = serde_json::json!({
275            "id": "file-xyz",
276            "deleted": true,
277            "object": "file"
278        });
279        let resp: DeleteFileResponse = serde_json::from_value(json).unwrap();
280        assert_eq!(resp.id, "file-xyz");
281        assert!(resp.deleted);
282    }
283
284    // ── FileListResponse ───────────────────────────────────────────────
285
286    #[test]
287    fn file_list_response_deserialize() {
288        let json = serde_json::json!({
289            "data": [{
290                "id": "file-1",
291                "filename": "a.txt",
292                "bytes": 100,
293                "created_at": 0,
294                "purpose": "assistants"
295            }],
296            "object": "list"
297        });
298        let list: FileListResponse = serde_json::from_value(json).unwrap();
299        assert_eq!(list.data.len(), 1);
300        assert_eq!(list.data[0].id, "file-1");
301    }
302
303    #[test]
304    fn file_upload_initialize_request_roundtrip() {
305        let request = FileUploadInitializeRequest::new("video.mp4", FilePurpose::FineTune)
306            .total_size_bytes(12345);
307        let json = serde_json::to_value(&request).unwrap();
308        assert_eq!(json["filename"], "video.mp4");
309        assert_eq!(json["purpose"], "fine_tune");
310        assert_eq!(json["total_size_bytes"], 12345);
311    }
312
313    #[test]
314    fn file_upload_chunks_request_roundtrip() {
315        let request = FileUploadChunksRequest::new("upload-1", 2, "YQ==").checksum("abc");
316        let json = serde_json::to_value(&request).unwrap();
317        assert_eq!(json["upload_id"], "upload-1");
318        assert_eq!(json["part"], 2);
319        assert_eq!(json["chunk"], "YQ==");
320        assert_eq!(json["checksum"], "abc");
321    }
322
323    #[test]
324    fn file_upload_chunks_response_deserialize() {
325        let json = serde_json::json!({
326            "upload_id": "upload-1",
327            "complete": true,
328            "status": "complete",
329            "uploaded_part": 2,
330        });
331        let response: FileUploadChunksResponse = serde_json::from_value(json).unwrap();
332        assert_eq!(response.upload_id, "upload-1");
333        assert!(response.complete);
334        assert_eq!(response.uploaded_part, Some(2));
335    }
336}