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#[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#[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#[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#[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#[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#[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#[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#[derive(Serialize, Deserialize, Debug, Clone)]
108pub struct AnalyticsFilter {
109 pub field: String,
110 pub operator: String,
111 pub value: AnalyticsFilterValue,
112}
113
114#[derive(Serialize, Deserialize, Debug, Clone)]
116pub struct AnalyticsTimeRange {
117 pub start: String,
118 pub end: String,
119}
120
121#[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#[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#[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#[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
258pub 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
290pub 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}