Skip to main content

openrouter_rs/api/
files.rs

1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use reqwest::{Client as HttpClient, multipart};
5use serde::{Deserialize, Serialize};
6use urlencoding::encode;
7
8use crate::{
9    error::OpenRouterError,
10    transport::{request as transport_request, response as transport_response},
11};
12
13#[derive(Serialize)]
14struct FileWorkspaceQuery {
15    #[serde(skip_serializing_if = "Option::is_none")]
16    workspace_id: Option<String>,
17}
18
19#[derive(Serialize)]
20struct ListFilesQuery {
21    #[serde(skip_serializing_if = "Option::is_none")]
22    limit: Option<u32>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    cursor: Option<String>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    workspace_id: Option<String>,
27}
28
29/// Metadata describing a stored file.
30#[derive(Serialize, Deserialize, Debug, Clone)]
31#[non_exhaustive]
32pub struct FileMetadata {
33    pub id: String,
34    #[serde(rename = "type")]
35    pub object_type: String,
36    pub filename: String,
37    pub mime_type: String,
38    pub size_bytes: u64,
39    pub created_at: String,
40    pub downloadable: bool,
41    #[serde(flatten)]
42    pub extra: HashMap<String, serde_json::Value>,
43}
44
45/// A paginated page of files.
46#[derive(Serialize, Deserialize, Debug, Clone)]
47#[non_exhaustive]
48pub struct FileListResponse {
49    pub data: Vec<FileMetadata>,
50    pub has_more: bool,
51    pub first_id: Option<String>,
52    pub last_id: Option<String>,
53    pub cursor: Option<String>,
54    #[serde(flatten)]
55    pub extra: HashMap<String, serde_json::Value>,
56}
57
58/// Confirmation that a file was deleted.
59#[derive(Serialize, Deserialize, Debug, Clone)]
60#[non_exhaustive]
61pub struct FileDeleteResponse {
62    pub id: String,
63    #[serde(rename = "type")]
64    pub object_type: String,
65    #[serde(flatten)]
66    pub extra: HashMap<String, serde_json::Value>,
67}
68
69/// File upload payload for `POST /files`.
70#[derive(Debug, Clone, Builder)]
71#[builder(build_fn(error = "OpenRouterError"))]
72#[non_exhaustive]
73pub struct UploadFileRequest {
74    #[builder(setter(into))]
75    pub filename: String,
76    #[builder(setter(into))]
77    pub content: Vec<u8>,
78    #[builder(setter(into, strip_option), default)]
79    pub mime_type: Option<String>,
80}
81
82impl UploadFileRequest {
83    pub fn builder() -> UploadFileRequestBuilder {
84        UploadFileRequestBuilder::default()
85    }
86}
87
88fn workspace_query(workspace_id: Option<&str>) -> FileWorkspaceQuery {
89    FileWorkspaceQuery {
90        workspace_id: workspace_id.map(ToOwned::to_owned),
91    }
92}
93
94fn apply_workspace_query(
95    req: reqwest::RequestBuilder,
96    workspace_id: Option<&str>,
97) -> reqwest::RequestBuilder {
98    let query = workspace_query(workspace_id);
99    if query.workspace_id.is_none() {
100        req
101    } else {
102        req.query(&query)
103    }
104}
105
106/// List files in the default or selected workspace (`GET /files`).
107pub async fn list_files(
108    base_url: &str,
109    api_key: &str,
110    limit: Option<u32>,
111    cursor: Option<&str>,
112    workspace_id: Option<&str>,
113) -> Result<FileListResponse, OpenRouterError> {
114    let http_client = crate::transport::new_client()?;
115    list_files_with_client(&http_client, base_url, api_key, limit, cursor, workspace_id).await
116}
117
118pub(crate) async fn list_files_with_client(
119    http_client: &HttpClient,
120    base_url: &str,
121    api_key: &str,
122    limit: Option<u32>,
123    cursor: Option<&str>,
124    workspace_id: Option<&str>,
125) -> Result<FileListResponse, OpenRouterError> {
126    let url = format!("{base_url}/files");
127    let query = ListFilesQuery {
128        limit,
129        cursor: cursor.map(ToOwned::to_owned),
130        workspace_id: workspace_id.map(ToOwned::to_owned),
131    };
132    let req =
133        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
134    let response =
135        if query.limit.is_none() && query.cursor.is_none() && query.workspace_id.is_none() {
136            req.send().await?
137        } else {
138            req.query(&query).send().await?
139        };
140
141    if response.status().is_success() {
142        transport_response::parse_json_response(response, "file list").await
143    } else {
144        transport_response::handle_error(response).await?;
145        unreachable!()
146    }
147}
148
149/// Upload a file into the default or selected workspace (`POST /files`).
150pub async fn upload_file(
151    base_url: &str,
152    api_key: &str,
153    request: &UploadFileRequest,
154    workspace_id: Option<&str>,
155) -> Result<FileMetadata, OpenRouterError> {
156    let http_client = crate::transport::new_client()?;
157    upload_file_with_client(&http_client, base_url, api_key, request, workspace_id).await
158}
159
160pub(crate) async fn upload_file_with_client(
161    http_client: &HttpClient,
162    base_url: &str,
163    api_key: &str,
164    request: &UploadFileRequest,
165    workspace_id: Option<&str>,
166) -> Result<FileMetadata, OpenRouterError> {
167    let url = format!("{base_url}/files");
168    let mut part =
169        multipart::Part::bytes(request.content.clone()).file_name(request.filename.clone());
170    if let Some(mime_type) = &request.mime_type {
171        part = part
172            .mime_str(mime_type)
173            .map_err(|error| OpenRouterError::ConfigError(error.to_string()))?;
174    }
175    let form = multipart::Form::new().part("file", part);
176    let req =
177        transport_request::with_bearer_auth(transport_request::post(http_client, &url), api_key);
178    let response = apply_workspace_query(req, workspace_id)
179        .multipart(form)
180        .send()
181        .await?;
182
183    if response.status().is_success() {
184        transport_response::parse_json_response(response, "file upload").await
185    } else {
186        transport_response::handle_error(response).await?;
187        unreachable!()
188    }
189}
190
191/// Get metadata for one file (`GET /files/{file_id}`).
192pub async fn get_file_metadata(
193    base_url: &str,
194    api_key: &str,
195    file_id: &str,
196    workspace_id: Option<&str>,
197) -> Result<FileMetadata, OpenRouterError> {
198    let http_client = crate::transport::new_client()?;
199    get_file_metadata_with_client(&http_client, base_url, api_key, file_id, workspace_id).await
200}
201
202pub(crate) async fn get_file_metadata_with_client(
203    http_client: &HttpClient,
204    base_url: &str,
205    api_key: &str,
206    file_id: &str,
207    workspace_id: Option<&str>,
208) -> Result<FileMetadata, OpenRouterError> {
209    let encoded_id = encode(file_id);
210    let url = format!("{base_url}/files/{encoded_id}");
211    let req =
212        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
213    let response = apply_workspace_query(req, workspace_id).send().await?;
214
215    if response.status().is_success() {
216        transport_response::parse_json_response(response, "file metadata").await
217    } else {
218        transport_response::handle_error(response).await?;
219        unreachable!()
220    }
221}
222
223/// Download raw file content (`GET /files/{file_id}/content`).
224pub async fn download_file_content(
225    base_url: &str,
226    api_key: &str,
227    file_id: &str,
228    workspace_id: Option<&str>,
229) -> Result<Vec<u8>, OpenRouterError> {
230    let http_client = crate::transport::new_client()?;
231    download_file_content_with_client(&http_client, base_url, api_key, file_id, workspace_id).await
232}
233
234pub(crate) async fn download_file_content_with_client(
235    http_client: &HttpClient,
236    base_url: &str,
237    api_key: &str,
238    file_id: &str,
239    workspace_id: Option<&str>,
240) -> Result<Vec<u8>, OpenRouterError> {
241    let encoded_id = encode(file_id);
242    let url = format!("{base_url}/files/{encoded_id}/content");
243    let req =
244        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
245    let response = apply_workspace_query(req, workspace_id).send().await?;
246
247    if response.status().is_success() {
248        Ok(response.bytes().await?.to_vec())
249    } else {
250        transport_response::handle_error(response).await?;
251        unreachable!()
252    }
253}
254
255/// Delete one file (`DELETE /files/{file_id}`).
256pub async fn delete_file(
257    base_url: &str,
258    api_key: &str,
259    file_id: &str,
260    workspace_id: Option<&str>,
261) -> Result<FileDeleteResponse, OpenRouterError> {
262    let http_client = crate::transport::new_client()?;
263    delete_file_with_client(&http_client, base_url, api_key, file_id, workspace_id).await
264}
265
266pub(crate) async fn delete_file_with_client(
267    http_client: &HttpClient,
268    base_url: &str,
269    api_key: &str,
270    file_id: &str,
271    workspace_id: Option<&str>,
272) -> Result<FileDeleteResponse, OpenRouterError> {
273    let encoded_id = encode(file_id);
274    let url = format!("{base_url}/files/{encoded_id}");
275    let req =
276        transport_request::with_bearer_auth(transport_request::delete(http_client, &url), api_key);
277    let response = apply_workspace_query(req, workspace_id).send().await?;
278
279    if response.status().is_success() {
280        transport_response::parse_json_response(response, "file deletion").await
281    } else {
282        transport_response::handle_error(response).await?;
283        unreachable!()
284    }
285}