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// New model format responses using ProviderModelMetadata
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ProviderModelItemV2 {
163    pub provider_id: String,
164    pub provider_name: String,
165    pub category: String,
166    pub model: crate::types::models::ProviderModelMetadata,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct ProviderModelsResponseV2 {
171    pub items: Vec<ProviderModelItemV2>,
172    pub pagination: PaginationInfo,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct ModelDetailV2 {
177    pub provider_id: String,
178    pub provider_name: String,
179    pub category: String,
180    pub model: crate::types::models::ProviderModelMetadata,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub statistics: Option<ModelStatistics>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ModelDetail {
187    pub provider_id: String,
188    pub provider_name: String,
189    pub category: String,
190    pub model: Model,
191    pub statistics: Option<ModelStatistics>,
192}
193
194// Provider metadata endpoint types
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ProviderMetadataResponse {
197    pub provider_id: String,
198    pub provider_name: String,
199    pub category: String,
200    pub models: Vec<ModelInfo>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ModelInfo {
205    pub name: String,
206    pub pricing: Vec<PriceInfo>,
207}
208
209// User balance endpoint types
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct UserBalanceResponse {
212    pub balance: Vec<UserBalanceItem>,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct UserBalanceItem {
217    pub token: String,
218    pub available: u64,
219    pub locked: u64,
220    pub total: u64,
221}
222
223// Helper function to convert RequestSettled to UserActivityItem
224impl UserActivityItem {
225    pub fn from_request_settled(
226        receipt: RequestReceipt,
227        _provider_name: String,
228        model_name: Option<String>,
229    ) -> Self {
230        Self {
231            timestamp: receipt.timestamp,
232            provider_id: receipt.service_id.to_string(),
233            model_name,
234            prompt_tokens: receipt.prompt_tokens,
235            completion_tokens: receipt.completion_tokens,
236            spending_token: receipt.token_mint.to_string(),
237            spending_amount: receipt.amount,
238            finish_reason: receipt.finish_reason,
239            request_id: receipt.request_id.to_string(),
240            streamed: receipt.streamed,
241            latency: receipt.latency,
242            generation_time: receipt.generation_time,
243        }
244    }
245}
246
247// Leaderboard endpoint types
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct LeaderboardQuery {
250    pub start_date: Option<String>, // Format: YYYY-MM-DD, if not provided, use 30 days ago
251    pub end_date: Option<String>,   // Format: YYYY-MM-DD, if not provided, use current date
252}
253
254impl LeaderboardQuery {
255    pub fn get_date_range(&self) -> Result<(i64, i64)> {
256        use chrono::{Days, NaiveDate, Utc};
257
258        // Parse start date
259        let start_date = if let Some(start_str) = &self.start_date {
260            NaiveDate::parse_from_str(start_str, "%Y-%m-%d")
261                .with_context(|| format!("Invalid start_date format: {}", start_str))?
262        } else {
263            // Default to 30 days ago
264            Utc::now()
265                .date_naive()
266                .checked_sub_days(Days::new(30))
267                .context("Failed to calculate default start date")?
268        };
269
270        // Parse end date
271        let end_date = if let Some(end_str) = &self.end_date {
272            NaiveDate::parse_from_str(end_str, "%Y-%m-%d")
273                .with_context(|| format!("Invalid end_date format: {}", end_str))?
274        } else {
275            // Default to current date
276            Utc::now().date_naive()
277        };
278
279        // Validate date range
280        if start_date > end_date {
281            anyhow::bail!("start_date must be before or equal to end_date");
282        }
283
284        // Convert to UTC timestamps
285        let start_of_day = start_date
286            .and_hms_opt(0, 0, 0)
287            .context("Failed to create start time")?
288            .and_utc()
289            .timestamp_millis();
290
291        let end_of_day = end_date
292            .and_hms_opt(23, 59, 59)
293            .context("Failed to create end time")?
294            .and_utc()
295            .timestamp_millis();
296
297        Ok((start_of_day, end_of_day))
298    }
299
300    pub fn get_date_range_string(&self) -> Result<String> {
301        use chrono::{Days, NaiveDate, Utc};
302
303        let start_date = if let Some(start_str) = &self.start_date {
304            NaiveDate::parse_from_str(start_str, "%Y-%m-%d")
305                .with_context(|| format!("Invalid start_date format: {}", start_str))?
306        } else {
307            Utc::now()
308                .date_naive()
309                .checked_sub_days(Days::new(30))
310                .context("Failed to calculate default start date")?
311        };
312
313        let end_date = if let Some(end_str) = &self.end_date {
314            NaiveDate::parse_from_str(end_str, "%Y-%m-%d")
315                .with_context(|| format!("Invalid end_date format: {}", end_str))?
316        } else {
317            Utc::now().date_naive()
318        };
319
320        if start_date == end_date {
321            Ok(start_date.format("%Y-%m-%d").to_string())
322        } else {
323            Ok(format!(
324                "{} to {}",
325                start_date.format("%Y-%m-%d"),
326                end_date.format("%Y-%m-%d")
327            ))
328        }
329    }
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct DailyModelUsageItem {
334    pub rank: u64,
335    pub model_display_name: String,
336    pub token_usage: u64,
337    pub model_provider: String,
338    pub api_provider: String,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct DailyModelUsageData {
343    pub date: String, // YYYY-MM-DD format
344    pub items: Vec<DailyModelUsageItem>,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct ModelUsageLeaderboardResponse {
349    pub date_range: String,
350    pub daily_data: Vec<DailyModelUsageData>,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct DailyApiProviderUsageItem {
355    pub rank: u64,
356    pub api_provider_name: String,
357    pub token_usage: u64,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct DailyApiProviderUsageData {
362    pub date: String, // YYYY-MM-DD format
363    pub items: Vec<DailyApiProviderUsageItem>,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct ApiProviderUsageLeaderboardResponse {
368    pub date_range: String,
369    pub daily_data: Vec<DailyApiProviderUsageData>,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct DailyModelProviderUsageItem {
374    pub rank: u64,
375    pub model_provider_name: String,
376    pub token_usage: u64,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct DailyModelProviderUsageData {
381    pub date: String, // YYYY-MM-DD format
382    pub items: Vec<DailyModelProviderUsageItem>,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct ModelProviderUsageLeaderboardResponse {
387    pub date_range: String,
388    pub daily_data: Vec<DailyModelProviderUsageData>,
389}
390
391// Helper function to convert ProviderMetadata to responses
392impl ProviderModelItem {
393    pub fn from_provider_metadata(
394        provider_id: String,
395        metadata: &ProviderMetadata,
396        statistics: Option<ModelStatistics>,
397    ) -> Vec<Self> {
398        match metadata {
399            ProviderMetadata::ModelProvider(model_provider) => {
400                // Properly serialize the category to match the serde snake_case format
401                let category = serde_json::to_value(&model_provider.category)
402                    .ok()
403                    .and_then(|v| v.as_str().map(|s| s.to_string()))
404                    .unwrap_or_else(|| "unknown".to_string());
405
406                model_provider
407                    .models
408                    .iter()
409                    .map(|model| Self {
410                        provider_id: provider_id.clone(),
411                        provider_name: model_provider.name.clone(),
412                        category: category.clone(),
413                        model: model.clone(),
414                        statistics: statistics.clone(),
415                    })
416                    .collect()
417            }
418        }
419    }
420}
421
422impl ProviderMetadataResponse {
423    pub fn from_provider_metadata(provider_id: String, metadata: ProviderMetadata) -> Self {
424        match metadata {
425            ProviderMetadata::ModelProvider(model_provider) => {
426                // Properly serialize the category to match the serde snake_case format
427                let category = serde_json::to_value(&model_provider.category)
428                    .ok()
429                    .and_then(|v| v.as_str().map(|s| s.to_string()))
430                    .unwrap_or_else(|| "unknown".to_string());
431
432                Self {
433                    provider_id,
434                    provider_name: model_provider.name,
435                    category,
436                    models: model_provider
437                        .models
438                        .into_iter()
439                        .map(|model| ModelInfo {
440                            name: model.name,
441                            pricing: model
442                                .pricing
443                                .into_iter()
444                                .map(|p| PriceInfo {
445                                    token: p.token,
446                                    input_price: p.input_price,
447                                    output_price: p.output_price,
448                                })
449                                .collect(),
450                        })
451                        .collect(),
452                }
453            }
454        }
455    }
456}