Skip to main content

openrouter_rs/api/
workspaces.rs

1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use reqwest::Client as HttpClient;
5use serde::{Deserialize, Serialize, Serializer, ser::SerializeMap};
6use serde_json::Value;
7use urlencoding::encode;
8
9use crate::{
10    error::OpenRouterError,
11    transport::{request as transport_request, response as transport_response},
12    types::{ApiResponse, PaginationOptions},
13};
14
15#[derive(Serialize)]
16struct ListWorkspacesQuery {
17    #[serde(skip_serializing_if = "Option::is_none")]
18    offset: Option<u32>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    limit: Option<u32>,
21}
22
23#[derive(Serialize, Deserialize, Debug, Clone)]
24#[non_exhaustive]
25pub struct Workspace {
26    pub id: String,
27    pub name: String,
28    pub slug: String,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub description: Option<String>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub default_text_model: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub default_image_model: Option<String>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub default_provider_sort: Option<String>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub io_logging_api_key_ids: Option<Vec<u64>>,
39    pub io_logging_sampling_rate: f64,
40    pub is_observability_io_logging_enabled: bool,
41    pub is_observability_broadcast_enabled: bool,
42    pub is_data_discount_logging_enabled: bool,
43    pub created_at: String,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub updated_at: Option<String>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub created_by: Option<String>,
48}
49
50#[derive(Serialize, Deserialize, Debug, Clone)]
51#[non_exhaustive]
52pub struct WorkspaceListResponse {
53    pub data: Vec<Workspace>,
54    pub total_count: f64,
55}
56
57#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
58#[builder(build_fn(error = "OpenRouterError"))]
59#[non_exhaustive]
60pub struct CreateWorkspaceRequest {
61    #[builder(setter(into))]
62    pub name: String,
63    #[builder(setter(into, strip_option), default)]
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub slug: Option<String>,
66    #[builder(setter(into, strip_option), default)]
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub description: Option<String>,
69    #[builder(setter(into, strip_option), default)]
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub default_text_model: Option<String>,
72    #[builder(setter(into, strip_option), default)]
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub default_image_model: Option<String>,
75    #[builder(setter(into, strip_option), default)]
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub default_provider_sort: Option<String>,
78    #[builder(setter(strip_option), default)]
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub io_logging_api_key_ids: Option<Vec<u64>>,
81    #[builder(setter(strip_option), default)]
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub io_logging_sampling_rate: Option<f64>,
84    #[builder(setter(strip_option), default)]
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub is_data_discount_logging_enabled: Option<bool>,
87    #[builder(setter(strip_option), default)]
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub is_observability_broadcast_enabled: Option<bool>,
90    #[builder(setter(strip_option), default)]
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub is_observability_io_logging_enabled: Option<bool>,
93}
94
95impl CreateWorkspaceRequest {
96    pub fn builder() -> CreateWorkspaceRequestBuilder {
97        CreateWorkspaceRequestBuilder::default()
98    }
99}
100
101#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
102#[builder(build_fn(error = "OpenRouterError"))]
103#[non_exhaustive]
104pub struct UpdateWorkspaceRequest {
105    #[builder(setter(into, strip_option), default)]
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub name: Option<String>,
108    #[builder(setter(into, strip_option), default)]
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub slug: Option<String>,
111    #[builder(setter(into, strip_option), default)]
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub description: Option<String>,
114    #[builder(setter(into, strip_option), default)]
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub default_text_model: Option<String>,
117    #[builder(setter(into, strip_option), default)]
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub default_image_model: Option<String>,
120    #[builder(setter(into, strip_option), default)]
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub default_provider_sort: Option<String>,
123    #[builder(setter(strip_option), default)]
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub io_logging_api_key_ids: Option<Vec<u64>>,
126    #[builder(setter(strip_option), default)]
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub io_logging_sampling_rate: Option<f64>,
129    #[builder(setter(strip_option), default)]
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub is_data_discount_logging_enabled: Option<bool>,
132    #[builder(setter(strip_option), default)]
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub is_observability_broadcast_enabled: Option<bool>,
135    #[builder(setter(strip_option), default)]
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub is_observability_io_logging_enabled: Option<bool>,
138}
139
140#[derive(Debug, Clone, Copy)]
141pub struct UpdateWorkspaceRequestWithClearedIoLoggingApiKeyIds<'a> {
142    request: &'a UpdateWorkspaceRequest,
143}
144
145impl Serialize for UpdateWorkspaceRequestWithClearedIoLoggingApiKeyIds<'_> {
146    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
147    where
148        S: Serializer,
149    {
150        let mut map = serializer.serialize_map(None)?;
151        if let Some(value) = &self.request.name {
152            map.serialize_entry("name", value)?;
153        }
154        if let Some(value) = &self.request.slug {
155            map.serialize_entry("slug", value)?;
156        }
157        if let Some(value) = &self.request.description {
158            map.serialize_entry("description", value)?;
159        }
160        if let Some(value) = &self.request.default_text_model {
161            map.serialize_entry("default_text_model", value)?;
162        }
163        if let Some(value) = &self.request.default_image_model {
164            map.serialize_entry("default_image_model", value)?;
165        }
166        if let Some(value) = &self.request.default_provider_sort {
167            map.serialize_entry("default_provider_sort", value)?;
168        }
169        map.serialize_entry("io_logging_api_key_ids", &Option::<Vec<u64>>::None)?;
170        if let Some(value) = &self.request.io_logging_sampling_rate {
171            map.serialize_entry("io_logging_sampling_rate", value)?;
172        }
173        if let Some(value) = &self.request.is_data_discount_logging_enabled {
174            map.serialize_entry("is_data_discount_logging_enabled", value)?;
175        }
176        if let Some(value) = &self.request.is_observability_broadcast_enabled {
177            map.serialize_entry("is_observability_broadcast_enabled", value)?;
178        }
179        if let Some(value) = &self.request.is_observability_io_logging_enabled {
180            map.serialize_entry("is_observability_io_logging_enabled", value)?;
181        }
182        map.end()
183    }
184}
185
186impl UpdateWorkspaceRequest {
187    pub fn builder() -> UpdateWorkspaceRequestBuilder {
188        UpdateWorkspaceRequestBuilder::default()
189    }
190
191    pub fn with_cleared_io_logging_api_key_ids(
192        &self,
193    ) -> UpdateWorkspaceRequestWithClearedIoLoggingApiKeyIds<'_> {
194        UpdateWorkspaceRequestWithClearedIoLoggingApiKeyIds { request: self }
195    }
196}
197
198#[derive(Serialize, Deserialize, Debug, Clone)]
199struct DeleteWorkspaceResponse {
200    deleted: bool,
201}
202
203#[derive(Serialize, Deserialize, Debug, Clone)]
204#[non_exhaustive]
205pub struct WorkspaceMember {
206    pub id: String,
207    pub workspace_id: String,
208    pub user_id: String,
209    pub role: String,
210    pub created_at: String,
211}
212
213#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
214#[builder(build_fn(error = "OpenRouterError"))]
215#[non_exhaustive]
216pub struct WorkspaceMembersRequest {
217    pub user_ids: Vec<String>,
218}
219
220impl WorkspaceMembersRequest {
221    pub fn builder() -> WorkspaceMembersRequestBuilder {
222        WorkspaceMembersRequestBuilder::default()
223    }
224}
225
226#[derive(Serialize, Deserialize, Debug, Clone)]
227#[non_exhaustive]
228pub struct WorkspaceMembersAddResponse {
229    pub added_count: f64,
230    pub data: Vec<WorkspaceMember>,
231}
232
233#[derive(Serialize, Deserialize, Debug, Clone)]
234#[non_exhaustive]
235pub struct WorkspaceMembersRemoveResponse {
236    pub removed_count: f64,
237}
238
239#[derive(Serialize, Deserialize, Debug, Clone)]
240#[non_exhaustive]
241pub struct WorkspaceBudget {
242    pub id: String,
243    pub workspace_id: String,
244    pub limit_usd: f64,
245    pub reset_interval: Option<String>,
246    pub created_at: String,
247    pub updated_at: String,
248    #[serde(flatten)]
249    pub extra: HashMap<String, Value>,
250}
251
252#[derive(Serialize, Deserialize, Debug, Clone)]
253#[non_exhaustive]
254pub struct ListWorkspaceBudgetsResponse {
255    pub data: Vec<WorkspaceBudget>,
256}
257
258#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
259#[builder(build_fn(error = "OpenRouterError"))]
260#[non_exhaustive]
261pub struct UpsertWorkspaceBudgetRequest {
262    pub limit_usd: f64,
263}
264
265impl UpsertWorkspaceBudgetRequest {
266    pub fn builder() -> UpsertWorkspaceBudgetRequestBuilder {
267        UpsertWorkspaceBudgetRequestBuilder::default()
268    }
269}
270
271#[derive(Serialize, Deserialize, Debug, Clone)]
272struct DeleteWorkspaceBudgetResponse {
273    deleted: bool,
274}
275
276pub async fn list_workspaces(
277    base_url: &str,
278    management_key: &str,
279    pagination: Option<PaginationOptions>,
280) -> Result<WorkspaceListResponse, OpenRouterError> {
281    let http_client = crate::transport::new_client()?;
282    list_workspaces_with_client(&http_client, base_url, management_key, pagination).await
283}
284
285pub(crate) async fn list_workspaces_with_client(
286    http_client: &HttpClient,
287    base_url: &str,
288    management_key: &str,
289    pagination: Option<PaginationOptions>,
290) -> Result<WorkspaceListResponse, OpenRouterError> {
291    let url = format!("{base_url}/workspaces");
292    let query = ListWorkspacesQuery {
293        offset: pagination.and_then(|p| p.offset),
294        limit: pagination.and_then(|p| p.limit),
295    };
296    let req = transport_request::with_bearer_auth(
297        transport_request::get(http_client, &url),
298        management_key,
299    );
300    let response = if query.offset.is_none() && query.limit.is_none() {
301        req.send().await?
302    } else {
303        req.query(&query).send().await?
304    };
305
306    if response.status().is_success() {
307        transport_response::parse_json_response(response, "workspace list").await
308    } else {
309        transport_response::handle_error(response).await?;
310        unreachable!()
311    }
312}
313
314pub async fn create_workspace(
315    base_url: &str,
316    management_key: &str,
317    request: &CreateWorkspaceRequest,
318) -> Result<Workspace, OpenRouterError> {
319    let http_client = crate::transport::new_client()?;
320    create_workspace_with_client(&http_client, base_url, management_key, request).await
321}
322
323pub(crate) async fn create_workspace_with_client(
324    http_client: &HttpClient,
325    base_url: &str,
326    management_key: &str,
327    request: &CreateWorkspaceRequest,
328) -> Result<Workspace, OpenRouterError> {
329    let url = format!("{base_url}/workspaces");
330    let response = transport_request::with_bearer_auth(
331        transport_request::post(http_client, &url),
332        management_key,
333    )
334    .json(request)
335    .send()
336    .await?;
337
338    if response.status().is_success() {
339        let payload: ApiResponse<Workspace> =
340            transport_response::parse_json_response(response, "workspace creation").await?;
341        Ok(payload.data)
342    } else {
343        transport_response::handle_error(response).await?;
344        unreachable!()
345    }
346}
347
348pub async fn get_workspace(
349    base_url: &str,
350    management_key: &str,
351    id: &str,
352) -> Result<Workspace, OpenRouterError> {
353    let http_client = crate::transport::new_client()?;
354    get_workspace_with_client(&http_client, base_url, management_key, id).await
355}
356
357pub(crate) async fn get_workspace_with_client(
358    http_client: &HttpClient,
359    base_url: &str,
360    management_key: &str,
361    id: &str,
362) -> Result<Workspace, OpenRouterError> {
363    let url = format!("{base_url}/workspaces/{}", encode(id));
364    let response = transport_request::with_bearer_auth(
365        transport_request::get(http_client, &url),
366        management_key,
367    )
368    .send()
369    .await?;
370
371    if response.status().is_success() {
372        let payload: ApiResponse<Workspace> =
373            transport_response::parse_json_response(response, "workspace lookup").await?;
374        Ok(payload.data)
375    } else {
376        transport_response::handle_error(response).await?;
377        unreachable!()
378    }
379}
380
381pub async fn update_workspace(
382    base_url: &str,
383    management_key: &str,
384    id: &str,
385    request: &UpdateWorkspaceRequest,
386) -> Result<Workspace, OpenRouterError> {
387    let http_client = crate::transport::new_client()?;
388    update_workspace_with_client(&http_client, base_url, management_key, id, request).await
389}
390
391pub async fn update_workspace_with_cleared_io_logging_api_key_ids(
392    base_url: &str,
393    management_key: &str,
394    id: &str,
395    request: &UpdateWorkspaceRequest,
396) -> Result<Workspace, OpenRouterError> {
397    let http_client = crate::transport::new_client()?;
398    update_workspace_with_cleared_io_logging_api_key_ids_with_client(
399        &http_client,
400        base_url,
401        management_key,
402        id,
403        request,
404    )
405    .await
406}
407
408pub(crate) async fn update_workspace_with_client(
409    http_client: &HttpClient,
410    base_url: &str,
411    management_key: &str,
412    id: &str,
413    request: &UpdateWorkspaceRequest,
414) -> Result<Workspace, OpenRouterError> {
415    update_workspace_payload_with_client(http_client, base_url, management_key, id, request).await
416}
417
418pub(crate) async fn update_workspace_with_cleared_io_logging_api_key_ids_with_client(
419    http_client: &HttpClient,
420    base_url: &str,
421    management_key: &str,
422    id: &str,
423    request: &UpdateWorkspaceRequest,
424) -> Result<Workspace, OpenRouterError> {
425    let request = request.with_cleared_io_logging_api_key_ids();
426    update_workspace_payload_with_client(http_client, base_url, management_key, id, &request).await
427}
428
429async fn update_workspace_payload_with_client<T: Serialize + ?Sized>(
430    http_client: &HttpClient,
431    base_url: &str,
432    management_key: &str,
433    id: &str,
434    request: &T,
435) -> Result<Workspace, OpenRouterError> {
436    let url = format!("{base_url}/workspaces/{}", encode(id));
437    let response = transport_request::with_bearer_auth(
438        transport_request::patch(http_client, &url),
439        management_key,
440    )
441    .json(request)
442    .send()
443    .await?;
444
445    if response.status().is_success() {
446        let payload: ApiResponse<Workspace> =
447            transport_response::parse_json_response(response, "workspace update").await?;
448        Ok(payload.data)
449    } else {
450        transport_response::handle_error(response).await?;
451        unreachable!()
452    }
453}
454
455pub async fn delete_workspace(
456    base_url: &str,
457    management_key: &str,
458    id: &str,
459) -> Result<bool, OpenRouterError> {
460    let http_client = crate::transport::new_client()?;
461    delete_workspace_with_client(&http_client, base_url, management_key, id).await
462}
463
464pub(crate) async fn delete_workspace_with_client(
465    http_client: &HttpClient,
466    base_url: &str,
467    management_key: &str,
468    id: &str,
469) -> Result<bool, OpenRouterError> {
470    let url = format!("{base_url}/workspaces/{}", encode(id));
471    let response = transport_request::with_bearer_auth(
472        transport_request::delete(http_client, &url),
473        management_key,
474    )
475    .send()
476    .await?;
477
478    if response.status().is_success() {
479        let payload: DeleteWorkspaceResponse =
480            transport_response::parse_json_response(response, "workspace deletion").await?;
481        Ok(payload.deleted)
482    } else {
483        transport_response::handle_error(response).await?;
484        unreachable!()
485    }
486}
487
488pub async fn list_workspace_budgets(
489    base_url: &str,
490    management_key: &str,
491    id: &str,
492) -> Result<ListWorkspaceBudgetsResponse, OpenRouterError> {
493    let http_client = crate::transport::new_client()?;
494    list_workspace_budgets_with_client(&http_client, base_url, management_key, id).await
495}
496
497pub(crate) async fn list_workspace_budgets_with_client(
498    http_client: &HttpClient,
499    base_url: &str,
500    management_key: &str,
501    id: &str,
502) -> Result<ListWorkspaceBudgetsResponse, OpenRouterError> {
503    let url = format!("{base_url}/workspaces/{}/budgets", encode(id));
504    let response = transport_request::with_bearer_auth(
505        transport_request::get(http_client, &url),
506        management_key,
507    )
508    .send()
509    .await?;
510
511    if response.status().is_success() {
512        transport_response::parse_json_response(response, "workspace budget list").await
513    } else {
514        transport_response::handle_error(response).await?;
515        unreachable!()
516    }
517}
518
519pub async fn upsert_workspace_budget(
520    base_url: &str,
521    management_key: &str,
522    id: &str,
523    interval: &str,
524    request: &UpsertWorkspaceBudgetRequest,
525) -> Result<WorkspaceBudget, OpenRouterError> {
526    let http_client = crate::transport::new_client()?;
527    upsert_workspace_budget_with_client(
528        &http_client,
529        base_url,
530        management_key,
531        id,
532        interval,
533        request,
534    )
535    .await
536}
537
538pub(crate) async fn upsert_workspace_budget_with_client(
539    http_client: &HttpClient,
540    base_url: &str,
541    management_key: &str,
542    id: &str,
543    interval: &str,
544    request: &UpsertWorkspaceBudgetRequest,
545) -> Result<WorkspaceBudget, OpenRouterError> {
546    let url = format!(
547        "{base_url}/workspaces/{}/budgets/{}",
548        encode(id),
549        encode(interval)
550    );
551    let response = transport_request::with_bearer_auth(
552        transport_request::put(http_client, &url),
553        management_key,
554    )
555    .json(request)
556    .send()
557    .await?;
558
559    if response.status().is_success() {
560        let payload: ApiResponse<WorkspaceBudget> =
561            transport_response::parse_json_response(response, "workspace budget upsert").await?;
562        Ok(payload.data)
563    } else {
564        transport_response::handle_error(response).await?;
565        unreachable!()
566    }
567}
568
569pub async fn delete_workspace_budget(
570    base_url: &str,
571    management_key: &str,
572    id: &str,
573    interval: &str,
574) -> Result<bool, OpenRouterError> {
575    let http_client = crate::transport::new_client()?;
576    delete_workspace_budget_with_client(&http_client, base_url, management_key, id, interval).await
577}
578
579pub(crate) async fn delete_workspace_budget_with_client(
580    http_client: &HttpClient,
581    base_url: &str,
582    management_key: &str,
583    id: &str,
584    interval: &str,
585) -> Result<bool, OpenRouterError> {
586    let url = format!(
587        "{base_url}/workspaces/{}/budgets/{}",
588        encode(id),
589        encode(interval)
590    );
591    let response = transport_request::with_bearer_auth(
592        transport_request::delete(http_client, &url),
593        management_key,
594    )
595    .send()
596    .await?;
597
598    if response.status().is_success() {
599        let payload: DeleteWorkspaceBudgetResponse =
600            transport_response::parse_json_response(response, "workspace budget deletion").await?;
601        Ok(payload.deleted)
602    } else {
603        transport_response::handle_error(response).await?;
604        unreachable!()
605    }
606}
607
608pub async fn add_workspace_members(
609    base_url: &str,
610    management_key: &str,
611    id: &str,
612    request: &WorkspaceMembersRequest,
613) -> Result<WorkspaceMembersAddResponse, OpenRouterError> {
614    let http_client = crate::transport::new_client()?;
615    add_workspace_members_with_client(&http_client, base_url, management_key, id, request).await
616}
617
618pub(crate) async fn add_workspace_members_with_client(
619    http_client: &HttpClient,
620    base_url: &str,
621    management_key: &str,
622    id: &str,
623    request: &WorkspaceMembersRequest,
624) -> Result<WorkspaceMembersAddResponse, OpenRouterError> {
625    let url = format!("{base_url}/workspaces/{}/members/add", encode(id));
626    let response = transport_request::with_bearer_auth(
627        transport_request::post(http_client, &url),
628        management_key,
629    )
630    .json(request)
631    .send()
632    .await?;
633
634    if response.status().is_success() {
635        transport_response::parse_json_response(response, "workspace member bulk add").await
636    } else {
637        transport_response::handle_error(response).await?;
638        unreachable!()
639    }
640}
641
642pub async fn remove_workspace_members(
643    base_url: &str,
644    management_key: &str,
645    id: &str,
646    request: &WorkspaceMembersRequest,
647) -> Result<WorkspaceMembersRemoveResponse, OpenRouterError> {
648    let http_client = crate::transport::new_client()?;
649    remove_workspace_members_with_client(&http_client, base_url, management_key, id, request).await
650}
651
652pub(crate) async fn remove_workspace_members_with_client(
653    http_client: &HttpClient,
654    base_url: &str,
655    management_key: &str,
656    id: &str,
657    request: &WorkspaceMembersRequest,
658) -> Result<WorkspaceMembersRemoveResponse, OpenRouterError> {
659    let url = format!("{base_url}/workspaces/{}/members/remove", encode(id));
660    let response = transport_request::with_bearer_auth(
661        transport_request::post(http_client, &url),
662        management_key,
663    )
664    .json(request)
665    .send()
666    .await?;
667
668    if response.status().is_success() {
669        transport_response::parse_json_response(response, "workspace member bulk removal").await
670    } else {
671        transport_response::handle_error(response).await?;
672        unreachable!()
673    }
674}