use crate::cost::pricing::{Micros, Rate};
use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Unpriced {
#[default]
Refused,
Allowed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Budget {
cap: Micros,
currency: String,
unpriced: Unpriced,
}
impl Budget {
pub fn of(cap: Micros, currency: impl Into<String>) -> Self {
Self {
cap,
currency: currency.into(),
unpriced: Unpriced::Refused,
}
}
#[must_use]
pub fn allowing_unpriced(mut self) -> Self {
self.unpriced = Unpriced::Allowed;
self
}
pub fn cap(&self) -> Micros {
self.cap
}
pub fn currency(&self) -> &str {
&self.currency
}
pub fn unpriced(&self) -> Unpriced {
self.unpriced
}
pub fn most_a_reply_can_cost(max_tokens: u32, rate: &Rate) -> Micros {
Micros(i64::from(max_tokens).saturating_mul(rate.output.0) / 1_000_000)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Spending {
pub cap: Micros,
pub spent: Micros,
pub remaining: Micros,
pub currency: String,
pub unmeasured: usize,
}
impl Spending {
pub fn is_exact(&self) -> bool {
self.unmeasured == 0
}
}
#[derive(Debug, Default)]
pub(crate) struct Purse {
spent: AtomicI64,
unmeasured: AtomicUsize,
}
impl Purse {
pub(crate) fn remaining(&self, cap: Micros) -> Micros {
Micros(
cap.0
.saturating_sub(self.spent.load(Ordering::Relaxed))
.max(0),
)
}
pub(crate) fn spend(&self, amount: Micros) {
self.spent.fetch_add(amount.0, Ordering::Relaxed);
}
pub(crate) fn unmeasured(&self) {
self.unmeasured.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn measured_after_all(&self) {
let _ = self
.unmeasured
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_sub(1));
}
pub(crate) fn spending(&self, budget: &Budget) -> Spending {
Spending {
cap: budget.cap,
spent: Micros(self.spent.load(Ordering::Relaxed)),
remaining: self.remaining(budget.cap),
currency: budget.currency.clone(),
unmeasured: self.unmeasured.load(Ordering::Relaxed),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn five() -> Budget {
Budget::of(Micros(5_000_000), "USD")
}
#[test]
fn a_budget_refuses_what_it_cannot_check_unless_told_otherwise() {
assert_eq!(five().unpriced(), Unpriced::Refused);
assert_eq!(five().allowing_unpriced().unpriced(), Unpriced::Allowed);
}
#[test]
fn what_is_left_never_goes_below_zero() {
let purse = Purse::default();
purse.spend(Micros(9_000_000));
assert_eq!(purse.remaining(Micros(5_000_000)), Micros(0));
}
#[test]
fn the_most_a_reply_can_cost_is_an_upper_bound_rather_than_a_guess() {
let rate = Rate {
output: Micros(15_000_000),
..Rate::default()
};
assert_eq!(
Budget::most_a_reply_can_cost(1_000_000, &rate),
Micros(15_000_000)
);
assert_eq!(Budget::most_a_reply_can_cost(1_000, &rate), Micros(15_000));
}
#[test]
fn spending_says_whether_it_is_the_whole_figure() {
let purse = Purse::default();
purse.spend(Micros(1_000_000));
assert!(purse.spending(&five()).is_exact());
purse.unmeasured();
let spending = purse.spending(&five());
assert!(!spending.is_exact());
assert_eq!(spending.spent, Micros(1_000_000), "what is known is known");
assert_eq!(spending.remaining, Micros(4_000_000));
}
#[test]
fn a_call_priced_later_stops_counting_as_unmeasured() {
let purse = Purse::default();
purse.unmeasured();
purse.measured_after_all();
assert!(purse.spending(&five()).is_exact());
purse.measured_after_all();
assert_eq!(purse.spending(&five()).unmeasured, 0);
}
}