Skip to main content

openrouter_rs/api/
analytics.rs

1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use reqwest::Client as HttpClient;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::{
9    error::OpenRouterError,
10    strip_option_vec_setter,
11    transport::{request as transport_request, response as transport_response},
12    types::ApiResponse,
13};
14
15/// One analytics metric definition returned by `GET /analytics/meta`.
16#[derive(Serialize, Deserialize, Debug, Clone)]
17#[non_exhaustive]
18pub struct AnalyticsMetric {
19    pub name: String,
20    pub display_label: String,
21    pub is_rate: bool,
22    pub display_format: String,
23    #[serde(flatten)]
24    pub extra: HashMap<String, Value>,
25}
26
27/// One analytics dimension definition returned by `GET /analytics/meta`.
28#[derive(Serialize, Deserialize, Debug, Clone)]
29#[non_exhaustive]
30pub struct AnalyticsDimension {
31    pub name: String,
32    pub display_label: String,
33    #[serde(flatten)]
34    pub extra: HashMap<String, Value>,
35}
36
37/// One analytics filter operator definition returned by `GET /analytics/meta`.
38#[derive(Serialize, Deserialize, Debug, Clone)]
39#[non_exhaustive]
40pub struct AnalyticsOperator {
41    pub name: String,
42    pub value_type: String,
43    #[serde(flatten)]
44    pub extra: HashMap<String, Value>,
45}
46
47/// One analytics granularity definition returned by `GET /analytics/meta`.
48#[derive(Serialize, Deserialize, Debug, Clone)]
49#[non_exhaustive]
50pub struct AnalyticsGranularity {
51    pub name: String,
52    pub display_label: String,
53    #[serde(flatten)]
54    pub extra: HashMap<String, Value>,
55}
56
57/// Analytics query metadata.
58#[derive(Serialize, Deserialize, Debug, Clone)]
59#[non_exhaustive]
60pub struct AnalyticsMeta {
61    pub metrics: Vec<AnalyticsMetric>,
62    pub dimensions: Vec<AnalyticsDimension>,
63    pub operators: Vec<AnalyticsOperator>,
64    pub granularities: Vec<AnalyticsGranularity>,
65    #[serde(flatten)]
66    pub extra: HashMap<String, Value>,
67}
68
69/// Scalar value used inside analytics filters.
70#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
71#[serde(untagged)]
72#[non_exhaustive]
73pub enum AnalyticsFilterScalar {
74    String(String),
75    Number(f64),
76}
77
78impl From<&str> for AnalyticsFilterScalar {
79    fn from(value: &str) -> Self {
80        Self::String(value.to_string())
81    }
82}
83
84impl From<String> for AnalyticsFilterScalar {
85    fn from(value: String) -> Self {
86        Self::String(value)
87    }
88}
89
90impl From<f64> for AnalyticsFilterScalar {
91    fn from(value: f64) -> Self {
92        Self::Number(value)
93    }
94}
95
96/// Filter value accepted by analytics query requests.
97#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
98#[serde(untagged)]
99#[non_exhaustive]
100pub enum AnalyticsFilterValue {
101    String(String),
102    Number(f64),
103    Array(Vec<AnalyticsFilterScalar>),
104}
105
106/// One analytics filter.
107#[derive(Serialize, Deserialize, Debug, Clone)]
108pub struct AnalyticsFilter {
109    pub field: String,
110    pub operator: String,
111    pub value: AnalyticsFilterValue,
112}
113
114/// Analytics time range.
115#[derive(Serialize, Deserialize, Debug, Clone)]
116pub struct AnalyticsTimeRange {
117    pub start: String,
118    pub end: String,
119}
120
121/// Analytics ordering clause.
122#[derive(Serialize, Deserialize, Debug, Clone)]
123pub struct AnalyticsOrderBy {
124    pub field: String,
125    pub direction: String,
126}
127
128#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
129#[builder(build_fn(error = "OpenRouterError"))]
130pub struct AnalyticsClassifierDimensions {
131    #[builder(setter(into))]
132    pub classifier_id: String,
133    #[builder(setter(custom), default)]
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub dimension_names: Option<Vec<String>>,
136    #[builder(setter(strip_option), default)]
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub include_nulls: Option<bool>,
139}
140
141impl AnalyticsClassifierDimensions {
142    pub fn builder() -> AnalyticsClassifierDimensionsBuilder {
143        AnalyticsClassifierDimensionsBuilder::default()
144    }
145}
146
147impl AnalyticsClassifierDimensionsBuilder {
148    strip_option_vec_setter!(dimension_names, String);
149}
150
151#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
152#[builder(build_fn(error = "OpenRouterError"))]
153pub struct AnalyticsClassifierFilters {
154    #[builder(setter(into))]
155    pub classifier_id: String,
156    #[builder(setter(custom))]
157    pub filters: Vec<AnalyticsFilter>,
158}
159
160impl AnalyticsClassifierFilters {
161    pub fn builder() -> AnalyticsClassifierFiltersBuilder {
162        AnalyticsClassifierFiltersBuilder::default()
163    }
164}
165
166impl AnalyticsClassifierFiltersBuilder {
167    pub fn filters<T, S>(&mut self, items: T) -> &mut Self
168    where
169        T: IntoIterator<Item = S>,
170        S: Into<AnalyticsFilter>,
171    {
172        self.filters = Some(items.into_iter().map(Into::into).collect());
173        self
174    }
175}
176
177/// Request payload for `POST /analytics/query`.
178#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
179#[builder(build_fn(error = "OpenRouterError"))]
180#[non_exhaustive]
181pub struct AnalyticsQueryRequest {
182    #[builder(setter(custom))]
183    pub metrics: Vec<String>,
184    #[builder(setter(custom), default)]
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub dimensions: Option<Vec<String>>,
187    #[builder(setter(custom), default)]
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub filters: Option<Vec<AnalyticsFilter>>,
190    #[builder(setter(into, strip_option), default)]
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub granularity: Option<String>,
193    #[builder(setter(strip_option), default)]
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub group_limit: Option<u32>,
196    #[builder(setter(strip_option), default)]
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub limit: Option<u32>,
199    #[builder(setter(strip_option), default)]
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub order_by: Option<AnalyticsOrderBy>,
202    #[builder(setter(strip_option), default)]
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub time_range: Option<AnalyticsTimeRange>,
205    #[builder(setter(strip_option), default)]
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub classifier_dimensions: Option<AnalyticsClassifierDimensions>,
208    #[builder(setter(strip_option), default)]
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub classifier_filters: Option<AnalyticsClassifierFilters>,
211}
212
213impl AnalyticsQueryRequest {
214    pub fn builder() -> AnalyticsQueryRequestBuilder {
215        AnalyticsQueryRequestBuilder::default()
216    }
217}
218
219impl AnalyticsQueryRequestBuilder {
220    pub fn metrics<T, S>(&mut self, items: T) -> &mut Self
221    where
222        T: IntoIterator<Item = S>,
223        S: Into<String>,
224    {
225        self.metrics = Some(items.into_iter().map(Into::into).collect());
226        self
227    }
228
229    strip_option_vec_setter!(dimensions, String);
230    strip_option_vec_setter!(filters, AnalyticsFilter);
231}
232
233/// Metadata returned with analytics query rows.
234#[derive(Serialize, Deserialize, Debug, Clone)]
235#[non_exhaustive]
236pub struct AnalyticsQueryMetadata {
237    pub query_time_ms: f64,
238    pub row_count: u64,
239    pub truncated: bool,
240    #[serde(flatten)]
241    pub extra: HashMap<String, Value>,
242}
243
244/// Analytics query result payload.
245#[derive(Serialize, Deserialize, Debug, Clone)]
246#[non_exhaustive]
247pub struct AnalyticsQueryResponse {
248    #[serde(default, rename = "cachedAt")]
249    pub cached_at: Option<f64>,
250    pub data: Vec<HashMap<String, Value>>,
251    pub metadata: AnalyticsQueryMetadata,
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub warnings: Option<Vec<String>>,
254    #[serde(flatten)]
255    pub extra: HashMap<String, Value>,
256}
257
258/// Get analytics query metadata (`GET /analytics/meta`).
259pub async fn get_analytics_meta(
260    base_url: &str,
261    management_key: &str,
262) -> Result<AnalyticsMeta, OpenRouterError> {
263    let http_client = crate::transport::new_client()?;
264    get_analytics_meta_with_client(&http_client, base_url, management_key).await
265}
266
267pub(crate) async fn get_analytics_meta_with_client(
268    http_client: &HttpClient,
269    base_url: &str,
270    management_key: &str,
271) -> Result<AnalyticsMeta, OpenRouterError> {
272    let url = format!("{base_url}/analytics/meta");
273    let response = transport_request::with_bearer_auth(
274        transport_request::get(http_client, &url),
275        management_key,
276    )
277    .send()
278    .await?;
279
280    if response.status().is_success() {
281        let payload: ApiResponse<AnalyticsMeta> =
282            transport_response::parse_json_response(response, "analytics meta").await?;
283        Ok(payload.data)
284    } else {
285        transport_response::handle_error(response).await?;
286        unreachable!()
287    }
288}
289
290/// Query analytics (`POST /analytics/query`).
291pub async fn query_analytics(
292    base_url: &str,
293    management_key: &str,
294    request: &AnalyticsQueryRequest,
295) -> Result<AnalyticsQueryResponse, OpenRouterError> {
296    let http_client = crate::transport::new_client()?;
297    query_analytics_with_client(&http_client, base_url, management_key, request).await
298}
299
300pub(crate) async fn query_analytics_with_client(
301    http_client: &HttpClient,
302    base_url: &str,
303    management_key: &str,
304    request: &AnalyticsQueryRequest,
305) -> Result<AnalyticsQueryResponse, OpenRouterError> {
306    let url = format!("{base_url}/analytics/query");
307    let response = transport_request::with_bearer_auth(
308        transport_request::post(http_client, &url),
309        management_key,
310    )
311    .json(request)
312    .send()
313    .await?;
314
315    if response.status().is_success() {
316        let payload: ApiResponse<AnalyticsQueryResponse> =
317            transport_response::parse_json_response(response, "analytics query").await?;
318        Ok(payload.data)
319    } else {
320        transport_response::handle_error(response).await?;
321        unreachable!()
322    }
323}