use ratatui::style::Style;
use ratatui::text::Span;
use crate::shared::config::InterfaceSettings;
use crate::shared::i18n::Locale;
use crate::shared::theme::Palette;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TokenCounter {
pub generating: bool,
pub tokens: u64,
pub context: Option<u64>,
pub context_exact: bool,
pub reasoning: u32,
pub cached: Option<u32>,
pub notes: CounterNotes,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CounterNotes {
pub reasoning: bool,
pub cache: bool,
}
impl Default for CounterNotes {
fn default() -> Self {
Self {
reasoning: true,
cache: true,
}
}
}
impl CounterNotes {
pub fn of(interface: &InterfaceSettings) -> Self {
Self {
reasoning: interface.show_reasoning_tokens,
cache: interface.show_cache_share,
}
}
}
impl TokenCounter {
pub fn forms(&self, loc: &'static Locale) -> Vec<String> {
if self.tokens == 0 && self.context.is_none() {
return Vec::new();
}
let total = (self.context.unwrap_or(0) + self.tokens).to_string();
let approx = if self.context.is_some() && !self.context_exact {
"~"
} else {
""
};
let label = |reason: &str| {
loc.tf(
"ui.status.tokens",
&[("approx", approx), ("total", &total), ("reason", reason)],
)
};
let notes = self.notes_text(loc);
let mut forms = vec![label(¬es)];
if !notes.is_empty() {
forms.push(label(""));
}
forms
}
pub fn span(&self, palette: &Palette, loc: &'static Locale) -> Option<Span<'static>> {
let full = self.forms(loc).into_iter().next()?;
Some(Span::styled(full, self.style(palette)))
}
pub fn style(&self, palette: &Palette) -> Style {
if self.generating {
palette.accent_style()
} else {
palette.muted_style()
}
}
fn notes_text(&self, loc: &'static Locale) -> String {
let mut notes = Vec::new();
if self.notes.reasoning && self.reasoning > 0 {
notes.push(loc.tf(
"ui.status.tokens.reasoning",
&[("n", &self.reasoning.to_string())],
));
}
if self.notes.cache
&& let Some(pct) = cache_share(self.cached, self.context)
{
notes.push(loc.tf("ui.status.tokens.cache", &[("pct", &pct.to_string())]));
}
if notes.is_empty() {
String::new()
} else {
format!(" ({})", notes.join(" · "))
}
}
}
fn cache_share(cached: Option<u32>, context: Option<u64>) -> Option<u64> {
let context = context.filter(|c| *c > 0)?;
Some((u64::from(cached?) * 100 / context).min(100))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::i18n::Lang::{En, Ru};
use crate::shared::i18n::locale;
fn counter(reasoning: u32, cached: Option<u32>) -> TokenCounter {
TokenCounter {
tokens: 5,
context: Some(8409),
context_exact: true,
reasoning,
cached,
..TokenCounter::default()
}
}
fn full(c: &TokenCounter, lang: crate::shared::i18n::Lang) -> String {
c.forms(locale(lang)).remove(0)
}
#[test]
fn the_total_sums_the_conversation_and_the_reply() {
let ru = locale(Ru);
let at = |tokens, context, exact| TokenCounter {
tokens,
context,
context_exact: exact,
..TokenCounter::default()
};
assert!(at(0, None, false).forms(ru).is_empty());
assert!(at(0, None, false).span(&Palette::default(), ru).is_none());
assert_eq!(at(42, None, false).forms(ru), ["токены: 42"]);
assert_eq!(at(42, Some(1000), false).forms(ru), ["токены: ~1042"]);
assert_eq!(at(42, Some(1000), true).forms(ru), ["токены: 1042"]);
assert_eq!(at(0, Some(1000), false).forms(ru), ["токены: ~1000"]);
}
#[test]
fn the_notes_share_one_parenthesis() {
assert_eq!(
full(&counter(0, Some(8406)), En),
"tokens: 8414 (cache 99%)"
);
assert_eq!(full(&counter(0, Some(8406)), Ru), "токены: 8414 (кэш 99%)");
assert_eq!(full(&counter(0, Some(0)), En), "tokens: 8414 (cache 0%)");
assert_eq!(
counter(120, Some(8192)).forms(locale(En)),
["tokens: 8414 (reasoning 120 · cache 97%)", "tokens: 8414"]
);
assert_eq!(
full(&counter(120, Some(8192)), Ru),
"токены: 8414 (рассужд. 120 · кэш 97%)"
);
assert_eq!(counter(0, None).forms(locale(En)), ["tokens: 8414"]);
}
#[test]
fn each_note_follows_its_setting() {
let mut c = counter(120, Some(8192));
c.notes.reasoning = false;
assert_eq!(full(&c, En), "tokens: 8414 (cache 97%)");
c.notes = CounterNotes {
reasoning: true,
cache: false,
};
assert_eq!(full(&c, En), "tokens: 8414 (reasoning 120)");
c.notes.reasoning = false;
assert_eq!(c.forms(locale(En)), ["tokens: 8414"]);
assert_eq!(
CounterNotes::of(&InterfaceSettings::default()),
CounterNotes::default()
);
let only_cache = InterfaceSettings {
show_reasoning_tokens: false,
..InterfaceSettings::default()
};
assert_eq!(
CounterNotes::of(&only_cache),
CounterNotes {
reasoning: false,
cache: true
}
);
assert!(CounterNotes::default().reasoning && CounterNotes::default().cache);
}
#[test]
fn bright_while_the_turn_runs_muted_after() {
let p = Palette::default();
let mut c = counter(0, None);
assert_eq!(c.span(&p, locale(Ru)).unwrap().style, p.muted_style());
c.generating = true;
assert_eq!(c.span(&p, locale(Ru)).unwrap().style, p.accent_style());
}
#[test]
fn the_cache_share_rounds_down_and_needs_both_figures() {
assert_eq!(cache_share(Some(8406), Some(8409)), Some(99));
assert_eq!(cache_share(Some(8409), Some(8409)), Some(100));
assert_eq!(cache_share(Some(7782), Some(9825)), Some(79));
assert_eq!(cache_share(Some(0), Some(9825)), Some(0));
assert_eq!(cache_share(None, Some(9825)), None);
assert_eq!(cache_share(Some(10), None), None);
assert_eq!(cache_share(Some(10), Some(0)), None);
assert_eq!(cache_share(Some(20), Some(10)), Some(100));
}
}