1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8#[cfg(feature = "pricing")]
9pub mod pricing;
10
11#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
13pub struct Usage {
14 pub requests: u64,
16 pub input_tokens: u64,
23 #[serde(default)]
25 pub cache_write_tokens: u64,
26 #[serde(default)]
28 pub cache_read_tokens: u64,
29 pub output_tokens: u64,
31 pub total_tokens: u64,
33 #[serde(default)]
35 pub tool_calls: u64,
36}
37
38impl Usage {
39 pub const fn add_assign(&mut self, other: &Self) {
41 self.requests = self.requests.saturating_add(other.requests);
42 self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
43 self.cache_write_tokens = self
44 .cache_write_tokens
45 .saturating_add(other.cache_write_tokens);
46 self.cache_read_tokens = self
47 .cache_read_tokens
48 .saturating_add(other.cache_read_tokens);
49 self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
50 self.total_tokens = self.total_tokens.saturating_add(other.total_tokens);
51 self.tool_calls = self.tool_calls.saturating_add(other.tool_calls);
52 }
53
54 #[must_use]
56 pub const fn is_empty(&self) -> bool {
57 self.requests == 0
58 && self.input_tokens == 0
59 && self.cache_write_tokens == 0
60 && self.cache_read_tokens == 0
61 && self.output_tokens == 0
62 && self.total_tokens == 0
63 && self.tool_calls == 0
64 }
65
66 #[must_use]
68 pub const fn with_additional_tool_calls(mut self, tool_calls: u64) -> Self {
69 self.tool_calls = self.tool_calls.saturating_add(tool_calls);
70 self
71 }
72}
73
74#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Serialize)]
76pub struct PricingEstimate {
77 #[serde(default)]
79 pub amount_micros_usd: u64,
80}
81
82impl PricingEstimate {
83 #[must_use]
85 pub const fn from_micros_usd(amount_micros_usd: u64) -> Self {
86 Self { amount_micros_usd }
87 }
88
89 pub const fn add_assign(&mut self, other: &Self) {
91 self.amount_micros_usd = self
92 .amount_micros_usd
93 .saturating_add(other.amount_micros_usd);
94 }
95
96 #[must_use]
98 pub const fn is_zero(&self) -> bool {
99 self.amount_micros_usd == 0
100 }
101}
102
103#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
105pub struct UsageSnapshotEntry {
106 pub agent_id: String,
108 pub agent_name: String,
110 pub model_id: String,
112 pub usage: Usage,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub estimate_pricing: Option<PricingEstimate>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub usage_id: Option<String>,
120 #[serde(default = "default_usage_source")]
122 pub source: String,
123}
124
125#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
127pub struct UsageAgentTotal {
128 pub agent_name: String,
130 pub model_id: String,
132 pub usage: Usage,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub estimate_pricing: Option<PricingEstimate>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub usage_id: Option<String>,
140 #[serde(default = "default_usage_source")]
142 pub source: String,
143}
144
145#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
150pub struct UsageSnapshot {
151 pub run_id: String,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub latest_usage: Option<Usage>,
160 #[serde(default)]
162 pub total_usage: Usage,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub estimate_pricing: Option<PricingEstimate>,
166 #[serde(default, skip_serializing_if = "Vec::is_empty")]
168 pub entries: Vec<UsageSnapshotEntry>,
169 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
171 pub agent_usages: BTreeMap<String, UsageAgentTotal>,
172 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
174 pub model_usages: BTreeMap<String, Usage>,
175 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
177 pub model_estimate_pricing: BTreeMap<String, PricingEstimate>,
178}
179
180fn default_usage_source() -> String {
181 "model_request".to_string()
182}
183
184#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
186pub struct UsageLimits {
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub request_limit: Option<u64>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub input_tokens_limit: Option<u64>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub output_tokens_limit: Option<u64>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub total_tokens_limit: Option<u64>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub tool_calls_limit: Option<u64>,
202 #[cfg(feature = "pricing")]
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub cost_budget: Option<pricing::CostBudget>,
206}
207
208impl UsageLimits {
209 #[must_use]
211 pub const fn new() -> Self {
212 Self {
213 request_limit: None,
214 input_tokens_limit: None,
215 output_tokens_limit: None,
216 total_tokens_limit: None,
217 tool_calls_limit: None,
218 #[cfg(feature = "pricing")]
219 cost_budget: None,
220 }
221 }
222
223 #[must_use]
225 pub const fn with_request_limit(mut self, limit: u64) -> Self {
226 self.request_limit = Some(limit);
227 self
228 }
229
230 #[must_use]
232 pub const fn with_input_tokens_limit(mut self, limit: u64) -> Self {
233 self.input_tokens_limit = Some(limit);
234 self
235 }
236
237 #[must_use]
239 pub const fn with_output_tokens_limit(mut self, limit: u64) -> Self {
240 self.output_tokens_limit = Some(limit);
241 self
242 }
243
244 #[must_use]
246 pub const fn with_total_tokens_limit(mut self, limit: u64) -> Self {
247 self.total_tokens_limit = Some(limit);
248 self
249 }
250
251 #[must_use]
253 pub const fn with_tool_calls_limit(mut self, limit: u64) -> Self {
254 self.tool_calls_limit = Some(limit);
255 self
256 }
257
258 #[cfg(feature = "pricing")]
260 #[must_use]
261 pub const fn with_cost_budget(mut self, budget: pricing::CostBudget) -> Self {
262 self.cost_budget = Some(budget);
263 self
264 }
265
266 #[cfg(feature = "pricing")]
268 #[must_use]
269 pub fn estimate_cost_micros(&self, usage: &Usage) -> Option<u64> {
270 self.cost_budget
271 .as_ref()
272 .map(|budget| budget.estimate_micros(usage))
273 }
274
275 #[cfg(feature = "pricing")]
277 #[must_use]
278 pub fn estimate_pricing(&self, usage: &Usage) -> Option<PricingEstimate> {
279 self.cost_budget
280 .as_ref()
281 .map(|budget| budget.estimate_pricing(usage))
282 }
283
284 pub const fn check_before_request(&self, current: &Usage) -> Result<(), UsageLimitError> {
290 if let Some(limit) = self.request_limit {
291 let next = current.requests.saturating_add(1);
292 if next > limit {
293 return Err(UsageLimitError::NextRequest {
294 limit,
295 next_requests: next,
296 });
297 }
298 }
299 Ok(())
300 }
301
302 pub const fn check_tool_calls(&self, projected: &Usage) -> Result<(), UsageLimitError> {
308 match self.tool_calls_limit {
309 Some(limit) if projected.tool_calls > limit => Err(UsageLimitError::ToolCalls {
310 limit,
311 tool_calls: projected.tool_calls,
312 }),
313 _ => Ok(()),
314 }
315 }
316
317 pub fn check_usage(&self, usage: &Usage) -> Result<(), UsageLimitError> {
323 check_limit(
324 UsageTokenKind::InputTokens,
325 self.input_tokens_limit,
326 usage.input_tokens,
327 )?;
328 check_limit(
329 UsageTokenKind::OutputTokens,
330 self.output_tokens_limit,
331 usage.output_tokens,
332 )?;
333 check_limit(
334 UsageTokenKind::TotalTokens,
335 self.total_tokens_limit,
336 usage.total_tokens,
337 )?;
338 #[cfg(feature = "pricing")]
339 if let Some(budget) = &self.cost_budget {
340 budget.check_usage(usage)?;
341 }
342 Ok(())
343 }
344}
345
346#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
348#[serde(rename_all = "snake_case")]
349pub enum UsageTokenKind {
350 InputTokens,
352 OutputTokens,
354 TotalTokens,
356}
357
358impl std::fmt::Display for UsageTokenKind {
359 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360 let value = match self {
361 Self::InputTokens => "input_tokens",
362 Self::OutputTokens => "output_tokens",
363 Self::TotalTokens => "total_tokens",
364 };
365 formatter.write_str(value)
366 }
367}
368
369#[derive(Clone, Debug, Error, Deserialize, Eq, PartialEq, Serialize)]
371pub enum UsageLimitError {
372 #[error(
374 "the next request would exceed the request_limit of {limit} (next_requests={next_requests})"
375 )]
376 NextRequest {
377 limit: u64,
379 next_requests: u64,
381 },
382 #[error("exceeded the {kind}_limit of {limit} ({kind}={actual})")]
384 Token {
385 kind: UsageTokenKind,
387 limit: u64,
389 actual: u64,
391 },
392 #[cfg(feature = "pricing")]
394 #[error("exceeded the total_cost_limit_micros of {limit_micros} (cost_micros={actual_micros})")]
395 Cost {
396 limit_micros: u64,
398 actual_micros: u64,
400 },
401 #[error(
403 "the next tool call(s) would exceed the tool_calls_limit of {limit} (tool_calls={tool_calls})"
404 )]
405 ToolCalls {
406 limit: u64,
408 tool_calls: u64,
410 },
411}
412
413const fn check_limit(
414 kind: UsageTokenKind,
415 limit: Option<u64>,
416 actual: u64,
417) -> Result<(), UsageLimitError> {
418 match limit {
419 Some(limit) if actual > limit => Err(UsageLimitError::Token {
420 kind,
421 limit,
422 actual,
423 }),
424 _ => Ok(()),
425 }
426}
427
428pub fn add_optional_pricing(
430 total: &mut Option<PricingEstimate>,
431 estimate: Option<&PricingEstimate>,
432) {
433 if let Some(estimate) = estimate {
434 match total {
435 Some(total) => total.add_assign(estimate),
436 None => *total = Some(estimate.clone()),
437 }
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn usage_add_assign_and_empty_work() {
447 let mut usage = Usage {
448 requests: 1,
449 input_tokens: 2,
450 cache_write_tokens: 7,
451 cache_read_tokens: 11,
452 output_tokens: 3,
453 total_tokens: 5,
454 tool_calls: 1,
455 };
456 usage.add_assign(&Usage {
457 requests: 2,
458 input_tokens: 4,
459 cache_write_tokens: 13,
460 cache_read_tokens: 17,
461 output_tokens: 6,
462 total_tokens: 10,
463 tool_calls: 3,
464 });
465 assert_eq!(usage.requests, 3);
466 assert_eq!(usage.input_tokens, 6);
467 assert_eq!(usage.cache_write_tokens, 20);
468 assert_eq!(usage.cache_read_tokens, 28);
469 assert_eq!(usage.output_tokens, 9);
470 assert_eq!(usage.total_tokens, 15);
471 assert_eq!(usage.tool_calls, 4);
472 assert_eq!(usage.clone().with_additional_tool_calls(2).tool_calls, 6);
473 assert!(Usage::default().is_empty());
474 assert!(!usage.is_empty());
475 }
476
477 #[test]
478 fn usage_add_assign_saturates() {
479 let mut usage = Usage {
480 requests: u64::MAX,
481 input_tokens: u64::MAX,
482 cache_write_tokens: u64::MAX,
483 cache_read_tokens: u64::MAX,
484 output_tokens: u64::MAX,
485 total_tokens: u64::MAX,
486 tool_calls: u64::MAX,
487 };
488 usage.add_assign(&Usage {
489 requests: 1,
490 input_tokens: 1,
491 cache_write_tokens: 1,
492 cache_read_tokens: 1,
493 output_tokens: 1,
494 total_tokens: 1,
495 tool_calls: 1,
496 });
497 assert_eq!(usage.requests, u64::MAX);
498 assert_eq!(usage.input_tokens, u64::MAX);
499 assert_eq!(usage.cache_write_tokens, u64::MAX);
500 assert_eq!(usage.cache_read_tokens, u64::MAX);
501 assert_eq!(usage.output_tokens, u64::MAX);
502 assert_eq!(usage.total_tokens, u64::MAX);
503 assert_eq!(usage.tool_calls, u64::MAX);
504 }
505
506 #[test]
507 fn usage_limit_error_token_kind_is_owned_ser_de_contract() {
508 let error = UsageLimitError::Token {
509 kind: UsageTokenKind::TotalTokens,
510 limit: 5,
511 actual: 6,
512 };
513 let value = match serde_json::to_value(&error) {
514 Ok(value) => value,
515 Err(err) => panic!("usage limit error should serialize: {err}"),
516 };
517 let restored: UsageLimitError = match serde_json::from_value(value) {
518 Ok(restored) => restored,
519 Err(err) => panic!("usage limit error should deserialize: {err}"),
520 };
521 assert_eq!(restored, error);
522 }
523
524 #[test]
525 fn usage_snapshot_accepts_missing_pricing_fields() {
526 let snapshot: UsageSnapshot = match serde_json::from_value(serde_json::json!({
527 "run_id": "run_1",
528 "total_usage": {
529 "requests": 1,
530 "input_tokens": 2,
531 "output_tokens": 3,
532 "total_tokens": 5
533 }
534 })) {
535 Ok(snapshot) => snapshot,
536 Err(err) => panic!("usage snapshot should deserialize: {err}"),
537 };
538 assert_eq!(snapshot.run_id, "run_1");
539 assert!(snapshot.estimate_pricing.is_none());
540 assert!(snapshot.model_estimate_pricing.is_empty());
541 }
542}