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#[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#[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#[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#[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#[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#[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#[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#[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#[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
226pub 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
269pub 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
300pub 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
342pub 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
384pub 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
416pub 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
449pub 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
481pub 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
515pub 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
547pub 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}