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