Skip to main content

llm_browser_testkit/
reporting.rs

1//! Cost and token report printer.
2
3use crate::costs::UsageSnapshot;
4
5/// Prints a cost report to stderr after all tests complete.
6pub fn print_report(per_test: &[(String, UsageSnapshot)], global: &UsageSnapshot) {
7    if per_test.is_empty() {
8        return;
9    }
10
11    eprintln!();
12    eprintln!("═══════════════════════════════════════════════");
13    eprintln!("  COST REPORT");
14    eprintln!("═══════════════════════════════════════════════");
15
16    for (test_name, snapshot) in per_test {
17        eprintln!(
18            "  Test: \"{test_name}\" — ${cost:.4} | {tokens} tokens | {calls} calls",
19            cost = snapshot.total_cost,
20            tokens = snapshot.total_tokens,
21            calls = snapshot.total_calls,
22        );
23        for (ep_name, ep_usage) in &snapshot.endpoints {
24            if ep_usage.calls == 0 {
25                continue;
26            }
27            eprintln!(
28                "    endpoint.{ep_name}:   {calls:>3} calls, {tokens:>7} tokens, ${cost:.4}",
29                calls = ep_usage.calls,
30                tokens = ep_usage.input_tokens + ep_usage.output_tokens,
31                cost = ep_usage.cost,
32            );
33        }
34    }
35
36    eprintln!("───────────────────────────────────────────────");
37    eprintln!("  GLOBAL SUMMARY");
38    eprintln!("    Total cost:     ${cost:.4}", cost = global.total_cost);
39    eprintln!("    Total tokens:   {tokens}", tokens = global.total_tokens);
40    eprintln!("    Total calls:    {calls}", calls = global.total_calls);
41    eprintln!("═══════════════════════════════════════════════");
42}
43
44/// Prints a budget exceeded warning to stderr.
45pub fn print_budget_warning(message: &str) {
46    eprintln!("  ⚠️  BUDGET WARNING: {message}");
47}
48
49/// Prints a budget exceeded hard error to stderr.
50pub fn print_budget_error(message: &str) {
51    eprintln!("  🛑 BUDGET EXCEEDED: {message}");
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use crate::costs::{EndpointUsage, UsageSnapshot};
58    use std::collections::HashMap;
59
60    fn make_snapshot(cost: f64, tokens: u64, calls: u64) -> UsageSnapshot {
61        let mut eps = HashMap::new();
62        eps.insert(
63            "default".to_owned(),
64            EndpointUsage {
65                calls,
66                input_tokens: tokens / 2,
67                output_tokens: tokens / 2,
68                cost,
69            },
70        );
71        UsageSnapshot::from_endpoints(&eps)
72    }
73
74    #[test]
75    fn test_print_report_empty() {
76        // Should return early, no panic
77        print_report(&[], &UsageSnapshot::default());
78    }
79
80    #[test]
81    fn test_print_report_single_test() {
82        let per_test = vec![("test1".to_owned(), make_snapshot(0.05, 500, 3))];
83        // Should not panic
84        print_report(&per_test, &make_snapshot(0.05, 500, 3));
85    }
86
87    #[test]
88    fn test_print_report_zero_cost() {
89        let per_test = vec![("free".to_owned(), make_snapshot(0.0, 0, 0))];
90        print_report(&per_test, &make_snapshot(0.0, 0, 0));
91    }
92
93    #[test]
94    fn test_print_budget_warning_no_panic() {
95        print_budget_warning("cost limit $1.00 exceeded");
96    }
97
98    #[test]
99    fn test_print_budget_error_no_panic() {
100        print_budget_error("cost limit $5.00 exceeded");
101    }
102}