aimo_client/types/
statistics.rs

1use aimo_core::{
2    provider::{Model, ProviderMetadata},
3    receipt::RequestReceipt,
4};
5use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7
8// Common pagination and filtering structures
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PaginationQuery {
11    pub page: Option<String>,
12    pub limit: Option<String>,
13}
14
15impl PaginationQuery {
16    pub fn get_page(&self) -> u64 {
17        self.page
18            .as_ref()
19            .and_then(|p| p.parse().ok())
20            .unwrap_or(1)
21            .max(1)
22    }
23
24    pub fn get_limit(&self) -> u64 {
25        self.limit
26            .as_ref()
27            .and_then(|l| l.parse().ok())
28            .unwrap_or(20)
29            .min(100)
30            .max(1)
31    }
32}
33
34impl Default for PaginationQuery {
35    fn default() -> Self {
36        Self {
37            page: Some("1".to_string()),
38            limit: Some("20".to_string()),
39        }
40    }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SortQuery {
45    pub sort_by: Option<String>,
46    pub sort_order: Option<SortOrder>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum SortOrder {
52    Asc,
53    Desc,
54}
55
56impl Default for SortOrder {
57    fn default() -> Self {
58        Self::Desc
59    }
60}
61
62// User activity endpoint types
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct UserActivityQuery {
65    #[serde(flatten)]
66    pub pagination: PaginationQuery,
67    #[serde(flatten)]
68    pub sort: SortQuery,
69    pub provider_id: Option<String>,
70    pub model_name: Option<String>,
71    pub from_timestamp: Option<String>,
72    pub to_timestamp: Option<String>,
73}
74
75impl UserActivityQuery {
76    pub fn get_from_timestamp(&self) -> Option<i64> {
77        self.from_timestamp.as_ref().and_then(|t| t.parse().ok())
78    }
79
80    pub fn get_to_timestamp(&self) -> Option<i64> {
81        self.to_timestamp.as_ref().and_then(|t| t.parse().ok())
82    }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct UserActivityItem {
87    pub timestamp: i64,
88    pub provider_id: String,
89    pub model_name: Option<String>,
90    pub prompt_tokens: u64,
91    pub completion_tokens: u64,
92    pub spending_token: String,
93    pub spending_amount: u64,
94    pub finish_reason: String,
95    pub request_id: String,
96    pub streamed: Option<bool>,
97    pub latency: Option<i64>,
98    pub generation_time: Option<i64>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct UserActivityResponse {
103    pub items: Vec<UserActivityItem>,
104    pub pagination: PaginationInfo,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct PaginationInfo {
109    pub page: u64,
110    pub limit: u64,
111    pub total_items: u64,
112    pub total_pages: u64,
113}
114
115// Provider models endpoint types
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct ProviderModelsQuery {
118    #[serde(flatten)]
119    pub pagination: PaginationQuery,
120    #[serde(flatten)]
121    pub sort: SortQuery,
122    pub provider_id: Option<String>,
123    pub provider_name: Option<String>,
124    pub model_name: Option<String>,
125    pub category: Option<String>,
126    pub include_stats: Option<bool>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ProviderModelItem {
131    pub provider_id: String,
132    pub provider_name: String,
133    pub category: String,
134    pub model: Model,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub statistics: Option<ModelStatistics>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ModelStatistics {
141    pub total_requests: u64,
142    pub total_tokens: u64,
143    pub avg_latency: f64,
144    pub avg_generation_time: f64,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct PriceInfo {
149    pub token: String,
150    pub input_price: u64,
151    pub output_price: u64,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ProviderModelsResponse {
156    pub items: Vec<ProviderModelItem>,
157    pub pagination: PaginationInfo,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ModelDetail {
162    pub provider_id: String,
163    pub provider_name: String,
164    pub category: String,
165    pub model: Model,
166    pub statistics: Option<ModelStatistics>,
167}
168
169// Provider metadata endpoint types
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct ProviderMetadataResponse {
172    pub provider_id: String,
173    pub provider_name: String,
174    pub category: String,
175    pub models: Vec<ModelInfo>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ModelInfo {
180    pub name: String,
181    pub pricing: Vec<PriceInfo>,
182}
183
184// User balance endpoint types
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct UserBalanceResponse {
187    pub balance: Vec<UserBalanceItem>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct UserBalanceItem {
192    pub token: String,
193    pub available: u64,
194    pub locked: u64,
195    pub total: u64,
196}
197
198// Helper function to convert RequestSettled to UserActivityItem
199impl UserActivityItem {
200    pub fn from_request_settled(
201        receipt: RequestReceipt,
202        _provider_name: String,
203        model_name: Option<String>,
204    ) -> Self {
205        Self {
206            timestamp: receipt.timestamp,
207            provider_id: receipt.service_id.to_string(),
208            model_name,
209            prompt_tokens: receipt.prompt_tokens,
210            completion_tokens: receipt.completion_tokens,
211            spending_token: receipt.token_mint.to_string(),
212            spending_amount: receipt.amount,
213            finish_reason: receipt.finish_reason,
214            request_id: receipt.request_id.to_string(),
215            streamed: receipt.streamed,
216            latency: receipt.latency,
217            generation_time: receipt.generation_time,
218        }
219    }
220}
221
222// Leaderboard endpoint types
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct LeaderboardQuery {
225    pub start_date: Option<String>, // Format: YYYY-MM-DD, if not provided, use 30 days ago
226    pub end_date: Option<String>,   // Format: YYYY-MM-DD, if not provided, use current date
227}
228
229impl LeaderboardQuery {
230    pub fn get_date_range(&self) -> Result<(i64, i64)> {
231        use chrono::{Days, NaiveDate, Utc};
232
233        // Parse start date
234        let start_date = if let Some(start_str) = &self.start_date {
235            NaiveDate::parse_from_str(start_str, "%Y-%m-%d")
236                .with_context(|| format!("Invalid start_date format: {}", start_str))?
237        } else {
238            // Default to 30 days ago
239            Utc::now()
240                .date_naive()
241                .checked_sub_days(Days::new(30))
242                .context("Failed to calculate default start date")?
243        };
244
245        // Parse end date
246        let end_date = if let Some(end_str) = &self.end_date {
247            NaiveDate::parse_from_str(end_str, "%Y-%m-%d")
248                .with_context(|| format!("Invalid end_date format: {}", end_str))?
249        } else {
250            // Default to current date
251            Utc::now().date_naive()
252        };
253
254        // Validate date range
255        if start_date > end_date {
256            anyhow::bail!("start_date must be before or equal to end_date");
257        }
258
259        // Convert to UTC timestamps
260        let start_of_day = start_date
261            .and_hms_opt(0, 0, 0)
262            .context("Failed to create start time")?
263            .and_utc()
264            .timestamp_millis();
265
266        let end_of_day = end_date
267            .and_hms_opt(23, 59, 59)
268            .context("Failed to create end time")?
269            .and_utc()
270            .timestamp_millis();
271
272        Ok((start_of_day, end_of_day))
273    }
274
275    pub fn get_date_range_string(&self) -> Result<String> {
276        use chrono::{Days, NaiveDate, Utc};
277
278        let start_date = if let Some(start_str) = &self.start_date {
279            NaiveDate::parse_from_str(start_str, "%Y-%m-%d")
280                .with_context(|| format!("Invalid start_date format: {}", start_str))?
281        } else {
282            Utc::now()
283                .date_naive()
284                .checked_sub_days(Days::new(30))
285                .context("Failed to calculate default start date")?
286        };
287
288        let end_date = if let Some(end_str) = &self.end_date {
289            NaiveDate::parse_from_str(end_str, "%Y-%m-%d")
290                .with_context(|| format!("Invalid end_date format: {}", end_str))?
291        } else {
292            Utc::now().date_naive()
293        };
294
295        if start_date == end_date {
296            Ok(start_date.format("%Y-%m-%d").to_string())
297        } else {
298            Ok(format!(
299                "{} to {}",
300                start_date.format("%Y-%m-%d"),
301                end_date.format("%Y-%m-%d")
302            ))
303        }
304    }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct DailyModelUsageItem {
309    pub rank: u64,
310    pub model_display_name: String,
311    pub token_usage: u64,
312    pub model_provider: String,
313    pub api_provider: String,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct DailyModelUsageData {
318    pub date: String, // YYYY-MM-DD format
319    pub items: Vec<DailyModelUsageItem>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct ModelUsageLeaderboardResponse {
324    pub date_range: String,
325    pub daily_data: Vec<DailyModelUsageData>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct DailyApiProviderUsageItem {
330    pub rank: u64,
331    pub api_provider_name: String,
332    pub token_usage: u64,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct DailyApiProviderUsageData {
337    pub date: String, // YYYY-MM-DD format
338    pub items: Vec<DailyApiProviderUsageItem>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct ApiProviderUsageLeaderboardResponse {
343    pub date_range: String,
344    pub daily_data: Vec<DailyApiProviderUsageData>,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct DailyModelProviderUsageItem {
349    pub rank: u64,
350    pub model_provider_name: String,
351    pub token_usage: u64,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct DailyModelProviderUsageData {
356    pub date: String, // YYYY-MM-DD format
357    pub items: Vec<DailyModelProviderUsageItem>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct ModelProviderUsageLeaderboardResponse {
362    pub date_range: String,
363    pub daily_data: Vec<DailyModelProviderUsageData>,
364}
365
366// Helper function to convert ProviderMetadata to responses
367impl ProviderModelItem {
368    pub fn from_provider_metadata(
369        provider_id: String,
370        metadata: &ProviderMetadata,
371        statistics: Option<ModelStatistics>,
372    ) -> Vec<Self> {
373        match metadata {
374            ProviderMetadata::ModelProvider(model_provider) => {
375                // Properly serialize the category to match the serde snake_case format
376                let category = serde_json::to_value(&model_provider.category)
377                    .ok()
378                    .and_then(|v| v.as_str().map(|s| s.to_string()))
379                    .unwrap_or_else(|| "unknown".to_string());
380
381                model_provider
382                    .models
383                    .iter()
384                    .map(|model| Self {
385                        provider_id: provider_id.clone(),
386                        provider_name: model_provider.name.clone(),
387                        category: category.clone(),
388                        model: model.clone(),
389                        statistics: statistics.clone(),
390                    })
391                    .collect()
392            }
393        }
394    }
395}
396
397impl ProviderMetadataResponse {
398    pub fn from_provider_metadata(provider_id: String, metadata: ProviderMetadata) -> Self {
399        match metadata {
400            ProviderMetadata::ModelProvider(model_provider) => {
401                // Properly serialize the category to match the serde snake_case format
402                let category = serde_json::to_value(&model_provider.category)
403                    .ok()
404                    .and_then(|v| v.as_str().map(|s| s.to_string()))
405                    .unwrap_or_else(|| "unknown".to_string());
406
407                Self {
408                    provider_id,
409                    provider_name: model_provider.name,
410                    category,
411                    models: model_provider
412                        .models
413                        .into_iter()
414                        .map(|model| ModelInfo {
415                            name: model.name,
416                            pricing: model
417                                .pricing
418                                .into_iter()
419                                .map(|p| PriceInfo {
420                                    token: p.token,
421                                    input_price: p.input_price,
422                                    output_price: p.output_price,
423                                })
424                                .collect(),
425                        })
426                        .collect(),
427                }
428            }
429        }
430    }
431}