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