Skip to main content

llmsdk_provider/
files_model.rs

1//! File upload model trait and supporting types.
2//!
3//! Mirrors `@ai-sdk/provider/src/files/v4/*`. Implementations let callers
4//! upload a file once and receive a [`ProviderReference`] that can be reused
5//! across subsequent calls without re-uploading the bytes.
6//!
7//! Only providers that expose a file-management endpoint implement this
8//! trait (e.g. `Anthropic`'s `POST /v1/files`). The trait is intentionally
9//! kept separate from [`crate::Provider`] so providers without a files
10//! endpoint don't have to stub it out.
11// Rust guideline compliant 2026-02-21
12
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15
16use crate::error::Result;
17use crate::shared::{FileBytes, ProviderMetadata, ProviderOptions, ProviderReference, Warning};
18
19/// Contract every file-upload model implements.
20///
21/// Mirrors `FilesV4`.
22#[async_trait]
23pub trait FilesModel: Send + Sync + std::fmt::Debug {
24    /// Provider id, e.g. `"anthropic.files"`.
25    fn provider(&self) -> &str;
26
27    /// Specification version (currently `"v4"`).
28    fn specification_version(&self) -> &'static str {
29        "v4"
30    }
31
32    /// Upload a file to the provider and return a reusable reference.
33    ///
34    /// # Errors
35    ///
36    /// Returns a [`crate::ProviderError`] when the upstream call fails or
37    /// the response is malformed. Implementations should return
38    /// [`crate::ProviderError::invalid_argument`] when given an
39    /// [`UploadFileData`] variant the endpoint cannot handle.
40    async fn upload_file(&self, options: UploadFileOptions) -> Result<UploadFileResult>;
41}
42
43/// Options for one [`FilesModel::upload_file`] call.
44///
45/// Mirrors `FilesV4UploadFileCallOptions`.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct UploadFileOptions {
48    /// File payload.
49    pub data: UploadFileData,
50    /// IANA media type (e.g. `"application/pdf"`).
51    #[serde(rename = "mediaType")]
52    pub media_type: String,
53    /// Optional filename forwarded to the provider.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub filename: Option<String>,
56    /// Provider-specific options.
57    #[serde(
58        default,
59        rename = "providerOptions",
60        skip_serializing_if = "Option::is_none"
61    )]
62    pub provider_options: Option<ProviderOptions>,
63}
64
65/// Payload variants accepted by [`FilesModel::upload_file`].
66///
67/// Mirrors `SharedV4FileDataData | SharedV4FileDataText` (the V4 spec
68/// excludes URL / reference inputs because the call would have nothing
69/// to upload).
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71#[serde(tag = "type", rename_all = "kebab-case")]
72pub enum UploadFileData {
73    /// Inline bytes or base64-encoded string.
74    Data {
75        /// Raw bytes or base64 string.
76        data: FileBytes,
77    },
78    /// Inline UTF-8 text.
79    Text {
80        /// Text content.
81        text: String,
82    },
83}
84
85/// Result of [`FilesModel::upload_file`].
86///
87/// Mirrors `FilesV4UploadFileResult`.
88#[derive(Debug, Clone)]
89pub struct UploadFileResult {
90    /// `{ providerId → fileId }` reference reusable in later calls.
91    pub provider_reference: ProviderReference,
92    /// Media type reported by the provider (may differ from input).
93    pub media_type: Option<String>,
94    /// Filename reported by the provider.
95    pub filename: Option<String>,
96    /// Provider-specific metadata.
97    pub provider_metadata: Option<ProviderMetadata>,
98    /// Warnings (e.g. setting coerced away).
99    pub warnings: Vec<Warning>,
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use serde_json::json;
106
107    #[test]
108    fn options_serde_roundtrip_data() {
109        let opts = UploadFileOptions {
110            data: UploadFileData::Data {
111                data: FileBytes::Base64("aGVsbG8=".into()),
112            },
113            media_type: "text/plain".into(),
114            filename: Some("hi.txt".into()),
115            provider_options: None,
116        };
117        let json = serde_json::to_value(&opts).unwrap();
118        assert_eq!(json["mediaType"], "text/plain");
119        assert_eq!(json["data"]["type"], "data");
120        assert_eq!(json["data"]["data"], "aGVsbG8=");
121        let back: UploadFileOptions = serde_json::from_value(json).unwrap();
122        assert_eq!(back.media_type, "text/plain");
123    }
124
125    #[test]
126    fn options_serde_roundtrip_text() {
127        let opts = UploadFileOptions {
128            data: UploadFileData::Text {
129                text: "hello".into(),
130            },
131            media_type: "text/plain".into(),
132            filename: None,
133            provider_options: None,
134        };
135        let json = serde_json::to_value(&opts).unwrap();
136        assert_eq!(json["data"], json!({ "type": "text", "text": "hello" }));
137    }
138
139    #[test]
140    fn upload_data_tagged_correctly() {
141        let v = serde_json::to_value(UploadFileData::Data {
142            data: FileBytes::Bytes(vec![1, 2, 3]),
143        })
144        .unwrap();
145        assert_eq!(v["type"], "data");
146    }
147}