1use derive_builder::Builder;
2use reqwest::Client as HttpClient;
3use serde::{Deserialize, Serialize, Serializer, ser::SerializeMap};
4use urlencoding::encode;
5
6use crate::{
7 error::OpenRouterError,
8 strip_option_vec_setter,
9 transport::{request as transport_request, response as transport_response},
10 types::{ApiResponse, PaginationOptions},
11};
12
13#[derive(Serialize)]
14struct ListByokKeysQuery {
15 #[serde(skip_serializing_if = "Option::is_none")]
16 offset: Option<u32>,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 limit: Option<u32>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 workspace_id: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 provider: Option<String>,
23}
24
25#[derive(Serialize, Deserialize, Debug, Clone)]
27#[non_exhaustive]
28pub struct ByokKey {
29 pub id: String,
30 pub provider: String,
31 pub workspace_id: String,
32 pub label: String,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub name: Option<String>,
35 pub disabled: bool,
36 pub is_fallback: bool,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub allowed_models: Option<Vec<String>>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub allowed_api_key_hashes: Option<Vec<String>>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub allowed_user_ids: Option<Vec<String>>,
43 pub sort_order: i64,
44 pub created_at: String,
45}
46
47#[derive(Serialize, Deserialize, Debug, Clone)]
49#[non_exhaustive]
50pub struct ByokKeyListResponse {
51 pub data: Vec<ByokKey>,
52 pub total_count: u64,
53}
54
55#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
57#[builder(build_fn(error = "OpenRouterError"))]
58#[non_exhaustive]
59pub struct CreateByokKeyRequest {
60 #[builder(setter(into))]
61 pub provider: String,
62 #[builder(setter(into))]
63 pub key: String,
64 #[builder(setter(into, strip_option), default)]
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub name: Option<String>,
67 #[builder(setter(into, strip_option), default)]
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub workspace_id: Option<String>,
70 #[builder(setter(custom), default)]
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub allowed_models: Option<Vec<String>>,
73 #[builder(setter(custom), default)]
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub allowed_user_ids: Option<Vec<String>>,
76 #[builder(setter(strip_option), default)]
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub disabled: Option<bool>,
79 #[builder(setter(strip_option), default)]
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub is_fallback: Option<bool>,
82}
83
84impl CreateByokKeyRequest {
85 pub fn builder() -> CreateByokKeyRequestBuilder {
86 CreateByokKeyRequestBuilder::default()
87 }
88}
89
90impl CreateByokKeyRequestBuilder {
91 strip_option_vec_setter!(allowed_models, String);
92 strip_option_vec_setter!(allowed_user_ids, String);
93}
94
95#[derive(Deserialize, Debug, Clone, Builder)]
97#[builder(build_fn(error = "OpenRouterError"))]
98#[non_exhaustive]
99pub struct UpdateByokKeyRequest {
100 #[builder(setter(into, strip_option), default)]
101 pub key: Option<String>,
102 #[builder(setter(custom), default)]
103 pub name: Option<String>,
104 #[serde(skip)]
105 #[builder(setter(custom), default)]
106 clear_name: bool,
107 #[builder(setter(custom), default)]
108 pub allowed_models: Option<Vec<String>>,
109 #[serde(skip)]
110 #[builder(setter(custom), default)]
111 clear_allowed_models: bool,
112 #[builder(setter(custom), default)]
113 pub allowed_user_ids: Option<Vec<String>>,
114 #[serde(skip)]
115 #[builder(setter(custom), default)]
116 clear_allowed_user_ids: bool,
117 #[builder(setter(strip_option), default)]
118 pub disabled: Option<bool>,
119 #[builder(setter(strip_option), default)]
120 pub is_fallback: Option<bool>,
121}
122
123impl Serialize for UpdateByokKeyRequest {
124 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
125 where
126 S: Serializer,
127 {
128 let mut map = serializer.serialize_map(None)?;
129 if let Some(value) = &self.key {
130 map.serialize_entry("key", value)?;
131 }
132 if self.clear_name {
133 map.serialize_entry("name", &Option::<String>::None)?;
134 } else if let Some(value) = &self.name {
135 map.serialize_entry("name", value)?;
136 }
137 if self.clear_allowed_models {
138 map.serialize_entry("allowed_models", &Option::<Vec<String>>::None)?;
139 } else if let Some(value) = &self.allowed_models {
140 map.serialize_entry("allowed_models", value)?;
141 }
142 if self.clear_allowed_user_ids {
143 map.serialize_entry("allowed_user_ids", &Option::<Vec<String>>::None)?;
144 } else if let Some(value) = &self.allowed_user_ids {
145 map.serialize_entry("allowed_user_ids", value)?;
146 }
147 if let Some(value) = &self.disabled {
148 map.serialize_entry("disabled", value)?;
149 }
150 if let Some(value) = &self.is_fallback {
151 map.serialize_entry("is_fallback", value)?;
152 }
153 map.end()
154 }
155}
156
157impl UpdateByokKeyRequest {
158 pub fn builder() -> UpdateByokKeyRequestBuilder {
159 UpdateByokKeyRequestBuilder::default()
160 }
161}
162
163impl UpdateByokKeyRequestBuilder {
164 pub fn name(&mut self, value: impl Into<String>) -> &mut Self {
165 self.name = Some(Some(value.into()));
166 self.clear_name = Some(false);
167 self
168 }
169
170 pub fn clear_name(&mut self) -> &mut Self {
171 self.name = Some(None);
172 self.clear_name = Some(true);
173 self
174 }
175
176 pub fn allowed_models<T, S>(&mut self, items: T) -> &mut Self
177 where
178 T: IntoIterator<Item = S>,
179 S: Into<String>,
180 {
181 self.allowed_models = Some(Some(items.into_iter().map(Into::into).collect()));
182 self.clear_allowed_models = Some(false);
183 self
184 }
185
186 pub fn allowed_user_ids<T, S>(&mut self, items: T) -> &mut Self
187 where
188 T: IntoIterator<Item = S>,
189 S: Into<String>,
190 {
191 self.allowed_user_ids = Some(Some(items.into_iter().map(Into::into).collect()));
192 self.clear_allowed_user_ids = Some(false);
193 self
194 }
195
196 pub fn clear_allowed_models(&mut self) -> &mut Self {
197 self.allowed_models = Some(None);
198 self.clear_allowed_models = Some(true);
199 self
200 }
201
202 pub fn clear_allowed_user_ids(&mut self) -> &mut Self {
203 self.allowed_user_ids = Some(None);
204 self.clear_allowed_user_ids = Some(true);
205 self
206 }
207}
208
209#[derive(Serialize, Deserialize, Debug, Clone)]
210struct DeleteByokKeyResponse {
211 deleted: bool,
212}
213
214pub async fn list_byok_keys(
216 base_url: &str,
217 management_key: &str,
218 pagination: Option<PaginationOptions>,
219 workspace_id: Option<&str>,
220 provider: Option<&str>,
221) -> Result<ByokKeyListResponse, OpenRouterError> {
222 let http_client = crate::transport::new_client()?;
223 list_byok_keys_with_client(
224 &http_client,
225 base_url,
226 management_key,
227 pagination,
228 workspace_id,
229 provider,
230 )
231 .await
232}
233
234pub(crate) async fn list_byok_keys_with_client(
235 http_client: &HttpClient,
236 base_url: &str,
237 management_key: &str,
238 pagination: Option<PaginationOptions>,
239 workspace_id: Option<&str>,
240 provider: Option<&str>,
241) -> Result<ByokKeyListResponse, OpenRouterError> {
242 let url = format!("{base_url}/byok");
243 let query = ListByokKeysQuery {
244 offset: pagination.and_then(|p| p.offset),
245 limit: pagination.and_then(|p| p.limit),
246 workspace_id: workspace_id.map(ToOwned::to_owned),
247 provider: provider.map(ToOwned::to_owned),
248 };
249 let req = transport_request::with_bearer_auth(
250 transport_request::get(http_client, &url),
251 management_key,
252 );
253 let response = if query.offset.is_none()
254 && query.limit.is_none()
255 && query.workspace_id.is_none()
256 && query.provider.is_none()
257 {
258 req.send().await?
259 } else {
260 req.query(&query).send().await?
261 };
262
263 if response.status().is_success() {
264 transport_response::parse_json_response(response, "BYOK key list").await
265 } else {
266 transport_response::handle_error(response).await?;
267 unreachable!()
268 }
269}
270
271pub async fn create_byok_key(
273 base_url: &str,
274 management_key: &str,
275 request: &CreateByokKeyRequest,
276) -> Result<ByokKey, OpenRouterError> {
277 let http_client = crate::transport::new_client()?;
278 create_byok_key_with_client(&http_client, base_url, management_key, request).await
279}
280
281pub(crate) async fn create_byok_key_with_client(
282 http_client: &HttpClient,
283 base_url: &str,
284 management_key: &str,
285 request: &CreateByokKeyRequest,
286) -> Result<ByokKey, OpenRouterError> {
287 let url = format!("{base_url}/byok");
288 let response = transport_request::with_bearer_auth(
289 transport_request::post(http_client, &url),
290 management_key,
291 )
292 .json(request)
293 .send()
294 .await?;
295
296 if response.status().is_success() {
297 let payload: ApiResponse<ByokKey> =
298 transport_response::parse_json_response(response, "BYOK key creation").await?;
299 Ok(payload.data)
300 } else {
301 transport_response::handle_error(response).await?;
302 unreachable!()
303 }
304}
305
306pub async fn get_byok_key(
308 base_url: &str,
309 management_key: &str,
310 id: &str,
311) -> Result<ByokKey, OpenRouterError> {
312 let http_client = crate::transport::new_client()?;
313 get_byok_key_with_client(&http_client, base_url, management_key, id).await
314}
315
316pub(crate) async fn get_byok_key_with_client(
317 http_client: &HttpClient,
318 base_url: &str,
319 management_key: &str,
320 id: &str,
321) -> Result<ByokKey, OpenRouterError> {
322 let url = format!("{base_url}/byok/{}", encode(id));
323 let response = transport_request::with_bearer_auth(
324 transport_request::get(http_client, &url),
325 management_key,
326 )
327 .send()
328 .await?;
329
330 if response.status().is_success() {
331 let payload: ApiResponse<ByokKey> =
332 transport_response::parse_json_response(response, "BYOK key lookup").await?;
333 Ok(payload.data)
334 } else {
335 transport_response::handle_error(response).await?;
336 unreachable!()
337 }
338}
339
340pub async fn update_byok_key(
342 base_url: &str,
343 management_key: &str,
344 id: &str,
345 request: &UpdateByokKeyRequest,
346) -> Result<ByokKey, OpenRouterError> {
347 let http_client = crate::transport::new_client()?;
348 update_byok_key_with_client(&http_client, base_url, management_key, id, request).await
349}
350
351pub(crate) async fn update_byok_key_with_client(
352 http_client: &HttpClient,
353 base_url: &str,
354 management_key: &str,
355 id: &str,
356 request: &UpdateByokKeyRequest,
357) -> Result<ByokKey, OpenRouterError> {
358 let url = format!("{base_url}/byok/{}", encode(id));
359 let response = transport_request::with_bearer_auth(
360 transport_request::patch(http_client, &url),
361 management_key,
362 )
363 .json(request)
364 .send()
365 .await?;
366
367 if response.status().is_success() {
368 let payload: ApiResponse<ByokKey> =
369 transport_response::parse_json_response(response, "BYOK key update").await?;
370 Ok(payload.data)
371 } else {
372 transport_response::handle_error(response).await?;
373 unreachable!()
374 }
375}
376
377pub async fn delete_byok_key(
379 base_url: &str,
380 management_key: &str,
381 id: &str,
382) -> Result<bool, OpenRouterError> {
383 let http_client = crate::transport::new_client()?;
384 delete_byok_key_with_client(&http_client, base_url, management_key, id).await
385}
386
387pub(crate) async fn delete_byok_key_with_client(
388 http_client: &HttpClient,
389 base_url: &str,
390 management_key: &str,
391 id: &str,
392) -> Result<bool, OpenRouterError> {
393 let url = format!("{base_url}/byok/{}", encode(id));
394 let response = transport_request::with_bearer_auth(
395 transport_request::delete(http_client, &url),
396 management_key,
397 )
398 .send()
399 .await?;
400
401 if response.status().is_success() {
402 let payload: DeleteByokKeyResponse =
403 transport_response::parse_json_response(response, "BYOK key deletion").await?;
404 Ok(payload.deleted)
405 } else {
406 transport_response::handle_error(response).await?;
407 unreachable!()
408 }
409}