1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::RwLock;
11use std::time::{Duration, SystemTime, UNIX_EPOCH};
12
13#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
19pub struct TokenPricing {
20 pub input_per_million: f64,
22 pub output_per_million: f64,
24}
25
26impl TokenPricing {
27 #[must_use]
29 pub const fn new(input_per_million: f64, output_per_million: f64) -> Self {
30 Self { input_per_million, output_per_million }
31 }
32
33 #[must_use]
35 pub fn calculate(&self, input_tokens: u64, output_tokens: u64) -> f64 {
36 let input_cost = (input_tokens as f64 / 1_000_000.0) * self.input_per_million;
37 let output_cost = (output_tokens as f64 / 1_000_000.0) * self.output_per_million;
38 input_cost + output_cost
39 }
40
41 #[must_use]
43 pub fn for_model(model: &str) -> Self {
44 const MODEL_PRICING: &[(&[&str], f64, f64)] = &[
45 (&["gpt-4o"], 2.50, 10.00),
46 (&["gpt-4-turbo", "gpt-4"], 10.00, 30.00),
47 (&["gpt-3.5"], 0.50, 1.50),
48 (&["claude-3-opus"], 15.00, 75.00),
49 (&["claude-3-sonnet", "claude-3.5"], 3.00, 15.00),
50 (&["claude-3-haiku"], 0.25, 1.25),
51 (&["llama", "mistral"], 0.20, 0.20),
52 ];
53 let lower = model.to_lowercase();
54 MODEL_PRICING
55 .iter()
56 .find(|(patterns, _, _)| patterns.iter().any(|p| lower.contains(p)))
57 .map(|(_, input, output)| Self::new(*input, *output))
58 .unwrap_or_else(|| Self::new(1.00, 2.00))
59 }
60}
61
62impl Default for TokenPricing {
63 fn default() -> Self {
64 Self::new(1.00, 2.00)
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct UsageRecord {
75 pub timestamp: u64,
76 pub backend: String,
77 pub model: String,
78 pub input_tokens: u64,
79 pub output_tokens: u64,
80 pub cost_usd: f64,
81}
82
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85pub struct DailyUsage {
86 pub date: String,
88 pub total_input_tokens: u64,
90 pub total_output_tokens: u64,
92 pub total_cost_usd: f64,
94 pub request_count: u64,
96 pub by_model: HashMap<String, f64>,
98}
99
100impl DailyUsage {
101 #[must_use]
103 pub fn today() -> Self {
104 Self { date: Self::current_date(), ..Default::default() }
105 }
106
107 #[must_use]
109 pub fn current_date() -> String {
110 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
111 let days = now / 86400;
113 let year = 1970 + (days / 365); let day_of_year = days % 365;
115 let month = day_of_year / 30 + 1;
116 let day = day_of_year % 30 + 1;
117 format!("{}-{:02}-{:02}", year, month.min(12), day.min(31))
118 }
119
120 pub fn add(&mut self, record: &UsageRecord) {
122 self.total_input_tokens += record.input_tokens;
123 self.total_output_tokens += record.output_tokens;
124 self.total_cost_usd += record.cost_usd;
125 self.request_count += 1;
126 *self.by_model.entry(record.model.clone()).or_insert(0.0) += record.cost_usd;
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
136pub enum CircuitState {
137 #[default]
139 Closed,
140 Open,
142 HalfOpen,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct CircuitBreakerConfig {
149 pub daily_budget_usd: f64,
151 pub warning_threshold: f64,
153 pub max_request_cost_usd: f64,
155 pub cooldown_seconds: u64,
157}
158
159impl Default for CircuitBreakerConfig {
160 fn default() -> Self {
161 Self {
162 daily_budget_usd: 10.0, warning_threshold: 0.8, max_request_cost_usd: 1.0, cooldown_seconds: 3600, }
167 }
168}
169
170impl CircuitBreakerConfig {
171 #[must_use]
173 pub fn with_budget(daily_budget_usd: f64) -> Self {
174 Self { daily_budget_usd, ..Default::default() }
175 }
176}
177
178pub struct CostCircuitBreaker {
182 config: CircuitBreakerConfig,
183 accumulated_millicents: AtomicU64,
185 current_date: RwLock<String>,
187 state: RwLock<CircuitState>,
189 opened_at: RwLock<Option<u64>>,
191}
192
193impl CostCircuitBreaker {
194 #[must_use]
196 pub fn new(config: CircuitBreakerConfig) -> Self {
197 Self {
198 config,
199 accumulated_millicents: AtomicU64::new(0),
200 current_date: RwLock::new(DailyUsage::current_date()),
201 state: RwLock::new(CircuitState::Closed),
202 opened_at: RwLock::new(None),
203 }
204 }
205
206 #[must_use]
208 pub fn with_defaults() -> Self {
209 Self::new(CircuitBreakerConfig::default())
210 }
211
212 fn read_state(&self) -> CircuitState {
214 *self.state.read().expect("circuit breaker state lock poisoned")
215 }
216
217 fn write_state(&self, new_state: CircuitState) {
218 *self.state.write().expect("circuit breaker state lock poisoned") = new_state;
219 }
220
221 fn read_opened_at(&self) -> Option<u64> {
222 *self.opened_at.read().expect("circuit breaker opened_at lock poisoned")
223 }
224
225 fn write_opened_at(&self, timestamp: Option<u64>) {
226 *self.opened_at.write().expect("circuit breaker opened_at lock poisoned") = timestamp;
227 }
228
229 fn read_current_date(&self) -> String {
230 self.current_date.read().expect("circuit breaker current_date lock poisoned").clone()
231 }
232
233 fn write_current_date(&self, date: String) {
234 *self.current_date.write().expect("circuit breaker current_date lock poisoned") = date;
235 }
236
237 pub fn check(&self, estimated_cost_usd: f64) -> Result<(), CircuitBreakerError> {
239 self.maybe_reset_daily();
241
242 if estimated_cost_usd > self.config.max_request_cost_usd {
244 return Err(CircuitBreakerError::RequestTooExpensive {
245 estimated: estimated_cost_usd,
246 limit: self.config.max_request_cost_usd,
247 });
248 }
249
250 match self.read_state() {
252 CircuitState::Open => {
253 if self.cooldown_elapsed() {
255 self.write_state(CircuitState::HalfOpen);
256 } else {
257 return Err(CircuitBreakerError::BudgetExceeded {
258 spent: self.accumulated_usd(),
259 budget: self.config.daily_budget_usd,
260 });
261 }
262 }
263 CircuitState::HalfOpen | CircuitState::Closed => {}
264 }
265
266 let current = self.accumulated_usd();
268 if current + estimated_cost_usd > self.config.daily_budget_usd {
269 self.write_state(CircuitState::Open);
270 self.write_opened_at(Some(Self::current_timestamp()));
271 return Err(CircuitBreakerError::BudgetExceeded {
272 spent: current,
273 budget: self.config.daily_budget_usd,
274 });
275 }
276
277 Ok(())
278 }
279
280 pub fn record(&self, actual_cost_usd: f64) {
282 let millicents = (actual_cost_usd * 100_000.0) as u64;
283 self.accumulated_millicents.fetch_add(millicents, Ordering::SeqCst);
284
285 if self.accumulated_usd() >= self.config.daily_budget_usd {
287 self.write_state(CircuitState::Open);
288 self.write_opened_at(Some(Self::current_timestamp()));
289 }
290 }
291
292 #[must_use]
294 pub fn accumulated_usd(&self) -> f64 {
295 self.accumulated_millicents.load(Ordering::SeqCst) as f64 / 100_000.0
296 }
297
298 #[must_use]
300 pub fn remaining_usd(&self) -> f64 {
301 (self.config.daily_budget_usd - self.accumulated_usd()).max(0.0)
302 }
303
304 #[must_use]
306 pub fn utilization(&self) -> f64 {
307 if self.config.daily_budget_usd == 0.0 {
308 return if self.accumulated_usd() > 0.0 { 1.0 } else { 0.0 };
309 }
310 self.accumulated_usd() / self.config.daily_budget_usd
311 }
312
313 #[must_use]
315 pub fn is_warning(&self) -> bool {
316 self.utilization() >= self.config.warning_threshold
317 }
318
319 #[must_use]
321 pub fn state(&self) -> CircuitState {
322 self.read_state()
323 }
324
325 pub fn reset(&self) {
327 self.accumulated_millicents.store(0, Ordering::SeqCst);
328 self.write_state(CircuitState::Closed);
329 self.write_opened_at(None);
330 self.write_current_date(DailyUsage::current_date());
331 }
332
333 fn maybe_reset_daily(&self) {
334 let today = DailyUsage::current_date();
335 let current = self.read_current_date();
336 if current != today {
337 drop(current);
338 self.reset();
339 }
340 }
341
342 fn cooldown_elapsed(&self) -> bool {
343 if let Some(opened) = self.read_opened_at() {
344 let now = Self::current_timestamp();
345 now - opened >= self.config.cooldown_seconds
346 } else {
347 true
348 }
349 }
350
351 fn current_timestamp() -> u64 {
352 SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO).as_secs()
353 }
354}
355
356impl Default for CostCircuitBreaker {
357 fn default() -> Self {
358 Self::with_defaults()
359 }
360}
361
362#[derive(Debug, Clone, PartialEq)]
364pub enum CircuitBreakerError {
365 BudgetExceeded { spent: f64, budget: f64 },
367 RequestTooExpensive { estimated: f64, limit: f64 },
369}
370
371impl std::fmt::Display for CircuitBreakerError {
372 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373 match self {
374 Self::BudgetExceeded { spent, budget } => {
375 write!(f, "Daily budget exceeded: ${:.2} spent of ${:.2} budget", spent, budget)
376 }
377 Self::RequestTooExpensive { estimated, limit } => {
378 write!(f, "Request too expensive: ${:.2} estimated, ${:.2} limit", estimated, limit)
379 }
380 }
381 }
382}
383
384impl std::error::Error for CircuitBreakerError {}
385
386#[cfg(test)]
391#[allow(non_snake_case)]
392mod tests {
393 use super::*;
394
395 #[test]
400 fn test_SERVE_CBR_001_pricing_calculate() {
401 let pricing = TokenPricing::new(1.0, 2.0); let cost = pricing.calculate(1_000_000, 500_000);
403 assert!((cost - 2.0).abs() < 0.001); }
405
406 #[test]
407 fn test_SERVE_CBR_001_pricing_small_amounts() {
408 let pricing = TokenPricing::new(10.0, 30.0); let cost = pricing.calculate(1000, 500);
410 assert!((cost - 0.025).abs() < 0.001);
412 }
413
414 #[test]
415 fn test_SERVE_CBR_001_pricing_for_model_gpt4() {
416 let pricing = TokenPricing::for_model("gpt-4-turbo");
417 assert_eq!(pricing.input_per_million, 10.0);
418 assert_eq!(pricing.output_per_million, 30.0);
419 }
420
421 #[test]
422 fn test_SERVE_CBR_001_pricing_for_model_claude() {
423 let pricing = TokenPricing::for_model("claude-3-sonnet");
424 assert_eq!(pricing.input_per_million, 3.0);
425 assert_eq!(pricing.output_per_million, 15.0);
426 }
427
428 #[test]
429 fn test_SERVE_CBR_001_pricing_for_model_llama() {
430 let pricing = TokenPricing::for_model("llama-3.1-70b");
431 assert_eq!(pricing.input_per_million, 0.20);
432 }
433
434 #[test]
435 fn test_SERVE_CBR_001_pricing_default() {
436 let pricing = TokenPricing::default();
437 assert_eq!(pricing.input_per_million, 1.0);
438 assert_eq!(pricing.output_per_million, 2.0);
439 }
440
441 #[test]
446 fn test_SERVE_CBR_002_daily_usage_add() {
447 let mut usage = DailyUsage::today();
448 let record = UsageRecord {
449 timestamp: 0,
450 backend: "openai".to_string(),
451 model: "gpt-4".to_string(),
452 input_tokens: 1000,
453 output_tokens: 500,
454 cost_usd: 0.025,
455 };
456 usage.add(&record);
457 assert_eq!(usage.total_input_tokens, 1000);
458 assert_eq!(usage.total_output_tokens, 500);
459 assert!((usage.total_cost_usd - 0.025).abs() < 0.001);
460 assert_eq!(usage.request_count, 1);
461 }
462
463 #[test]
464 fn test_SERVE_CBR_002_daily_usage_by_model() {
465 let mut usage = DailyUsage::today();
466 usage.add(&UsageRecord {
467 timestamp: 0,
468 backend: "openai".to_string(),
469 model: "gpt-4".to_string(),
470 input_tokens: 1000,
471 output_tokens: 500,
472 cost_usd: 1.0,
473 });
474 usage.add(&UsageRecord {
475 timestamp: 0,
476 backend: "openai".to_string(),
477 model: "gpt-3.5".to_string(),
478 input_tokens: 1000,
479 output_tokens: 500,
480 cost_usd: 0.1,
481 });
482 assert_eq!(usage.by_model.get("gpt-4"), Some(&1.0));
483 assert_eq!(usage.by_model.get("gpt-3.5"), Some(&0.1));
484 }
485
486 #[test]
491 fn test_SERVE_CBR_003_default_config() {
492 let config = CircuitBreakerConfig::default();
493 assert_eq!(config.daily_budget_usd, 10.0);
494 assert_eq!(config.warning_threshold, 0.8);
495 }
496
497 #[test]
498 fn test_SERVE_CBR_003_check_allows_under_budget() {
499 let cb = CostCircuitBreaker::new(CircuitBreakerConfig::with_budget(10.0));
500 assert!(cb.check(1.0).is_ok());
501 }
502
503 #[test]
504 fn test_SERVE_CBR_003_check_blocks_over_budget() {
505 let cb = CostCircuitBreaker::new(CircuitBreakerConfig::with_budget(1.0));
506 cb.record(0.9);
507 let result = cb.check(0.2);
508 assert!(result.is_err());
509 }
510
511 #[test]
512 fn test_SERVE_CBR_003_record_accumulates() {
513 let cb = CostCircuitBreaker::with_defaults();
514 cb.record(1.0);
515 cb.record(2.0);
516 assert!((cb.accumulated_usd() - 3.0).abs() < 0.001);
517 }
518
519 #[test]
524 fn test_SERVE_CBR_004_initial_state_closed() {
525 let cb = CostCircuitBreaker::with_defaults();
526 assert_eq!(cb.state(), CircuitState::Closed);
527 }
528
529 #[test]
530 fn test_SERVE_CBR_004_opens_on_budget_exceed() {
531 let cb = CostCircuitBreaker::new(CircuitBreakerConfig::with_budget(1.0));
532 cb.record(1.0);
533 assert_eq!(cb.state(), CircuitState::Open);
534 }
535
536 #[test]
537 fn test_SERVE_CBR_004_reset_closes_circuit() {
538 let cb = CostCircuitBreaker::new(CircuitBreakerConfig::with_budget(1.0));
539 cb.record(1.0);
540 assert_eq!(cb.state(), CircuitState::Open);
541 cb.reset();
542 assert_eq!(cb.state(), CircuitState::Closed);
543 assert!((cb.accumulated_usd()).abs() < 0.001);
544 }
545
546 #[test]
551 fn test_SERVE_CBR_005_rejects_expensive_request() {
552 let config = CircuitBreakerConfig { max_request_cost_usd: 0.5, ..Default::default() };
553 let cb = CostCircuitBreaker::new(config);
554 let result = cb.check(1.0);
555 assert!(matches!(result, Err(CircuitBreakerError::RequestTooExpensive { .. })));
556 }
557
558 #[test]
559 fn test_SERVE_CBR_005_allows_cheap_request() {
560 let config = CircuitBreakerConfig { max_request_cost_usd: 1.0, ..Default::default() };
561 let cb = CostCircuitBreaker::new(config);
562 assert!(cb.check(0.5).is_ok());
563 }
564
565 #[test]
570 fn test_SERVE_CBR_006_utilization_percentage() {
571 let cb = CostCircuitBreaker::new(CircuitBreakerConfig::with_budget(10.0));
572 cb.record(5.0);
573 assert!((cb.utilization() - 0.5).abs() < 0.001);
574 }
575
576 #[test]
577 fn test_SERVE_CBR_006_remaining_budget() {
578 let cb = CostCircuitBreaker::new(CircuitBreakerConfig::with_budget(10.0));
579 cb.record(3.0);
580 assert!((cb.remaining_usd() - 7.0).abs() < 0.001);
581 }
582
583 #[test]
584 fn test_SERVE_CBR_006_warning_threshold() {
585 let config = CircuitBreakerConfig {
586 daily_budget_usd: 10.0,
587 warning_threshold: 0.8,
588 ..Default::default()
589 };
590 let cb = CostCircuitBreaker::new(config);
591 cb.record(7.0);
592 assert!(!cb.is_warning());
593 cb.record(1.0);
594 assert!(cb.is_warning());
595 }
596
597 #[test]
602 fn test_SERVE_CBR_007_budget_exceeded_display() {
603 let err = CircuitBreakerError::BudgetExceeded { spent: 10.5, budget: 10.0 };
604 let msg = err.to_string();
605 assert!(msg.contains("10.50"));
606 assert!(msg.contains("10.00"));
607 assert!(msg.contains("exceeded"));
608 }
609
610 #[test]
611 fn test_SERVE_CBR_007_request_expensive_display() {
612 let err = CircuitBreakerError::RequestTooExpensive { estimated: 5.0, limit: 1.0 };
613 let msg = err.to_string();
614 assert!(msg.contains("5.00"));
615 assert!(msg.contains("1.00"));
616 assert!(msg.contains("expensive"));
617 }
618
619 #[test]
624 fn test_SERVE_CBR_008_open_state_blocks_during_cooldown() {
625 let config = CircuitBreakerConfig {
626 daily_budget_usd: 1.0,
627 max_request_cost_usd: 5.0,
628 cooldown_seconds: 3600, ..Default::default()
630 };
631 let cb = CostCircuitBreaker::new(config);
632
633 cb.record(1.0);
635 assert_eq!(cb.state(), CircuitState::Open);
636
637 let result = cb.check(0.01);
639 assert!(result.is_err());
640 assert!(matches!(result, Err(CircuitBreakerError::BudgetExceeded { .. })));
641 }
642
643 #[test]
644 fn test_SERVE_CBR_008_cooldown_elapsed_with_no_opened_at() {
645 let cb = CostCircuitBreaker::with_defaults();
647 assert!(cb.cooldown_elapsed());
649 }
650
651 #[test]
652 fn test_SERVE_CBR_008_cooldown_elapsed_recently_opened() {
653 let config = CircuitBreakerConfig {
654 daily_budget_usd: 1.0,
655 max_request_cost_usd: 5.0,
656 cooldown_seconds: 3600,
657 ..Default::default()
658 };
659 let cb = CostCircuitBreaker::new(config);
660
661 cb.record(1.0);
663 assert_eq!(cb.state(), CircuitState::Open);
664
665 assert!(!cb.cooldown_elapsed());
667 }
668
669 #[test]
670 fn test_SERVE_CBR_008_cooldown_elapsed_with_zero_cooldown() {
671 let config = CircuitBreakerConfig {
672 daily_budget_usd: 1.0,
673 max_request_cost_usd: 5.0,
674 cooldown_seconds: 0, ..Default::default()
676 };
677 let cb = CostCircuitBreaker::new(config);
678
679 cb.record(1.0);
681 assert_eq!(cb.state(), CircuitState::Open);
682
683 assert!(cb.cooldown_elapsed());
685 }
686
687 #[test]
688 fn test_SERVE_CBR_008_half_open_after_cooldown() {
689 let config = CircuitBreakerConfig {
690 daily_budget_usd: 10.0,
691 max_request_cost_usd: 5.0,
692 cooldown_seconds: 0, ..Default::default()
694 };
695 let cb = CostCircuitBreaker::new(config);
696
697 cb.record(10.0);
699 assert_eq!(cb.state(), CircuitState::Open);
700
701 let result = cb.check(0.5);
704 assert!(result.is_err());
705 }
706
707 #[test]
708 fn test_SERVE_CBR_008_check_transitions_open_to_halfopen_then_allows() {
709 let config = CircuitBreakerConfig {
710 daily_budget_usd: 10.0,
711 max_request_cost_usd: 5.0,
712 cooldown_seconds: 0, ..Default::default()
714 };
715 let cb = CostCircuitBreaker::new(config);
716
717 cb.record(5.0);
719 cb.write_state(CircuitState::Open);
720 cb.write_opened_at(Some(CostCircuitBreaker::current_timestamp()));
721
722 let result = cb.check(1.0);
725 assert!(result.is_ok());
726 }
727
728 #[test]
733 fn test_SERVE_CBR_009_check_opens_circuit_on_budget_cross() {
734 let config = CircuitBreakerConfig {
735 daily_budget_usd: 5.0,
736 max_request_cost_usd: 10.0,
737 ..Default::default()
738 };
739 let cb = CostCircuitBreaker::new(config);
740
741 cb.record(4.5);
743 assert_eq!(cb.state(), CircuitState::Closed);
744
745 let result = cb.check(1.0);
747 assert!(result.is_err());
748 assert_eq!(cb.state(), CircuitState::Open);
749 }
750
751 #[test]
752 fn test_SERVE_CBR_009_check_budget_just_under_allows() {
753 let config = CircuitBreakerConfig {
754 daily_budget_usd: 5.0,
755 max_request_cost_usd: 10.0,
756 ..Default::default()
757 };
758 let cb = CostCircuitBreaker::new(config);
759
760 cb.record(4.0);
761 let result = cb.check(0.5);
763 assert!(result.is_ok());
764 }
765
766 #[test]
771 fn test_SERVE_CBR_010_read_opened_at_none_initially() {
772 let cb = CostCircuitBreaker::with_defaults();
773 assert_eq!(cb.read_opened_at(), None);
774 }
775
776 #[test]
777 fn test_SERVE_CBR_010_write_and_read_opened_at() {
778 let cb = CostCircuitBreaker::with_defaults();
779 let ts = CostCircuitBreaker::current_timestamp();
780 cb.write_opened_at(Some(ts));
781 assert_eq!(cb.read_opened_at(), Some(ts));
782 }
783
784 #[test]
785 fn test_SERVE_CBR_010_write_opened_at_clears() {
786 let cb = CostCircuitBreaker::with_defaults();
787 cb.write_opened_at(Some(12345));
788 cb.write_opened_at(None);
789 assert_eq!(cb.read_opened_at(), None);
790 }
791
792 #[test]
797 fn test_SERVE_CBR_011_error_trait_budget_exceeded() {
798 let err: Box<dyn std::error::Error> =
799 Box::new(CircuitBreakerError::BudgetExceeded { spent: 10.0, budget: 5.0 });
800 assert!(err.to_string().contains("exceeded"));
801 }
802
803 #[test]
804 fn test_SERVE_CBR_011_error_trait_request_expensive() {
805 let err: Box<dyn std::error::Error> =
806 Box::new(CircuitBreakerError::RequestTooExpensive { estimated: 3.0, limit: 1.0 });
807 assert!(err.to_string().contains("expensive"));
808 }
809
810 #[test]
815 fn test_SERVE_CBR_012_pricing_gpt4o() {
816 let pricing = TokenPricing::for_model("gpt-4o-mini");
817 assert_eq!(pricing.input_per_million, 2.50);
818 assert_eq!(pricing.output_per_million, 10.00);
819 }
820
821 #[test]
822 fn test_SERVE_CBR_012_pricing_gpt35() {
823 let pricing = TokenPricing::for_model("gpt-3.5-turbo");
824 assert_eq!(pricing.input_per_million, 0.50);
825 }
826
827 #[test]
828 fn test_SERVE_CBR_012_pricing_claude_opus() {
829 let pricing = TokenPricing::for_model("claude-3-opus-20240229");
830 assert_eq!(pricing.input_per_million, 15.00);
831 assert_eq!(pricing.output_per_million, 75.00);
832 }
833
834 #[test]
835 fn test_SERVE_CBR_012_pricing_claude_haiku() {
836 let pricing = TokenPricing::for_model("claude-3-haiku-20240307");
837 assert_eq!(pricing.input_per_million, 0.25);
838 assert_eq!(pricing.output_per_million, 1.25);
839 }
840
841 #[test]
842 fn test_SERVE_CBR_012_pricing_claude_35() {
843 let pricing = TokenPricing::for_model("claude-3.5-sonnet");
844 assert_eq!(pricing.input_per_million, 3.00);
845 }
846
847 #[test]
848 fn test_SERVE_CBR_012_pricing_mistral() {
849 let pricing = TokenPricing::for_model("mistral-7b");
850 assert_eq!(pricing.input_per_million, 0.20);
851 }
852
853 #[test]
854 fn test_SERVE_CBR_012_pricing_unknown_model() {
855 let pricing = TokenPricing::for_model("totally-unknown-model");
856 assert_eq!(pricing.input_per_million, 1.00);
857 assert_eq!(pricing.output_per_million, 2.00);
858 }
859
860 #[test]
865 fn test_SERVE_CBR_013_current_date_format() {
866 let date = DailyUsage::current_date();
867 assert_eq!(date.len(), 10);
869 assert_eq!(&date[4..5], "-");
870 assert_eq!(&date[7..8], "-");
871 }
872
873 #[test]
874 fn test_SERVE_CBR_013_today_has_current_date() {
875 let usage = DailyUsage::today();
876 let expected = DailyUsage::current_date();
877 assert_eq!(usage.date, expected);
878 assert_eq!(usage.total_input_tokens, 0);
879 assert_eq!(usage.total_cost_usd, 0.0);
880 }
881
882 #[test]
887 fn test_SERVE_CBR_014_remaining_usd_clamped_to_zero() {
888 let cb = CostCircuitBreaker::new(CircuitBreakerConfig::with_budget(1.0));
889 cb.record(2.0); assert!((cb.remaining_usd()).abs() < 0.001); }
892}