1use crate::telemetry::{ContextLimit, CostEstimate, TOKEN_USAGE, TokenUsageSnapshot};
2use crate::tui::theme::Theme;
3use ratatui::{
4 Frame,
5 style::{Color, Modifier, Style},
6 text::{Line, Span},
7 widgets::{Paragraph, Wrap},
8};
9
10pub struct TokenDisplay {
12 model_context_limits: std::collections::HashMap<String, u64>,
13}
14
15impl TokenDisplay {
16 pub fn new() -> Self {
17 let mut limits = std::collections::HashMap::new();
18
19 limits.insert("gpt-4".to_string(), 128_000);
21 limits.insert("gpt-4-turbo".to_string(), 128_000);
22 limits.insert("gpt-4o".to_string(), 128_000);
23 limits.insert("gpt-4o-mini".to_string(), 128_000);
24 limits.insert("claude-3-5-sonnet".to_string(), 200_000);
25 limits.insert("claude-3-5-haiku".to_string(), 200_000);
26 limits.insert("claude-3-opus".to_string(), 200_000);
27 limits.insert("gemini-2.0-flash".to_string(), 1_000_000);
28 limits.insert("gemini-1.5-flash".to_string(), 1_000_000);
29 limits.insert("gemini-1.5-pro".to_string(), 2_000_000);
30 limits.insert("k1.5".to_string(), 200_000);
31 limits.insert("k1.6".to_string(), 200_000);
32
33 Self {
34 model_context_limits: limits,
35 }
36 }
37
38 pub fn get_context_limit(&self, model: &str) -> Option<u64> {
40 self.model_context_limits.get(model).copied()
41 }
42
43 fn get_model_pricing(&self, model: &str) -> (f64, f64) {
45 match model.to_lowercase().as_str() {
46 m if m.contains("gpt-4o-mini") => (0.15, 0.60), m if m.contains("gpt-4o") => (2.50, 10.00), m if m.contains("gpt-4-turbo") => (10.00, 30.00), m if m.contains("gpt-4") => (30.00, 60.00), m if m.contains("claude-3-5-sonnet") => (3.00, 15.00), m if m.contains("claude-3-5-haiku") => (0.80, 4.00), m if m.contains("claude-3-opus") => (15.00, 75.00), m if m.contains("gemini-2.0-flash") => (0.075, 0.30), m if m.contains("gemini-1.5-flash") => (0.075, 0.30), m if m.contains("gemini-1.5-pro") => (1.25, 5.00), m if m.contains("glm-4") => (0.50, 0.50), m if m.contains("k1.5") => (8.00, 8.00), m if m.contains("k1.6") => (6.00, 6.00), _ => (1.00, 3.00), }
61 }
62
63 pub fn calculate_cost_for_tokens(&self, model: &str, input_tokens: u64, output_tokens: u64) -> CostEstimate {
65 let (input_price, output_price) = self.get_model_pricing(model);
66 CostEstimate::from_tokens(
67 &crate::telemetry::TokenCounts::new(input_tokens, output_tokens),
68 input_price,
69 output_price,
70 )
71 }
72
73 pub fn create_status_bar(&self, theme: &Theme) -> Line<'_> {
75 let global_snapshot = TOKEN_USAGE.global_snapshot();
76 let model_snapshots = TOKEN_USAGE.model_snapshots();
77
78 let total_tokens = global_snapshot.totals.total();
79 let session_cost = self.calculate_session_cost();
80
81 let mut spans = Vec::new();
82
83 spans.push(Span::styled(
85 " ? ",
86 Style::default()
87 .fg(theme.status_bar_foreground.to_color())
88 .bg(theme.status_bar_background.to_color()),
89 ));
90 spans.push(Span::raw(" Help "));
91
92 spans.push(Span::styled(
94 " Tab ",
95 Style::default()
96 .fg(theme.status_bar_foreground.to_color())
97 .bg(theme.status_bar_background.to_color()),
98 ));
99 spans.push(Span::raw(" Switch Agent "));
100
101 spans.push(Span::styled(
103 " Ctrl+C ",
104 Style::default()
105 .fg(theme.status_bar_foreground.to_color())
106 .bg(theme.status_bar_background.to_color()),
107 ));
108 spans.push(Span::raw(" Quit "));
109
110 spans.push(Span::styled(
112 format!(" Tokens: {} ", total_tokens),
113 Style::default().fg(theme.timestamp_color.to_color()),
114 ));
115
116 spans.push(Span::styled(
118 format!(" Cost: {} ", session_cost.format_smart()),
119 Style::default().fg(theme.timestamp_color.to_color()),
120 ));
121
122 if let Some(warning) = self.get_context_warning(&model_snapshots) {
124 spans.push(Span::styled(
125 format!(" {} ", warning),
126 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
127 ));
128 }
129
130 Line::from(spans)
131 }
132
133 pub fn calculate_session_cost(&self) -> CostEstimate {
135 let model_snapshots = TOKEN_USAGE.model_snapshots();
136 let mut total = CostEstimate::default();
137
138 for snapshot in model_snapshots {
139 let model_cost = self.calculate_cost_for_tokens(
140 &snapshot.name,
141 snapshot.totals.input,
142 snapshot.totals.output,
143 );
144 total.input_cost += model_cost.input_cost;
145 total.output_cost += model_cost.output_cost;
146 total.total_cost += model_cost.total_cost;
147 }
148
149 total
150 }
151
152 fn get_context_warning(&self, model_snapshots: &[TokenUsageSnapshot]) -> Option<String> {
154 if model_snapshots.is_empty() {
155 return None;
156 }
157
158 let active_model = model_snapshots.iter().max_by_key(|s| s.totals.total())?;
160
161 if let Some(limit) = self.get_context_limit(&active_model.name) {
162 let context = ContextLimit::new(active_model.totals.total(), limit);
163
164 if context.percentage >= 75.0 {
165 return Some(format!("⚠️ Context: {:.1}%", context.percentage));
166 }
167 }
168
169 None
170 }
171
172 pub fn create_detailed_display(&self) -> Vec<String> {
174 let mut lines = Vec::new();
175 let global_snapshot = TOKEN_USAGE.global_snapshot();
176 let model_snapshots = TOKEN_USAGE.model_snapshots();
177
178 lines.push("".to_string());
179 lines.push(" TOKEN USAGE & COSTS".to_string());
180 lines.push(" ===================".to_string());
181 lines.push("".to_string());
182
183 let total_cost = self.calculate_session_cost();
185 lines.push(format!(
186 " Total: {} tokens ({} requests) - {}",
187 global_snapshot.totals.total(),
188 global_snapshot.request_count,
189 total_cost.format_currency()
190 ));
191 lines.push(format!(
192 " Current: {} in / {} out",
193 global_snapshot.totals.input, global_snapshot.totals.output
194 ));
195 lines.push("".to_string());
196
197 if !model_snapshots.is_empty() {
199 lines.push(" BY MODEL:".to_string());
200
201 for snapshot in model_snapshots.iter().take(5) {
202 let model_cost = self.calculate_cost_for_tokens(
203 &snapshot.name,
204 snapshot.totals.input,
205 snapshot.totals.output,
206 );
207 lines.push(format!(
208 " {}: {} tokens ({} requests) - {}",
209 snapshot.name,
210 snapshot.totals.total(),
211 snapshot.request_count,
212 model_cost.format_currency()
213 ));
214
215 if let Some(limit) = self.get_context_limit(&snapshot.name) {
217 let context = ContextLimit::new(snapshot.totals.total(), limit);
218 if context.percentage >= 50.0 {
219 lines.push(format!(
220 " Context: {:.1}% of {} tokens",
221 context.percentage, limit
222 ));
223 }
224 }
225 }
226
227 if model_snapshots.len() > 5 {
228 lines.push(format!(
229 " ... and {} more models",
230 model_snapshots.len() - 5
231 ));
232 }
233 lines.push("".to_string());
234 }
235
236 lines.push(" COST ESTIMATES:".to_string());
238 lines.push(format!(
239 " Session total: {}",
240 total_cost.format_currency()
241 ));
242 lines.push(" Based on approximate pricing".to_string());
243
244 lines
245 }
246}
247
248impl Default for TokenDisplay {
249 fn default() -> Self {
250 Self::new()
251 }
252}