Skip to main content

lean_ctx/core/context_kernel/
provider_display.rs

1//! Dashboard and CLI formatting for per-provider token statistics.
2
3use 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
46/// Formats provider statistics as a multi-line dashboard table.
47pub 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
85/// Summarizes provider request counts on one line.
86pub 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
104/// Serializes provider statistics into dashboard-ready JSON.
105pub 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}