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/// Query parameters shared by provider-backed file operations.
30#[derive(Serialize, Debug, Clone, Default, Builder)]
31#[builder(build_fn(error = "OpenRouterError"), default)]
32#[non_exhaustive]
33pub struct FileQuery {
34    #[builder(setter(into, strip_option))]
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub workspace_id: Option<String>,
37    #[builder(setter(into, strip_option))]
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub provider: Option<String>,
40}
41
42impl FileQuery {
43    pub fn builder() -> FileQueryBuilder {
44        FileQueryBuilder::default()
45    }
46}
47
48/// Query parameters for provider-aware file listing.
49#[derive(Serialize, Debug, Clone, Default, Builder)]
50#[builder(build_fn(error = "OpenRouterError"), default)]
51#[non_exhaustive]
52pub struct ListFilesParams {
53    #[builder(setter(strip_option))]
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub limit: Option<u32>,
56    #[builder(setter(into, strip_option))]
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub cursor: Option<String>,
59    #[builder(setter(into, strip_option))]
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub workspace_id: Option<String>,
62    #[builder(setter(into, strip_option))]
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub provider: Option<String>,
65    #[builder(setter(into, strip_option))]
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub after: Option<String>,
68    #[builder(setter(into, strip_option))]
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub after_id: Option<String>,
71    #[builder(setter(into, strip_option))]
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub before_id: Option<String>,
74    #[builder(setter(into, strip_option))]
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub order: Option<String>,
77}
78
79impl ListFilesParams {
80    pub fn builder() -> ListFilesParamsBuilder {
81        ListFilesParamsBuilder::default()
82    }
83}
84
85/// Metadata describing a stored file.
86#[derive(Serialize, Deserialize, Debug, Clone)]
87#[non_exhaustive]
88pub struct FileMetadata {
89    pub id: String,
90    #[serde(rename = "type")]
91    pub object_type: String,
92    pub filename: String,
93    pub mime_type: String,
94    pub size_bytes: u64,
95    pub created_at: String,
96    pub downloadable: bool,
97    #[serde(flatten)]
98    pub extra: HashMap<String, serde_json::Value>,
99}
100
101/// A paginated page of files.
102#[derive(Serialize, Deserialize, Debug, Clone)]
103#[non_exhaustive]
104pub struct FileListResponse {
105    pub data: Vec<FileMetadata>,
106    pub has_more: bool,
107    pub first_id: Option<String>,
108    pub last_id: Option<String>,
109    pub cursor: Option<String>,
110    #[serde(flatten)]
111    pub extra: HashMap<String, serde_json::Value>,
112}
113
114/// Confirmation that a file was deleted.
115#[derive(Serialize, Deserialize, Debug, Clone)]
116#[non_exhaustive]
117pub struct FileDeleteResponse {
118    pub id: String,
119    #[serde(rename = "type")]
120    pub object_type: String,
121    #[serde(flatten)]
122    pub extra: HashMap<String, serde_json::Value>,
123}
124
125/// File metadata in the shape negotiated with a backing provider.
126#[derive(Serialize, Deserialize, Debug, Clone)]
127#[non_exhaustive]
128pub struct ProviderFileMetadata {
129    #[serde(rename = "_shape")]
130    pub shape: String,
131    pub id: String,
132    pub filename: String,
133    #[serde(default, rename = "type")]
134    pub object_type: Option<String>,
135    #[serde(default)]
136    pub object: Option<String>,
137    #[serde(default)]
138    pub mime_type: Option<String>,
139    #[serde(default)]
140    pub size_bytes: Option<u64>,
141    #[serde(default)]
142    pub bytes: Option<u64>,
143    pub created_at: serde_json::Value,
144    #[serde(default)]
145    pub downloadable: Option<bool>,
146    #[serde(default)]
147    pub purpose: Option<String>,
148    #[serde(default)]
149    pub status: Option<String>,
150    #[serde(flatten)]
151    pub extra: HashMap<String, serde_json::Value>,
152}
153
154/// A provider-shaped page of files.
155#[derive(Serialize, Deserialize, Debug, Clone)]
156#[non_exhaustive]
157pub struct ProviderFileListResponse {
158    #[serde(rename = "_shape")]
159    pub shape: String,
160    pub data: Vec<ProviderFileMetadata>,
161    pub has_more: bool,
162    pub first_id: Option<String>,
163    pub last_id: Option<String>,
164    #[serde(default)]
165    pub cursor: Option<String>,
166    #[serde(default)]
167    pub object: Option<String>,
168    #[serde(flatten)]
169    pub extra: HashMap<String, serde_json::Value>,
170}
171
172/// Provider-shaped confirmation that a file was deleted.
173#[derive(Serialize, Deserialize, Debug, Clone)]
174#[non_exhaustive]
175pub struct ProviderFileDeleteResponse {
176    #[serde(rename = "_shape")]
177    pub shape: String,
178    pub id: String,
179    #[serde(default, rename = "type")]
180    pub object_type: Option<String>,
181    #[serde(default)]
182    pub object: Option<String>,
183    #[serde(default)]
184    pub deleted: Option<bool>,
185    #[serde(flatten)]
186    pub extra: HashMap<String, serde_json::Value>,
187}
188
189/// File upload payload for `POST /files`.
190#[derive(Debug, Clone, Builder)]
191#[builder(build_fn(error = "OpenRouterError"))]
192#[non_exhaustive]
193pub struct UploadFileRequest {
194    #[builder(setter(into))]
195    pub filename: String,
196    #[builder(setter(into))]
197    pub content: Vec<u8>,
198    #[builder(setter(into, strip_option), default)]
199    pub mime_type: Option<String>,
200}
201
202impl UploadFileRequest {
203    pub fn builder() -> UploadFileRequestBuilder {
204        UploadFileRequestBuilder::default()
205    }
206}
207
208fn workspace_query(workspace_id: Option<&str>) -> FileWorkspaceQuery {
209    FileWorkspaceQuery {
210        workspace_id: workspace_id.map(ToOwned::to_owned),
211    }
212}
213
214fn apply_workspace_query(
215    req: reqwest::RequestBuilder,
216    workspace_id: Option<&str>,
217) -> reqwest::RequestBuilder {
218    let query = workspace_query(workspace_id);
219    if query.workspace_id.is_none() {
220        req
221    } else {
222        req.query(&query)
223    }
224}
225
226/// List files in the default or selected workspace (`GET /files`).
227pub async fn list_files(
228    base_url: &str,
229    api_key: &str,
230    limit: Option<u32>,
231    cursor: Option<&str>,
232    workspace_id: Option<&str>,
233) -> Result<FileListResponse, OpenRouterError> {
234    let http_client = crate::transport::new_client()?;
235    list_files_with_client(&http_client, base_url, api_key, limit, cursor, workspace_id).await
236}
237
238pub(crate) async fn list_files_with_client(
239    http_client: &HttpClient,
240    base_url: &str,
241    api_key: &str,
242    limit: Option<u32>,
243    cursor: Option<&str>,
244    workspace_id: Option<&str>,
245) -> Result<FileListResponse, OpenRouterError> {
246    let url = format!("{base_url}/files");
247    let query = ListFilesQuery {
248        limit,
249        cursor: cursor.map(ToOwned::to_owned),
250        workspace_id: workspace_id.map(ToOwned::to_owned),
251    };
252    let req =
253        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
254    let response =
255        if query.limit.is_none() && query.cursor.is_none() && query.workspace_id.is_none() {
256            req.send().await?
257        } else {
258            req.query(&query).send().await?
259        };
260
261    if response.status().is_success() {
262        transport_response::parse_json_response(response, "file list").await
263    } else {
264        transport_response::handle_error(response).await?;
265        unreachable!()
266    }
267}
268
269/// List files using a provider-native response shape (`GET /files`).
270pub async fn list_provider_files(
271    base_url: &str,
272    api_key: &str,
273    params: &ListFilesParams,
274) -> Result<ProviderFileListResponse, OpenRouterError> {
275    let http_client = crate::transport::new_client()?;
276    list_provider_files_with_client(&http_client, base_url, api_key, params).await
277}
278
279pub(crate) async fn list_provider_files_with_client(
280    http_client: &HttpClient,
281    base_url: &str,
282    api_key: &str,
283    params: &ListFilesParams,
284) -> Result<ProviderFileListResponse, OpenRouterError> {
285    let url = format!("{base_url}/files");
286    let response =
287        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
288            .query(params)
289            .send()
290            .await?;
291
292    if response.status().is_success() {
293        transport_response::parse_json_response(response, "provider file list").await
294    } else {
295        transport_response::handle_error(response).await?;
296        unreachable!()
297    }
298}
299
300/// Upload a file into the default or selected workspace (`POST /files`).
301pub async fn upload_file(
302    base_url: &str,
303    api_key: &str,
304    request: &UploadFileRequest,
305    workspace_id: Option<&str>,
306) -> Result<FileMetadata, OpenRouterError> {
307    let http_client = crate::transport::new_client()?;
308    upload_file_with_client(&http_client, base_url, api_key, request, workspace_id).await
309}
310
311pub(crate) async fn upload_file_with_client(
312    http_client: &HttpClient,
313    base_url: &str,
314    api_key: &str,
315    request: &UploadFileRequest,
316    workspace_id: Option<&str>,
317) -> Result<FileMetadata, OpenRouterError> {
318    let url = format!("{base_url}/files");
319    let mut part =
320        multipart::Part::bytes(request.content.clone()).file_name(request.filename.clone());
321    if let Some(mime_type) = &request.mime_type {
322        part = part
323            .mime_str(mime_type)
324            .map_err(|error| OpenRouterError::ConfigError(error.to_string()))?;
325    }
326    let form = multipart::Form::new().part("file", part);
327    let req =
328        transport_request::with_bearer_auth(transport_request::post(http_client, &url), api_key);
329    let response = apply_workspace_query(req, workspace_id)
330        .multipart(form)
331        .send()
332        .await?;
333
334    if response.status().is_success() {
335        transport_response::parse_json_response(response, "file upload").await
336    } else {
337        transport_response::handle_error(response).await?;
338        unreachable!()
339    }
340}
341
342/// Upload a file using a provider-native response shape (`POST /files`).
343pub async fn upload_provider_file(
344    base_url: &str,
345    api_key: &str,
346    request: &UploadFileRequest,
347    query: &FileQuery,
348) -> Result<ProviderFileMetadata, OpenRouterError> {
349    let http_client = crate::transport::new_client()?;
350    upload_provider_file_with_client(&http_client, base_url, api_key, request, query).await
351}
352
353pub(crate) async fn upload_provider_file_with_client(
354    http_client: &HttpClient,
355    base_url: &str,
356    api_key: &str,
357    request: &UploadFileRequest,
358    query: &FileQuery,
359) -> Result<ProviderFileMetadata, OpenRouterError> {
360    let url = format!("{base_url}/files");
361    let mut part =
362        multipart::Part::bytes(request.content.clone()).file_name(request.filename.clone());
363    if let Some(mime_type) = &request.mime_type {
364        part = part
365            .mime_str(mime_type)
366            .map_err(|error| OpenRouterError::ConfigError(error.to_string()))?;
367    }
368    let form = multipart::Form::new().part("file", part);
369    let response =
370        transport_request::with_bearer_auth(transport_request::post(http_client, &url), api_key)
371            .query(query)
372            .multipart(form)
373            .send()
374            .await?;
375
376    if response.status().is_success() {
377        transport_response::parse_json_response(response, "provider file upload").await
378    } else {
379        transport_response::handle_error(response).await?;
380        unreachable!()
381    }
382}
383
384/// Get metadata for one file (`GET /files/{file_id}`).
385pub async fn get_file_metadata(
386    base_url: &str,
387    api_key: &str,
388    file_id: &str,
389    workspace_id: Option<&str>,
390) -> Result<FileMetadata, OpenRouterError> {
391    let http_client = crate::transport::new_client()?;
392    get_file_metadata_with_client(&http_client, base_url, api_key, file_id, workspace_id).await
393}
394
395pub(crate) async fn get_file_metadata_with_client(
396    http_client: &HttpClient,
397    base_url: &str,
398    api_key: &str,
399    file_id: &str,
400    workspace_id: Option<&str>,
401) -> Result<FileMetadata, OpenRouterError> {
402    let encoded_id = encode(file_id);
403    let url = format!("{base_url}/files/{encoded_id}");
404    let req =
405        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
406    let response = apply_workspace_query(req, workspace_id).send().await?;
407
408    if response.status().is_success() {
409        transport_response::parse_json_response(response, "file metadata").await
410    } else {
411        transport_response::handle_error(response).await?;
412        unreachable!()
413    }
414}
415
416/// Get metadata using a provider-native response shape (`GET /files/{file_id}`).
417pub async fn get_provider_file_metadata(
418    base_url: &str,
419    api_key: &str,
420    file_id: &str,
421    query: &FileQuery,
422) -> Result<ProviderFileMetadata, OpenRouterError> {
423    let http_client = crate::transport::new_client()?;
424    get_provider_file_metadata_with_client(&http_client, base_url, api_key, file_id, query).await
425}
426
427pub(crate) async fn get_provider_file_metadata_with_client(
428    http_client: &HttpClient,
429    base_url: &str,
430    api_key: &str,
431    file_id: &str,
432    query: &FileQuery,
433) -> Result<ProviderFileMetadata, OpenRouterError> {
434    let url = format!("{base_url}/files/{}", encode(file_id));
435    let response =
436        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
437            .query(query)
438            .send()
439            .await?;
440
441    if response.status().is_success() {
442        transport_response::parse_json_response(response, "provider file metadata").await
443    } else {
444        transport_response::handle_error(response).await?;
445        unreachable!()
446    }
447}
448
449/// Download raw file content (`GET /files/{file_id}/content`).
450pub async fn download_file_content(
451    base_url: &str,
452    api_key: &str,
453    file_id: &str,
454    workspace_id: Option<&str>,
455) -> Result<Vec<u8>, OpenRouterError> {
456    let http_client = crate::transport::new_client()?;
457    download_file_content_with_client(&http_client, base_url, api_key, file_id, workspace_id).await
458}
459
460pub(crate) async fn download_file_content_with_client(
461    http_client: &HttpClient,
462    base_url: &str,
463    api_key: &str,
464    file_id: &str,
465    workspace_id: Option<&str>,
466) -> Result<Vec<u8>, OpenRouterError> {
467    let encoded_id = encode(file_id);
468    let url = format!("{base_url}/files/{encoded_id}/content");
469    let req =
470        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key);
471    let response = apply_workspace_query(req, workspace_id).send().await?;
472
473    if response.status().is_success() {
474        Ok(response.bytes().await?.to_vec())
475    } else {
476        transport_response::handle_error(response).await?;
477        unreachable!()
478    }
479}
480
481/// Download raw content from a selected provider (`GET /files/{file_id}/content`).
482pub async fn download_provider_file_content(
483    base_url: &str,
484    api_key: &str,
485    file_id: &str,
486    query: &FileQuery,
487) -> Result<Vec<u8>, OpenRouterError> {
488    let http_client = crate::transport::new_client()?;
489    download_provider_file_content_with_client(&http_client, base_url, api_key, file_id, query)
490        .await
491}
492
493pub(crate) async fn download_provider_file_content_with_client(
494    http_client: &HttpClient,
495    base_url: &str,
496    api_key: &str,
497    file_id: &str,
498    query: &FileQuery,
499) -> Result<Vec<u8>, OpenRouterError> {
500    let url = format!("{base_url}/files/{}/content", encode(file_id));
501    let response =
502        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
503            .query(query)
504            .send()
505            .await?;
506
507    if response.status().is_success() {
508        Ok(response.bytes().await?.to_vec())
509    } else {
510        transport_response::handle_error(response).await?;
511        unreachable!()
512    }
513}
514
515/// Delete one file (`DELETE /files/{file_id}`).
516pub async fn delete_file(
517    base_url: &str,
518    api_key: &str,
519    file_id: &str,
520    workspace_id: Option<&str>,
521) -> Result<FileDeleteResponse, OpenRouterError> {
522    let http_client = crate::transport::new_client()?;
523    delete_file_with_client(&http_client, base_url, api_key, file_id, workspace_id).await
524}
525
526pub(crate) async fn delete_file_with_client(
527    http_client: &HttpClient,
528    base_url: &str,
529    api_key: &str,
530    file_id: &str,
531    workspace_id: Option<&str>,
532) -> Result<FileDeleteResponse, OpenRouterError> {
533    let encoded_id = encode(file_id);
534    let url = format!("{base_url}/files/{encoded_id}");
535    let req =
536        transport_request::with_bearer_auth(transport_request::delete(http_client, &url), api_key);
537    let response = apply_workspace_query(req, workspace_id).send().await?;
538
539    if response.status().is_success() {
540        transport_response::parse_json_response(response, "file deletion").await
541    } else {
542        transport_response::handle_error(response).await?;
543        unreachable!()
544    }
545}
546
547/// Delete a file using a provider-native response shape (`DELETE /files/{file_id}`).
548pub async fn delete_provider_file(
549    base_url: &str,
550    api_key: &str,
551    file_id: &str,
552    query: &FileQuery,
553) -> Result<ProviderFileDeleteResponse, OpenRouterError> {
554    let http_client = crate::transport::new_client()?;
555    delete_provider_file_with_client(&http_client, base_url, api_key, file_id, query).await
556}
557
558pub(crate) async fn delete_provider_file_with_client(
559    http_client: &HttpClient,
560    base_url: &str,
561    api_key: &str,
562    file_id: &str,
563    query: &FileQuery,
564) -> Result<ProviderFileDeleteResponse, OpenRouterError> {
565    let url = format!("{base_url}/files/{}", encode(file_id));
566    let response =
567        transport_request::with_bearer_auth(transport_request::delete(http_client, &url), api_key)
568            .query(query)
569            .send()
570            .await?;
571
572    if response.status().is_success() {
573        transport_response::parse_json_response(response, "provider file deletion").await
574    } else {
575        transport_response::handle_error(response).await?;
576        unreachable!()
577    }
578}