1use aimo_core::{
2 provider::{Model, ProviderMetadata},
3 receipt::RequestReceipt,
4};
5use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7
8#[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#[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#[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#[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#[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
198impl 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#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct LeaderboardQuery {
225 pub start_date: Option<String>, pub end_date: Option<String>, }
228
229impl LeaderboardQuery {
230 pub fn get_date_range(&self) -> Result<(i64, i64)> {
231 use chrono::{Days, NaiveDate, Utc};
232
233 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 Utc::now()
240 .date_naive()
241 .checked_sub_days(Days::new(30))
242 .context("Failed to calculate default start date")?
243 };
244
245 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 Utc::now().date_naive()
252 };
253
254 if start_date > end_date {
256 anyhow::bail!("start_date must be before or equal to end_date");
257 }
258
259 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, 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, 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, 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
366impl 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 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 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}