use std::sync::Mutex;
use tracing::warn;
use crate::classify::tiers::llm_prompt::LlmUsage;
pub const JEV_INPUT_PRICE_PER_MTOK_USD: f64 = 0.042;
pub const JEV_OUTPUT_PRICE_PER_MTOK_USD: f64 = 0.0;
pub(crate) struct JevBudget {
cap_tokens: u64,
state: Mutex<BudgetState>,
}
#[derive(Default)]
struct BudgetState {
spent: u64,
reserved: u64,
input: u64,
output: u64,
exhausted: bool,
}
impl JevBudget {
pub(crate) fn new(cap_usd: f64) -> Self {
let tokens = cap_usd / JEV_INPUT_PRICE_PER_MTOK_USD * 1_000_000.0;
let cap_tokens = if tokens.is_finite() && tokens > 0.0 {
tokens.floor() as u64
} else {
0
};
Self {
cap_tokens,
state: Mutex::new(BudgetState::default()),
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, BudgetState> {
self.state.lock().unwrap_or_else(|p| p.into_inner())
}
pub(crate) fn reserve(&self, input_tokens: u64) -> bool {
let mut s = self.lock();
let total = s
.spent
.saturating_add(s.reserved)
.saturating_add(input_tokens);
if !s.exhausted && total <= self.cap_tokens {
s.reserved += input_tokens;
return true;
}
if !s.exhausted {
s.exhausted = true;
warn!(
spent_tokens = s.spent,
spent_usd = cost(s.spent, 0),
cost_usd = cost(s.input, s.output),
cap_usd = self.cap_usd(),
"Jev spend cap reached; remaining calls are skipped"
);
}
false
}
pub(crate) fn settle(
&self,
reservation: u64,
per_attempt: u64,
attempts: u32,
usage: Option<LlmUsage>,
) {
let mut s = self.lock();
s.reserved = s.reserved.saturating_sub(reservation);
let attempts = u64::from(attempts.max(1));
let billed = match usage {
Some(u) => {
s.input += u.input_tokens;
s.output += u.output_tokens;
u.input_tokens
.saturating_add(per_attempt.saturating_mul(attempts - 1))
}
None => per_attempt.saturating_mul(attempts),
};
s.spent = s.spent.saturating_add(billed);
if billed > reservation && !s.exhausted {
s.exhausted = true;
warn!("Jev call billed more than its reservation; remaining calls are skipped");
}
}
pub(crate) fn cost_usd(&self) -> f64 {
let s = self.lock();
cost(s.input, s.output)
}
pub(crate) fn spent_usd(&self) -> f64 {
cost(self.lock().spent, 0)
}
#[cfg(test)]
pub(crate) fn spent_tokens(&self) -> u64 {
self.lock().spent
}
pub(crate) fn cap_usd(&self) -> f64 {
self.cap_tokens as f64 * JEV_INPUT_PRICE_PER_MTOK_USD / 1_000_000.0
}
}
fn cost(input: u64, output: u64) -> f64 {
(input as f64 * JEV_INPUT_PRICE_PER_MTOK_USD + output as f64 * JEV_OUTPUT_PRICE_PER_MTOK_USD)
/ 1_000_000.0
}