lean_ctx/core/context_kernel/
provider_display.rs1use serde::Serialize;
4use serde_json::Value;
5
6use super::envelope_bridge::ProviderStat;
7use super::provider_parity;
8
9const HEADER: &str = "Provider │ Reqs │ Input │ Output │ Cache │ Avg In";
10const RULE: &str = "─────────────┼──────┼──────────┼──────────┼─────────┼───────";
11
12#[derive(Serialize)]
13struct ProviderJsonRow {
14 provider_name: &'static str,
15 request_count: usize,
16 total_input: usize,
17 total_output: usize,
18 total_cache_read: usize,
19 avg_input: usize,
20}
21
22fn separated(value: usize) -> String {
23 let mut formatted = value.to_string();
24 let mut index = formatted.len();
25 while index > 3 {
26 index -= 3;
27 formatted.insert(index, ',');
28 }
29 formatted
30}
31
32fn average_input(stat: &ProviderStat) -> usize {
33 stat.total_input
34 .checked_div(stat.request_count)
35 .unwrap_or(0)
36}
37
38fn row(name: &str, values: [usize; 5]) -> String {
39 let values = values.map(separated);
40 format!(
41 "{name:<12} │ {:>4} │ {:>8} │ {:>8} │ {:>7} │ {:>6}",
42 values[0], values[1], values[2], values[3], values[4]
43 )
44}
45
46pub fn format_provider_table(stats: &[ProviderStat]) -> String {
48 if stats.is_empty() {
49 return "No provider data available.".to_owned();
50 }
51 let mut lines = vec![HEADER.to_owned(), RULE.to_owned()];
52 let mut totals = (0usize, 0usize, 0usize, 0usize);
53 for stat in stats {
54 totals.0 = totals.0.saturating_add(stat.request_count);
55 totals.1 = totals.1.saturating_add(stat.total_input);
56 totals.2 = totals.2.saturating_add(stat.total_output);
57 totals.3 = totals.3.saturating_add(stat.total_cache_read);
58 let input = stat.total_input;
59 lines.push(row(
60 provider_parity::provider_display_name(stat.provider),
61 [
62 stat.request_count,
63 input,
64 stat.total_output,
65 stat.total_cache_read,
66 average_input(stat),
67 ],
68 ));
69 }
70 lines.push(RULE.to_owned());
71 let (requests, input, output, cache) = totals;
72 lines.push(row(
73 "Total",
74 [
75 requests,
76 input,
77 output,
78 cache,
79 input.checked_div(requests).unwrap_or(0),
80 ],
81 ));
82 lines.join("\n")
83}
84
85pub fn provider_summary_oneliner(stats: &[ProviderStat]) -> String {
87 if stats.is_empty() {
88 return "No provider data".to_owned();
89 }
90 let providers = stats
91 .iter()
92 .map(|stat| {
93 format!(
94 "{}({})",
95 provider_parity::provider_display_name(stat.provider),
96 stat.request_count
97 )
98 })
99 .collect::<Vec<_>>()
100 .join(" ");
101 format!("{} providers: {providers}", stats.len())
102}
103
104pub fn provider_json(stats: &[ProviderStat]) -> Value {
106 let rows = stats.iter().map(|stat| ProviderJsonRow {
107 provider_name: provider_parity::provider_display_name(stat.provider),
108 request_count: stat.request_count,
109 total_input: stat.total_input,
110 total_output: stat.total_output,
111 total_cache_read: stat.total_cache_read,
112 avg_input: average_input(stat),
113 });
114 serde_json::to_value(rows.collect::<Vec<_>>()).unwrap_or_else(|_| Value::Array(Vec::new()))
115}
116
117#[cfg(test)]
118mod tests {
119 use super::{format_provider_table, provider_json, provider_summary_oneliner};
120 use crate::core::context_kernel::envelope_bridge::ProviderStat;
121 use crate::core::context_kernel::token_envelope::ProviderKind;
122
123 fn stats() -> [ProviderStat; 2] {
124 let open_ai = ProviderStat {
125 provider: ProviderKind::OpenAi,
126 request_count: 42,
127 total_input: 125_400,
128 ..ProviderStat::default()
129 };
130 let anthropic = ProviderStat {
131 provider: ProviderKind::Anthropic,
132 request_count: 18,
133 total_input: 54_000,
134 ..ProviderStat::default()
135 };
136 [open_ai, anthropic]
137 }
138
139 #[test]
140 fn table_with_data() {
141 let table = format_provider_table(&stats());
142 assert!(table.contains("│") && table.contains("OpenAI") && table.contains("Anthropic"));
143 }
144
145 #[test]
146 fn table_empty() {
147 assert_eq!(format_provider_table(&[]), "No provider data available.");
148 }
149
150 #[test]
151 fn oneliner_format() {
152 let line = provider_summary_oneliner(&stats());
153 assert!(line.contains("OpenAI(42)") && line.contains("Anthropic(18)"));
154 }
155
156 #[test]
157 fn json_array_valid() {
158 let json = provider_json(&stats());
159 assert!(json.is_array());
160 assert_eq!(json[0]["provider_name"], "OpenAI");
161 assert_eq!(json[0]["request_count"], 42);
162 assert_eq!(json[0]["avg_input"], 2_985);
163 }
164
165 #[test]
166 fn oneliner_empty() {
167 assert_eq!(provider_summary_oneliner(&[]), "No provider data");
168 }
169}