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)]
130#[builder(build_fn(error = "OpenRouterError"))]
131#[non_exhaustive]
132pub struct AnalyticsQueryRequest {
133 #[builder(setter(custom))]
134 pub metrics: Vec<String>,
135 #[builder(setter(custom), default)]
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub dimensions: Option<Vec<String>>,
138 #[builder(setter(custom), default)]
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub filters: Option<Vec<AnalyticsFilter>>,
141 #[builder(setter(into, strip_option), default)]
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub granularity: Option<String>,
144 #[builder(setter(strip_option), default)]
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub group_limit: Option<u32>,
147 #[builder(setter(strip_option), default)]
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub limit: Option<u32>,
150 #[builder(setter(strip_option), default)]
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub order_by: Option<AnalyticsOrderBy>,
153 #[builder(setter(strip_option), default)]
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub time_range: Option<AnalyticsTimeRange>,
156}
157
158impl AnalyticsQueryRequest {
159 pub fn builder() -> AnalyticsQueryRequestBuilder {
160 AnalyticsQueryRequestBuilder::default()
161 }
162}
163
164impl AnalyticsQueryRequestBuilder {
165 pub fn metrics<T, S>(&mut self, items: T) -> &mut Self
166 where
167 T: IntoIterator<Item = S>,
168 S: Into<String>,
169 {
170 self.metrics = Some(items.into_iter().map(Into::into).collect());
171 self
172 }
173
174 strip_option_vec_setter!(dimensions, String);
175 strip_option_vec_setter!(filters, AnalyticsFilter);
176}
177
178#[derive(Serialize, Deserialize, Debug, Clone)]
180#[non_exhaustive]
181pub struct AnalyticsQueryMetadata {
182 pub query_time_ms: f64,
183 pub row_count: u64,
184 pub truncated: bool,
185 #[serde(flatten)]
186 pub extra: HashMap<String, Value>,
187}
188
189#[derive(Serialize, Deserialize, Debug, Clone)]
191#[non_exhaustive]
192pub struct AnalyticsQueryResponse {
193 #[serde(default, rename = "cachedAt")]
194 pub cached_at: Option<f64>,
195 pub data: Vec<HashMap<String, Value>>,
196 pub metadata: AnalyticsQueryMetadata,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub warnings: Option<Vec<String>>,
199 #[serde(flatten)]
200 pub extra: HashMap<String, Value>,
201}
202
203pub async fn get_analytics_meta(
205 base_url: &str,
206 management_key: &str,
207) -> Result<AnalyticsMeta, OpenRouterError> {
208 let http_client = crate::transport::new_client()?;
209 get_analytics_meta_with_client(&http_client, base_url, management_key).await
210}
211
212pub(crate) async fn get_analytics_meta_with_client(
213 http_client: &HttpClient,
214 base_url: &str,
215 management_key: &str,
216) -> Result<AnalyticsMeta, OpenRouterError> {
217 let url = format!("{base_url}/analytics/meta");
218 let response = transport_request::with_bearer_auth(
219 transport_request::get(http_client, &url),
220 management_key,
221 )
222 .send()
223 .await?;
224
225 if response.status().is_success() {
226 let payload: ApiResponse<AnalyticsMeta> =
227 transport_response::parse_json_response(response, "analytics meta").await?;
228 Ok(payload.data)
229 } else {
230 transport_response::handle_error(response).await?;
231 unreachable!()
232 }
233}
234
235pub async fn query_analytics(
237 base_url: &str,
238 management_key: &str,
239 request: &AnalyticsQueryRequest,
240) -> Result<AnalyticsQueryResponse, OpenRouterError> {
241 let http_client = crate::transport::new_client()?;
242 query_analytics_with_client(&http_client, base_url, management_key, request).await
243}
244
245pub(crate) async fn query_analytics_with_client(
246 http_client: &HttpClient,
247 base_url: &str,
248 management_key: &str,
249 request: &AnalyticsQueryRequest,
250) -> Result<AnalyticsQueryResponse, OpenRouterError> {
251 let url = format!("{base_url}/analytics/query");
252 let response = transport_request::with_bearer_auth(
253 transport_request::post(http_client, &url),
254 management_key,
255 )
256 .json(request)
257 .send()
258 .await?;
259
260 if response.status().is_success() {
261 let payload: ApiResponse<AnalyticsQueryResponse> =
262 transport_response::parse_json_response(response, "analytics query").await?;
263 Ok(payload.data)
264 } else {
265 transport_response::handle_error(response).await?;
266 unreachable!()
267 }
268}