use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum PlanType {
Subscription,
Metered,
#[default]
Unknown,
}
#[derive(Debug, Clone, Serialize)]
pub struct RateWindow {
pub name: String,
pub used: Option<f64>,
pub limit: Option<f64>,
pub remaining_percent: Option<f64>,
pub resets_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct QuotaSnapshot {
pub provider: String,
pub plan: Option<String>,
#[serde(default)]
pub plan_type: PlanType,
#[serde(default)]
pub token_limit: Option<f64>,
pub rate_windows: Vec<RateWindow>,
pub fetched_at: DateTime<Utc>,
pub error: Option<String>,
}
impl QuotaSnapshot {
pub fn blank(provider: impl Into<String>) -> Self {
Self {
provider: provider.into(),
plan: None,
plan_type: PlanType::Unknown,
token_limit: None,
rate_windows: Vec::new(),
fetched_at: Utc::now(),
error: None,
}
}
pub fn is_subscription_signal(&self) -> bool {
if self.error.is_some() || self.plan_type != PlanType::Subscription {
return false;
}
self.rate_windows
.iter()
.any(|w| w.remaining_percent.is_some() || w.resets_at.is_some())
}
pub fn best_remaining_percent(&self) -> Option<f64> {
self.rate_windows
.iter()
.filter_map(|w| w.remaining_percent)
.next()
}
pub fn best_resets_at(&self) -> Option<DateTime<Utc>> {
self.rate_windows.iter().filter_map(|w| w.resets_at).next()
}
}
#[async_trait]
pub trait QuotaFetcher: Send + Sync {
fn provider(&self) -> &str;
fn has_credentials(&self, api_key: Option<&str>) -> bool {
api_key.is_some_and(|k| !k.is_empty())
}
async fn fetch(&self, api_key: Option<&str>) -> anyhow::Result<QuotaSnapshot>;
}