use crate::telemetry::{ContextLimit, CostEstimate, TOKEN_USAGE, TokenUsageSnapshot};
use crate::tui::theme::Theme;
use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span},
};
pub struct TokenDisplay {
model_context_limits: std::collections::HashMap<String, u64>,
}
impl TokenDisplay {
pub fn new() -> Self {
let mut limits = std::collections::HashMap::new();
limits.insert("gpt-4".to_string(), 128_000);
limits.insert("gpt-4-turbo".to_string(), 128_000);
limits.insert("gpt-4o".to_string(), 128_000);
limits.insert("gpt-4o-mini".to_string(), 128_000);
limits.insert("claude-3-5-sonnet".to_string(), 200_000);
limits.insert("claude-3-5-haiku".to_string(), 200_000);
limits.insert("claude-3-opus".to_string(), 200_000);
limits.insert("claude-opus-4-6".to_string(), 200_000);
limits.insert("gemini-2.0-flash".to_string(), 1_000_000);
limits.insert("gemini-1.5-flash".to_string(), 1_000_000);
limits.insert("gemini-1.5-pro".to_string(), 2_000_000);
limits.insert("k1.5".to_string(), 200_000);
limits.insert("k1.6".to_string(), 200_000);
Self {
model_context_limits: limits,
}
}
pub fn get_context_limit(&self, model: &str) -> Option<u64> {
self.model_context_limits.get(model).copied()
}
fn get_model_pricing(&self, model: &str) -> (f64, f64) {
match model.to_lowercase().as_str() {
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-opus") => (5.00, 25.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), }
}
pub fn calculate_cost_for_tokens(
&self,
model: &str,
input_tokens: u64,
output_tokens: u64,
) -> CostEstimate {
let (input_price, output_price) = self.get_model_pricing(model);
CostEstimate::from_tokens(
&crate::telemetry::TokenCounts::new(input_tokens, output_tokens),
input_price,
output_price,
)
}
pub fn create_status_bar(&self, theme: &Theme) -> Line<'_> {
let global_snapshot = TOKEN_USAGE.global_snapshot();
let model_snapshots = TOKEN_USAGE.model_snapshots();
let total_tokens = global_snapshot.totals.total();
let session_cost = self.calculate_session_cost();
let tps_display = self.get_tps_display();
let mut spans = Vec::new();
spans.push(Span::styled(
" ? ",
Style::default()
.fg(theme.status_bar_foreground.to_color())
.bg(theme.status_bar_background.to_color()),
));
spans.push(Span::raw(" Help "));
spans.push(Span::styled(
" Tab ",
Style::default()
.fg(theme.status_bar_foreground.to_color())
.bg(theme.status_bar_background.to_color()),
));
spans.push(Span::raw(" Switch Agent "));
spans.push(Span::styled(
" Ctrl+C ",
Style::default()
.fg(theme.status_bar_foreground.to_color())
.bg(theme.status_bar_background.to_color()),
));
spans.push(Span::raw(" Quit "));
spans.push(Span::styled(
format!(" Tokens: {} ", total_tokens),
Style::default().fg(theme.timestamp_color.to_color()),
));
if let Some(tps) = tps_display {
spans.push(Span::styled(
format!(" TPS: {} ", tps),
Style::default().fg(Color::Cyan),
));
}
spans.push(Span::styled(
format!(" Cost: {} ", session_cost.format_smart()),
Style::default().fg(theme.timestamp_color.to_color()),
));
if let Some(warning) = self.get_context_warning(&model_snapshots) {
spans.push(Span::styled(
format!(" {} ", warning),
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
));
}
Line::from(spans)
}
pub fn calculate_session_cost(&self) -> CostEstimate {
let model_snapshots = TOKEN_USAGE.model_snapshots();
let mut total = CostEstimate::default();
for snapshot in model_snapshots {
let model_cost = self.calculate_cost_for_tokens(
&snapshot.name,
snapshot.totals.input,
snapshot.totals.output,
);
total.input_cost += model_cost.input_cost;
total.output_cost += model_cost.output_cost;
total.total_cost += model_cost.total_cost;
}
total
}
fn get_context_warning(&self, model_snapshots: &[TokenUsageSnapshot]) -> Option<String> {
if model_snapshots.is_empty() {
return None;
}
let active_model = model_snapshots.iter().max_by_key(|s| s.totals.total())?;
if let Some(limit) = self.get_context_limit(&active_model.name) {
let context = ContextLimit::new(active_model.totals.total(), limit);
if context.percentage >= 75.0 {
return Some(format!("⚠️ Context: {:.1}%", context.percentage));
}
}
None
}
fn get_tps_display(&self) -> Option<String> {
use crate::telemetry::PROVIDER_METRICS;
let snapshots = PROVIDER_METRICS.all_snapshots();
if snapshots.is_empty() {
return None;
}
let most_active = snapshots
.iter()
.filter(|s| s.avg_tps > 0.0)
.max_by(|a, b| {
a.total_output_tokens
.partial_cmp(&b.total_output_tokens)
.unwrap_or(std::cmp::Ordering::Equal)
})?;
let tps = most_active.avg_tps;
let formatted = if tps >= 100.0 {
format!("{:.0}", tps)
} else if tps >= 10.0 {
format!("{:.1}", tps)
} else {
format!("{:.2}", tps)
};
Some(formatted)
}
pub fn create_detailed_display(&self) -> Vec<String> {
use crate::telemetry::PROVIDER_METRICS;
let mut lines = Vec::new();
let global_snapshot = TOKEN_USAGE.global_snapshot();
let model_snapshots = TOKEN_USAGE.model_snapshots();
lines.push("".to_string());
lines.push(" TOKEN USAGE & COSTS".to_string());
lines.push(" ===================".to_string());
lines.push("".to_string());
let total_cost = self.calculate_session_cost();
lines.push(format!(
" Total: {} tokens ({} requests) - {}",
global_snapshot.totals.total(),
global_snapshot.request_count,
total_cost.format_currency()
));
lines.push(format!(
" Current: {} in / {} out",
global_snapshot.totals.input, global_snapshot.totals.output
));
lines.push("".to_string());
if !model_snapshots.is_empty() {
lines.push(" BY MODEL:".to_string());
for snapshot in model_snapshots.iter().take(5) {
let model_cost = self.calculate_cost_for_tokens(
&snapshot.name,
snapshot.totals.input,
snapshot.totals.output,
);
lines.push(format!(
" {}: {} tokens ({} requests) - {}",
snapshot.name,
snapshot.totals.total(),
snapshot.request_count,
model_cost.format_currency()
));
if let Some(limit) = self.get_context_limit(&snapshot.name) {
let context = ContextLimit::new(snapshot.totals.total(), limit);
if context.percentage >= 50.0 {
lines.push(format!(
" Context: {:.1}% of {} tokens",
context.percentage, limit
));
}
}
}
if model_snapshots.len() > 5 {
lines.push(format!(
" ... and {} more models",
model_snapshots.len() - 5
));
}
lines.push("".to_string());
}
let provider_snapshots = PROVIDER_METRICS.all_snapshots();
if !provider_snapshots.is_empty() {
lines.push(" PROVIDER PERFORMANCE:".to_string());
for snapshot in provider_snapshots.iter().take(5) {
if snapshot.request_count > 0 {
lines.push(format!(
" {}: {:.1} avg TPS | {:.0}ms avg latency | {} reqs",
snapshot.provider,
snapshot.avg_tps,
snapshot.avg_latency_ms,
snapshot.request_count
));
if snapshot.request_count >= 5 {
lines.push(format!(
" p50: {:.1} TPS / {:.0}ms | p95: {:.1} TPS / {:.0}ms",
snapshot.p50_tps,
snapshot.p50_latency_ms,
snapshot.p95_tps,
snapshot.p95_latency_ms
));
}
}
}
lines.push("".to_string());
}
lines.push(" COST ESTIMATES:".to_string());
lines.push(format!(
" Session total: {}",
total_cost.format_currency()
));
lines.push(" Based on approximate pricing".to_string());
lines
}
}
impl Default for TokenDisplay {
fn default() -> Self {
Self::new()
}
}