use aimo_core::{
provider::{Model, ProviderMetadata},
receipt::RequestReceipt,
};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationQuery {
pub page: Option<String>,
pub limit: Option<String>,
}
impl PaginationQuery {
pub fn get_page(&self) -> u64 {
self.page
.as_ref()
.and_then(|p| p.parse().ok())
.unwrap_or(1)
.max(1)
}
pub fn get_limit(&self) -> u64 {
self.limit
.as_ref()
.and_then(|l| l.parse().ok())
.unwrap_or(20)
.min(100)
.max(1)
}
}
impl Default for PaginationQuery {
fn default() -> Self {
Self {
page: Some("1".to_string()),
limit: Some("20".to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SortQuery {
pub sort_by: Option<String>,
pub sort_order: Option<SortOrder>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SortOrder {
Asc,
Desc,
}
impl Default for SortOrder {
fn default() -> Self {
Self::Desc
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserActivityQuery {
#[serde(flatten)]
pub pagination: PaginationQuery,
#[serde(flatten)]
pub sort: SortQuery,
pub provider_id: Option<String>,
pub model_name: Option<String>,
pub from_timestamp: Option<String>,
pub to_timestamp: Option<String>,
}
impl UserActivityQuery {
pub fn get_from_timestamp(&self) -> Option<i64> {
self.from_timestamp.as_ref().and_then(|t| t.parse().ok())
}
pub fn get_to_timestamp(&self) -> Option<i64> {
self.to_timestamp.as_ref().and_then(|t| t.parse().ok())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserActivityItem {
pub timestamp: i64,
pub provider_id: String,
pub model_name: Option<String>,
pub prompt_tokens: u64,
pub completion_tokens: u64,
pub spending_token: String,
pub spending_amount: u64,
pub finish_reason: String,
pub request_id: String,
pub streamed: Option<bool>,
pub latency: Option<i64>,
pub generation_time: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserActivityResponse {
pub items: Vec<UserActivityItem>,
pub pagination: PaginationInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationInfo {
pub page: u64,
pub limit: u64,
pub total_items: u64,
pub total_pages: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderModelsQuery {
#[serde(flatten)]
pub pagination: PaginationQuery,
#[serde(flatten)]
pub sort: SortQuery,
pub provider_id: Option<String>,
pub provider_name: Option<String>,
pub model_name: Option<String>,
pub category: Option<String>,
pub include_stats: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderModelItem {
pub provider_id: String,
pub provider_name: String,
pub category: String,
pub model: Model,
#[serde(skip_serializing_if = "Option::is_none")]
pub statistics: Option<ModelStatistics>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelStatistics {
pub total_requests: u64,
pub total_tokens: u64,
pub avg_latency: f64,
pub avg_generation_time: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceInfo {
pub token: String,
pub input_price: u64,
pub output_price: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderModelsResponse {
pub items: Vec<ProviderModelItem>,
pub pagination: PaginationInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderModelItemV2 {
pub provider_id: String,
pub provider_name: String,
pub category: String,
pub model: crate::types::models::ProviderModelMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderModelsResponseV2 {
pub items: Vec<ProviderModelItemV2>,
pub pagination: PaginationInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelDetailV2 {
pub provider_id: String,
pub provider_name: String,
pub category: String,
pub model: crate::types::models::ProviderModelMetadata,
#[serde(skip_serializing_if = "Option::is_none")]
pub statistics: Option<ModelStatistics>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelDetail {
pub provider_id: String,
pub provider_name: String,
pub category: String,
pub model: Model,
pub statistics: Option<ModelStatistics>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderMetadataResponse {
pub provider_id: String,
pub provider_name: String,
pub category: String,
pub models: Vec<ModelInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelInfo {
pub name: String,
pub pricing: Vec<PriceInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserBalanceResponse {
pub balance: Vec<UserBalanceItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserBalanceItem {
pub token: String,
pub available: u64,
pub locked: u64,
pub total: u64,
}
impl UserActivityItem {
pub fn from_request_settled(
receipt: RequestReceipt,
_provider_name: String,
model_name: Option<String>,
) -> Self {
Self {
timestamp: receipt.timestamp,
provider_id: receipt.service_id.to_string(),
model_name,
prompt_tokens: receipt.prompt_tokens,
completion_tokens: receipt.completion_tokens,
spending_token: receipt.token_mint.to_string(),
spending_amount: receipt.amount,
finish_reason: receipt.finish_reason,
request_id: receipt.request_id.to_string(),
streamed: receipt.streamed,
latency: receipt.latency,
generation_time: receipt.generation_time,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeaderboardQuery {
pub start_date: Option<String>, pub end_date: Option<String>, }
impl LeaderboardQuery {
pub fn get_date_range(&self) -> Result<(i64, i64)> {
use chrono::{Days, NaiveDate, Utc};
let start_date = if let Some(start_str) = &self.start_date {
NaiveDate::parse_from_str(start_str, "%Y-%m-%d")
.with_context(|| format!("Invalid start_date format: {}", start_str))?
} else {
Utc::now()
.date_naive()
.checked_sub_days(Days::new(30))
.context("Failed to calculate default start date")?
};
let end_date = if let Some(end_str) = &self.end_date {
NaiveDate::parse_from_str(end_str, "%Y-%m-%d")
.with_context(|| format!("Invalid end_date format: {}", end_str))?
} else {
Utc::now().date_naive()
};
if start_date > end_date {
anyhow::bail!("start_date must be before or equal to end_date");
}
let start_of_day = start_date
.and_hms_opt(0, 0, 0)
.context("Failed to create start time")?
.and_utc()
.timestamp_millis();
let end_of_day = end_date
.and_hms_opt(23, 59, 59)
.context("Failed to create end time")?
.and_utc()
.timestamp_millis();
Ok((start_of_day, end_of_day))
}
pub fn get_date_range_string(&self) -> Result<String> {
use chrono::{Days, NaiveDate, Utc};
let start_date = if let Some(start_str) = &self.start_date {
NaiveDate::parse_from_str(start_str, "%Y-%m-%d")
.with_context(|| format!("Invalid start_date format: {}", start_str))?
} else {
Utc::now()
.date_naive()
.checked_sub_days(Days::new(30))
.context("Failed to calculate default start date")?
};
let end_date = if let Some(end_str) = &self.end_date {
NaiveDate::parse_from_str(end_str, "%Y-%m-%d")
.with_context(|| format!("Invalid end_date format: {}", end_str))?
} else {
Utc::now().date_naive()
};
if start_date == end_date {
Ok(start_date.format("%Y-%m-%d").to_string())
} else {
Ok(format!(
"{} to {}",
start_date.format("%Y-%m-%d"),
end_date.format("%Y-%m-%d")
))
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DailyModelUsageItem {
pub rank: u64,
pub model_display_name: String,
pub token_usage: u64,
pub model_provider: String,
pub api_provider: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DailyModelUsageData {
pub date: String, pub items: Vec<DailyModelUsageItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelUsageLeaderboardResponse {
pub date_range: String,
pub daily_data: Vec<DailyModelUsageData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DailyApiProviderUsageItem {
pub rank: u64,
pub api_provider_name: String,
pub token_usage: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DailyApiProviderUsageData {
pub date: String, pub items: Vec<DailyApiProviderUsageItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiProviderUsageLeaderboardResponse {
pub date_range: String,
pub daily_data: Vec<DailyApiProviderUsageData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DailyModelProviderUsageItem {
pub rank: u64,
pub model_provider_name: String,
pub token_usage: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DailyModelProviderUsageData {
pub date: String, pub items: Vec<DailyModelProviderUsageItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelProviderUsageLeaderboardResponse {
pub date_range: String,
pub daily_data: Vec<DailyModelProviderUsageData>,
}
impl ProviderModelItem {
pub fn from_provider_metadata(
provider_id: String,
metadata: &ProviderMetadata,
statistics: Option<ModelStatistics>,
) -> Vec<Self> {
match metadata {
ProviderMetadata::ModelProvider(model_provider) => {
let category = serde_json::to_value(&model_provider.category)
.ok()
.and_then(|v| v.as_str().map(|s| s.to_string()))
.unwrap_or_else(|| "unknown".to_string());
model_provider
.models
.iter()
.map(|model| Self {
provider_id: provider_id.clone(),
provider_name: model_provider.name.clone(),
category: category.clone(),
model: model.clone(),
statistics: statistics.clone(),
})
.collect()
}
}
}
}
impl ProviderMetadataResponse {
pub fn from_provider_metadata(provider_id: String, metadata: ProviderMetadata) -> Self {
match metadata {
ProviderMetadata::ModelProvider(model_provider) => {
let category = serde_json::to_value(&model_provider.category)
.ok()
.and_then(|v| v.as_str().map(|s| s.to_string()))
.unwrap_or_else(|| "unknown".to_string());
Self {
provider_id,
provider_name: model_provider.name,
category,
models: model_provider
.models
.into_iter()
.map(|model| ModelInfo {
name: model.name,
pricing: model
.pricing
.into_iter()
.map(|p| PriceInfo {
token: p.token,
input_price: p.input_price,
output_price: p.output_price,
})
.collect(),
})
.collect(),
}
}
}
}
}