Skip to main content

openrouter_rs/api/
byok.rs

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/// Bring-your-own-key provider credential returned by `/byok`.
26#[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/// Paginated BYOK credential list response.
48#[derive(Serialize, Deserialize, Debug, Clone)]
49#[non_exhaustive]
50pub struct ByokKeyListResponse {
51    pub data: Vec<ByokKey>,
52    pub total_count: u64,
53}
54
55/// Request payload for creating a BYOK credential (`POST /byok`).
56#[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/// Request payload for updating a BYOK credential (`PATCH /byok/{id}`).
96#[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
214/// List BYOK provider credentials (`GET /byok`). Requires a management key.
215pub 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
271/// Create a BYOK provider credential (`POST /byok`). Requires a management key.
272pub 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
306/// Get a BYOK provider credential (`GET /byok/{id}`). Requires a management key.
307pub 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
340/// Update a BYOK provider credential (`PATCH /byok/{id}`). Requires a management key.
341pub 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
377/// Delete a BYOK provider credential (`DELETE /byok/{id}`). Requires a management key.
378pub 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}