use serde_json::Value;
#[derive(Debug, Clone, PartialEq)]
pub struct Quota {
pub percentage: f64,
pub resets_at: Option<i64>,
}
pub fn is_spent(quota: &Quota) -> bool {
quota.percentage >= 100.0
}
pub const QUOTA_TTL_MS: i64 = 60_000;
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status: u16,
pub body: Option<Value>,
}
impl HttpResponse {
pub fn ok(&self) -> bool {
(200..300).contains(&self.status)
}
}
#[derive(Debug, thiserror::Error)]
#[error("{0}")]
pub struct FetchError(pub String);
#[derive(Debug, Clone)]
pub struct HttpRequest {
pub headers: Vec<(String, String)>,
pub timeout_ms: u64,
}
pub trait Fetch: Send + Sync {
fn fetch(
&self,
url: String,
request: HttpRequest,
) -> impl std::future::Future<Output = Result<HttpResponse, FetchError>> + Send;
}
pub struct QuotaGate<R> {
read: R,
now: Box<dyn Fn() -> i64 + Send + Sync>,
held: Option<Quota>,
held_at: i64,
}
impl<R, F> QuotaGate<R>
where
R: Fn() -> F,
F: Future<Output = Option<Quota>>,
{
pub fn new(read: R, now: impl Fn() -> i64 + Send + Sync + 'static) -> Self {
Self {
read,
now: Box::new(now),
held: None,
held_at: 0,
}
}
pub async fn current(&mut self) -> Option<Quota> {
let at = (self.now)();
if let Some(held) = &self.held {
let until = held.resets_at;
if is_spent(held) && until.is_some_and(|until| at < until) {
return self.held.clone();
}
if !is_spent(held) && at - self.held_at < QUOTA_TTL_MS {
return self.held.clone();
}
}
let fresh = (self.read)().await?;
self.held = Some(fresh.clone());
self.held_at = at;
Some(fresh)
}
#[allow(
dead_code,
reason = "the tests expire the held window rather than waiting out its lifetime"
)]
pub fn forget(&mut self) {
self.held = None;
self.held_at = 0;
}
}
pub struct UsageSource<R> {
pub provider: String,
pub gate: QuotaGate<R>,
}
pub fn spent_message(provider: &str, relative: Option<&str>) -> String {
let back = relative.map_or(String::new(), |relative| format!("; it resets {relative}"));
format!("{provider}'s usage window is spent, so this cannot run yet{back}")
}
pub const UNKNOWN_QUOTA: &str = "the model provider did not say what is left of the usage window";
pub fn quota_message(provider: &str, quota: &Quota, relative: Option<&str>) -> String {
let left = (100.0 - quota.percentage).round().max(0.0);
let state = if is_spent(quota) {
format!("{provider}'s usage window is spent")
} else {
format!("{left}% of {provider}'s usage window is left")
};
match relative {
None => state,
Some(relative) => format!("{state}, and it resets {relative}"),
}
}
pub const STATUS_LIMIT: usize = 128;
#[derive(Debug, Clone)]
pub struct Window {
pub provider: String,
pub quota: Quota,
pub relative: Option<String>,
}
pub fn usage_status(windows: &[Window]) -> Option<String> {
let first = windows.first()?;
if windows.len() == 1 {
let left = (100.0 - first.quota.percentage).round().max(0.0);
if is_spent(&first.quota) {
return Some(match &first.relative {
None => "usage spent".to_owned(),
Some(relative) => format!("usage spent, back {relative}"),
});
}
return Some(match &first.relative {
None => format!("{left}% usage left"),
Some(relative) => format!("{left}% usage left, resets {relative}"),
});
}
let mut parts: Vec<String> = Vec::new();
for window in windows {
let left = (100.0 - window.quota.percentage).round().max(0.0);
let segment = if is_spent(&window.quota) {
match &window.relative {
None => format!("{} spent", window.provider),
Some(relative) => format!("{} spent, back {relative}", window.provider),
}
} else {
format!("{} {left}%", window.provider)
};
let candidate = parts
.iter()
.chain(std::iter::once(&segment))
.cloned()
.collect::<Vec<_>>()
.join(" | ");
if candidate.len() > STATUS_LIMIT {
break;
}
parts.push(segment);
}
(!parts.is_empty()).then(|| parts.join(" | "))
}
#[cfg(test)]
mod tests;