1use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct TokenPool {
10 pub name: String,
12 pub capacity: u64,
14 pub used: u64,
16 pub reserved: u64,
18}
19
20impl TokenPool {
21 pub fn new(name: impl Into<String>, capacity: u64) -> Self {
23 Self {
24 name: name.into(),
25 capacity,
26 used: 0,
27 reserved: 0,
28 }
29 }
30
31 pub fn available(&self) -> u64 {
33 self.capacity
34 .saturating_sub(self.used.saturating_add(self.reserved))
35 }
36
37 pub fn can_reserve(&self, tokens: u64) -> bool {
39 self.available() >= tokens
40 }
41
42 pub fn reserve(&mut self, tokens: u64) -> bool {
44 if !self.can_reserve(tokens) {
45 return false;
46 }
47 self.reserved += tokens;
48 true
49 }
50
51 pub fn commit(&mut self, reserved: u64, actual: u64) {
53 self.reserved = self.reserved.saturating_sub(reserved);
54 self.used = self.used.saturating_add(actual);
55 }
56
57 pub fn release(&mut self, tokens: u64) {
59 self.reserved = self.reserved.saturating_sub(tokens);
60 }
61
62 pub fn utilization(&self) -> f64 {
64 if self.capacity == 0 {
65 return 0.0;
66 }
67 self.used as f64 / self.capacity as f64
68 }
69}
70
71pub struct TokenBudget {
73 pools: HashMap<String, TokenPool>,
74}
75
76impl TokenBudget {
77 pub fn new() -> Self {
79 Self {
80 pools: HashMap::new(),
81 }
82 }
83
84 pub fn add_pool(&mut self, pool: TokenPool) {
86 self.pools.insert(pool.name.clone(), pool);
87 }
88
89 pub fn get_pool(&self, name: &str) -> Option<&TokenPool> {
91 self.pools.get(name)
92 }
93
94 pub fn get_pool_mut(&mut self, name: &str) -> Option<&mut TokenPool> {
96 self.pools.get_mut(name)
97 }
98
99 pub fn pools(&self) -> &HashMap<String, TokenPool> {
101 &self.pools
102 }
103
104 pub fn check(&self, pool_name: &str, tokens: u64) -> bool {
106 self.pools
107 .get(pool_name)
108 .map(|p| p.can_reserve(tokens))
109 .unwrap_or(false)
110 }
111
112 pub fn reserve(&mut self, pool_name: &str, tokens: u64) -> bool {
114 self.pools
115 .get_mut(pool_name)
116 .map(|p| p.reserve(tokens))
117 .unwrap_or(false)
118 }
119
120 pub fn report(&mut self, pool_name: &str, reserved: u64, actual: u64) {
122 if let Some(pool) = self.pools.get_mut(pool_name) {
123 pool.commit(reserved, actual);
124 }
125 }
126}
127
128impl Default for TokenBudget {
129 fn default() -> Self {
130 Self::new()
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn pool_basic() {
140 let mut pool = TokenPool::new("test", 1000);
141 assert_eq!(pool.available(), 1000);
142 assert!(pool.reserve(400));
143 assert_eq!(pool.available(), 600);
144 pool.commit(400, 350);
145 assert_eq!(pool.used, 350);
146 assert_eq!(pool.reserved, 0);
147 assert_eq!(pool.available(), 650);
148 }
149
150 #[test]
151 fn pool_over_budget() {
152 let pool = TokenPool::new("test", 100);
153 assert!(!pool.can_reserve(200));
154 }
155
156 #[test]
157 fn pool_release() {
158 let mut pool = TokenPool::new("test", 1000);
159 pool.reserve(500);
160 pool.release(500);
161 assert_eq!(pool.reserved, 0);
162 assert_eq!(pool.available(), 1000);
163 }
164
165 #[test]
166 fn pool_utilization() {
167 let mut pool = TokenPool::new("test", 1000);
168 pool.used = 750;
169 assert!((pool.utilization() - 0.75).abs() < f64::EPSILON);
170 }
171
172 #[test]
173 fn budget_multi_pool() {
174 let mut budget = TokenBudget::new();
175 budget.add_pool(TokenPool::new("default", 10000));
176 budget.add_pool(TokenPool::new("agent-1", 5000));
177
178 assert!(budget.check("default", 8000));
179 assert!(!budget.check("agent-1", 8000));
180 assert!(!budget.check("nonexistent", 1));
181 }
182
183 #[test]
184 fn budget_reserve_report() {
185 let mut budget = TokenBudget::new();
186 budget.add_pool(TokenPool::new("pool", 1000));
187 assert!(budget.reserve("pool", 500));
188 budget.report("pool", 500, 420);
189 let pool = budget.get_pool("pool").unwrap();
190 assert_eq!(pool.used, 420);
191 assert_eq!(pool.reserved, 0);
192 }
193}