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, 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#[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#[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#[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
106pub 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
149pub 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
191pub 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
223pub 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
255pub 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}