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)]
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#[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#[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
223impl 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#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct LeaderboardQuery {
250 pub start_date: Option<String>, pub end_date: Option<String>, }
253
254impl LeaderboardQuery {
255 pub fn get_date_range(&self) -> Result<(i64, i64)> {
256 use chrono::{Days, NaiveDate, Utc};
257
258 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 Utc::now()
265 .date_naive()
266 .checked_sub_days(Days::new(30))
267 .context("Failed to calculate default start date")?
268 };
269
270 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 Utc::now().date_naive()
277 };
278
279 if start_date > end_date {
281 anyhow::bail!("start_date must be before or equal to end_date");
282 }
283
284 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, 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, 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, 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
391impl 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 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 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}