use crate::models::{config::BudgetConfig, stats::StatsCache};
use chrono::{Datelike, Local};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlertLevel {
Safe,
Warning,
Critical,
Exceeded,
}
#[derive(Debug, Clone)]
pub struct QuotaStatus {
pub current_cost: f64,
pub budget_limit: Option<f64>,
pub usage_pct: f64,
pub projected_monthly_cost: f64,
pub projected_overage: Option<f64>,
pub alert_level: AlertLevel,
}
pub fn calculate_quota_status(stats: &StatsCache, budget: &BudgetConfig) -> QuotaStatus {
let current_cost = calculate_month_to_date_cost(stats);
let usage_pct = if let Some(limit) = budget.monthly_limit {
(current_cost / limit * 100.0).min(999.9)
} else {
0.0
};
let alert_level = determine_alert_level(usage_pct, budget);
let projected_monthly_cost = project_monthly_cost(stats, current_cost);
let projected_overage = if let Some(limit) = budget.monthly_limit {
if projected_monthly_cost > limit {
Some(projected_monthly_cost - limit)
} else {
None
}
} else {
None
};
QuotaStatus {
current_cost,
budget_limit: budget.monthly_limit,
usage_pct,
projected_monthly_cost,
projected_overage,
alert_level,
}
}
fn calculate_month_to_date_cost(stats: &StatsCache) -> f64 {
let total_cost: f64 = stats.model_usage.values().map(|m| m.cost_usd).sum();
if total_cost == 0.0 {
return 0.0;
}
let now = Local::now();
let month_prefix = format!("{}-{:02}", now.year(), now.month());
let mtd_tokens: u64 = stats
.daily_model_tokens
.iter()
.filter(|d| d.date.starts_with(&month_prefix))
.flat_map(|d| d.tokens_by_model.values())
.sum();
let total_tokens: u64 = stats
.model_usage
.values()
.map(|m| m.input_tokens + m.output_tokens)
.sum();
if total_tokens == 0 {
return 0.0;
}
total_cost * (mtd_tokens as f64 / total_tokens as f64)
}
fn project_monthly_cost(_stats: &StatsCache, mtd_cost: f64) -> f64 {
let now = Local::now();
let current_day = now.day() as f64;
if current_day < 1.0 {
return mtd_cost * 30.0; }
let daily_avg = mtd_cost / current_day;
daily_avg * 30.0
}
fn determine_alert_level(usage_pct: f64, budget: &BudgetConfig) -> AlertLevel {
if usage_pct >= 100.0 {
AlertLevel::Exceeded
} else if usage_pct >= budget.critical_threshold {
AlertLevel::Critical
} else if usage_pct >= budget.warning_threshold {
AlertLevel::Warning
} else {
AlertLevel::Safe
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::stats::ModelUsage;
use std::collections::HashMap;
fn mock_stats_with_mtd_ratio(total_cost: f64, mtd_ratio: f64, first_date: &str) -> StatsCache {
use crate::models::stats::DailyModelTokens;
let total_tokens = 15000u64;
let mtd_tokens = (total_tokens as f64 * mtd_ratio) as u64;
let mut model_usage = HashMap::new();
model_usage.insert(
"claude-sonnet-4".to_string(),
ModelUsage {
input_tokens: total_tokens * 2 / 3,
output_tokens: total_tokens / 3,
cost_usd: total_cost,
..Default::default()
},
);
let now = Local::now();
let current_day = now.day();
let mut daily_model_tokens = Vec::new();
if mtd_tokens > 0 {
for day in 1..=current_day {
let mut tokens_by_model = HashMap::new();
tokens_by_model.insert(
"claude-sonnet-4".to_string(),
mtd_tokens / current_day as u64,
);
daily_model_tokens.push(DailyModelTokens {
date: format!("{}-{:02}-{:02}", now.year(), now.month(), day),
tokens_by_model,
});
}
}
StatsCache {
model_usage,
daily_model_tokens,
first_session_date: Some(first_date.to_string()),
..Default::default()
}
}
fn mock_stats_with_total_cost(total_cost: f64, first_date: &str) -> StatsCache {
mock_stats_with_mtd_ratio(total_cost, 1.0, first_date)
}
#[test]
fn test_quota_status_safe() {
let stats = mock_stats_with_mtd_ratio(100.0, 0.2, "2024-01-01");
let budget = BudgetConfig {
monthly_limit: Some(50.0),
warning_threshold: 75.0,
critical_threshold: 90.0,
};
let status = calculate_quota_status(&stats, &budget);
assert_eq!(status.alert_level, AlertLevel::Safe);
assert!(status.usage_pct < 75.0);
}
#[test]
fn test_quota_status_warning() {
let stats = mock_stats_with_total_cost(500.0, "2024-01-01");
let budget = BudgetConfig {
monthly_limit: Some(100.0),
warning_threshold: 75.0,
critical_threshold: 90.0,
};
let status = calculate_quota_status(&stats, &budget);
assert!(
status.alert_level == AlertLevel::Warning
|| status.alert_level == AlertLevel::Critical
|| status.alert_level == AlertLevel::Exceeded
);
}
#[test]
fn test_quota_no_limit() {
let stats = mock_stats_with_total_cost(1000.0, "2024-01-01");
let budget = BudgetConfig {
monthly_limit: None,
warning_threshold: 75.0,
critical_threshold: 90.0,
};
let status = calculate_quota_status(&stats, &budget);
assert_eq!(status.alert_level, AlertLevel::Safe);
assert_eq!(status.usage_pct, 0.0);
assert!(status.projected_overage.is_none());
}
#[test]
fn test_determine_alert_level() {
let budget = BudgetConfig {
monthly_limit: Some(100.0),
warning_threshold: 75.0,
critical_threshold: 90.0,
};
assert_eq!(determine_alert_level(50.0, &budget), AlertLevel::Safe);
assert_eq!(determine_alert_level(75.0, &budget), AlertLevel::Warning);
assert_eq!(determine_alert_level(90.0, &budget), AlertLevel::Critical);
assert_eq!(determine_alert_level(100.0, &budget), AlertLevel::Exceeded);
assert_eq!(determine_alert_level(120.0, &budget), AlertLevel::Exceeded);
}
}