use crate::llm::TokenUsage;
use async_trait::async_trait;
#[derive(Debug, Clone)]
pub enum BudgetDecision {
Allow,
SoftLimit {
resource: String,
consumed: f64,
limit: f64,
message: Option<String>,
},
Deny {
resource: String,
reason: String,
},
}
#[async_trait]
pub trait BudgetGuard: Send + Sync {
async fn check_before_llm(
&self,
session_id: &str,
estimated_prompt_tokens: usize,
) -> BudgetDecision {
let _ = (session_id, estimated_prompt_tokens);
BudgetDecision::Allow
}
async fn record_after_llm(&self, session_id: &str, usage: &TokenUsage) {
let _ = (session_id, usage);
}
async fn check_before_tool(&self, session_id: &str, tool_name: &str) -> BudgetDecision {
let _ = (session_id, tool_name);
BudgetDecision::Allow
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopBudgetGuard;
#[async_trait]
impl BudgetGuard for NoopBudgetGuard {}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[tokio::test]
async fn noop_allows_everything() {
let guard = NoopBudgetGuard;
assert!(matches!(
guard.check_before_llm("s", 1000).await,
BudgetDecision::Allow
));
assert!(matches!(
guard.check_before_tool("s", "bash").await,
BudgetDecision::Allow
));
guard.record_after_llm("s", &TokenUsage::default()).await;
}
#[derive(Debug, Default)]
struct CountingGuard {
llm_checks: AtomicUsize,
records: AtomicUsize,
}
#[async_trait]
impl BudgetGuard for CountingGuard {
async fn check_before_llm(&self, _: &str, _: usize) -> BudgetDecision {
self.llm_checks.fetch_add(1, Ordering::SeqCst);
BudgetDecision::Deny {
resource: "llm_tokens".to_string(),
reason: "budget exhausted in test".to_string(),
}
}
async fn record_after_llm(&self, _: &str, _: &TokenUsage) {
self.records.fetch_add(1, Ordering::SeqCst);
}
}
#[tokio::test]
async fn custom_guard_can_deny() {
let guard: Arc<dyn BudgetGuard> = Arc::new(CountingGuard::default());
let decision = guard.check_before_llm("s", 100).await;
match decision {
BudgetDecision::Deny { resource, .. } => assert_eq!(resource, "llm_tokens"),
other => panic!("expected Deny, got {other:?}"),
}
}
}