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}