Skip to main content

openrouter_rs/api/
observability.rs

1use derive_builder::Builder;
2use reqwest::Client as HttpClient;
3use serde::{Deserialize, Serialize, Serializer, ser::SerializeMap};
4use serde_json::Value;
5use urlencoding::encode;
6
7use crate::{
8    error::OpenRouterError,
9    strip_option_vec_setter,
10    transport::{request as transport_request, response as transport_response},
11    types::{ApiResponse, PaginationOptions},
12};
13
14#[derive(Serialize)]
15struct ListObservabilityDestinationsQuery {
16    #[serde(skip_serializing_if = "Option::is_none")]
17    offset: Option<u32>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    limit: Option<u32>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    workspace_id: Option<String>,
22}
23
24/// Structured observability routing rules.
25#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
26#[builder(build_fn(error = "OpenRouterError"))]
27#[non_exhaustive]
28pub struct ObservabilityFilterRulesConfig {
29    pub groups: Vec<ObservabilityFilterGroup>,
30    #[builder(setter(strip_option), default)]
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub enabled: Option<bool>,
33}
34
35impl ObservabilityFilterRulesConfig {
36    pub fn builder() -> ObservabilityFilterRulesConfigBuilder {
37        ObservabilityFilterRulesConfigBuilder::default()
38    }
39}
40
41/// One observability filter group.
42#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
43#[builder(build_fn(error = "OpenRouterError"))]
44#[non_exhaustive]
45pub struct ObservabilityFilterGroup {
46    pub rules: Vec<ObservabilityFilterRule>,
47    #[builder(setter(into, strip_option), default)]
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub logic: Option<String>,
50}
51
52impl ObservabilityFilterGroup {
53    pub fn builder() -> ObservabilityFilterGroupBuilder {
54        ObservabilityFilterGroupBuilder::default()
55    }
56}
57
58/// One observability filter rule.
59#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
60#[builder(build_fn(error = "OpenRouterError"))]
61#[non_exhaustive]
62pub struct ObservabilityFilterRule {
63    #[builder(setter(into))]
64    pub field: String,
65    #[builder(setter(into))]
66    pub operator: String,
67    #[builder(setter(strip_option), default)]
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub value: Option<Value>,
70}
71
72impl ObservabilityFilterRule {
73    pub fn builder() -> ObservabilityFilterRuleBuilder {
74        ObservabilityFilterRuleBuilder::default()
75    }
76}
77
78/// Observability destination returned by `/observability/destinations`.
79#[derive(Serialize, Deserialize, Debug, Clone)]
80#[non_exhaustive]
81pub struct ObservabilityDestination {
82    pub id: String,
83    pub workspace_id: String,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub name: Option<String>,
86    pub enabled: bool,
87    pub privacy_mode: bool,
88    pub sampling_rate: f64,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub api_key_hashes: Option<Vec<String>>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub filter_rules: Option<ObservabilityFilterRulesConfig>,
93    pub created_at: String,
94    pub updated_at: String,
95    #[serde(rename = "type")]
96    pub destination_type: String,
97    pub config: Value,
98}
99
100/// Paginated observability destination list response.
101#[derive(Serialize, Deserialize, Debug, Clone)]
102#[non_exhaustive]
103pub struct ObservabilityDestinationListResponse {
104    pub data: Vec<ObservabilityDestination>,
105    pub total_count: u64,
106}
107
108/// Request payload for creating an observability destination.
109#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
110#[builder(build_fn(error = "OpenRouterError"))]
111#[non_exhaustive]
112pub struct CreateObservabilityDestinationRequest {
113    #[serde(rename = "type")]
114    #[builder(setter(into))]
115    pub destination_type: String,
116    #[builder(setter(into))]
117    pub name: String,
118    pub config: Value,
119    #[builder(setter(into, strip_option), default)]
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub workspace_id: Option<String>,
122    #[builder(setter(custom), default)]
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub api_key_hashes: Option<Vec<String>>,
125    #[builder(setter(strip_option), default)]
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub enabled: Option<bool>,
128    #[builder(setter(strip_option), default)]
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub privacy_mode: Option<bool>,
131    #[builder(setter(strip_option), default)]
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub sampling_rate: Option<f64>,
134    #[builder(setter(strip_option), default)]
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub filter_rules: Option<ObservabilityFilterRulesConfig>,
137}
138
139impl CreateObservabilityDestinationRequest {
140    pub fn builder() -> CreateObservabilityDestinationRequestBuilder {
141        CreateObservabilityDestinationRequestBuilder::default()
142    }
143}
144
145impl CreateObservabilityDestinationRequestBuilder {
146    strip_option_vec_setter!(api_key_hashes, String);
147}
148
149/// Request payload for updating an observability destination.
150#[derive(Deserialize, Debug, Clone, Builder)]
151#[builder(build_fn(error = "OpenRouterError"))]
152#[non_exhaustive]
153pub struct UpdateObservabilityDestinationRequest {
154    #[builder(setter(into, strip_option), default)]
155    pub name: Option<String>,
156    #[builder(setter(strip_option), default)]
157    pub config: Option<Value>,
158    #[builder(setter(custom), default)]
159    pub api_key_hashes: Option<Vec<String>>,
160    #[serde(skip)]
161    #[builder(setter(custom), default)]
162    clear_api_key_hashes: bool,
163    #[builder(setter(strip_option), default)]
164    pub enabled: Option<bool>,
165    #[builder(setter(strip_option), default)]
166    pub privacy_mode: Option<bool>,
167    #[builder(setter(strip_option), default)]
168    pub sampling_rate: Option<f64>,
169    #[builder(setter(custom), default)]
170    pub filter_rules: Option<ObservabilityFilterRulesConfig>,
171    #[serde(skip)]
172    #[builder(setter(custom), default)]
173    clear_filter_rules: bool,
174}
175
176impl Serialize for UpdateObservabilityDestinationRequest {
177    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
178    where
179        S: Serializer,
180    {
181        let mut map = serializer.serialize_map(None)?;
182        if let Some(value) = &self.name {
183            map.serialize_entry("name", value)?;
184        }
185        if let Some(value) = &self.config {
186            map.serialize_entry("config", value)?;
187        }
188        if self.clear_api_key_hashes {
189            map.serialize_entry("api_key_hashes", &Option::<Vec<String>>::None)?;
190        } else if let Some(value) = &self.api_key_hashes {
191            map.serialize_entry("api_key_hashes", value)?;
192        }
193        if let Some(value) = &self.enabled {
194            map.serialize_entry("enabled", value)?;
195        }
196        if let Some(value) = &self.privacy_mode {
197            map.serialize_entry("privacy_mode", value)?;
198        }
199        if let Some(value) = &self.sampling_rate {
200            map.serialize_entry("sampling_rate", value)?;
201        }
202        if self.clear_filter_rules {
203            map.serialize_entry(
204                "filter_rules",
205                &Option::<ObservabilityFilterRulesConfig>::None,
206            )?;
207        } else if let Some(value) = &self.filter_rules {
208            map.serialize_entry("filter_rules", value)?;
209        }
210        map.end()
211    }
212}
213
214impl UpdateObservabilityDestinationRequest {
215    pub fn builder() -> UpdateObservabilityDestinationRequestBuilder {
216        UpdateObservabilityDestinationRequestBuilder::default()
217    }
218}
219
220impl UpdateObservabilityDestinationRequestBuilder {
221    pub fn api_key_hashes<T, S>(&mut self, items: T) -> &mut Self
222    where
223        T: IntoIterator<Item = S>,
224        S: Into<String>,
225    {
226        self.api_key_hashes = Some(Some(items.into_iter().map(Into::into).collect()));
227        self.clear_api_key_hashes = Some(false);
228        self
229    }
230
231    pub fn clear_api_key_hashes(&mut self) -> &mut Self {
232        self.api_key_hashes = Some(None);
233        self.clear_api_key_hashes = Some(true);
234        self
235    }
236
237    pub fn filter_rules(&mut self, value: ObservabilityFilterRulesConfig) -> &mut Self {
238        self.filter_rules = Some(Some(value));
239        self.clear_filter_rules = Some(false);
240        self
241    }
242
243    pub fn clear_filter_rules(&mut self) -> &mut Self {
244        self.filter_rules = Some(None);
245        self.clear_filter_rules = Some(true);
246        self
247    }
248}
249
250#[derive(Serialize, Deserialize, Debug, Clone)]
251struct DeleteObservabilityDestinationResponse {
252    deleted: bool,
253}
254
255/// List observability destinations (`GET /observability/destinations`).
256pub async fn list_observability_destinations(
257    base_url: &str,
258    management_key: &str,
259    pagination: Option<PaginationOptions>,
260    workspace_id: Option<&str>,
261) -> Result<ObservabilityDestinationListResponse, OpenRouterError> {
262    let http_client = crate::transport::new_client()?;
263    list_observability_destinations_with_client(
264        &http_client,
265        base_url,
266        management_key,
267        pagination,
268        workspace_id,
269    )
270    .await
271}
272
273pub(crate) async fn list_observability_destinations_with_client(
274    http_client: &HttpClient,
275    base_url: &str,
276    management_key: &str,
277    pagination: Option<PaginationOptions>,
278    workspace_id: Option<&str>,
279) -> Result<ObservabilityDestinationListResponse, OpenRouterError> {
280    let url = format!("{base_url}/observability/destinations");
281    let query = ListObservabilityDestinationsQuery {
282        offset: pagination.and_then(|p| p.offset),
283        limit: pagination.and_then(|p| p.limit),
284        workspace_id: workspace_id.map(ToOwned::to_owned),
285    };
286    let req = transport_request::with_bearer_auth(
287        transport_request::get(http_client, &url),
288        management_key,
289    );
290    let response =
291        if query.offset.is_none() && query.limit.is_none() && query.workspace_id.is_none() {
292            req.send().await?
293        } else {
294            req.query(&query).send().await?
295        };
296
297    if response.status().is_success() {
298        transport_response::parse_json_response(response, "observability destination list").await
299    } else {
300        transport_response::handle_error(response).await?;
301        unreachable!()
302    }
303}
304
305/// Create an observability destination (`POST /observability/destinations`).
306pub async fn create_observability_destination(
307    base_url: &str,
308    management_key: &str,
309    request: &CreateObservabilityDestinationRequest,
310) -> Result<ObservabilityDestination, OpenRouterError> {
311    let http_client = crate::transport::new_client()?;
312    create_observability_destination_with_client(&http_client, base_url, management_key, request)
313        .await
314}
315
316pub(crate) async fn create_observability_destination_with_client(
317    http_client: &HttpClient,
318    base_url: &str,
319    management_key: &str,
320    request: &CreateObservabilityDestinationRequest,
321) -> Result<ObservabilityDestination, OpenRouterError> {
322    let url = format!("{base_url}/observability/destinations");
323    let response = transport_request::with_bearer_auth(
324        transport_request::post(http_client, &url),
325        management_key,
326    )
327    .json(request)
328    .send()
329    .await?;
330
331    if response.status().is_success() {
332        let payload: ApiResponse<ObservabilityDestination> =
333            transport_response::parse_json_response(response, "observability destination creation")
334                .await?;
335        Ok(payload.data)
336    } else {
337        transport_response::handle_error(response).await?;
338        unreachable!()
339    }
340}
341
342/// Get an observability destination (`GET /observability/destinations/{id}`).
343pub async fn get_observability_destination(
344    base_url: &str,
345    management_key: &str,
346    id: &str,
347) -> Result<ObservabilityDestination, OpenRouterError> {
348    let http_client = crate::transport::new_client()?;
349    get_observability_destination_with_client(&http_client, base_url, management_key, id).await
350}
351
352pub(crate) async fn get_observability_destination_with_client(
353    http_client: &HttpClient,
354    base_url: &str,
355    management_key: &str,
356    id: &str,
357) -> Result<ObservabilityDestination, OpenRouterError> {
358    let url = format!("{base_url}/observability/destinations/{}", encode(id));
359    let response = transport_request::with_bearer_auth(
360        transport_request::get(http_client, &url),
361        management_key,
362    )
363    .send()
364    .await?;
365
366    if response.status().is_success() {
367        let payload: ApiResponse<ObservabilityDestination> =
368            transport_response::parse_json_response(response, "observability destination lookup")
369                .await?;
370        Ok(payload.data)
371    } else {
372        transport_response::handle_error(response).await?;
373        unreachable!()
374    }
375}
376
377/// Update an observability destination (`PATCH /observability/destinations/{id}`).
378pub async fn update_observability_destination(
379    base_url: &str,
380    management_key: &str,
381    id: &str,
382    request: &UpdateObservabilityDestinationRequest,
383) -> Result<ObservabilityDestination, OpenRouterError> {
384    let http_client = crate::transport::new_client()?;
385    update_observability_destination_with_client(
386        &http_client,
387        base_url,
388        management_key,
389        id,
390        request,
391    )
392    .await
393}
394
395pub(crate) async fn update_observability_destination_with_client(
396    http_client: &HttpClient,
397    base_url: &str,
398    management_key: &str,
399    id: &str,
400    request: &UpdateObservabilityDestinationRequest,
401) -> Result<ObservabilityDestination, OpenRouterError> {
402    let url = format!("{base_url}/observability/destinations/{}", encode(id));
403    let response = transport_request::with_bearer_auth(
404        transport_request::patch(http_client, &url),
405        management_key,
406    )
407    .json(request)
408    .send()
409    .await?;
410
411    if response.status().is_success() {
412        let payload: ApiResponse<ObservabilityDestination> =
413            transport_response::parse_json_response(response, "observability destination update")
414                .await?;
415        Ok(payload.data)
416    } else {
417        transport_response::handle_error(response).await?;
418        unreachable!()
419    }
420}
421
422/// Delete an observability destination (`DELETE /observability/destinations/{id}`).
423pub async fn delete_observability_destination(
424    base_url: &str,
425    management_key: &str,
426    id: &str,
427) -> Result<bool, OpenRouterError> {
428    let http_client = crate::transport::new_client()?;
429    delete_observability_destination_with_client(&http_client, base_url, management_key, id).await
430}
431
432pub(crate) async fn delete_observability_destination_with_client(
433    http_client: &HttpClient,
434    base_url: &str,
435    management_key: &str,
436    id: &str,
437) -> Result<bool, OpenRouterError> {
438    let url = format!("{base_url}/observability/destinations/{}", encode(id));
439    let response = transport_request::with_bearer_auth(
440        transport_request::delete(http_client, &url),
441        management_key,
442    )
443    .send()
444    .await?;
445
446    if response.status().is_success() {
447        let payload: DeleteObservabilityDestinationResponse =
448            transport_response::parse_json_response(response, "observability destination deletion")
449                .await?;
450        Ok(payload.deleted)
451    } else {
452        transport_response::handle_error(response).await?;
453        unreachable!()
454    }
455}