use std::collections::BTreeMap;
use std::env;
use std::io::{self, IsTerminal};
use chrono::{DateTime, Datelike, Local, Utc};
use costroid_core::{
AggregateTotals, Alert, AlertLevel, AnomaliesView, Anomaly, AnomalySignal, BenchFrontier,
BenchView, BudgetExcludedTool, BudgetExclusion, BudgetPace, BudgetRow, BudgetScope, BudgetView,
CostLane, CostLaneSummary, FocusRecord, ForecastView, FrontierPoint, FrontierStanding, GroupBy,
LimitAvailability, LimitSummary, ModelRow, ModelsView, NowSummary, OverlayModel, Period,
PeriodRange, ProviderCapabilityView, ProviderStatus, ProviderStatusKind, QuotaEta,
QuotaEtaOutcome, QuotaEtaUnavailable, RepricingDelta, RepricingStatus, SpendForecast,
TokenTotals, TrendsSummary,
};
use costroid_providers::{AuthMethod, DataSource, LimitKind, LimitMeasure, ProviderId};
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
#[cfg(any(feature = "connect", test))]
use costroid_core::{
AmountConfidence, BilledAbsence, CostReconciliation, DayReconciliation, ModelReconciliation,
ReconciledReportStatus, UsdAmount, VendorBilled, VendorReportUnavailable,
};
const COST_BAR_WIDTH: usize = 12;
const DEFAULT_RENDER_WIDTH: usize = 64;
const LIMIT_BAR_WIDTH: usize = 12;
const STATUS_BAR_WIDTH: usize = 4;
const WARN_FRACTION: f64 = costroid_core::ALERT_WARN_FRACTION;
const CRITICAL_FRACTION: f64 = costroid_core::ALERT_CRITICAL_FRACTION;
const LIMIT_FRESHNESS_STAMP_MINUTES: i64 = 10;
const UNVERIFIED_CUE: &str = " ? unverified";
const CLAUDE_CHAT_CAVEAT: &str =
"reflects Claude Code's view; claude.ai chat usage may make true usage higher.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RenderMode {
Braille,
Ascii,
Plain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RenderOptions {
pub mode: RenderMode,
pub ansi: bool,
pub width: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SemanticStyle {
Plain,
Strong,
Warn,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LimitState {
Normal,
Warn,
Critical,
Over,
}
impl RenderOptions {
pub(crate) fn plain() -> Self {
Self {
mode: RenderMode::Plain,
ansi: false,
width: DEFAULT_RENDER_WIDTH,
}
}
pub(crate) fn with_width(mut self, width: usize) -> Self {
self.width = width.max(1);
self
}
#[cfg(test)]
fn braille(ansi: bool) -> Self {
Self {
mode: RenderMode::Braille,
ansi,
width: DEFAULT_RENDER_WIDTH,
}
}
#[cfg(test)]
fn ascii(ansi: bool) -> Self {
Self {
mode: RenderMode::Ascii,
ansi,
width: DEFAULT_RENDER_WIDTH,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StyledDocument {
pub(crate) lines: Vec<StyledLine>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StyledLine {
pub(crate) spans: Vec<StyledSpan>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StyledSpan {
pub(crate) content: String,
pub(crate) style: SemanticStyle,
}
impl StyledDocument {
pub(crate) fn new() -> Self {
Self { lines: Vec::new() }
}
pub(crate) fn push(&mut self, line: StyledLine) {
self.lines.push(line);
}
pub(crate) fn render(&self, options: RenderOptions) -> String {
let mut out = String::new();
for line in &self.lines {
out.push_str(&line.render(options));
out.push('\n');
}
out
}
}
impl StyledLine {
pub(crate) fn new() -> Self {
Self { spans: Vec::new() }
}
pub(crate) fn plain(content: impl Into<String>) -> Self {
Self {
spans: vec![StyledSpan::plain(content)],
}
}
pub(crate) fn push_plain(&mut self, content: impl Into<String>) {
self.spans.push(StyledSpan::plain(content));
}
pub(crate) fn push_styled(&mut self, content: impl Into<String>, style: SemanticStyle) {
self.spans.push(StyledSpan {
content: content.into(),
style,
});
}
pub(crate) fn render(&self, options: RenderOptions) -> String {
let mut out = String::new();
for span in &self.spans {
out.push_str(&span.render(options));
}
out
}
}
impl StyledSpan {
pub(crate) fn plain(content: impl Into<String>) -> Self {
Self {
content: content.into(),
style: SemanticStyle::Plain,
}
}
fn styled(content: impl Into<String>, style: SemanticStyle) -> Self {
Self {
content: content.into(),
style,
}
}
fn render(&self, options: RenderOptions) -> String {
if !options.ansi || options.mode == RenderMode::Plain || self.style == SemanticStyle::Plain
{
return self.content.clone();
}
let code = match self.style {
SemanticStyle::Plain => return self.content.clone(),
SemanticStyle::Strong => "1",
SemanticStyle::Warn => "33;1",
SemanticStyle::Critical => "31;1",
};
format!("\x1b[{code}m{}\x1b[0m", self.content)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct EnvSnapshot {
term: Option<String>,
lang: Option<String>,
lc_all: Option<String>,
lc_ctype: Option<String>,
no_color: Option<String>,
}
impl EnvSnapshot {
fn current() -> Self {
Self {
term: env::var("TERM").ok(),
lang: env::var("LANG").ok(),
lc_all: env::var("LC_ALL").ok(),
lc_ctype: env::var("LC_CTYPE").ok(),
no_color: env::var("NO_COLOR").ok(),
}
}
}
pub(crate) fn detect_render_options(plain: bool) -> RenderOptions {
select_render_options(plain, io::stdout().is_terminal(), &EnvSnapshot::current())
}
fn select_render_options(plain: bool, stdout_is_tty: bool, env: &EnvSnapshot) -> RenderOptions {
if plain || !stdout_is_tty {
return RenderOptions::plain();
}
let no_color = env
.no_color
.as_deref()
.map(str::trim)
.is_some_and(|value| !value.is_empty());
let mode = if braille_capable(env) {
RenderMode::Braille
} else {
RenderMode::Ascii
};
RenderOptions {
mode,
ansi: !no_color,
width: DEFAULT_RENDER_WIDTH,
}
}
fn braille_capable(env: &EnvSnapshot) -> bool {
if env
.term
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|term| term.eq_ignore_ascii_case("dumb"))
.unwrap_or(true)
{
return false;
}
match locale_value(env) {
Some(locale) => {
let locale = locale.to_ascii_uppercase();
locale.contains("UTF-8") || locale.contains("UTF8")
}
None => true,
}
}
fn locale_value(env: &EnvSnapshot) -> Option<&str> {
[&env.lc_all, &env.lc_ctype, &env.lang]
.into_iter()
.filter_map(Option::as_deref)
.map(str::trim)
.find(|value| !value.is_empty())
}
#[cfg(test)]
pub(crate) fn render_now(summary: &NowSummary, options: RenderOptions) -> String {
render_now_document(summary, options).render(options)
}
pub(crate) fn render_trends(summary: &TrendsSummary, options: RenderOptions) -> String {
render_trends_document(summary, options).render(options)
}
pub(crate) fn render_frontier(view: &BenchView, options: RenderOptions) -> String {
render_frontier_document(view, options).render(options)
}
pub(crate) fn render_frontier_document(view: &BenchView, options: RenderOptions) -> StyledDocument {
match options.mode {
RenderMode::Plain => render_frontier_plain_document(view),
RenderMode::Braille | RenderMode::Ascii => render_frontier_visual_document(view, options),
}
}
#[cfg(any(feature = "connect", test))]
pub(crate) fn render_reconciliation(
vendor: &str,
window_label: &str,
recon: &CostReconciliation,
options: RenderOptions,
) -> String {
render_reconciliation_document(vendor, window_label, recon, options).render(options)
}
#[cfg(any(feature = "connect", test))]
pub(crate) fn render_reconciliation_document(
vendor: &str,
window_label: &str,
recon: &CostReconciliation,
options: RenderOptions,
) -> StyledDocument {
let mut out = StyledDocument::new();
let (total_est, total_billed) = reconciliation_totals(recon);
let money = match total_billed {
Some(billed) => format!(
"est {} / inv {}",
format_money(&total_est, Some("USD"), true),
format_money(&billed, Some("USD"), false),
),
None => format!("est {}", format_money(&total_est, Some("USD"), true)),
};
if options.mode == RenderMode::Plain {
push_line(&mut out, &format!("costroid reconcile {vendor} {money}"));
} else {
push_header_line(&mut out, mark(options), vendor, money, options);
}
push_line(
&mut out,
&fold_ascii(&format!("estimate vs invoice — {window_label}"), options),
);
push_line(
&mut out,
"Local figures are estimates (your tokens x current prices); the vendor invoice is the source of truth.",
);
if let ReconciledReportStatus::Unavailable(reason) = &recon.report {
push_line(
&mut out,
&fold_ascii(
&format!(
"vendor invoice unavailable: {}",
report_unavailable_text(reason, vendor)
),
options,
),
);
}
push_reconcile_rule(&mut out, options);
if recon.days.is_empty() {
push_line(
&mut out,
"No local usage recorded for this vendor in this window.",
);
}
for day in &recon.days {
push_reconciliation_day(&mut out, day, vendor, options);
}
push_reconciliation_footnotes(&mut out, recon, options);
out
}
#[cfg(any(feature = "connect", test))]
fn push_reconciliation_day(
out: &mut StyledDocument,
day: &DayReconciliation,
vendor: &str,
options: RenderOptions,
) {
let line = format!(
"{date} est {est} {inv} {var}",
date = day.date,
est = format_money(&day.local_estimate.as_usd(), Some("USD"), true),
inv = reconcile_billed_cell(&day.vendor_billed, vendor),
var = reconcile_variance_cell(
day.variance,
day.variance_pct,
billed_usd(&day.vendor_billed),
options
),
);
push_line(out, &fold_ascii(&line, options));
for model in &day.by_model {
push_reconciliation_model(out, model, vendor, options);
}
}
#[cfg(any(feature = "connect", test))]
fn push_reconciliation_model(
out: &mut StyledDocument,
model: &ModelReconciliation,
vendor: &str,
options: RenderOptions,
) {
let marker = if matches!(model.confidence, Some(AmountConfidence::DerivedBestEffort)) {
" *"
} else {
""
};
let line = format!(
" {name:<22} est {est} {inv} {var}{marker}",
name = model.model,
est = format_money(&model.local_estimate.as_usd(), Some("USD"), true),
inv = reconcile_billed_cell(&model.vendor_billed, vendor),
var = reconcile_variance_cell(
model.variance,
model.variance_pct,
billed_usd(&model.vendor_billed),
options
),
);
push_line(out, &fold_ascii(&line, options));
}
#[cfg(any(feature = "connect", test))]
fn reconcile_billed_cell(billed: &VendorBilled, vendor: &str) -> String {
match billed {
VendorBilled::Billed(amount) => {
format!("inv {}", format_money(&amount.as_usd(), Some("USD"), false))
}
VendorBilled::Unavailable(absence) => match absence {
BilledAbsence::DayNotCovered => "report doesn't cover this day".to_string(),
BilledAbsence::ModelNotInReport => "not attributed by the vendor".to_string(),
BilledAbsence::ReportUnavailable(reason) => report_unavailable_text(reason, vendor),
},
}
}
#[cfg(any(feature = "connect", test))]
fn billed_usd(billed: &VendorBilled) -> Option<Decimal> {
match billed {
VendorBilled::Billed(amount) => Some(amount.as_usd()),
VendorBilled::Unavailable(_) => None,
}
}
#[cfg(any(feature = "connect", test))]
fn report_unavailable_text(reason: &VendorReportUnavailable, vendor: &str) -> String {
match reason {
VendorReportUnavailable::NotConnected => format!("connect {vendor} first"),
other => other.message(),
}
}
#[cfg(any(feature = "connect", test))]
fn reconcile_variance_cell(
variance: Option<UsdAmount>,
variance_pct: Option<Decimal>,
billed_usd: Option<Decimal>,
options: RenderOptions,
) -> String {
let variance = match variance {
Some(variance) => variance.as_usd(),
None => return dash(options).to_string(),
};
if variance.is_zero() {
return "exact".to_string();
}
let over = variance > Decimal::ZERO;
let word = if over { "over" } else { "under" };
let abs = variance.abs();
let variance_subcent = abs.round_dp(2).is_zero();
let money = if variance_subcent {
format!("<$0.01 {word}")
} else {
let sign = if over { "+" } else { "-" };
format!("{sign}{} {word}", format_money(&abs, Some("USD"), false))
};
match variance_pct {
Some(_)
if !variance_subcent
&& matches!(billed_usd, Some(billed) if !billed.is_zero() && billed.round_dp(2).is_zero()) =>
{
format!("{money} (vs <$0.01 billed)")
}
Some(pct) => format!("{money} ({})", format_signed_pct(pct)),
None => format!("{money} (vs $0 billed)"),
}
}
#[cfg(any(feature = "connect", test))]
fn format_signed_pct(pct: Decimal) -> String {
let mut magnitude = pct.abs().round_dp(1);
magnitude.rescale(1);
let sign = if pct.is_sign_negative() { "-" } else { "+" };
format!("{sign}{magnitude}%")
}
#[cfg(any(feature = "connect", test))]
fn push_reconcile_rule(out: &mut StyledDocument, options: RenderOptions) {
let glyph = if options.mode == RenderMode::Braille {
"─"
} else {
"-"
};
push_line(out, &glyph.repeat(options.width.max(1)));
}
#[cfg(any(feature = "connect", test))]
fn reconciliation_totals(recon: &CostReconciliation) -> (Decimal, Option<Decimal>) {
let total_est = recon
.days
.iter()
.fold(Decimal::ZERO, |acc, day| acc + day.local_estimate.as_usd());
let total_billed = match recon.report {
ReconciledReportStatus::Available => Some(recon.days.iter().fold(
Decimal::ZERO,
|acc, day| match &day.vendor_billed {
VendorBilled::Billed(amount) => acc + amount.as_usd(),
VendorBilled::Unavailable(_) => acc,
},
)),
ReconciledReportStatus::Unavailable(_) => None,
};
(total_est, total_billed)
}
#[cfg(any(feature = "connect", test))]
fn push_reconciliation_footnotes(
out: &mut StyledDocument,
recon: &CostReconciliation,
options: RenderOptions,
) {
if recon.caveats.per_model_derived_best_effort {
push_line(
out,
&fold_ascii(
"* OpenAI per-model figures are best-effort (derived from line items).",
options,
),
);
}
if recon.caveats.priority_tier_absent {
push_line(
out,
&fold_ascii(
"Note: Anthropic Priority-Tier spend isn't in this report — the bill may be higher.",
options,
),
);
}
let some_day_uncovered = matches!(recon.report, ReconciledReportStatus::Available)
&& recon.days.iter().any(|day| {
matches!(
day.vendor_billed,
VendorBilled::Unavailable(BilledAbsence::DayNotCovered)
)
});
if some_day_uncovered {
push_line(
out,
"Note: the invoice total covers only the days this report spans; days outside it show \"report doesn't cover this day\".",
);
}
}
#[cfg(any(feature = "connect", test))]
fn dash(options: RenderOptions) -> &'static str {
if options.mode == RenderMode::Braille {
"—"
} else {
"-"
}
}
#[cfg(any(feature = "connect", test))]
fn fold_ascii(value: &str, options: RenderOptions) -> String {
if options.mode == RenderMode::Braille {
value.to_string()
} else {
value
.replace('—', "-")
.replace('…', "...")
.replace('×', "x")
.replace('·', "-")
}
}
const SCATTER_HEIGHT: usize = 6;
const SCATTER_DOT_AT: [[u8; 2]; 4] = [[1, 4], [2, 5], [3, 6], [7, 8]];
struct PlotPoint {
x: f64,
y: f64,
on_frontier: bool,
}
fn render_frontier_visual_document(view: &BenchView, options: RenderOptions) -> StyledDocument {
let mut out = StyledDocument::new();
push_header_line(
&mut out,
mark(options),
"cost vs quality",
format_money(&total_overlay_spend(view), Some("USD"), true),
options,
);
for frontier in &view.frontiers {
push_rule(&mut out, options);
push_line(
&mut out,
&format!(
"{} {} {} as of {}",
frontier.name,
if options.mode == RenderMode::Ascii {
"-"
} else {
"—"
},
role_label(&frontier.role),
frontier.as_of
),
);
push_line(&mut out, &format!(" {}", frontier.cost_note));
push_line(&mut out, &format!(" source: {}", frontier.source));
let points = plot_points(frontier);
let width = scatter_width(options);
for row in scatter_rows(&points, width, SCATTER_HEIGHT, options) {
push_line(&mut out, &format!(" {row}"));
}
push_line(&mut out, " x: cost/task -> y: score (high = top)");
for point in &frontier.points {
push_line(&mut out, &format!(" {}", point_line(point, options)));
}
}
push_rule(&mut out, options);
push_line(&mut out, "your models (API-billed):");
if view.no_api_usage || view.overlay.is_empty() {
push_line(&mut out, " no API-billed usage to compare");
} else {
for model in &view.overlay {
out.push(overlay_line(model));
}
}
push_rule(&mut out, options);
push_line(
&mut out,
&mode_insight(frontier_insight_line(view), options),
);
push_provider_notes(&mut out, &view.providers);
push_empty_provider_guidance(&mut out, &view.providers);
out
}
fn render_frontier_plain_document(view: &BenchView) -> StyledDocument {
let mut out = StyledDocument::new();
push_line(&mut out, "costroid frontier");
for frontier in &view.frontiers {
push_line(
&mut out,
&format!(
"{} - {}, source {}, as of {}",
frontier.name,
role_label(&frontier.role),
frontier.source,
frontier.as_of
),
);
push_line(&mut out, &format!(" caveat: {}", frontier.cost_note));
for point in &frontier.points {
push_line(&mut out, &format!(" {}", plain_point_line(point, view)));
}
}
if view.no_api_usage || view.overlay.is_empty() {
push_line(&mut out, "your models: no API-billed usage to compare");
} else {
push_line(&mut out, "your models (API-billed):");
for model in &view.overlay {
push_line(&mut out, &format!(" {}", plain_overlay_line(model)));
}
}
push_line(&mut out, &plain_frontier_insight_line(view));
push_provider_notes(&mut out, &view.providers);
push_empty_provider_guidance(&mut out, &view.providers);
out
}
fn total_overlay_spend(view: &BenchView) -> Decimal {
view.overlay
.iter()
.fold(Decimal::ZERO, |total, model| total + model.billed_cost)
}
fn role_label(role: &str) -> String {
match role {
"primary" => "primary (neutral)".to_string(),
"corroborating" => "corroborating (vendor)".to_string(),
other => other.to_string(),
}
}
fn plot_points(frontier: &BenchFrontier) -> Vec<PlotPoint> {
frontier
.points
.iter()
.filter_map(|point| {
let cost = point.cost_per_task_usd?;
Some(PlotPoint {
x: cost.to_f64().unwrap_or(0.0),
y: point.score_pct.to_f64().unwrap_or(0.0),
on_frontier: point.standing == FrontierStanding::OnFrontier,
})
})
.collect()
}
fn frontier_standing_text(standing: &FrontierStanding) -> String {
match standing {
FrontierStanding::OnFrontier => "on frontier".to_string(),
FrontierStanding::Dominated { by } => format!("off (dominated by {by})"),
FrontierStanding::CostUnknown => "score only".to_string(),
}
}
fn point_line(point: &FrontierPoint, options: RenderOptions) -> String {
let cost = match point.cost_per_task_usd {
Some(value) => format!("@ {}", format_money(&value, Some("USD"), true)),
None => "@ cost n/a".to_string(),
};
let standing = frontier_standing_text(&point.standing);
let note = point
.note
.as_deref()
.map(|note| {
let separator = if options.mode == RenderMode::Ascii {
"--"
} else {
"—"
};
format!(" {separator} {note}")
})
.unwrap_or_default();
format!(
"{} {}% {} {}{}",
point.label,
score_text(point),
cost,
standing,
note
)
}
fn plain_point_line(point: &FrontierPoint, view: &BenchView) -> String {
let cost = match point.cost_per_task_usd {
Some(value) => format!("{}/task", format_money(&value, Some("USD"), true)),
None => "n/a".to_string(),
};
let standing = match &point.standing {
FrontierStanding::OnFrontier => "yes".to_string(),
FrontierStanding::Dominated { by } => format!("no (dominated by {by})"),
FrontierStanding::CostUnknown => "n/a (no published cost)".to_string(),
};
let spend = view
.overlay
.iter()
.find(|model| model.model_id == point.model_id)
.map(|model| format_money(&model.billed_cost, Some("USD"), true))
.unwrap_or_else(|| "none".to_string());
let note = point
.note
.as_deref()
.map(|note| format!(" -- {note}"))
.unwrap_or_default();
format!(
"{}: score {}%, cost {}, on frontier: {}, your API spend: {}{}",
point.label,
score_text(point),
cost,
standing,
spend,
note
)
}
fn score_text(point: &FrontierPoint) -> String {
point.score_pct.normalize().to_string()
}
fn overlay_line(model: &OverlayModel) -> StyledLine {
if !model.fully_priced {
return StyledLine::plain(format!(
" {} spend not fully priced (frontier comparison unavailable)",
model.model_id
));
}
let mut line = StyledLine::new();
line.push_plain(format!(" {} spent ", model.model_id));
line.push_styled(
format_money(&model.billed_cost, Some("USD"), true),
SemanticStyle::Strong,
);
if let Some(delta) = best_delta(model) {
let phrase = delta_phrase(delta);
if !phrase.is_empty() {
line.push_plain(phrase);
}
}
line
}
fn plain_overlay_line(model: &OverlayModel) -> String {
if !model.fully_priced {
return format!(
"{}: spend not fully priced (frontier comparison unavailable)",
model.model_id
);
}
let mut line = format!(
"{}: spent {}",
model.model_id,
format_money(&model.billed_cost, Some("USD"), true)
);
if let Some(delta) = best_delta(model) {
line.push_str(&delta_phrase(delta));
}
line
}
fn best_delta(model: &OverlayModel) -> Option<&RepricingDelta> {
model
.repricing
.iter()
.filter(|delta| delta.status == RepricingStatus::Computed)
.min_by(|left, right| left.delta_usd.cmp(&right.delta_usd))
}
fn delta_phrase(delta: &RepricingDelta) -> String {
let money = format_money(&delta.delta_usd.abs(), Some("USD"), true);
if delta.delta_usd < Decimal::ZERO {
format!(
"; {} costs about {} less at equal volume",
delta.target_model_id, money
)
} else if delta.delta_usd > Decimal::ZERO {
format!(
"; {} costs about {} more at equal volume",
delta.target_model_id, money
)
} else {
String::new()
}
}
fn frontier_insight_line(view: &BenchView) -> String {
if view.no_api_usage || view.overlay.is_empty() {
return "◆ no API-billed usage to compare against the frontier. (estimated)".to_string();
}
let top = view
.overlay
.iter()
.max_by(|left, right| left.billed_cost.cmp(&right.billed_cost));
match top {
Some(model) => {
if let Some(delta) = best_delta(model) {
if delta.delta_usd < Decimal::ZERO {
return format!(
"◆ {} drove most of your API spend; {} sits cheaper on the frontier at equal volume. (estimated)",
model.model_id, delta.target_model_id
);
}
}
if model
.appearances
.iter()
.any(|appearance| appearance.standing == FrontierStanding::OnFrontier)
{
format!(
"◆ {} drove most of your API spend and already sits on the frontier. (estimated)",
model.model_id
)
} else {
format!(
"◆ {} drove most of your API spend. (estimated)",
model.model_id
)
}
}
None => "◆ no API-billed usage to compare against the frontier. (estimated)".to_string(),
}
}
fn plain_frontier_insight_line(view: &BenchView) -> String {
frontier_insight_line(view).replace('◆', "insight:")
}
fn scatter_width(options: RenderOptions) -> usize {
options.width.saturating_sub(4).clamp(20, 40)
}
fn scatter_rows(
points: &[PlotPoint],
width: usize,
height: usize,
options: RenderOptions,
) -> Vec<String> {
match options.mode {
RenderMode::Braille => braille_scatter(points, width, height, true),
_ => ascii_scatter(points, width, height),
}
}
fn braille_scatter(
points: &[PlotPoint],
w_cells: usize,
h_cells: usize,
draw_line: bool,
) -> Vec<String> {
let w = w_cells.max(1);
let h = h_cells.max(1);
if points.is_empty() {
return vec![braille_blank().to_string().repeat(w); h];
}
let dot_w = w * 2;
let dot_h = h * 4;
let mut buf = vec![0_u8; w * h];
let (x_min, x_max) = min_max(points.iter().map(|point| point.x));
let (y_min, y_max) = min_max(points.iter().map(|point| point.y));
for point in points {
let dx = axis_index(point.x, x_min, x_max, dot_w, false);
let dy = axis_index(point.y, y_min, y_max, dot_h, true);
set_scatter_dot(&mut buf, w, h, dx, dy);
}
let mut frontier_dots: Vec<(usize, usize)> = points
.iter()
.filter(|point| point.on_frontier)
.map(|point| {
(
axis_index(point.x, x_min, x_max, dot_w, false),
axis_index(point.y, y_min, y_max, dot_h, true),
)
})
.collect();
frontier_dots.sort_by_key(|&(dx, _)| dx);
for &(dx, dy) in &frontier_dots {
let cx = (dx / 2).min(w - 1);
let cy = (dy / 4).min(h - 1);
buf[cy * w + cx] = 0xFF;
}
if draw_line {
for pair in frontier_dots.windows(2) {
scatter_line(&mut buf, w, h, pair[0], pair[1]);
}
}
(0..h)
.map(|cy| {
(0..w)
.map(|cx| byte_to_braille(buf[cy * w + cx]))
.collect::<String>()
})
.collect()
}
fn ascii_scatter(points: &[PlotPoint], w_cells: usize, h_cells: usize) -> Vec<String> {
let w = w_cells.max(1);
let h = h_cells.max(1);
let mut grid = vec![vec![' '; w]; h];
if !points.is_empty() {
let (x_min, x_max) = min_max(points.iter().map(|point| point.x));
let (y_min, y_max) = min_max(points.iter().map(|point| point.y));
for point in points {
let cx = axis_index(point.x, x_min, x_max, w, false);
let cy = axis_index(point.y, y_min, y_max, h, true);
let glyph = if point.on_frontier { '#' } else { '.' };
if !(grid[cy][cx] == '#' && glyph == '.') {
grid[cy][cx] = glyph;
}
}
}
grid.into_iter()
.map(|row| row.into_iter().collect())
.collect()
}
fn set_scatter_dot(buf: &mut [u8], w: usize, h: usize, dx: usize, dy: usize) {
let cx = (dx / 2).min(w - 1);
let cy = (dy / 4).min(h - 1);
let dot = SCATTER_DOT_AT[dy % 4][dx % 2];
buf[cy * w + cx] |= bit_for_dot(dot);
}
fn scatter_line(buf: &mut [u8], w: usize, h: usize, a: (usize, usize), b: (usize, usize)) {
let mut x0 = a.0 as isize;
let mut y0 = a.1 as isize;
let x1 = b.0 as isize;
let y1 = b.1 as isize;
let dx = (x1 - x0).abs();
let dy = -(y1 - y0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
loop {
let px = usize::try_from(x0).unwrap_or(0);
let py = usize::try_from(y0).unwrap_or(0);
set_scatter_dot(buf, w, h, px, py);
if x0 == x1 && y0 == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
x0 += sx;
}
if e2 <= dx {
err += dx;
y0 += sy;
}
}
}
fn min_max(values: impl Iterator<Item = f64>) -> (f64, f64) {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for value in values {
if value < lo {
lo = value;
}
if value > hi {
hi = value;
}
}
if lo.is_finite() && hi.is_finite() {
(lo, hi)
} else {
(0.0, 1.0)
}
}
fn axis_index(value: f64, lo: f64, hi: f64, n: usize, invert: bool) -> usize {
let max_index = n.saturating_sub(1) as f64;
let fraction = if hi > lo {
(value - lo) / (hi - lo)
} else {
0.5
};
let fraction = if invert { 1.0 - fraction } else { fraction };
(fraction * max_index).round().clamp(0.0, max_index) as usize
}
pub(crate) fn render_statusline(summary: &NowSummary, options: RenderOptions) -> String {
render_statusline_line(summary, options).render(options)
}
pub(crate) fn render_now_document(summary: &NowSummary, options: RenderOptions) -> StyledDocument {
match options.mode {
RenderMode::Plain => render_now_plain_document(summary),
RenderMode::Braille | RenderMode::Ascii => render_now_visual_document(summary, options),
}
}
pub(crate) fn render_now_with_alerts_document(
summary: &NowSummary,
alerts: &[Alert],
options: RenderOptions,
) -> StyledDocument {
let mut out = StyledDocument::new();
push_alert_banner(&mut out, alerts, options);
out.lines
.extend(render_now_document(summary, options).lines);
out
}
pub(crate) fn render_now_with_alerts(
summary: &NowSummary,
alerts: &[Alert],
options: RenderOptions,
) -> String {
render_now_with_alerts_document(summary, alerts, options).render(options)
}
pub(crate) fn render_trends_document(
summary: &TrendsSummary,
options: RenderOptions,
) -> StyledDocument {
match options.mode {
RenderMode::Plain => render_trends_plain_document(summary),
RenderMode::Braille | RenderMode::Ascii => render_trends_visual_document(summary, options),
}
}
#[cfg(test)]
pub(crate) fn render_providers(
capabilities: &[ProviderCapabilityView],
statuses: &[ProviderStatus],
options: RenderOptions,
) -> String {
render_providers_document(capabilities, statuses, options).render(options)
}
pub(crate) fn render_providers_document(
capabilities: &[ProviderCapabilityView],
statuses: &[ProviderStatus],
options: RenderOptions,
) -> StyledDocument {
match options.mode {
RenderMode::Plain => render_providers_plain_document(capabilities, statuses),
RenderMode::Braille | RenderMode::Ascii => {
render_providers_visual_document(capabilities, statuses, options)
}
}
}
fn render_providers_visual_document(
capabilities: &[ProviderCapabilityView],
statuses: &[ProviderStatus],
options: RenderOptions,
) -> StyledDocument {
let mut out = StyledDocument::new();
let mut header = StyledLine::new();
header.push_plain(format!("{} costroid", mark(options)));
header.push_styled(" providers", SemanticStyle::Strong);
out.push(header);
if capabilities.is_empty() {
push_rule(&mut out, options);
push_line(&mut out, "no providers to describe");
return out;
}
for capability in capabilities {
push_rule(&mut out, options);
let status = find_status(statuses, capability.provider);
let mut head = StyledLine::new();
head.push_styled(provider_name(capability.provider), SemanticStyle::Strong);
head.push_plain(format!(" ({})", provider_state_word(status)));
out.push(head);
push_line(
&mut out,
&format!(" api cost {}", data_source_phrase(capability.api_cost)),
);
push_line(
&mut out,
&format!(" quota {}", quota_phrase(capability)),
);
push_line(
&mut out,
&format!(" model mix {}", data_source_phrase(capability.model_mix)),
);
push_line(
&mut out,
&format!(" auth {}", auth_phrase(capability.auth)),
);
if let Some(note) = status.and_then(|status| status.message.as_deref()) {
push_line(&mut out, &format!(" note: {note}"));
}
}
out
}
fn render_providers_plain_document(
capabilities: &[ProviderCapabilityView],
statuses: &[ProviderStatus],
) -> StyledDocument {
let mut out = StyledDocument::new();
push_line(&mut out, "costroid providers");
if capabilities.is_empty() {
push_line(&mut out, "no providers to describe");
return out;
}
for capability in capabilities {
let status = find_status(statuses, capability.provider);
push_line(
&mut out,
&format!(
"{} ({}):",
provider_name(capability.provider),
provider_state_word(status)
),
);
push_line(
&mut out,
&format!(" api cost: {}", data_source_phrase(capability.api_cost)),
);
push_line(&mut out, &format!(" quota: {}", quota_phrase(capability)));
push_line(
&mut out,
&format!(" model mix: {}", data_source_phrase(capability.model_mix)),
);
push_line(
&mut out,
&format!(" auth: {}", auth_phrase(capability.auth)),
);
if let Some(note) = status.and_then(|status| status.message.as_deref()) {
push_line(&mut out, &format!(" note: {note}"));
}
}
out
}
fn find_status(statuses: &[ProviderStatus], provider: ProviderId) -> Option<&ProviderStatus> {
statuses.iter().find(|status| status.provider == provider)
}
fn provider_state_word(status: Option<&ProviderStatus>) -> &'static str {
match status {
Some(status) => provider_status(status.status),
None => "not detected",
}
}
fn data_source_phrase(source: DataSource) -> &'static str {
match source {
DataSource::LocalArtifact => "from local logs",
DataSource::SanctionedHook => "from the statusLine capture; run setup-statusline",
DataSource::SanctionedOauth | DataSource::ApiKey => "via your connected key",
DataSource::Unavailable => "no sanctioned source",
}
}
fn auth_phrase(auth: AuthMethod) -> &'static str {
match auth {
AuthMethod::None => "no login required",
AuthMethod::Oauth => "sanctioned OAuth",
AuthMethod::ApiKey => "your own API key",
}
}
fn quota_phrase(capability: &ProviderCapabilityView) -> String {
let source = data_source_phrase(capability.subscription_quota);
if capability.quota_kinds.is_empty() {
source.to_string()
} else {
let kinds = capability
.quota_kinds
.iter()
.map(|kind| limit_kind(*kind))
.collect::<Vec<_>>()
.join(", ");
format!("{source} ({kinds})")
}
}
#[cfg(feature = "connect")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ConnectionEntry {
pub(crate) vendor: String,
pub(crate) state: ConnectionState,
}
#[cfg(feature = "connect")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ConnectionState {
Connected {
org: Option<String>,
},
NotConnected,
Unavailable(String),
}
#[cfg(feature = "connect")]
pub(crate) fn push_provider_connection_lane(
out: &mut StyledDocument,
connections: &[ConnectionEntry],
options: RenderOptions,
) {
if connections.is_empty() {
return;
}
if options.mode != RenderMode::Plain {
push_rule(out, options);
}
push_line(out, "connections (your own usage API keys)");
let dash = if options.mode == RenderMode::Braille {
"—"
} else {
"-"
};
for entry in connections {
let detail = match &entry.state {
ConnectionState::Connected { org: Some(org) } => {
format!("connected {dash} organization {org}")
}
ConnectionState::Connected { org: None } => "connected".to_string(),
ConnectionState::NotConnected => "not connected".to_string(),
ConnectionState::Unavailable(message) => message.clone(),
};
push_line(
out,
&fold_for_ascii(&format!(" {:<10} {detail}", entry.vendor), options),
);
}
}
#[cfg(feature = "connect")]
fn fold_for_ascii(value: &str, options: RenderOptions) -> String {
let sanitized: String = value.chars().filter(|ch| !ch.is_control()).collect();
match options.mode {
RenderMode::Braille => sanitized,
RenderMode::Ascii | RenderMode::Plain => sanitized
.replace('—', "-")
.replace('…', "...")
.chars()
.map(|ch| if ch.is_ascii() { ch } else { '?' })
.collect(),
}
}
#[cfg(test)]
pub(crate) fn render_models(view: &ModelsView, options: RenderOptions) -> String {
render_models_document(view, options).render(options)
}
pub(crate) fn render_models_document(view: &ModelsView, options: RenderOptions) -> StyledDocument {
match options.mode {
RenderMode::Plain => render_models_plain_document(view),
RenderMode::Braille | RenderMode::Ascii => render_models_visual_document(view, options),
}
}
fn render_models_visual_document(view: &ModelsView, options: RenderOptions) -> StyledDocument {
let mut out = StyledDocument::new();
push_header_line(
&mut out,
mark(options),
"models",
format_money(&total_models_spend(view), Some("USD"), true),
options,
);
push_line(&mut out, "scope: all usage (all time)");
if view.no_api_usage || view.models.is_empty() {
push_rule(&mut out, options);
push_line(&mut out, "no API-billed usage to compare");
} else {
for model in &view.models {
push_rule(&mut out, options);
out.push(model_spend_line(model));
push_line(
&mut out,
&format!(" tokens: {}", format_token_mix(&model.totals.tokens)),
);
push_line(
&mut out,
&format!(
" frontier: {}",
model_standing_phrase(model.overlay.as_ref())
),
);
if let Some(phrase) = model_repricing_phrase(model.overlay.as_ref()) {
push_line(&mut out, &format!(" {phrase}"));
}
}
}
push_rule(&mut out, options);
push_models_disclaimer(&mut out, view);
push_provider_notes(&mut out, &view.providers);
push_empty_provider_guidance(&mut out, &view.providers);
out
}
fn render_models_plain_document(view: &ModelsView) -> StyledDocument {
let mut out = StyledDocument::new();
push_line(&mut out, "costroid models");
push_line(&mut out, "scope: all usage (all time)");
if view.no_api_usage || view.models.is_empty() {
push_line(&mut out, "no API-billed usage to compare");
} else {
for model in &view.models {
push_line(&mut out, &plain_model_line(model));
push_line(
&mut out,
&format!(" tokens: {}", format_token_mix(&model.totals.tokens)),
);
push_line(
&mut out,
&format!(
" frontier: {}",
model_standing_phrase(model.overlay.as_ref())
),
);
if let Some(phrase) = model_repricing_phrase(model.overlay.as_ref()) {
push_line(&mut out, &format!(" {phrase}"));
}
}
}
push_models_disclaimer(&mut out, view);
push_provider_notes(&mut out, &view.providers);
push_empty_provider_guidance(&mut out, &view.providers);
out
}
fn total_models_spend(view: &ModelsView) -> Decimal {
view.models.iter().fold(Decimal::ZERO, |total, model| {
total + model.totals.billed_cost
})
}
fn model_spend_line(model: &ModelRow) -> StyledLine {
let mut line = StyledLine::new();
line.push_styled(model.model.clone(), SemanticStyle::Strong);
line.push_plain(" spent ".to_string());
line.push_styled(
format_money(
&model.totals.billed_cost,
model.totals.currency.as_deref(),
true,
),
SemanticStyle::Strong,
);
let badge = pricing_badge_plain(&model.totals);
if !badge.is_empty() {
line.push_plain(format!(" ({})", badge.trim_start_matches(", ")));
}
line
}
fn plain_model_line(model: &ModelRow) -> String {
let mut line = format!(
"{}: spent {}",
model.model,
format_money(
&model.totals.billed_cost,
model.totals.currency.as_deref(),
true
)
);
let badge = pricing_badge_plain(&model.totals);
if !badge.is_empty() {
line.push_str(&format!(" ({})", badge.trim_start_matches(", ")));
}
line
}
fn format_token_mix(tokens: &TokenTotals) -> String {
format!(
"{} in / {} out / {} cache",
with_thousands(&tokens.input.to_string()),
with_thousands(&tokens.output.to_string()),
with_thousands(&(tokens.cache_read + tokens.cache_write).to_string()),
)
}
fn model_standing_phrase(overlay: Option<&OverlayModel>) -> String {
match overlay {
Some(model) if !model.appearances.is_empty() => model
.appearances
.iter()
.map(|appearance| {
format!(
"{} {}% - {}",
appearance.benchmark_name,
appearance.score_pct.normalize(),
frontier_standing_text(&appearance.standing)
)
})
.collect::<Vec<_>>()
.join("; "),
_ => "not benchmarked".to_string(),
}
}
fn model_repricing_phrase(overlay: Option<&OverlayModel>) -> Option<String> {
let delta = best_delta(overlay?)?;
let phrase = delta_phrase(delta);
if phrase.is_empty() {
None
} else {
Some(phrase.trim_start_matches("; ").to_string())
}
}
fn push_models_disclaimer(out: &mut StyledDocument, view: &ModelsView) {
push_line(
out,
&format!(
"{} (pricing as of {})",
view.disclaimer.note, view.disclaimer.pricing_as_of
),
);
}
const HISTORY_EXPORT_HINT: &str =
"export: costroid export --format json|csv (full FOCUS 1.3 record)";
#[cfg(test)]
pub(crate) fn render_history(rows: &[FocusRecord], options: RenderOptions) -> String {
render_history_document(rows, options).render(options)
}
pub(crate) fn render_history_document(
rows: &[FocusRecord],
options: RenderOptions,
) -> StyledDocument {
let mut ordered: Vec<&FocusRecord> = rows.iter().collect();
ordered.sort_by_key(|record| std::cmp::Reverse(record.charge_period_start));
match options.mode {
RenderMode::Plain => render_history_plain_document(&ordered),
RenderMode::Braille | RenderMode::Ascii => {
render_history_visual_document(&ordered, options)
}
}
}
fn render_history_visual_document(rows: &[&FocusRecord], options: RenderOptions) -> StyledDocument {
let mut out = StyledDocument::new();
push_header_line(
&mut out,
mark(options),
"history",
format_money(&history_api_spend(rows), Some("USD"), true),
options,
);
push_line(&mut out, &history_scope_line(rows.len()));
push_line(&mut out, HISTORY_EXPORT_HINT);
push_rule(&mut out, options);
if rows.is_empty() {
push_line(&mut out, "no usage recorded yet");
} else {
for record in rows {
out.push(history_row_line(record));
}
}
out
}
fn render_history_plain_document(rows: &[&FocusRecord]) -> StyledDocument {
let mut out = StyledDocument::new();
push_line(&mut out, "costroid history");
push_line(&mut out, &history_scope_line(rows.len()));
push_line(&mut out, HISTORY_EXPORT_HINT);
if rows.is_empty() {
push_line(&mut out, "no usage recorded yet");
} else {
for record in rows {
push_line(&mut out, &plain_history_row(record));
}
}
out
}
fn history_api_spend(rows: &[&FocusRecord]) -> Decimal {
rows.iter()
.filter(|record| record.x_access_path == "api")
.fold(Decimal::ZERO, |total, record| total + record.billed_cost)
}
fn history_scope_line(count: usize) -> String {
let noun = if count == 1 { "record" } else { "records" };
format!("scope: {count} {noun}, newest first (all time)")
}
fn history_time(record: &FocusRecord) -> String {
record
.charge_period_start
.format("%Y-%m-%d %H:%M")
.to_string()
}
fn history_tokens(record: &FocusRecord) -> String {
format!(
"{} {}",
with_thousands(&record.x_consumed_tokens.trunc().to_string()),
record.x_token_type
)
}
fn history_detail(record: &FocusRecord) -> String {
let mut detail = format!("{} {}", history_tokens(record), record.x_access_path);
if record.x_access_path == "api" {
detail.push_str(&format!(
" {}",
format_money(
&record.billed_cost,
Some(record.billing_currency.as_str()),
true
)
));
}
detail
}
fn history_row_line(record: &FocusRecord) -> StyledLine {
let mut line = StyledLine::new();
line.push_plain(format!("{} ", history_time(record)));
line.push_styled(record.x_model.clone(), SemanticStyle::Strong);
line.push_plain(format!(" {}", history_detail(record)));
line
}
fn plain_history_row(record: &FocusRecord) -> String {
format!(
"{} {} {}",
history_time(record),
record.x_model,
history_detail(record)
)
}
const BUDGET_CONFIG_HINT: &str = "no budget set - set targets in ~/.config/costroid/config.toml";
const BUDGET_ESTIMATE_NOTE: &str = "figures are local estimates (your tokens x current prices); \
run `costroid reconcile` to compare against the provider invoice.";
#[cfg(test)]
pub(crate) fn render_budget(view: &BudgetView, options: RenderOptions) -> String {
render_budget_document(view, options).render(options)
}
pub(crate) fn render_budget_document(view: &BudgetView, options: RenderOptions) -> StyledDocument {
match options.mode {
RenderMode::Plain => render_budget_plain_document(view),
RenderMode::Braille | RenderMode::Ascii => render_budget_visual_document(view, options),
}
}
fn render_budget_visual_document(view: &BudgetView, options: RenderOptions) -> StyledDocument {
let mut out = StyledDocument::new();
push_header_line(
&mut out,
mark(options),
"budget",
format_money(&view.spent_total_usd, Some("USD"), true),
options,
);
push_line(&mut out, &budget_scope_line(view));
if view.no_budget_set {
push_rule(&mut out, options);
push_budget_empty_state(&mut out);
return out;
}
let mut any = false;
for row in &view.rows {
push_rule(&mut out, options);
out.push(budget_row_meter_line(row, options));
push_line(&mut out, &format!(" {}", budget_pace_line(row, view)));
if let Some(over) = &row.over_by_usd {
push_line(&mut out, &format!(" over by {}", format_over_by(over)));
}
any = true;
}
if !view.excluded_tools.is_empty() {
push_rule(&mut out, options);
for excluded in &view.excluded_tools {
push_line(&mut out, &budget_excluded_line(excluded));
}
any = true;
}
if !any {
push_rule(&mut out, options);
push_line(&mut out, BUDGET_NO_USABLE_TARGETS);
}
push_rule(&mut out, options);
push_line(&mut out, BUDGET_ESTIMATE_NOTE);
out
}
fn render_budget_plain_document(view: &BudgetView) -> StyledDocument {
let mut out = StyledDocument::new();
push_line(&mut out, "costroid budget");
push_line(&mut out, &budget_scope_line(view));
if view.no_budget_set {
push_budget_empty_state(&mut out);
return out;
}
let mut any = false;
for row in &view.rows {
push_line(&mut out, &plain_budget_row(row));
push_line(&mut out, &format!(" {}", budget_pace_line(row, view)));
any = true;
}
for excluded in &view.excluded_tools {
push_line(&mut out, &budget_excluded_line(excluded));
any = true;
}
if !any {
push_line(&mut out, BUDGET_NO_USABLE_TARGETS);
}
push_line(&mut out, BUDGET_ESTIMATE_NOTE);
out
}
const BUDGET_NO_USABLE_TARGETS: &str =
"no usable budget targets - check ~/.config/costroid/config.toml";
fn budget_scope_line(view: &BudgetView) -> String {
format!(
"scope: API-lane spend this month ({} of month elapsed)",
percent(view.month_elapsed_fraction)
)
}
fn budget_row_meter_line(row: &BudgetRow, options: RenderOptions) -> StyledLine {
let state = budget_state(row);
let mut line = StyledLine::new();
line.push_styled(budget_scope_label(&row.scope), SemanticStyle::Strong);
line.push_plain(format!(
" {} / {} ",
format_money(&row.spent_usd, Some("USD"), true),
format_money(&row.target_usd, Some("USD"), true),
));
line.spans.push(StyledSpan::styled(
positional_meter_text(row.fraction, LIMIT_BAR_WIDTH, options),
state_style(state),
));
line.push_styled(
format!(" {}{}", percent(row.fraction), state_cue(state)),
state_style(state),
);
line
}
fn budget_state(row: &BudgetRow) -> LimitState {
if row.over_by_usd.is_some() {
LimitState::Over
} else if row.fraction >= CRITICAL_FRACTION {
LimitState::Critical
} else if row.fraction >= WARN_FRACTION {
LimitState::Warn
} else {
LimitState::Normal
}
}
fn plain_budget_row(row: &BudgetRow) -> String {
format!(
"{}: {} / {} budget ({}){}",
budget_scope_label(&row.scope),
format_money(&row.spent_usd, Some("USD"), true),
format_money(&row.target_usd, Some("USD"), true),
percent(row.fraction),
budget_plain_suffix(row),
)
}
fn format_over_by(over: &Decimal) -> String {
if over.round_dp(2) == Decimal::ZERO {
"<$0.01".to_string()
} else {
format_money(over, Some("USD"), true)
}
}
fn budget_plain_suffix(row: &BudgetRow) -> String {
match budget_state(row) {
LimitState::Normal => String::new(),
LimitState::Warn => " ! near budget".to_string(),
LimitState::Critical => " !! at budget".to_string(),
LimitState::Over => format!(
" !! OVER, over by {}",
format_over_by(&row.over_by_usd.unwrap_or(Decimal::ZERO))
),
}
}
fn budget_pace_line(row: &BudgetRow, view: &BudgetView) -> String {
format!(
"pace: {} used vs {} of month elapsed ({})",
percent(row.fraction),
percent(view.month_elapsed_fraction),
budget_pace_phrase(row.pace),
)
}
fn budget_pace_phrase(pace: BudgetPace) -> &'static str {
match pace {
BudgetPace::OnTrack => "on track",
BudgetPace::AheadOfPace => "ahead of pace",
BudgetPace::OverBudget => "over budget",
}
}
fn budget_scope_label(scope: &BudgetScope) -> String {
match scope {
BudgetScope::Total => "total (all tools)".to_string(),
BudgetScope::Tool(tool) => tool.clone(),
}
}
fn budget_excluded_line(excluded: &BudgetExcludedTool) -> String {
match excluded.reason {
BudgetExclusion::FlatFeeSubscription => format!(
"{}: flat-fee subscription - no $ budget applies (not API-billed)",
excluded.tool
),
BudgetExclusion::NotApiBilled => format!(
"{}: no API-billed usage - a $ budget tracks API spend only",
excluded.tool
),
}
}
fn push_budget_empty_state(out: &mut StyledDocument) {
push_line(out, BUDGET_CONFIG_HINT);
push_line(out, "");
push_line(out, "[budget]");
push_line(out, "total_monthly_usd = 100.00");
push_line(out, "");
push_line(out, "[budget.per_tool]");
push_line(out, "claude-code = 60.00");
push_line(out, "codex = 40.00");
}
const ALERTS_OFF_HINT: &str = "alerts are off - enable them in ~/.config/costroid/config.toml";
fn alert_window_phrase(kind: LimitKind) -> &'static str {
match kind {
LimitKind::FiveHour => "5-hour",
LimitKind::Weekly => "weekly",
LimitKind::Daily => "daily",
LimitKind::Monthly => "monthly",
LimitKind::BillingCycle => "billing-cycle",
}
}
fn alert_budget_scope(scope: &BudgetScope) -> String {
match scope {
BudgetScope::Total => "total budget".to_string(),
BudgetScope::Tool(tool) => format!("{tool} budget"),
}
}
fn alert_sentence(alert: &Alert) -> String {
match alert {
Alert::Quota {
tool,
kind,
fraction,
reset_in_seconds,
..
} => format!(
"{} {} limit at {}, resets in {}",
provider_name(*tool),
alert_window_phrase(*kind),
percent(*fraction),
reset_countdown(*reset_in_seconds),
),
Alert::Budget {
scope,
spent_usd,
target_usd,
over_by_usd,
} => format!(
"{} over by {}, spent {} of {}",
alert_budget_scope(scope),
format_over_by(over_by_usd),
format_money(spent_usd, Some("USD"), true),
format_money(target_usd, Some("USD"), true),
),
Alert::Forecast {
projected_month_usd,
target_usd,
projected_over_by_usd,
} => format!(
"total budget projected over by {}, {} projected of {}",
format_over_by(projected_over_by_usd),
format_money(projected_month_usd, Some("USD"), true),
format_money(target_usd, Some("USD"), true),
),
Alert::SpendSpike {
date,
value_usd,
baseline_median_usd,
magnitude,
} => {
let median_display = format_money(baseline_median_usd, Some("USD"), true);
let baseline_displays_zero = baseline_median_usd.round_dp(2) == Decimal::ZERO;
let comparison = match anomaly_multiple_phrase(*magnitude, baseline_displays_zero) {
Some(multiple) => format!("~{multiple}x your {median_display} norm"),
None => format!("well above your {median_display} norm"),
};
format!(
"daily spend spike: {} on {}, {comparison}",
format_money(value_usd, Some("USD"), true),
date.format("%b %d"),
)
}
}
}
fn alert_cue(alert: &Alert) -> &'static str {
if alert.is_critical() {
" !!"
} else {
" !"
}
}
fn alert_plain_cue(alert: &Alert) -> &'static str {
match alert {
Alert::Quota {
level: AlertLevel::Warn,
..
} => " (near limit)",
Alert::Quota {
level: AlertLevel::Critical,
..
} => " (critical)",
Alert::Budget { .. } => " (over budget)",
Alert::Forecast { .. } => " (projected over budget)",
Alert::SpendSpike { .. } => " (spend spike)",
}
}
fn alert_style(alert: &Alert) -> SemanticStyle {
if alert.is_critical() {
SemanticStyle::Critical
} else {
SemanticStyle::Warn
}
}
fn push_alert_lines(out: &mut StyledDocument, alerts: &[Alert], options: RenderOptions) {
for alert in alerts {
let sentence = alert_sentence(alert);
match options.mode {
RenderMode::Plain => {
push_line(out, &format!(" {sentence}{}", alert_plain_cue(alert)));
}
RenderMode::Braille | RenderMode::Ascii => {
let mut line = StyledLine::new();
line.push_plain(" ");
line.push_styled(
format!("{sentence}{}", alert_cue(alert)),
alert_style(alert),
);
out.push(line);
}
}
}
}
pub(crate) fn push_alert_banner(
out: &mut StyledDocument,
alerts: &[Alert],
options: RenderOptions,
) {
if alerts.is_empty() {
return;
}
match options.mode {
RenderMode::Plain => {
push_line(out, "alerts:");
push_alert_lines(out, alerts, options);
}
RenderMode::Braille | RenderMode::Ascii => {
let mut header = StyledLine::new();
header.push_plain(format!("{} ", mark(options)));
header.push_styled("alerts", SemanticStyle::Strong);
out.push(header);
push_alert_lines(out, alerts, options);
push_rule(out, options);
}
}
}
pub(crate) fn render_alerts(alerts: &[Alert], options: RenderOptions) -> String {
let mut out = StyledDocument::new();
push_alerts_header(&mut out, options);
if alerts.is_empty() {
push_line(&mut out, "no active alerts");
} else {
push_alert_lines(&mut out, alerts, options);
}
out.render(options)
}
pub(crate) fn render_alerts_off(options: RenderOptions) -> String {
let mut out = StyledDocument::new();
push_alerts_header(&mut out, options);
push_line(&mut out, ALERTS_OFF_HINT);
push_line(&mut out, "");
push_line(&mut out, "[alerts]");
push_line(&mut out, "enabled = true");
out.render(options)
}
fn push_alerts_header(out: &mut StyledDocument, options: RenderOptions) {
match options.mode {
RenderMode::Plain => push_line(out, "costroid alerts"),
RenderMode::Braille | RenderMode::Ascii => {
let mut header = StyledLine::new();
header.push_plain(format!("{} costroid", mark(options)));
header.push_styled(" alerts", SemanticStyle::Strong);
out.push(header);
}
}
}
pub(crate) fn alert_check_line(alerts: &[Alert]) -> String {
match alerts.first() {
None => String::new(),
Some(headline) if alerts.len() == 1 => format!("costroid: {}", alert_sentence(headline)),
Some(headline) => format!(
"costroid: {} active alerts; most pressing: {}",
alerts.len(),
alert_sentence(headline),
),
}
}
pub(crate) fn alerts_check_exit_code(alerts: &[Alert]) -> i32 {
if alerts.is_empty() {
0
} else {
1
}
}
const FORECAST_ESTIMATE_NOTE: &str =
"figures are local estimates (your tokens x current prices), projected - not the vendor invoice.";
const FORECAST_SERIES_LABEL_WIDTH: usize = 10;
#[cfg(test)]
pub(crate) fn render_forecast(view: &ForecastView, options: RenderOptions) -> String {
render_forecast_document(view, options).render(options)
}
pub(crate) fn render_forecast_document(
view: &ForecastView,
options: RenderOptions,
) -> StyledDocument {
match options.mode {
RenderMode::Plain => render_forecast_plain_document(view),
RenderMode::Braille | RenderMode::Ascii => render_forecast_visual_document(view, options),
}
}
fn render_forecast_visual_document(view: &ForecastView, options: RenderOptions) -> StyledDocument {
let mut out = StyledDocument::new();
push_header_line(
&mut out,
mark(options),
"forecast",
forecast_header_money(view)
.map(|money| format_money(&money, Some("USD"), true))
.unwrap_or_default(),
options,
);
push_line(&mut out, FORECAST_SCOPE_LINE);
if view.no_api_usage {
push_line(&mut out, FORECAST_NO_USAGE);
} else {
for line in forecast_spend_lines(view) {
push_line(&mut out, &line);
}
if let Some((actual, projected)) = forecast_series_glyphs(view, options) {
push_rule(&mut out, options);
push_line(
&mut out,
&format!(
"{:<width$}{actual}",
"actual",
width = FORECAST_SERIES_LABEL_WIDTH
),
);
if let Some(projected) = projected {
push_line(
&mut out,
&format!(
"{:<width$}{projected}",
"projected",
width = FORECAST_SERIES_LABEL_WIDTH
),
);
}
}
}
push_rule(&mut out, options);
push_forecast_quota_section(&mut out, view);
push_rule(&mut out, options);
push_line(&mut out, FORECAST_ESTIMATE_NOTE);
out
}
fn render_forecast_plain_document(view: &ForecastView) -> StyledDocument {
let mut out = StyledDocument::new();
push_line(&mut out, "costroid forecast");
push_line(&mut out, FORECAST_SCOPE_LINE);
if view.no_api_usage {
push_line(&mut out, FORECAST_NO_USAGE);
} else {
for line in forecast_spend_lines(view) {
push_line(&mut out, &line);
}
}
push_forecast_quota_section(&mut out, view);
push_line(&mut out, FORECAST_ESTIMATE_NOTE);
out
}
const FORECAST_SCOPE_LINE: &str = "scope: API-lane spend this month + quota burn (estimated)";
const FORECAST_NO_USAGE: &str = "no API usage recorded - nothing to forecast yet";
fn forecast_header_money(view: &ForecastView) -> Option<Decimal> {
if view.no_api_usage {
return None;
}
Some(match &view.spend {
SpendForecast::Projected {
projected_month_usd,
..
} => *projected_month_usd,
SpendForecast::InsufficientData {
spend_to_date_usd, ..
} => *spend_to_date_usd,
})
}
fn forecast_spend_lines(view: &ForecastView) -> Vec<String> {
match &view.spend {
SpendForecast::Projected {
projected_month_usd,
spend_to_date_usd,
days_elapsed,
days_in_month,
} => vec![
format!(
"projected {} by {} (estimated)",
format_money(projected_month_usd, Some("USD"), true),
forecast_month_end_label(view.generated_at, *days_in_month),
),
format!(
"spend so far {} over {} of {} days (estimated)",
format_money(spend_to_date_usd, Some("USD"), true),
days_elapsed,
days_in_month,
),
],
SpendForecast::InsufficientData {
spend_to_date_usd,
days_elapsed,
days_in_month,
min_days,
} => vec![
format!(
"insufficient data to project - {} of {} days elapsed (need {}+) (estimated)",
days_elapsed, days_in_month, min_days,
),
format!(
"spend so far {} over {} of {} days (estimated)",
format_money(spend_to_date_usd, Some("USD"), true),
days_elapsed,
days_in_month,
),
],
}
}
fn forecast_month_end_label(generated_at: DateTime<Utc>, days_in_month: u32) -> String {
let today = generated_at.date_naive();
match chrono::NaiveDate::from_ymd_opt(today.year(), today.month(), days_in_month) {
Some(end) => end.format("%b %d").to_string(),
None => "month end".to_string(),
}
}
fn forecast_series_glyphs(
view: &ForecastView,
options: RenderOptions,
) -> Option<(String, Option<String>)> {
if options.mode == RenderMode::Plain {
return None;
}
let (days_elapsed, days_in_month, projecting) = match &view.spend {
SpendForecast::Projected {
days_elapsed,
days_in_month,
..
} => (*days_elapsed, *days_in_month, true),
SpendForecast::InsufficientData {
days_elapsed,
days_in_month,
..
} => (*days_elapsed, *days_in_month, false),
};
let actual = sparkline(&forecast_actual_series(view, days_elapsed), options);
let projected = if projecting {
let remaining = days_in_month.saturating_sub(days_elapsed) as usize;
Some(forecast_projected_glyphs(remaining, options))
} else {
None
};
Some((actual, projected))
}
fn forecast_actual_series(view: &ForecastView, days_elapsed: u32) -> Vec<Decimal> {
let mut series = vec![Decimal::ZERO; days_elapsed as usize];
for day in &view.daily_actuals {
let index = day.date.day().saturating_sub(1) as usize;
if index < series.len() {
series[index] += day.spent_usd;
}
}
series
}
fn forecast_projected_glyphs(remaining: usize, options: RenderOptions) -> String {
match options.mode {
RenderMode::Plain => String::new(),
RenderMode::Ascii => "~".repeat(remaining),
RenderMode::Braille => braille_cell(&[4, 5]).to_string().repeat(remaining),
}
}
fn push_forecast_quota_section(out: &mut StyledDocument, view: &ForecastView) {
if view.quota_etas.is_empty() {
push_line(out, "quota: no quota windows tracked");
return;
}
push_line(out, "quota:");
for eta in &view.quota_etas {
push_line(out, &forecast_quota_line(eta));
}
}
fn forecast_quota_line(eta: &QuotaEta) -> String {
let window = format!(
"{} {}",
provider_name(eta.tool),
forecast_kind_word(eta.kind)
);
match &eta.outcome {
QuotaEtaOutcome::ProjectedHit { at, .. } => {
format!(
"{window}: projected to hit ~{} (UTC, estimated)",
at.format("%A")
)
}
QuotaEtaOutcome::ResetsFirst { .. } => {
format!("{window}: resets before you hit it (estimated)")
}
QuotaEtaOutcome::Unavailable { reason } => {
format!(
"{window}: ETA unavailable ({})",
quota_eta_unavailable_text(*reason)
)
}
}
}
fn forecast_kind_word(kind: LimitKind) -> &'static str {
match kind {
LimitKind::FiveHour => "5h",
LimitKind::Weekly => "weekly",
LimitKind::Daily => "daily",
LimitKind::Monthly => "monthly",
LimitKind::BillingCycle => "billing cycle",
}
}
fn quota_eta_unavailable_text(reason: QuotaEtaUnavailable) -> &'static str {
match reason {
QuotaEtaUnavailable::ReadingNotProjectable => "no fresh verified usage reading",
QuotaEtaUnavailable::WindowJustStarted => "window just started",
}
}
const ANOMALIES_ESTIMATE_NOTE: &str =
"figures are local estimates (your tokens x current prices), vs your own recent history.";
const ANOMALIES_QUOTA_DEFERRED_NOTE: &str =
"quota burn-rate anomalies need multi-day quota history, which local data does not keep - not shown.";
const ANOMALIES_SCOPE_LINE: &str =
"scope: API-lane spend spike + all-lane model mix vs your own recent history (estimated)";
const ANOMALIES_NO_USAGE: &str =
"no usage recorded yet - callouts need a few days of history (estimated)";
#[cfg(test)]
pub(crate) fn render_anomalies(view: &AnomaliesView, options: RenderOptions) -> String {
render_anomalies_document(view, options).render(options)
}
pub(crate) fn render_anomalies_document(
view: &AnomaliesView,
options: RenderOptions,
) -> StyledDocument {
match options.mode {
RenderMode::Plain => render_anomalies_plain_document(view),
RenderMode::Braille | RenderMode::Ascii => render_anomalies_visual_document(view, options),
}
}
fn render_anomalies_visual_document(
view: &AnomaliesView,
options: RenderOptions,
) -> StyledDocument {
let mut out = StyledDocument::new();
push_header_line(
&mut out,
mark(options),
"anomalies",
anomalies_header_label(view),
options,
);
push_line(&mut out, ANOMALIES_SCOPE_LINE);
push_anomalies_body(&mut out, view, options);
push_rule(&mut out, options);
push_line(&mut out, ANOMALIES_QUOTA_DEFERRED_NOTE);
push_line(&mut out, ANOMALIES_ESTIMATE_NOTE);
out
}
fn render_anomalies_plain_document(view: &AnomaliesView) -> StyledDocument {
let mut out = StyledDocument::new();
push_line(&mut out, "costroid anomalies");
push_line(&mut out, ANOMALIES_SCOPE_LINE);
push_anomalies_body(&mut out, view, RenderOptions::plain());
push_line(&mut out, ANOMALIES_QUOTA_DEFERRED_NOTE);
push_line(&mut out, ANOMALIES_ESTIMATE_NOTE);
out
}
fn anomalies_header_label(view: &AnomaliesView) -> String {
if !view.enough_history || view.anomalies.is_empty() {
String::new()
} else {
format!("{} flagged", view.anomalies.len())
}
}
fn push_anomalies_body(out: &mut StyledDocument, view: &AnomaliesView, options: RenderOptions) {
if view.no_usage {
push_line(out, ANOMALIES_NO_USAGE);
return;
}
if !view.enough_history {
push_line(
out,
&format!(
"not enough history yet - {} of {} days (estimated)",
view.history_days, view.min_history_days
),
);
return;
}
if view.anomalies.is_empty() {
push_line(
out,
&format!(
"no anomalies - usage in line with your {}-day norm (estimated)",
view.history_days
),
);
return;
}
for anomaly in &view.anomalies {
push_line(out, &anomaly_line(anomaly, options));
}
}
fn anomaly_marker(options: RenderOptions) -> &'static str {
match options.mode {
RenderMode::Braille => "◆ ",
RenderMode::Ascii | RenderMode::Plain => "* ",
}
}
fn anomaly_line(anomaly: &Anomaly, options: RenderOptions) -> String {
let marker = anomaly_marker(options);
match &anomaly.signal {
AnomalySignal::SpendSpike { date } => {
let median_display = format_money(&anomaly.baseline_median, Some("USD"), true);
let baseline_displays_zero = anomaly.baseline_median.round_dp(2) == Decimal::ZERO;
let comparison =
match anomaly_multiple_phrase(anomaly.magnitude, baseline_displays_zero) {
Some(multiple) => {
format!(
"~{multiple}x your {median_display} {}-day median",
anomaly.baseline_days
)
}
None => {
format!(
"well above your {median_display} {}-day median",
anomaly.baseline_days
)
}
};
format!(
"{marker}spend spike: {} on {}, {comparison} (estimated)",
format_money(&anomaly.value, Some("USD"), true),
date.format("%b %d"),
)
}
AnomalySignal::ModelMixShift { model } => {
let today_share = percent(anomaly.value.to_f64().unwrap_or(0.0));
let median_share = percent(anomaly.baseline_median.to_f64().unwrap_or(0.0));
let comparison = if anomaly.value > anomaly.baseline_median {
let baseline_displays_zero = median_share == "0%";
match anomaly_multiple_phrase(anomaly.magnitude, baseline_displays_zero) {
Some(multiple) => {
format!(
"~{multiple}x your {median_share} {}-day median",
anomaly.baseline_days
)
}
None => {
format!(
"up from your {median_share} {}-day median",
anomaly.baseline_days
)
}
}
} else {
format!(
"down from your {} {}-day median",
median_share, anomaly.baseline_days
)
};
format!("{marker}model mix shift: {model} at {today_share} of tokens, {comparison} (estimated)")
}
}
}
fn anomaly_multiple_phrase(
magnitude: Option<Decimal>,
baseline_displays_zero: bool,
) -> Option<String> {
let multiple = magnitude?;
if baseline_displays_zero {
return None;
}
let rounded = multiple.round_dp(1);
if rounded <= Decimal::ONE {
return None;
}
Some(rounded.to_string())
}
fn render_statusline_line(summary: &NowSummary, options: RenderOptions) -> StyledLine {
let api = sorted_lane_rows(&summary.current_costs, CostLane::Api);
let api_total = sum_costs(&api);
let spend = format_money(&api_total, Some("USD"), true);
let api_state = if api.is_empty() { " no api" } else { "" };
match options.mode {
RenderMode::Plain => match most_constrained_limit(&summary.limits) {
Some(limit) => StyledLine::plain(format!(
"costroid {spend},{} {}",
if api.is_empty() { " no API usage," } else { "" },
plain_limit_phrase(limit)
)),
None => StyledLine::plain(format!(
"costroid {spend}{}",
if api.is_empty() { ", no API usage" } else { "" }
)),
},
RenderMode::Braille | RenderMode::Ascii => match most_constrained_limit(&summary.limits) {
Some(limit) => {
let (fraction, reset) = limit_fraction_and_reset(limit);
let pct = percent(fraction);
let unverified = matches!(limit.availability, LimitAvailability::Unverified { .. });
let cue = if unverified {
UNVERIFIED_CUE.to_string()
} else {
state_cue(limit_state(fraction)).to_string()
};
let reset = reset
.map(|seconds| format!(" {}", compact_reset(seconds)))
.unwrap_or_default();
let mut line = StyledLine::new();
line.push_plain(format!("{} {spend}{api_state} ", mark(options)));
line.spans.push(limit_meter_with_confidence(
fraction,
unverified,
STATUS_BAR_WIDTH,
options,
));
line.push_plain(format!(" {pct}{cue}{reset}"));
line
}
None => StyledLine::plain(format!("{} {spend}{api_state}", mark(options))),
},
}
}
fn render_now_visual_document(summary: &NowSummary, options: RenderOptions) -> StyledDocument {
let mut out = StyledDocument::new();
let api = sorted_lane_rows(&summary.current_costs, CostLane::Api);
let api_total = sum_costs(&api);
push_header_line(
&mut out,
mark(options),
"this week",
format_money(&api_total, Some("USD"), true),
options,
);
push_rule(&mut out, options);
push_line(&mut out, "limits");
if summary.limits.is_empty() {
push_line(&mut out, " no local limit data found");
} else {
for limit in &summary.limits {
out.push(render_limit_line(limit, summary.generated_at, options));
if let Some(caveat) = claude_caveat(limit) {
push_line(&mut out, &format!(" {caveat}"));
}
}
}
push_rule(&mut out, options);
push_line(&mut out, "api costs (this week)");
if api.is_empty() {
push_line(&mut out, " no API usage in this period");
} else {
push_cost_rows(&mut out, &api, options);
}
push_non_api_sections(&mut out, &summary.current_costs, options);
push_rule(&mut out, options);
push_line(
&mut out,
&mode_insight(insight_line(&api, &summary.limits), options),
);
push_provider_notes(&mut out, &summary.providers);
push_empty_provider_guidance(&mut out, &summary.providers);
out
}
fn render_now_plain_document(summary: &NowSummary) -> StyledDocument {
let mut out = StyledDocument::new();
let api = sorted_lane_rows(&summary.current_costs, CostLane::Api);
let api_total = sum_costs(&api);
push_line(&mut out, "costroid now");
push_line(
&mut out,
&format!(
"period: this week, estimated API spend: {}",
format_money(&api_total, Some("USD"), true)
),
);
push_line(&mut out, "limits:");
if summary.limits.is_empty() {
push_line(&mut out, " no local limit data found");
} else {
for limit in &summary.limits {
push_line(&mut out, &plain_limit_line(limit, summary.generated_at));
if let Some(caveat) = claude_caveat(limit) {
push_line(&mut out, &format!(" {caveat}"));
}
}
}
push_line(&mut out, "api costs this week:");
if api.is_empty() {
push_line(&mut out, " no API usage in this period");
} else {
for row in &api {
push_line(&mut out, &plain_cost_line(row));
}
}
push_plain_non_api_sections(&mut out, &summary.current_costs);
push_line(&mut out, &plain_insight_line(&api, &summary.limits));
push_provider_notes(&mut out, &summary.providers);
push_empty_provider_guidance(&mut out, &summary.providers);
out
}
fn render_trends_visual_document(
summary: &TrendsSummary,
options: RenderOptions,
) -> StyledDocument {
let mut out = StyledDocument::new();
let api = sorted_lane_rows(&summary.totals, CostLane::Api);
let api_total = sum_costs(&api);
push_header_line(
&mut out,
mark(options),
period_scope(summary.period),
format_money(&api_total, Some("USD"), true),
options,
);
push_line(
&mut out,
&format!(
" {} group: {}",
period_tabs(summary.period),
group_tabs(summary.group_by)
),
);
push_rule(&mut out, options);
push_line(
&mut out,
&format!(" api spend / {}", period_bucket_label(summary.period)),
);
let values = api_bucket_values(summary);
if api.is_empty() {
push_line(&mut out, " no API usage in this period");
} else {
push_line(&mut out, &format!(" {}", sparkline(&values, options)));
push_line(
&mut out,
&format!(" {}", sparkline_labels(summary.period, values.len())),
);
}
push_rule(&mut out, options);
push_line(&mut out, "breakdown");
if api.is_empty() {
push_line(&mut out, " no API usage in this period");
} else {
push_cost_rows(&mut out, &api, options);
}
push_non_api_sections(&mut out, &summary.totals, options);
push_rule(&mut out, options);
push_line(&mut out, &mode_insight(insight_line(&api, &[]), options));
push_provider_notes(&mut out, &summary.providers);
push_empty_provider_guidance(&mut out, &summary.providers);
out
}
fn render_trends_plain_document(summary: &TrendsSummary) -> StyledDocument {
let mut out = StyledDocument::new();
let api = sorted_lane_rows(&summary.totals, CostLane::Api);
let api_total = sum_costs(&api);
push_line(&mut out, "costroid trends");
push_line(
&mut out,
&format!(
"period: {}, group: {}, estimated API spend: {}",
period_name(summary.period),
group_name(summary.group_by),
format_money(&api_total, Some("USD"), true)
),
);
push_line(&mut out, "api spend buckets:");
if api.is_empty() {
push_line(&mut out, " no API usage in this period");
} else {
for (range, value) in plain_api_bucket_values(summary) {
push_line(
&mut out,
&format!(
" {}: {}",
format_bucket_start(&range),
format_money(&value, Some("USD"), true)
),
);
}
}
push_line(&mut out, "breakdown:");
if api.is_empty() {
push_line(&mut out, " no API usage in this period");
} else {
for row in &api {
push_line(&mut out, &plain_cost_line(row));
}
}
push_plain_non_api_sections(&mut out, &summary.totals);
push_line(&mut out, &plain_insight_line(&api, &[]));
push_provider_notes(&mut out, &summary.providers);
push_empty_provider_guidance(&mut out, &summary.providers);
out
}
fn push_non_api_sections(
out: &mut StyledDocument,
rows: &[CostLaneSummary],
options: RenderOptions,
) {
let subscription = sorted_lane_rows(rows, CostLane::SubscriptionEstimate);
if !subscription.is_empty() {
push_rule(out, options);
push_line(
out,
"subscription API-equivalent value (estimate, not bill)",
);
push_cost_rows(out, &subscription, options);
}
let unknown = sorted_lane_rows(rows, CostLane::UnknownAccess);
if !unknown.is_empty() {
push_rule(out, options);
push_line(out, "unknown-access usage (partial)");
push_cost_rows(out, &unknown, options);
}
}
fn push_plain_non_api_sections(out: &mut StyledDocument, rows: &[CostLaneSummary]) {
let subscription = sorted_lane_rows(rows, CostLane::SubscriptionEstimate);
if !subscription.is_empty() {
push_line(out, "subscription API-equivalent value, estimate not bill:");
for row in &subscription {
push_line(out, &plain_cost_line(row));
}
}
let unknown = sorted_lane_rows(rows, CostLane::UnknownAccess);
if !unknown.is_empty() {
push_line(out, "unknown-access usage, partial:");
for row in &unknown {
push_line(out, &plain_cost_line(row));
}
}
}
fn push_cost_rows(out: &mut StyledDocument, rows: &[CostLaneSummary], options: RenderOptions) {
let max = rows
.iter()
.map(|row| row.totals.billed_cost)
.max()
.unwrap_or_default();
for row in rows {
let money = format_money(
&row.totals.billed_cost,
row.totals.currency.as_deref(),
row.totals.estimated_rows > 0,
);
let rendered_money_len = StyledSpan::styled(money.clone(), SemanticStyle::Strong)
.render(options)
.len();
let badge = pricing_badge(&row.totals);
let mut line = StyledLine::new();
line.push_plain(format!(" {:<18} ", display_group(&row.group.value)));
line.spans.push(cost_bar_span(
row.totals.billed_cost,
max,
cost_bar_width(options),
options,
));
line.push_plain(format!(
" {}",
" ".repeat(12_usize.saturating_sub(rendered_money_len))
));
line.push_styled(money, SemanticStyle::Strong);
line.push_plain(badge);
out.push(line);
}
}
fn plain_cost_line(row: &CostLaneSummary) -> String {
format!(
" {}: {}, {} tokens{}",
display_group(&row.group.value),
format_money(
&row.totals.billed_cost,
row.totals.currency.as_deref(),
row.totals.estimated_rows > 0
),
row.totals.tokens.total(),
pricing_badge_plain(&row.totals)
)
}
fn sorted_lane_rows(rows: &[CostLaneSummary], lane: CostLane) -> Vec<CostLaneSummary> {
let mut selected = rows
.iter()
.filter(|row| row.lane == lane)
.cloned()
.collect::<Vec<_>>();
selected.sort_by(|left, right| {
right
.totals
.billed_cost
.cmp(&left.totals.billed_cost)
.then_with(|| left.group.value.cmp(&right.group.value))
});
selected
}
fn sum_costs(rows: &[CostLaneSummary]) -> Decimal {
rows.iter()
.fold(Decimal::ZERO, |total, row| total + row.totals.billed_cost)
}
fn measure_fraction(measure: &LimitMeasure) -> Option<f64> {
match measure {
LimitMeasure::TokenFraction(fraction) => Some(*fraction),
LimitMeasure::Spend { .. } => None,
}
}
fn freshness_stamp(captured_at: DateTime<Utc>, generated_at: DateTime<Utc>) -> String {
if captured_at.timestamp() == 0 {
return " capture time unknown".to_string();
}
if (generated_at - captured_at).num_minutes() >= LIMIT_FRESHNESS_STAMP_MINUTES {
format!(" as of {}", captured_at.format("%H:%M"))
} else {
String::new()
}
}
fn claude_caveat(limit: &LimitSummary) -> Option<&'static str> {
let shows_usage = matches!(
limit.availability,
LimitAvailability::Available { .. }
| LimitAvailability::Unverified { .. }
| LimitAvailability::Estimated { .. }
);
(limit.tool == ProviderId::ClaudeCode && shows_usage).then_some(CLAUDE_CHAT_CAVEAT)
}
fn limit_meter_with_confidence(
fraction: f64,
unverified: bool,
width: usize,
options: RenderOptions,
) -> StyledSpan {
if unverified {
StyledSpan::styled(
positional_meter_text(fraction, width, options),
SemanticStyle::Strong,
)
} else {
limit_meter_span(fraction, width, options)
}
}
fn spend_text(used_usd: &Decimal, included_usd: &Option<Decimal>) -> String {
match included_usd {
Some(included) => format!(
"{} / {} used",
format_money(used_usd, Some("USD"), false),
format_money(included, Some("USD"), false)
),
None => format!("{} used", format_money(used_usd, Some("USD"), false)),
}
}
fn estimated_volume_text(volume_tokens: u64) -> String {
format!("{} tokens", with_thousands(&volume_tokens.to_string()))
}
fn estimated_value_suffix(estimated_usd: &Option<Decimal>) -> String {
match estimated_usd {
Some(value) => format!(" ({}, estimated)", format_money(value, Some("USD"), true)),
None => " (estimated)".to_string(),
}
}
fn render_limit_line(
limit: &LimitSummary,
generated_at: DateTime<Utc>,
options: RenderOptions,
) -> StyledLine {
let tool = provider_name(limit.tool);
let kind = limit_kind(limit.kind);
let stamp = freshness_stamp(limit.captured_at, generated_at);
match &limit.availability {
LimitAvailability::Available {
measure,
reset_in_seconds,
..
} => match measure {
LimitMeasure::TokenFraction(fraction) => {
let fraction = *fraction;
let cue = state_cue(limit_state(fraction));
let mut line = StyledLine::new();
line.push_plain(format!(" {tool:<12} {kind:<3} "));
line.spans
.push(limit_meter_span(fraction, LIMIT_BAR_WIDTH, options));
line.push_plain(format!(
" {}{} resets {}{}",
percent(fraction),
cue,
reset_countdown(*reset_in_seconds),
stamp
));
line
}
LimitMeasure::Spend {
used_usd,
included_usd,
} => StyledLine::plain(format!(
" {tool:<12} {kind:<3} {} resets {}{}",
spend_text(used_usd, included_usd),
reset_countdown(*reset_in_seconds),
stamp
)),
},
LimitAvailability::Partial {
measure,
reset_in_seconds,
reason,
..
} => {
let reset = reset_in_seconds
.map(reset_countdown)
.map(|value| format!(" resets {value}"))
.unwrap_or_default();
match measure {
Some(LimitMeasure::TokenFraction(fraction)) => {
let fraction = *fraction;
let cue = state_cue(limit_state(fraction));
let mut line = StyledLine::new();
line.push_plain(format!(" {tool:<12} {kind:<3} "));
line.spans
.push(limit_meter_span(fraction, LIMIT_BAR_WIDTH, options));
line.push_plain(format!(
" {}{} partial: {}{}{}",
percent(fraction),
cue,
reason,
reset,
stamp
));
line
}
Some(LimitMeasure::Spend {
used_usd,
included_usd,
}) => StyledLine::plain(format!(
" {tool:<12} {kind:<3} {} partial: {}{}{}",
spend_text(used_usd, included_usd),
reason,
reset,
stamp
)),
None => {
StyledLine::plain(format!(" {tool:<12} {kind:<3} partial: {reason}{reset}"))
}
}
}
LimitAvailability::Unverified {
measure,
reset_in_seconds,
..
} => {
let reset = reset_in_seconds
.map(reset_countdown)
.map(|value| format!(" resets {value}"))
.unwrap_or_default();
match measure {
LimitMeasure::TokenFraction(fraction) => {
let fraction = *fraction;
let mut line = StyledLine::new();
line.push_plain(format!(" {tool:<12} {kind:<3} "));
line.spans.push(limit_meter_with_confidence(
fraction,
true,
LIMIT_BAR_WIDTH,
options,
));
line.push_plain(format!(
" {}{}{}{}",
percent(fraction),
UNVERIFIED_CUE,
reset,
stamp
));
line
}
LimitMeasure::Spend {
used_usd,
included_usd,
} => StyledLine::plain(format!(
" {tool:<12} {kind:<3} {}{}{}{}",
spend_text(used_usd, included_usd),
UNVERIFIED_CUE,
reset,
stamp
)),
}
}
LimitAvailability::Estimated {
volume_tokens,
estimated_usd,
} => StyledLine::plain(format!(
" {tool:<12} {kind:<3} usage: {}{} {} quota % unavailable",
estimated_volume_text(*volume_tokens),
estimated_value_suffix(estimated_usd),
if options.mode == RenderMode::Ascii {
"--"
} else {
"—"
}
)),
LimitAvailability::Unavailable { reason } => {
StyledLine::plain(format!(" {tool:<12} {kind:<3} unavailable: {reason}"))
}
}
}
fn plain_limit_line(limit: &LimitSummary, generated_at: DateTime<Utc>) -> String {
let tool = provider_name(limit.tool);
let kind = limit_kind(limit.kind);
let stamp = freshness_stamp(limit.captured_at, generated_at);
match &limit.availability {
LimitAvailability::Available {
measure,
reset_in_seconds,
..
} => match measure {
LimitMeasure::TokenFraction(fraction) => {
let cue = plain_state_phrase(limit_state(*fraction));
format!(
" {tool} {kind}: {} used{cue}, resets in {}{stamp}",
percent(*fraction),
reset_countdown(*reset_in_seconds)
)
}
LimitMeasure::Spend {
used_usd,
included_usd,
} => format!(
" {tool} {kind}: {}, resets in {}{stamp}",
spend_text(used_usd, included_usd),
reset_countdown(*reset_in_seconds)
),
},
LimitAvailability::Partial {
measure,
reset_in_seconds,
reason,
..
} => {
let usage = match measure {
Some(LimitMeasure::TokenFraction(fraction)) => format!(
"{} used{}",
percent(*fraction),
plain_state_phrase(limit_state(*fraction))
),
Some(LimitMeasure::Spend {
used_usd,
included_usd,
}) => spend_text(used_usd, included_usd),
None => "usage unknown".to_string(),
};
let reset = reset_in_seconds
.map(reset_countdown)
.map(|value| format!(", resets in {value}"))
.unwrap_or_default();
let stamp = if measure.is_some() {
stamp
} else {
String::new()
};
format!(" {tool} {kind}: partial, {usage}{reset}, {reason}{stamp}")
}
LimitAvailability::Unverified {
measure,
reset_in_seconds,
..
} => {
let usage = match measure {
LimitMeasure::TokenFraction(fraction) => format!("{} used", percent(*fraction)),
LimitMeasure::Spend {
used_usd,
included_usd,
} => spend_text(used_usd, included_usd),
};
let reset = reset_in_seconds
.map(reset_countdown)
.map(|value| format!(", resets in {value}"))
.unwrap_or_default();
format!(" {tool} {kind}: {usage}{UNVERIFIED_CUE}{reset}{stamp}")
}
LimitAvailability::Estimated {
volume_tokens,
estimated_usd,
} => format!(
" {tool} {kind}: usage {}{}, quota % unavailable",
estimated_volume_text(*volume_tokens),
estimated_value_suffix(estimated_usd)
),
LimitAvailability::Unavailable { reason } => {
format!(" {tool} {kind}: unavailable, {reason}")
}
}
}
fn plain_limit_phrase(limit: &LimitSummary) -> String {
let tool = provider_name(limit.tool);
let kind = limit_kind(limit.kind);
match &limit.availability {
LimitAvailability::Available {
measure,
reset_in_seconds,
..
} => match measure {
LimitMeasure::TokenFraction(fraction) => format!(
"{tool} {kind} {} used{}, resets in {}",
percent(*fraction),
plain_state_phrase(limit_state(*fraction)),
compact_reset(*reset_in_seconds)
),
LimitMeasure::Spend {
used_usd,
included_usd,
} => format!(
"{tool} {kind} {}, resets in {}",
spend_text(used_usd, included_usd),
compact_reset(*reset_in_seconds)
),
},
LimitAvailability::Partial {
measure,
reset_in_seconds,
reason,
..
} => {
let usage = match measure {
Some(LimitMeasure::TokenFraction(fraction)) => format!(
"{}{}",
percent(*fraction),
plain_state_phrase(limit_state(*fraction))
),
Some(LimitMeasure::Spend {
used_usd,
included_usd,
}) => spend_text(used_usd, included_usd),
None => "unknown usage".to_string(),
};
let reset = reset_in_seconds
.map(compact_reset)
.map(|value| format!(", resets in {value}"))
.unwrap_or_default();
format!("{tool} {kind} partial, {usage}{reset}, {reason}")
}
LimitAvailability::Unverified {
measure,
reset_in_seconds,
..
} => {
let usage = match measure {
LimitMeasure::TokenFraction(fraction) => format!("{} used", percent(*fraction)),
LimitMeasure::Spend {
used_usd,
included_usd,
} => spend_text(used_usd, included_usd),
};
let reset = reset_in_seconds
.map(compact_reset)
.map(|value| format!(", resets in {value}"))
.unwrap_or_default();
format!("{tool} {kind} {usage}{UNVERIFIED_CUE}{reset}")
}
LimitAvailability::Estimated {
volume_tokens,
estimated_usd,
} => format!(
"{tool} {kind} usage {}{}, quota % unavailable",
estimated_volume_text(*volume_tokens),
estimated_value_suffix(estimated_usd)
),
LimitAvailability::Unavailable { reason } => {
format!("{tool} {kind} unavailable, {reason}")
}
}
}
fn most_constrained_limit(limits: &[LimitSummary]) -> Option<&LimitSummary> {
limits.iter().filter(has_fraction).max_by(|left, right| {
let left_fraction = limit_fraction(left).unwrap_or(0.0);
let right_fraction = limit_fraction(right).unwrap_or(0.0);
left_fraction.total_cmp(&right_fraction)
})
}
fn has_fraction(limit: &&LimitSummary) -> bool {
limit_fraction(limit).is_some()
}
fn limit_fraction(limit: &LimitSummary) -> Option<f64> {
match &limit.availability {
LimitAvailability::Available { measure, .. } => measure_fraction(measure),
LimitAvailability::Partial {
measure: Some(measure),
..
} => measure_fraction(measure),
LimitAvailability::Unverified { measure, .. } => measure_fraction(measure),
LimitAvailability::Partial { measure: None, .. }
| LimitAvailability::Estimated { .. }
| LimitAvailability::Unavailable { .. } => None,
}
}
fn limit_fraction_and_reset(limit: &LimitSummary) -> (f64, Option<i64>) {
match &limit.availability {
LimitAvailability::Available {
measure,
reset_in_seconds,
..
} => (
measure_fraction(measure).unwrap_or(0.0),
Some(*reset_in_seconds),
),
LimitAvailability::Partial {
measure,
reset_in_seconds,
..
} => (
measure.as_ref().and_then(measure_fraction).unwrap_or(0.0),
*reset_in_seconds,
),
LimitAvailability::Unverified {
measure,
reset_in_seconds,
..
} => (measure_fraction(measure).unwrap_or(0.0), *reset_in_seconds),
LimitAvailability::Estimated { .. } | LimitAvailability::Unavailable { .. } => (0.0, None),
}
}
fn limit_state(fraction: f64) -> LimitState {
if fraction >= 1.0 {
LimitState::Over
} else if fraction >= CRITICAL_FRACTION {
LimitState::Critical
} else if fraction >= WARN_FRACTION {
LimitState::Warn
} else {
LimitState::Normal
}
}
fn state_cue(state: LimitState) -> &'static str {
match state {
LimitState::Normal => "",
LimitState::Warn => " !",
LimitState::Critical => " !!",
LimitState::Over => " !! OVER",
}
}
fn plain_state_phrase(state: LimitState) -> &'static str {
match state {
LimitState::Normal => "",
LimitState::Warn => " (near limit)",
LimitState::Critical => " (critical)",
LimitState::Over => " (over limit)",
}
}
#[cfg(test)]
fn limit_meter(fraction: f64, width: usize, options: RenderOptions) -> String {
limit_meter_span(fraction, width, options).render(options)
}
fn limit_meter_span(fraction: f64, width: usize, options: RenderOptions) -> StyledSpan {
StyledSpan::styled(
positional_meter_text(fraction, width, options),
state_style(limit_state(fraction)),
)
}
fn state_style(state: LimitState) -> SemanticStyle {
match state {
LimitState::Normal => SemanticStyle::Strong,
LimitState::Warn => SemanticStyle::Warn,
LimitState::Critical | LimitState::Over => SemanticStyle::Critical,
}
}
fn cost_bar_span(
amount: Decimal,
max: Decimal,
width: usize,
options: RenderOptions,
) -> StyledSpan {
let fraction = if max > Decimal::ZERO {
(amount / max).to_f64().unwrap_or(0.0)
} else {
0.0
};
StyledSpan::styled(
positional_meter_text(fraction, width, options),
SemanticStyle::Strong,
)
}
fn positional_meter_text(fraction: f64, width: usize, options: RenderOptions) -> String {
let segments = meter_segments(fraction, width);
match options.mode {
RenderMode::Plain => String::new(),
RenderMode::Ascii => {
let mut meter = String::with_capacity(width + 2);
meter.push('[');
meter.extend(std::iter::repeat_n('#', segments.full));
if segments.partial {
meter.push('+');
}
meter.extend(std::iter::repeat_n('-', segments.remaining));
meter.push(']');
meter
}
RenderMode::Braille => {
let mut meter = String::with_capacity(width);
meter.extend(std::iter::repeat_n(braille_full(), segments.full));
if segments.partial {
meter.push(braille_left_column());
}
meter.extend(std::iter::repeat_n(braille_light(), segments.remaining));
meter
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct MeterSegments {
full: usize,
partial: bool,
remaining: usize,
}
fn meter_segments(fraction: f64, width: usize) -> MeterSegments {
if width == 0 {
return MeterSegments {
full: 0,
partial: false,
remaining: 0,
};
}
let clamped = fraction.clamp(0.0, 1.0);
if clamped >= 1.0 {
return MeterSegments {
full: width,
partial: false,
remaining: 0,
};
}
if clamped <= 0.0 {
return MeterSegments {
full: 0,
partial: false,
remaining: width,
};
}
let exact = clamped * width as f64;
let mut full = exact.floor() as usize;
let mut partial = exact - full as f64 >= 0.5;
if full == 0 && !partial {
partial = true;
}
if partial && full >= width {
partial = false;
}
let used = full + usize::from(partial);
if used > width {
full = width;
partial = false;
}
MeterSegments {
full,
partial,
remaining: width.saturating_sub(full + usize::from(partial)),
}
}
fn sparkline(values: &[Decimal], options: RenderOptions) -> String {
match options.mode {
RenderMode::Plain => String::new(),
RenderMode::Ascii => ascii_sparkline(values),
RenderMode::Braille => braille_sparkline(values),
}
}
fn ascii_sparkline(values: &[Decimal]) -> String {
let max = values.iter().copied().max().unwrap_or_default();
let ramp = ['.', ':', '-', '=', '+', '*', '#'];
values
.iter()
.map(|value| {
if max <= Decimal::ZERO || *value <= Decimal::ZERO {
'.'
} else {
let fraction = (*value / max).to_f64().unwrap_or(0.0);
let index =
((fraction * (ramp.len() - 1) as f64).round() as usize).min(ramp.len() - 1);
ramp[index]
}
})
.collect()
}
fn braille_sparkline(values: &[Decimal]) -> String {
let max = values.iter().copied().max().unwrap_or_default();
values
.iter()
.map(|value| {
if max <= Decimal::ZERO || *value <= Decimal::ZERO {
braille_blank()
} else {
let fraction = (*value / max).to_f64().unwrap_or(0.0);
let height = ((fraction * 4.0).round() as u8).clamp(1, 4);
braille_bar_height(height)
}
})
.collect()
}
fn api_bucket_values(summary: &TrendsSummary) -> Vec<Decimal> {
plain_api_bucket_values(summary)
.into_iter()
.map(|(_, value)| value)
.collect()
}
fn plain_api_bucket_values(summary: &TrendsSummary) -> Vec<(PeriodRange, Decimal)> {
let mut buckets = BTreeMap::<PeriodRange, Decimal>::new();
for bucket in summary
.buckets
.iter()
.filter(|bucket| bucket.lane == CostLane::Api)
{
let total = buckets.entry(bucket.period.clone()).or_default();
*total += bucket.totals.billed_cost;
}
buckets.into_iter().collect()
}
fn mark(options: RenderOptions) -> &'static str {
match options.mode {
RenderMode::Braille => "C⠉",
RenderMode::Ascii | RenderMode::Plain => "costroid",
}
}
fn bit_for_dot(dot: u8) -> u8 {
match dot {
1 => 1,
2 => 2,
3 => 4,
4 => 8,
5 => 16,
6 => 32,
7 => 64,
8 => 128,
_ => 0,
}
}
fn braille_cell(dots: &[u8]) -> char {
let mut mask = 0_u8;
for dot in dots {
mask |= bit_for_dot(*dot);
}
byte_to_braille(mask)
}
fn byte_to_braille(mask: u8) -> char {
char::from_u32(0x2800 + u32::from(mask)).unwrap_or('\u{2800}')
}
fn braille_blank() -> char {
braille_cell(&[])
}
fn braille_full() -> char {
braille_cell(&[1, 2, 3, 4, 5, 6, 7, 8])
}
fn braille_light() -> char {
braille_cell(&[7, 8])
}
fn braille_left_column() -> char {
braille_cell(&[1, 2, 3, 7])
}
fn braille_bar_height(height: u8) -> char {
match height {
0 => braille_blank(),
1 => braille_cell(&[7, 8]),
2 => braille_cell(&[3, 6, 7, 8]),
3 => braille_cell(&[2, 3, 5, 6, 7, 8]),
_ => braille_full(),
}
}
fn push_line(out: &mut StyledDocument, line: &str) {
out.push(StyledLine::plain(line));
}
fn push_rule(out: &mut StyledDocument, options: RenderOptions) {
let glyph = if options.mode == RenderMode::Ascii {
"-"
} else {
"─"
};
push_line(out, &glyph.repeat(options.width.max(1)));
}
fn mode_insight(line: String, options: RenderOptions) -> String {
if options.mode == RenderMode::Ascii {
line.replace('◆', "*")
} else {
line
}
}
fn push_header_line(
out: &mut StyledDocument,
mark: &str,
scope: &str,
money: String,
options: RenderOptions,
) {
let mut line = StyledLine::new();
if options.width == DEFAULT_RENDER_WIDTH {
line.push_plain(format!(
"{mark} costroid {scope} "
));
} else {
let left = format!("{mark} costroid");
let right = format!("{scope} ");
let spacing = options
.width
.saturating_sub(visible_width(&left) + visible_width(&right) + visible_width(&money))
.max(1);
line.push_plain(format!("{left}{}{right}", " ".repeat(spacing)));
}
line.push_styled(money, SemanticStyle::Strong);
out.push(line);
}
fn visible_width(value: &str) -> usize {
value.chars().count()
}
fn cost_bar_width(options: RenderOptions) -> usize {
if options.width <= DEFAULT_RENDER_WIDTH {
COST_BAR_WIDTH
} else {
(COST_BAR_WIDTH + (options.width - DEFAULT_RENDER_WIDTH) / 4).min(32)
}
}
fn push_provider_notes(out: &mut StyledDocument, providers: &[costroid_core::ProviderStatus]) {
for provider in providers
.iter()
.filter(|provider| provider.status != ProviderStatusKind::Available)
{
let message = provider
.message
.as_deref()
.unwrap_or("local data incomplete");
push_line(
out,
&format!(
"provider {} {}: {}",
provider_name(provider.provider),
provider_status(provider.status),
message
),
);
}
}
fn push_empty_provider_guidance(
out: &mut StyledDocument,
providers: &[costroid_core::ProviderStatus],
) {
if providers.iter().any(|provider| {
matches!(
provider.status,
ProviderStatusKind::Available | ProviderStatusKind::Detected
)
}) {
return;
}
push_line(out, "no providers detected");
push_line(
out,
"looked for Claude Code, Codex, and Cursor local logs under the usual home/config dirs.",
);
push_line(
out,
"under WSL, Costroid also checks Windows paths like /mnt/c/Users/<you>/...",
);
push_line(
out,
"set CLAUDE_CONFIG_DIR or CODEX_HOME if your logs live elsewhere.",
);
}
fn insight_line(api: &[CostLaneSummary], limits: &[LimitSummary]) -> String {
if let Some(limit) = most_constrained_limit(limits) {
let fraction = limit_fraction(limit).unwrap_or(0.0);
if fraction >= WARN_FRACTION {
let flag = if matches!(limit.availability, LimitAvailability::Unverified { .. }) {
" (unverified)"
} else {
""
};
return format!(
"◆ {} {} at {}{}, resets {}.",
provider_name(limit.tool),
limit_kind(limit.kind),
percent(fraction),
flag,
limit_fraction_and_reset(limit)
.1
.map(reset_countdown)
.unwrap_or_else(|| "unknown".to_string())
);
}
}
match api.first() {
Some(row) => format!(
"◆ {} drove most of your API spend in this period. (estimated)",
display_group(&row.group.value)
),
None => "◆ no API usage in this period. (estimated)".to_string(),
}
}
fn plain_insight_line(api: &[CostLaneSummary], limits: &[LimitSummary]) -> String {
insight_line(api, limits).replace('◆', "insight:")
}
fn pricing_badge(totals: &AggregateTotals) -> String {
let label = pricing_badge_plain(totals);
if label.is_empty() {
String::new()
} else {
format!(" [{}]", label.trim_start_matches(", "))
}
}
fn pricing_badge_plain(totals: &AggregateTotals) -> String {
let missing = totals.pricing_coverage.missing_price_rows;
let unknown = totals.pricing_coverage.unknown_model_rows;
let priced = totals.pricing_coverage.priced_rows;
if unknown > 0 {
if priced > 0 || missing > 0 {
", unknown model/partial".to_string()
} else {
", unknown model/unpriced".to_string()
}
} else if missing > 0 {
if priced > 0 {
", partial pricing".to_string()
} else {
", unpriced/partial".to_string()
}
} else {
String::new()
}
}
fn format_money(amount: &Decimal, currency: Option<&str>, estimated: bool) -> String {
let prefix = if estimated { "~" } else { "" };
let rounded = amount.round_dp(2).to_string();
let (whole, fraction) = match rounded.split_once('.') {
Some((whole, fraction)) => (whole, fraction),
None => (rounded.as_str(), ""),
};
let fraction = two_decimal_digits(fraction);
let whole = with_thousands(whole);
match currency.unwrap_or("USD") {
"USD" => format!("{prefix}${whole}.{fraction}"),
other => format!("{prefix}{other} {whole}.{fraction}"),
}
}
fn two_decimal_digits(value: &str) -> String {
let mut fraction = value.chars().take(2).collect::<String>();
while fraction.len() < 2 {
fraction.push('0');
}
fraction
}
fn with_thousands(value: &str) -> String {
let (sign, digits) = value
.strip_prefix('-')
.map(|digits| ("-", digits))
.unwrap_or(("", value));
let mut reversed = String::new();
for (index, ch) in digits.chars().rev().enumerate() {
if index > 0 && index % 3 == 0 {
reversed.push(',');
}
reversed.push(ch);
}
let grouped = reversed.chars().rev().collect::<String>();
format!("{sign}{grouped}")
}
fn percent(fraction: f64) -> String {
format!("{:.0}%", (fraction * 100.0).round())
}
fn reset_countdown(seconds: i64) -> String {
if seconds <= 0 {
return "<1m".to_string();
}
let minutes = seconds / 60;
if minutes < 1 {
"<1m".to_string()
} else if minutes < 60 {
format!("{minutes}m")
} else {
let hours = minutes / 60;
let remaining_minutes = minutes % 60;
if hours < 24 {
if remaining_minutes == 0 {
format!("{hours}h")
} else {
format!("{hours}h {remaining_minutes}m")
}
} else {
let days = hours / 24;
let remaining_hours = hours % 24;
if remaining_hours == 0 {
format!("{days}d")
} else {
format!("{days}d {remaining_hours}h")
}
}
}
}
fn compact_reset(seconds: i64) -> String {
reset_countdown(seconds).replace(' ', "")
}
fn display_group(value: &str) -> String {
let last = value
.rsplit(['/', '\\'])
.find(|part| !part.is_empty())
.unwrap_or(value);
last.replace('-', " ")
}
fn provider_name(provider: ProviderId) -> &'static str {
match provider {
ProviderId::ClaudeCode => "claude code",
ProviderId::Codex => "codex",
ProviderId::Cursor => "cursor",
}
}
fn provider_status(status: ProviderStatusKind) -> &'static str {
match status {
ProviderStatusKind::Available => "available",
ProviderStatusKind::Detected => "detected",
ProviderStatusKind::Partial => "partial",
ProviderStatusKind::Missing => "missing",
ProviderStatusKind::Error => "error",
}
}
fn limit_kind(kind: LimitKind) -> &'static str {
match kind {
LimitKind::FiveHour => "5h",
LimitKind::Weekly => "wk",
LimitKind::Daily => "1d",
LimitKind::Monthly => "mo",
LimitKind::BillingCycle => "cyc",
}
}
fn period_scope(period: Period) -> &'static str {
match period {
Period::Day => "today",
Period::Week => "this week",
Period::Month => "this month",
Period::Year => "this year",
}
}
fn period_name(period: Period) -> &'static str {
match period {
Period::Day => "day",
Period::Week => "week",
Period::Month => "month",
Period::Year => "year",
}
}
fn period_bucket_label(period: Period) -> &'static str {
match period {
Period::Day => "day",
Period::Week => "week",
Period::Month => "month",
Period::Year => "year",
}
}
fn group_name(group: GroupBy) -> &'static str {
match group {
GroupBy::Model => "model",
GroupBy::App => "app",
GroupBy::Total => "total",
}
}
fn period_tabs(active: Period) -> String {
[Period::Day, Period::Week, Period::Month, Period::Year]
.into_iter()
.map(|period| {
if period == active {
format!("({})", period_name(period))
} else {
format!("[{}]", period_name(period))
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn group_tabs(active: GroupBy) -> String {
[GroupBy::Model, GroupBy::App, GroupBy::Total]
.into_iter()
.map(|group| {
if group == active {
format!("({})", group_name(group))
} else {
group_name(group).to_string()
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn sparkline_labels(period: Period, len: usize) -> String {
if len == 0 {
return String::new();
}
match period {
Period::Day => "start now".to_string(),
Period::Week => "mon sun".to_string(),
Period::Month => (1..=len)
.map(|index| format!("w{index}"))
.collect::<Vec<_>>()
.join(" "),
Period::Year => "jan dec".to_string(),
}
}
fn format_bucket_start(range: &PeriodRange) -> String {
let local: DateTime<Local> = range.start.with_timezone(&Local);
local.format("%Y-%m-%d").to_string()
}
#[cfg(test)]
mod tests {
use chrono::{Duration, LocalResult, NaiveDate, TimeZone, Utc};
use costroid_core::{
reconcile_cost, AggregateTotals, CostLaneSummary, CostLineItem, CostReportCaveats,
CostReportOutcome, ForecastDay, GroupKey, LimitSummary, LocalCostEstimate, NowSummary,
PeriodRange, PricingCoverage, ProviderStatus, TrendsSummary, VendorCostDay,
VendorCostReport,
};
use rust_decimal::Decimal;
use super::*;
fn utc(year: i32, month: u32, day: u32, hour: u32, minute: u32) -> DateTime<Utc> {
match Utc.with_ymd_and_hms(year, month, day, hour, minute, 0) {
LocalResult::Single(value) => value,
LocalResult::Ambiguous(_, _) | LocalResult::None => {
panic!("test timestamp should be valid")
}
}
}
fn range(start: DateTime<Utc>, days: i64) -> PeriodRange {
PeriodRange {
start,
end: start + Duration::days(days),
}
}
fn local_midnight_utc(year: i32, month: u32, day: u32) -> DateTime<Utc> {
match Local.with_ymd_and_hms(year, month, day, 0, 0, 0) {
LocalResult::Single(value) | LocalResult::Ambiguous(value, _) => {
value.with_timezone(&Utc)
}
LocalResult::None => panic!("test local midnight should exist"),
}
}
fn totals(cost_cents: i64, priced: usize, missing: usize, unknown: usize) -> AggregateTotals {
AggregateTotals {
row_count: priced + missing + unknown,
billed_cost: Decimal::new(cost_cents, 2),
effective_cost: Decimal::new(cost_cents, 2),
currency: Some("USD".to_string()),
multiple_currencies: false,
tokens: costroid_core::TokenTotals {
input: 10,
output: 20,
cache_read: 30,
cache_write: 0,
},
pricing_coverage: PricingCoverage {
priced_rows: priced,
missing_price_rows: missing,
unknown_model_rows: unknown,
},
estimated_rows: priced + missing + unknown,
}
}
fn row(group: &str, lane: CostLane, cost_cents: i64) -> CostLaneSummary {
CostLaneSummary {
group: GroupKey {
kind: GroupBy::Model,
value: group.to_string(),
},
lane,
totals: totals(cost_cents, 3, 0, 0),
}
}
fn unpriced_row(group: &str, lane: CostLane) -> CostLaneSummary {
CostLaneSummary {
group: GroupKey {
kind: GroupBy::Model,
value: group.to_string(),
},
lane,
totals: totals(0, 0, 3, 0),
}
}
fn unknown_model_row(group: &str, lane: CostLane) -> CostLaneSummary {
CostLaneSummary {
group: GroupKey {
kind: GroupBy::Model,
value: group.to_string(),
},
lane,
totals: totals(0, 0, 0, 3),
}
}
fn limits() -> Vec<LimitSummary> {
let now = utc(2026, 6, 2, 9, 0);
vec![
LimitSummary {
tool: ProviderId::ClaudeCode,
plan: None,
kind: LimitKind::FiveHour,
label: Some("no captured reading; run `costroid setup-statusline`".to_string()),
captured_at: now,
availability: LimitAvailability::Unavailable {
reason: "no captured reading; run `costroid setup-statusline`".to_string(),
},
},
LimitSummary {
tool: ProviderId::Codex,
plan: Some("plus".to_string()),
kind: LimitKind::FiveHour,
label: None,
captured_at: now,
availability: LimitAvailability::Available {
measure: LimitMeasure::TokenFraction(0.78),
resets_at: now + Duration::minutes(41),
reset_in_seconds: 41 * 60,
},
},
LimitSummary {
tool: ProviderId::Codex,
plan: Some("plus".to_string()),
kind: LimitKind::Weekly,
label: None,
captured_at: now,
availability: LimitAvailability::Available {
measure: LimitMeasure::TokenFraction(0.92),
resets_at: now + Duration::hours(54),
reset_in_seconds: 54 * 60 * 60,
},
},
LimitSummary {
tool: ProviderId::Cursor,
plan: None,
kind: LimitKind::Weekly,
label: None,
captured_at: now,
availability: LimitAvailability::Partial {
measure: Some(LimitMeasure::TokenFraction(0.97)),
resets_at: None,
reset_in_seconds: None,
reason: "limit data incomplete".to_string(),
},
},
]
}
fn all_arms_limits() -> Vec<LimitSummary> {
let now = utc(2026, 6, 2, 9, 0);
let captured = now - Duration::minutes(30);
vec![
LimitSummary {
tool: ProviderId::ClaudeCode,
plan: Some("max".to_string()),
kind: LimitKind::FiveHour,
label: None,
captured_at: captured,
availability: LimitAvailability::Available {
measure: LimitMeasure::TokenFraction(0.55),
resets_at: now + Duration::hours(2),
reset_in_seconds: 2 * 60 * 60,
},
},
LimitSummary {
tool: ProviderId::ClaudeCode,
plan: Some("max".to_string()),
kind: LimitKind::Weekly,
label: None,
captured_at: captured,
availability: LimitAvailability::Unverified {
measure: LimitMeasure::TokenFraction(0.96),
resets_at: Some(now + Duration::hours(50)),
reset_in_seconds: Some(50 * 60 * 60),
},
},
LimitSummary {
tool: ProviderId::Codex,
plan: Some("plus".to_string()),
kind: LimitKind::FiveHour,
label: None,
captured_at: captured,
availability: LimitAvailability::Available {
measure: LimitMeasure::TokenFraction(0.40),
resets_at: now + Duration::minutes(41),
reset_in_seconds: 41 * 60,
},
},
LimitSummary {
tool: ProviderId::Cursor,
plan: Some("pro".to_string()),
kind: LimitKind::Monthly,
label: None,
captured_at: captured,
availability: LimitAvailability::Available {
measure: LimitMeasure::Spend {
used_usd: Decimal::new(1850, 2),
included_usd: Some(Decimal::new(2000, 2)),
},
resets_at: now + Duration::days(12),
reset_in_seconds: 12 * 24 * 60 * 60,
},
},
LimitSummary {
tool: ProviderId::Cursor,
plan: Some("pro".to_string()),
kind: LimitKind::Weekly,
label: None,
captured_at: captured,
availability: LimitAvailability::Partial {
measure: Some(LimitMeasure::TokenFraction(0.88)),
resets_at: None,
reset_in_seconds: None,
reason: "limit data incomplete".to_string(),
},
},
LimitSummary {
tool: ProviderId::ClaudeCode,
plan: Some("max".to_string()),
kind: LimitKind::Daily,
label: None,
captured_at: now,
availability: LimitAvailability::Estimated {
volume_tokens: 1_234_567,
estimated_usd: Some(Decimal::new(1234, 2)),
},
},
LimitSummary {
tool: ProviderId::Codex,
plan: Some("plus".to_string()),
kind: LimitKind::Daily,
label: None,
captured_at: now,
availability: LimitAvailability::Estimated {
volume_tokens: 5_000,
estimated_usd: None,
},
},
LimitSummary {
tool: ProviderId::Cursor,
plan: None,
kind: LimitKind::BillingCycle,
label: Some("no sanctioned source".to_string()),
captured_at: now,
availability: LimitAvailability::Unavailable {
reason: "no sanctioned source".to_string(),
},
},
]
}
fn all_arms_now() -> NowSummary {
let mut summary = priced_now();
summary.limits = all_arms_limits();
summary
}
fn provider_statuses() -> Vec<ProviderStatus> {
vec![
ProviderStatus {
provider: ProviderId::ClaudeCode,
status: ProviderStatusKind::Available,
files: 1,
usage_events: 1,
focus_rows: 3,
limit_windows: 2,
message: None,
},
ProviderStatus {
provider: ProviderId::Cursor,
status: ProviderStatusKind::Missing,
files: 0,
usage_events: 0,
focus_rows: 0,
limit_windows: 0,
message: Some("no local data found".to_string()),
},
]
}
fn provider_capabilities_fixture() -> Vec<ProviderCapabilityView> {
vec![
ProviderCapabilityView {
provider: ProviderId::ClaudeCode,
api_cost: DataSource::LocalArtifact,
subscription_quota: DataSource::SanctionedHook,
model_mix: DataSource::LocalArtifact,
auth: AuthMethod::None,
quota_kinds: vec![LimitKind::FiveHour, LimitKind::Weekly],
},
ProviderCapabilityView {
provider: ProviderId::Codex,
api_cost: DataSource::LocalArtifact,
subscription_quota: DataSource::LocalArtifact,
model_mix: DataSource::LocalArtifact,
auth: AuthMethod::None,
quota_kinds: vec![LimitKind::FiveHour, LimitKind::Weekly],
},
ProviderCapabilityView {
provider: ProviderId::Cursor,
api_cost: DataSource::Unavailable,
subscription_quota: DataSource::Unavailable,
model_mix: DataSource::LocalArtifact,
auth: AuthMethod::None,
quota_kinds: Vec::new(),
},
]
}
fn provider_statuses_for_tab() -> Vec<ProviderStatus> {
vec![
ProviderStatus {
provider: ProviderId::ClaudeCode,
status: ProviderStatusKind::Available,
files: 2,
usage_events: 4,
focus_rows: 9,
limit_windows: 2,
message: None,
},
ProviderStatus {
provider: ProviderId::Codex,
status: ProviderStatusKind::Missing,
files: 0,
usage_events: 0,
focus_rows: 0,
limit_windows: 0,
message: Some("no local data found".to_string()),
},
ProviderStatus {
provider: ProviderId::Cursor,
status: ProviderStatusKind::Detected,
files: 1,
usage_events: 0,
focus_rows: 0,
limit_windows: 0,
message: Some(
"BETA - model Composer 2.5 Fast (composer-2.5), logged in; \
usage unavailable - no sanctioned source; \
quota unavailable - no sanctioned source"
.to_string(),
),
},
]
}
fn priced_now() -> NowSummary {
let now = utc(2026, 6, 2, 9, 0);
NowSummary {
generated_at: now,
cost_period: range(utc(2026, 6, 1, 0, 0), 7),
group_by: GroupBy::Model,
limits: limits(),
current_costs: vec![
row("sonnet-4.6", CostLane::Api, 678),
row("gpt-5.5", CostLane::Api, 1130),
row("claude-opus-4.7", CostLane::Api, 2410),
unpriced_row("gpt-5.5", CostLane::SubscriptionEstimate),
unknown_model_row("mystery-model", CostLane::UnknownAccess),
],
providers: provider_statuses(),
}
}
fn subscription_only_now() -> NowSummary {
let mut summary = priced_now();
summary.current_costs = vec![unpriced_row("gpt-5.5", CostLane::SubscriptionEstimate)];
summary
}
fn priced_trends() -> TrendsSummary {
let now = utc(2026, 6, 2, 9, 0);
let week_one = range(local_midnight_utc(2026, 6, 1), 7);
let week_two = range(local_midnight_utc(2026, 6, 8), 7);
TrendsSummary {
generated_at: now,
period: Period::Month,
group_by: GroupBy::Model,
buckets: vec![
costroid_core::TrendBucket {
period: week_one,
group: GroupKey {
kind: GroupBy::Model,
value: "claude-opus-4.7".to_string(),
},
lane: CostLane::Api,
totals: totals(9600, 3, 0, 0),
},
costroid_core::TrendBucket {
period: week_two,
group: GroupKey {
kind: GroupBy::Model,
value: "gpt-5.5".to_string(),
},
lane: CostLane::Api,
totals: totals(4500, 3, 0, 0),
},
],
totals: vec![
row("sonnet-4.6", CostLane::Api, 2700),
row("gpt-5.5", CostLane::Api, 4500),
row("claude-opus-4.7", CostLane::Api, 9600),
unpriced_row("gpt-5.5", CostLane::SubscriptionEstimate),
],
providers: provider_statuses(),
}
}
#[test]
fn cli_mode_selection_keeps_braille_when_no_color_is_set() {
let env = EnvSnapshot {
term: Some("xterm-256color".to_string()),
lang: Some("en_US.UTF-8".to_string()),
lc_all: None,
lc_ctype: None,
no_color: Some("1".to_string()),
};
let options = select_render_options(false, true, &env);
assert_eq!(options.mode, RenderMode::Braille);
assert!(!options.ansi);
}
#[test]
fn cli_mode_selection_plain_wins_for_pipes_and_flag() {
let env = EnvSnapshot {
term: Some("xterm-256color".to_string()),
lang: Some("en_US.UTF-8".to_string()),
lc_all: None,
lc_ctype: None,
no_color: None,
};
assert_eq!(
select_render_options(true, true, &env),
RenderOptions::plain()
);
assert_eq!(
select_render_options(false, false, &env),
RenderOptions::plain()
);
}
#[test]
fn cli_mode_selection_ascii_is_for_braille_incapability() {
let env = EnvSnapshot {
term: Some("dumb".to_string()),
lang: Some("en_US.UTF-8".to_string()),
lc_all: None,
lc_ctype: None,
no_color: None,
};
let options = select_render_options(false, true, &env);
assert_eq!(options.mode, RenderMode::Ascii);
assert!(options.ansi);
}
#[test]
fn braille_codepoint_math_matches_design_system() {
assert_eq!(braille_cell(&[1, 4]), '⠉');
assert_eq!(braille_full(), '⣿');
assert_eq!(braille_left_column(), '⡇');
assert_eq!(braille_light(), '⣀');
}
#[test]
fn meter_segments_keep_braille_fill_visible_without_ansi() {
let meter = limit_meter(0.42, 6, RenderOptions::braille(false));
assert_eq!(meter, "⣿⣿⡇⣀⣀⣀");
}
#[test]
fn thresholds_have_non_color_cues() {
assert_eq!(state_cue(limit_state(0.79)), "");
assert_eq!(state_cue(limit_state(0.80)), " !");
assert_eq!(state_cue(limit_state(0.95)), " !!");
assert_eq!(state_cue(limit_state(1.0)), " !! OVER");
}
#[test]
fn reset_countdown_uses_compact_two_unit_format() {
assert_eq!(reset_countdown(30), "<1m");
assert_eq!(reset_countdown(46 * 60), "46m");
assert_eq!(reset_countdown((2 * 60 + 14) * 60), "2h 14m");
assert_eq!(reset_countdown((3 * 24 + 4) * 60 * 60), "3d 4h");
}
#[test]
fn money_formatting_marks_estimates() {
assert_eq!(
format_money(&Decimal::new(2410, 2), Some("USD"), true),
"~$24.10"
);
assert_eq!(
format_money(&Decimal::new(184000, 2), Some("USD"), false),
"$1,840.00"
);
}
#[test]
fn snapshot_now_braille() {
insta::assert_snapshot!(render_now(&priced_now(), RenderOptions::braille(true)));
}
#[test]
fn snapshot_now_braille_no_ansi() {
insta::assert_snapshot!(render_now(&priced_now(), RenderOptions::braille(false)));
}
#[test]
fn snapshot_now_ascii() {
insta::assert_snapshot!(render_now(&priced_now(), RenderOptions::ascii(false)));
}
#[test]
fn snapshot_now_plain() {
insta::assert_snapshot!(render_now(&priced_now(), RenderOptions::plain()));
}
#[test]
fn snapshot_now_all_arms_braille() {
insta::assert_snapshot!(render_now(&all_arms_now(), RenderOptions::braille(true)));
}
#[test]
fn snapshot_now_all_arms_braille_no_ansi() {
insta::assert_snapshot!(render_now(&all_arms_now(), RenderOptions::braille(false)));
}
#[test]
fn snapshot_now_all_arms_ascii() {
insta::assert_snapshot!(render_now(&all_arms_now(), RenderOptions::ascii(false)));
}
#[test]
fn snapshot_now_all_arms_plain() {
insta::assert_snapshot!(render_now(&all_arms_now(), RenderOptions::plain()));
}
#[test]
fn now_all_arms_render_honestly_in_plain() {
let output = render_now(&all_arms_now(), RenderOptions::plain());
assert!(
!output.contains('\u{1b}'),
"plain now must contain no ANSI escapes: {output}"
);
assert!(
output.contains("96% used ? unverified"),
"unverified reading must be flagged, never confident: {output}"
);
assert!(
output.contains("as of 08:30"),
"an aged reading must carry the freshness stamp: {output}"
);
assert!(
output.contains(
"reflects Claude Code's view; claude.ai chat usage may make true usage higher."
),
"Claude quota lines must carry the chat caveat: {output}"
);
assert!(
output.contains("$18.50 / $20.00 used"),
"Spend must render a dollar pool, never a fabricated %: {output}"
);
assert!(
output.contains("usage 1,234,567 tokens (~$12.34, estimated), quota % unavailable"),
"Estimated must show volume + value + unavailable-%: {output}"
);
assert!(
output.contains("usage 5,000 tokens (estimated), quota % unavailable"),
"unpriced Estimated must show volume alone, never a guessed price: {output}"
);
}
#[test]
fn now_all_arms_spend_draws_no_meter_in_braille() {
let output = render_now(&all_arms_now(), RenderOptions::braille(true));
let spend_line = output
.lines()
.find(|line| line.contains("$18.50 / $20.00 used"))
.unwrap_or_else(|| panic!("spend line should render: {output}"));
assert!(
!spend_line.contains('⣿') && !spend_line.contains('⣀'),
"spend line must not draw a meter: {spend_line}"
);
}
#[test]
fn statusline_flags_unverified_when_most_constrained() {
let braille = render_statusline(&all_arms_now(), RenderOptions::braille(true));
assert!(
braille.contains("96% ? unverified"),
"statusline must flag the unverified pick: {braille}"
);
assert!(
!braille.contains("96% !!"),
"unverified must never render as a confident alarm: {braille}"
);
let plain = render_statusline(&all_arms_now(), RenderOptions::plain());
assert!(plain.contains("? unverified"), "plain statusline: {plain}");
assert!(
!plain.contains('\u{1b}'),
"plain statusline must contain no ANSI: {plain}"
);
}
#[test]
fn snapshot_statusline_unverified_braille() {
insta::assert_snapshot!(render_statusline(
&all_arms_now(),
RenderOptions::braille(true)
));
}
#[test]
fn snapshot_statusline_unverified_plain() {
insta::assert_snapshot!(render_statusline(&all_arms_now(), RenderOptions::plain()));
}
#[test]
fn freshness_stamp_appears_only_past_the_threshold() {
let now = utc(2026, 6, 2, 9, 0);
assert_eq!(freshness_stamp(now - Duration::minutes(5), now), "");
assert_eq!(
freshness_stamp(now - Duration::minutes(10), now),
" as of 08:50"
);
}
#[test]
fn epoch_sentinel_discloses_unknown_capture_time_never_midnight() {
let now = utc(2026, 6, 2, 9, 0);
let stamp = freshness_stamp(Utc.timestamp_nanos(0), now);
assert_eq!(stamp, " capture time unknown");
assert!(!stamp.contains("as of"));
}
#[test]
fn plain_paths_carry_the_state_cue() {
let generated = utc(2026, 6, 2, 9, 0);
let available = LimitSummary {
tool: ProviderId::Codex,
plan: None,
kind: LimitKind::FiveHour,
label: None,
captured_at: generated,
availability: LimitAvailability::Available {
measure: LimitMeasure::TokenFraction(0.97),
resets_at: generated + Duration::hours(1),
reset_in_seconds: 3600,
},
};
assert!(plain_limit_line(&available, generated).contains("(critical)"));
assert!(plain_limit_phrase(&available).contains("(critical)"));
let partial = LimitSummary {
availability: LimitAvailability::Partial {
measure: Some(LimitMeasure::TokenFraction(0.97)),
resets_at: None,
reset_in_seconds: None,
reason: "limit data incomplete".to_string(),
},
..available
};
assert!(plain_limit_line(&partial, generated).contains("(critical)"));
assert!(plain_limit_phrase(&partial).contains("(critical)"));
}
#[test]
fn ascii_mode_output_is_pure_ascii() {
let now = render_now(&all_arms_now(), RenderOptions::ascii(false));
assert!(now.is_ascii(), "ascii now screen must be pure ASCII: {now}");
let trends = render_trends(&priced_trends(), RenderOptions::ascii(false));
assert!(
trends.is_ascii(),
"ascii trends screen must be pure ASCII: {trends}"
);
let frontier = render_frontier(&used_gpt(), RenderOptions::ascii(false));
assert!(
frontier.is_ascii(),
"ascii frontier screen must be pure ASCII: {frontier}"
);
let frontier_no_api = render_frontier(&bench_view_for(&[]), RenderOptions::ascii(false));
assert!(
frontier_no_api.is_ascii(),
"ascii frontier screen without API usage must be pure ASCII: {frontier_no_api}"
);
let statusline = render_statusline(&all_arms_now(), RenderOptions::ascii(false));
assert!(
statusline.is_ascii(),
"ascii statusline must be pure ASCII: {statusline}"
);
let providers = render_providers(
&provider_capabilities_fixture(),
&provider_statuses_for_tab(),
RenderOptions::ascii(false),
);
assert!(
providers.is_ascii(),
"ascii providers tab must be pure ASCII: {providers}"
);
let models = render_models(&models_view_fixture(), RenderOptions::ascii(false));
assert!(
models.is_ascii(),
"ascii models tab must be pure ASCII: {models}"
);
let history = render_history(&history_rows_fixture(), RenderOptions::ascii(false));
assert!(
history.is_ascii(),
"ascii history tab must be pure ASCII: {history}"
);
let budget = render_budget(&budget_view_fixture(), RenderOptions::ascii(false));
assert!(
budget.is_ascii(),
"ascii budget tab must be pure ASCII: {budget}"
);
let alerts_banner = render_now_with_alerts(
&all_arms_now(),
&alerts_fixture(),
RenderOptions::ascii(false),
);
assert!(
alerts_banner.is_ascii(),
"ascii alert banner must be pure ASCII: {alerts_banner}"
);
let alerts_cmd = render_alerts(&alerts_fixture(), RenderOptions::ascii(false));
assert!(
alerts_cmd.is_ascii(),
"ascii alerts command must be pure ASCII: {alerts_cmd}"
);
let alerts_off = render_alerts_off(RenderOptions::ascii(false));
assert!(
alerts_off.is_ascii(),
"ascii alerts-off must be pure ASCII: {alerts_off}"
);
for view in [
forecast_view_projected_fixture(),
forecast_view_insufficient_fixture(),
forecast_view_eta_unavailable_fixture(),
] {
let forecast = render_forecast(&view, RenderOptions::ascii(false));
assert!(
forecast.is_ascii(),
"ascii forecast tab must be pure ASCII: {forecast}"
);
}
for view in [
anomalies_view_flagged_fixture(),
anomalies_view_collapse_fixture(),
anomalies_view_suppressed_multiples_fixture(),
anomalies_view_clean_fixture(),
anomalies_view_insufficient_fixture(),
anomalies_view_no_usage_fixture(),
] {
let anomalies = render_anomalies(&view, RenderOptions::ascii(false));
assert!(
anomalies.is_ascii(),
"ascii anomalies tab must be pure ASCII: {anomalies}"
);
}
}
#[test]
fn plain_now_renders_cursor_detected_note_without_color() {
let mut summary = priced_now();
summary.providers = vec![ProviderStatus {
provider: ProviderId::Cursor,
status: ProviderStatusKind::Detected,
files: 1,
usage_events: 0,
focus_rows: 0,
limit_windows: 0,
message: Some(
"BETA - model Composer 2.5 Fast (composer-2.5), logged in; \
usage unavailable - no sanctioned source; quota unavailable - no sanctioned source"
.to_string(),
),
}];
let output = render_now(&summary, RenderOptions::plain());
assert!(
output.contains(
"provider cursor detected: BETA - model Composer 2.5 Fast (composer-2.5), logged in"
),
"plain now should render the cursor detected note: {output}"
);
assert!(output.contains("usage unavailable - no sanctioned source"));
assert!(output.contains("quota unavailable - no sanctioned source"));
assert!(
!output.contains('\u{1b}'),
"plain output must not contain ANSI escapes"
);
assert!(
output.is_ascii(),
"plain output must be pure ASCII: {output}"
);
}
#[test]
fn plain_mode_output_is_pure_ascii() {
let outputs = [
render_now(&all_arms_now(), RenderOptions::plain()),
render_now(&priced_now(), RenderOptions::plain()),
render_now(&subscription_only_now(), RenderOptions::plain()),
render_trends(&priced_trends(), RenderOptions::plain()),
render_statusline(&priced_now(), RenderOptions::plain()),
render_statusline(&subscription_only_now(), RenderOptions::plain()),
render_frontier(&used_gpt(), RenderOptions::plain()),
render_frontier(&bench_view_for(&[]), RenderOptions::plain()),
render_providers(
&provider_capabilities_fixture(),
&provider_statuses_for_tab(),
RenderOptions::plain(),
),
render_models(&models_view_fixture(), RenderOptions::plain()),
render_models(&empty_models_view(), RenderOptions::plain()),
render_history(&history_rows_fixture(), RenderOptions::plain()),
render_history(&[], RenderOptions::plain()),
render_budget(&budget_view_fixture(), RenderOptions::plain()),
render_budget(&empty_budget_view(), RenderOptions::plain()),
render_forecast(&forecast_view_projected_fixture(), RenderOptions::plain()),
render_forecast(
&forecast_view_insufficient_fixture(),
RenderOptions::plain(),
),
render_forecast(
&forecast_view_eta_unavailable_fixture(),
RenderOptions::plain(),
),
render_anomalies(&anomalies_view_flagged_fixture(), RenderOptions::plain()),
render_anomalies(&anomalies_view_collapse_fixture(), RenderOptions::plain()),
render_anomalies(
&anomalies_view_suppressed_multiples_fixture(),
RenderOptions::plain(),
),
render_anomalies(&anomalies_view_clean_fixture(), RenderOptions::plain()),
render_anomalies(
&anomalies_view_insufficient_fixture(),
RenderOptions::plain(),
),
render_anomalies(&anomalies_view_no_usage_fixture(), RenderOptions::plain()),
render_now_with_alerts(&all_arms_now(), &alerts_fixture(), RenderOptions::plain()),
render_alerts(&alerts_fixture(), RenderOptions::plain()),
render_alerts(&[], RenderOptions::plain()),
render_alerts_off(RenderOptions::plain()),
];
for output in outputs {
assert!(
output.is_ascii(),
"plain output must be pure ASCII: {output}"
);
}
}
#[test]
fn snapshot_now_subscription_only_has_explicit_empty_api_state() {
insta::assert_snapshot!(render_now(
&subscription_only_now(),
RenderOptions::braille(false)
));
}
#[test]
fn snapshot_trends_braille_with_ansi() {
insta::assert_snapshot!(render_trends(
&priced_trends(),
RenderOptions::braille(true)
));
}
#[test]
fn snapshot_trends_braille() {
insta::assert_snapshot!(render_trends(
&priced_trends(),
RenderOptions::braille(false)
));
}
#[test]
fn snapshot_trends_ascii() {
insta::assert_snapshot!(render_trends(&priced_trends(), RenderOptions::ascii(false)));
}
#[test]
fn snapshot_trends_plain() {
insta::assert_snapshot!(render_trends(&priced_trends(), RenderOptions::plain()));
}
#[test]
fn snapshot_providers_braille() {
insta::assert_snapshot!(render_providers(
&provider_capabilities_fixture(),
&provider_statuses_for_tab(),
RenderOptions::braille(false)
));
}
#[test]
fn snapshot_providers_ascii() {
insta::assert_snapshot!(render_providers(
&provider_capabilities_fixture(),
&provider_statuses_for_tab(),
RenderOptions::ascii(false)
));
}
#[test]
fn snapshot_providers_plain() {
insta::assert_snapshot!(render_providers(
&provider_capabilities_fixture(),
&provider_statuses_for_tab(),
RenderOptions::plain()
));
}
#[test]
fn providers_render_is_honest_and_cursor_is_detected_never_coming_soon() {
let output = render_providers(
&provider_capabilities_fixture(),
&provider_statuses_for_tab(),
RenderOptions::plain(),
);
assert!(output.contains("api cost: from local logs"));
assert!(
output.contains("quota: from the statusLine capture; run setup-statusline (5h, wk)")
);
assert!(output.contains("auth: no login required"));
assert!(output.contains("quota: from local logs (5h, wk)"));
assert!(output.contains("cursor (detected):"));
assert!(output.contains("api cost: no sanctioned source"));
assert!(output.contains("quota: no sanctioned source"));
assert!(!output.contains("coming soon"));
assert!(output.contains("note: no local data found"));
}
#[test]
fn providers_document_is_monochrome() {
let doc = render_providers_document(
&provider_capabilities_fixture(),
&provider_statuses_for_tab(),
RenderOptions::braille(true),
);
for line in &doc.lines {
for span in &line.spans {
assert!(
!matches!(span.style, SemanticStyle::Warn | SemanticStyle::Critical),
"providers tab must be monochrome (amber/red are reserved): {span:?}"
);
}
}
}
fn models_view_fixture() -> ModelsView {
let on_frontier = OverlayModel {
model_id: "gpt-5.5".to_string(),
raw_model: "gpt-5.5".to_string(),
billed_cost: Decimal::new(1234, 2),
tokens: costroid_core::TokenTotals {
input: 1_000,
output: 2_000,
cache_read: 500,
cache_write: 0,
},
appearances: vec![costroid_core::OverlayAppearance {
benchmark_name: "swe-bench".to_string(),
score_pct: Decimal::new(72, 0),
standing: FrontierStanding::OnFrontier,
}],
repricing: vec![RepricingDelta {
target_model_id: "claude-haiku-4".to_string(),
target_label: "claude haiku 4".to_string(),
delta_usd: Decimal::new(-400, 2),
status: RepricingStatus::Computed,
on_frontier_in: vec!["swe-bench".to_string()],
}],
fully_priced: true,
};
let dominated = OverlayModel {
model_id: "claude-opus-4".to_string(),
raw_model: "claude-opus-4".to_string(),
billed_cost: Decimal::new(880, 2),
tokens: costroid_core::TokenTotals::default(),
appearances: vec![costroid_core::OverlayAppearance {
benchmark_name: "swe-bench".to_string(),
score_pct: Decimal::new(70, 0),
standing: FrontierStanding::Dominated {
by: "gpt-5.5".to_string(),
},
}],
repricing: Vec::new(),
fully_priced: false,
};
ModelsView {
generated_at: utc(2026, 6, 2, 9, 0),
models: vec![
ModelRow {
model: "gpt-5.5".to_string(),
totals: totals(1234, 3, 0, 0),
overlay: Some(on_frontier),
},
ModelRow {
model: "claude-opus-4".to_string(),
totals: totals(880, 2, 1, 0),
overlay: Some(dominated),
},
ModelRow {
model: "mystery-model".to_string(),
totals: totals(210, 3, 0, 0),
overlay: None,
},
],
no_api_usage: false,
disclaimer: costroid_core::BenchDisclaimer {
note: "~ cost-only comparison at equal token volume; not a quality claim.",
pricing_as_of: "2026-05-01".to_string(),
},
providers: Vec::new(),
}
}
fn empty_models_view() -> ModelsView {
ModelsView {
generated_at: utc(2026, 6, 2, 9, 0),
models: Vec::new(),
no_api_usage: true,
disclaimer: costroid_core::BenchDisclaimer {
note: "~ cost-only comparison at equal token volume; not a quality claim.",
pricing_as_of: "2026-05-01".to_string(),
},
providers: Vec::new(),
}
}
#[test]
fn snapshot_models_braille() {
insta::assert_snapshot!(render_models(
&models_view_fixture(),
RenderOptions::braille(false)
));
}
#[test]
fn snapshot_models_ascii() {
insta::assert_snapshot!(render_models(
&models_view_fixture(),
RenderOptions::ascii(false)
));
}
#[test]
fn snapshot_models_plain() {
insta::assert_snapshot!(render_models(
&models_view_fixture(),
RenderOptions::plain()
));
}
#[test]
fn snapshot_models_no_api_usage() {
insta::assert_snapshot!(render_models(&empty_models_view(), RenderOptions::plain()));
}
#[test]
fn models_render_shows_spend_tokens_and_standing_unbenchmarked_is_a_gap() {
let output = render_models(&models_view_fixture(), RenderOptions::plain());
assert!(output.contains("gpt-5.5: spent ~$12.34"));
assert!(output.contains("tokens: 10 in / 20 out / 30 cache"));
assert!(output.contains("frontier: swe-bench 72% - on frontier"));
assert!(output.contains("frontier: swe-bench 70% - off (dominated by gpt-5.5)"));
assert!(output.contains("claude-haiku-4 costs about ~$4.00 less at equal volume"));
assert!(!output.to_lowercase().contains("switch to"));
assert!(output.contains("claude-opus-4: spent ~$8.80 (partial pricing)"));
assert!(output.contains("mystery-model: spent ~$2.10"));
assert!(output.contains("frontier: not benchmarked"));
assert!(output.contains("cost-only comparison at equal token volume"));
}
#[test]
fn models_document_is_monochrome() {
let doc = render_models_document(&models_view_fixture(), RenderOptions::braille(true));
for line in &doc.lines {
for span in &line.spans {
assert!(
!matches!(span.style, SemanticStyle::Warn | SemanticStyle::Critical),
"models tab must be monochrome (amber/red are reserved): {span:?}"
);
}
}
}
fn history_record(
model: &str,
when: DateTime<Utc>,
token_type: costroid_focus::TokenType,
tokens: u64,
cents: i64,
access: costroid_focus::FocusAccessPath,
) -> FocusRecord {
let input = costroid_focus::UnpricedUsage {
timestamp: when,
tool: "codex".to_string(),
model: model.to_string(),
token_type,
token_count: tokens,
project: Some("alpha-app".to_string()),
access_path: access,
service_name: "OpenAI API".to_string(),
service_provider_name: "OpenAI".to_string(),
host_provider_name: "OpenAI".to_string(),
invoice_issuer_name: "OpenAI".to_string(),
billing_currency: costroid_focus::DEFAULT_BILLING_CURRENCY.to_string(),
};
let mut record = match FocusRecord::unpriced_usage(input) {
Ok(record) => record,
Err(error) => panic!("history fixture record should be valid: {error}"),
};
let cost = Decimal::new(cents, 2);
record.billed_cost = cost;
record.effective_cost = cost;
record.x_pricing_status = "priced".to_string();
record
}
fn history_rows_fixture() -> Vec<FocusRecord> {
vec![
history_record(
"claude-sonnet-4",
utc(2026, 6, 2, 9, 0),
costroid_focus::TokenType::Output,
500_000,
0,
costroid_focus::FocusAccessPath::Subscription,
),
history_record(
"gpt-5.5",
utc(2026, 6, 2, 10, 30),
costroid_focus::TokenType::Output,
1_000_000,
1234,
costroid_focus::FocusAccessPath::Api,
),
]
}
#[test]
fn snapshot_history_braille() {
insta::assert_snapshot!(render_history(
&history_rows_fixture(),
RenderOptions::braille(false)
));
}
#[test]
fn snapshot_history_ascii() {
insta::assert_snapshot!(render_history(
&history_rows_fixture(),
RenderOptions::ascii(false)
));
}
#[test]
fn snapshot_history_plain() {
insta::assert_snapshot!(render_history(
&history_rows_fixture(),
RenderOptions::plain()
));
}
#[test]
fn snapshot_history_empty() {
insta::assert_snapshot!(render_history(&[], RenderOptions::plain()));
}
#[test]
fn history_lists_records_newest_first_with_raw_usage_and_api_only_cost() {
let output = render_history(&history_rows_fixture(), RenderOptions::plain());
let index_of = |needle: &str| match output.find(needle) {
Some(at) => at,
None => panic!("expected `{needle}` in history output: {output}"),
};
assert!(
index_of("gpt-5.5") < index_of("claude-sonnet-4"),
"records render newest-first: {output}"
);
assert!(output.contains("1,000,000 output"));
assert!(output.contains("500,000 output"));
assert!(output.contains("api ~$12.34"));
assert!(output.contains("claude-sonnet-4 500,000 output subscription"));
assert!(!output.contains("subscription ~$"));
assert!(output.contains("scope: 2 records, newest first (all time)"));
assert!(output.contains("costroid export --format json|csv"));
}
#[test]
fn history_empty_list_renders_without_panicking() {
let output = render_history(&[], RenderOptions::braille(false));
assert!(output.contains("scope: 0 records, newest first (all time)"));
assert!(output.contains("no usage recorded yet"));
}
#[test]
fn history_document_is_monochrome() {
let doc = render_history_document(&history_rows_fixture(), RenderOptions::braille(true));
for line in &doc.lines {
for span in &line.spans {
assert!(
!matches!(span.style, SemanticStyle::Warn | SemanticStyle::Critical),
"history tab must be monochrome (amber/red are reserved): {span:?}"
);
}
}
}
fn budget_row_fixture(
scope: BudgetScope,
spent: i64,
target: i64,
fraction: f64,
over: Option<i64>,
pace: BudgetPace,
) -> BudgetRow {
BudgetRow {
scope,
target_usd: Decimal::new(target, 2),
spent_usd: Decimal::new(spent, 2),
fraction,
over_by_usd: over.map(|cents| Decimal::new(cents, 2)),
pace,
}
}
fn budget_view_fixture() -> BudgetView {
BudgetView {
generated_at: utc(2026, 6, 16, 12, 0),
rows: vec![
budget_row_fixture(
BudgetScope::Tool("codex".to_string()),
6_000,
5_000,
1.2,
Some(1_000),
BudgetPace::OverBudget,
),
budget_row_fixture(
BudgetScope::Total,
8_400,
10_000,
0.84,
None,
BudgetPace::AheadOfPace,
),
budget_row_fixture(
BudgetScope::Tool("claude-code".to_string()),
1_500,
6_000,
0.25,
None,
BudgetPace::OnTrack,
),
],
excluded_tools: vec![BudgetExcludedTool {
tool: "cursor".to_string(),
reason: BudgetExclusion::FlatFeeSubscription,
}],
no_budget_set: false,
spent_total_usd: Decimal::new(8_400, 2),
month_elapsed_fraction: 0.5,
}
}
fn empty_budget_view() -> BudgetView {
BudgetView {
generated_at: utc(2026, 6, 16, 12, 0),
rows: Vec::new(),
excluded_tools: Vec::new(),
no_budget_set: true,
spent_total_usd: Decimal::ZERO,
month_elapsed_fraction: 0.5,
}
}
#[test]
fn snapshot_budget_braille() {
insta::assert_snapshot!(render_budget(
&budget_view_fixture(),
RenderOptions::braille(false)
));
}
#[test]
fn snapshot_budget_ascii() {
insta::assert_snapshot!(render_budget(
&budget_view_fixture(),
RenderOptions::ascii(false)
));
}
#[test]
fn snapshot_budget_plain() {
insta::assert_snapshot!(render_budget(
&budget_view_fixture(),
RenderOptions::plain()
));
}
#[test]
fn snapshot_budget_no_budget_set() {
insta::assert_snapshot!(render_budget(&empty_budget_view(), RenderOptions::plain()));
}
#[test]
fn budget_over_by_below_a_cent_renders_less_than_a_cent_not_zero() {
assert_eq!(format_over_by(&Decimal::new(3, 4)), "<$0.01"); assert_eq!(format_over_by(&Decimal::new(4, 3)), "<$0.01"); assert_eq!(format_over_by(&Decimal::new(1, 2)), "~$0.01"); assert_eq!(format_over_by(&Decimal::new(1_000, 2)), "~$10.00"); }
#[test]
fn budget_over_row_renders_over_cue_and_overshoot_amount() {
let output = render_budget(&budget_view_fixture(), RenderOptions::plain());
assert!(
output.contains("codex: ~$60.00 / ~$50.00 budget (120%)"),
"{output}"
);
assert!(output.contains("!! OVER, over by ~$10.00"), "{output}");
assert!(
output.contains("claude-code: ~$15.00 / ~$60.00 budget (25%)"),
"{output}"
);
assert!(
!output.contains("claude-code: ~$15.00 / ~$60.00 budget (25%) !"),
"{output}"
);
assert!(
output.contains("pace: 120% used vs 50% of month elapsed (over budget)"),
"{output}"
);
}
#[test]
fn budget_flat_fee_tool_gets_no_dollar_comparison() {
let output = render_budget(&budget_view_fixture(), RenderOptions::plain());
assert!(
output.contains("cursor: flat-fee subscription - no $ budget applies (not API-billed)"),
"{output}"
);
assert!(
!output.contains("cursor: ~$"),
"flat-fee tool must not show a $ budget: {output}"
);
}
#[test]
fn budget_exactly_at_budget_reads_at_budget_not_over() {
let view = BudgetView {
generated_at: utc(2026, 6, 16, 12, 0),
rows: vec![budget_row_fixture(
BudgetScope::Tool("codex".to_string()),
5_000,
5_000,
1.0,
None,
BudgetPace::AheadOfPace,
)],
excluded_tools: Vec::new(),
no_budget_set: false,
spent_total_usd: Decimal::new(5_000, 2),
month_elapsed_fraction: 0.5,
};
let output = render_budget(&view, RenderOptions::plain());
assert!(
output.contains("codex: ~$50.00 / ~$50.00 budget (100%) !! at budget"),
"{output}"
);
assert!(
!output.contains("OVER"),
"exactly-at-budget must not say OVER: {output}"
);
assert!(
!output.contains("over by"),
"exactly-at-budget has no overshoot: {output}"
);
}
#[test]
fn budget_not_api_billed_tool_gets_no_dollar_comparison_or_subscription_claim() {
let view = BudgetView {
generated_at: utc(2026, 6, 16, 12, 0),
rows: Vec::new(),
excluded_tools: vec![BudgetExcludedTool {
tool: "codex".to_string(),
reason: BudgetExclusion::NotApiBilled,
}],
no_budget_set: false,
spent_total_usd: Decimal::ZERO,
month_elapsed_fraction: 0.5,
};
let output = render_budget(&view, RenderOptions::plain());
assert!(
output.contains("codex: no API-billed usage - a $ budget tracks API spend only"),
"{output}"
);
assert!(!output.contains("codex: ~$"), "no $ comparison: {output}");
assert!(
!output.contains("subscription"),
"no unbacked subscription claim: {output}"
);
}
#[test]
fn budget_empty_state_points_to_config_file() {
let output = render_budget(&empty_budget_view(), RenderOptions::plain());
assert!(
output.contains("no budget set - set targets in ~/.config/costroid/config.toml"),
"{output}"
);
assert!(output.contains("[budget]"));
assert!(output.contains("total_monthly_usd = 100.00"));
assert!(output.contains("[budget.per_tool]"));
}
#[test]
fn budget_over_state_pairs_color_with_a_textual_cue() {
let doc = render_budget_document(&budget_view_fixture(), RenderOptions::braille(true));
let mut saw_colored = false;
for line in &doc.lines {
let colored = line
.spans
.iter()
.any(|span| matches!(span.style, SemanticStyle::Warn | SemanticStyle::Critical));
if colored {
saw_colored = true;
let text: String = line
.spans
.iter()
.map(|span| span.content.as_str())
.collect();
assert!(
text.contains('!'),
"a colored budget line must also carry a non-color cue: {text:?}"
);
}
}
assert!(
saw_colored,
"the over/near fixture should exercise an amber/red state"
);
}
fn alerts_fixture() -> Vec<Alert> {
vec![
Alert::Quota {
tool: ProviderId::ClaudeCode,
kind: LimitKind::FiveHour,
level: AlertLevel::Critical,
fraction: 0.97,
reset_in_seconds: 2_460, },
Alert::Budget {
scope: BudgetScope::Tool("codex".to_string()),
spent_usd: Decimal::new(6_000, 2),
target_usd: Decimal::new(5_000, 2),
over_by_usd: Decimal::new(1_000, 2),
},
Alert::Quota {
tool: ProviderId::ClaudeCode,
kind: LimitKind::Weekly,
level: AlertLevel::Warn,
fraction: 0.82,
reset_in_seconds: 187_200, },
]
}
#[test]
fn render_thresholds_are_the_one_core_source_never_forked() {
assert_eq!(WARN_FRACTION, costroid_core::ALERT_WARN_FRACTION);
assert_eq!(CRITICAL_FRACTION, costroid_core::ALERT_CRITICAL_FRACTION);
let defaults = costroid_core::AlertThresholds::default();
assert_eq!(WARN_FRACTION, defaults.quota_warn_fraction);
assert_eq!(CRITICAL_FRACTION, defaults.quota_critical_fraction);
}
#[test]
fn snapshot_alerts_now_banner_braille() {
insta::assert_snapshot!(render_now_with_alerts(
&priced_now(),
&alerts_fixture(),
RenderOptions::braille(true)
));
}
#[test]
fn snapshot_alerts_now_banner_ascii() {
insta::assert_snapshot!(render_now_with_alerts(
&priced_now(),
&alerts_fixture(),
RenderOptions::ascii(false)
));
}
#[test]
fn snapshot_alerts_now_banner_plain() {
insta::assert_snapshot!(render_now_with_alerts(
&priced_now(),
&alerts_fixture(),
RenderOptions::plain()
));
}
#[test]
fn alerts_banner_empty_is_byte_identical_to_plain_now() {
for options in [
RenderOptions::plain(),
RenderOptions::braille(false),
RenderOptions::ascii(false),
] {
assert_eq!(
render_now_with_alerts(&priced_now(), &[], options),
render_now(&priced_now(), options),
"an empty alert slice must leave the now view unchanged"
);
}
}
#[test]
fn snapshot_alerts_command_plain() {
insta::assert_snapshot!(render_alerts(&alerts_fixture(), RenderOptions::plain()));
}
#[test]
fn snapshot_alerts_command_braille() {
insta::assert_snapshot!(render_alerts(
&alerts_fixture(),
RenderOptions::braille(false)
));
}
#[test]
fn alerts_command_clear_state_is_honest() {
let output = render_alerts(&[], RenderOptions::plain());
assert!(output.contains("no active alerts"), "{output}");
assert!(output.is_ascii(), "{output}");
}
#[test]
fn alerts_command_off_state_points_at_config() {
let output = render_alerts_off(RenderOptions::plain());
assert!(output.contains("alerts are off"), "{output}");
assert!(
output.contains("~/.config/costroid/config.toml"),
"{output}"
);
assert!(output.contains("[alerts]"), "{output}");
assert!(output.contains("enabled = true"), "{output}");
assert!(output.is_ascii(), "{output}");
}
#[test]
fn quota_alert_copy_uses_quota_extension_framing_never_money() {
let quota = Alert::Quota {
tool: ProviderId::ClaudeCode,
kind: LimitKind::Weekly,
level: AlertLevel::Warn,
fraction: 0.82,
reset_in_seconds: 187_200,
};
let sentence = alert_sentence("a);
assert_eq!(sentence, "claude code weekly limit at 82%, resets in 2d 4h");
assert!(
!sentence.contains('$'),
"quota copy must not use money framing: {sentence}"
);
}
#[test]
fn budget_alert_copy_is_dollars() {
let budget = Alert::Budget {
scope: BudgetScope::Total,
spent_usd: Decimal::new(11_000, 2),
target_usd: Decimal::new(10_000, 2),
over_by_usd: Decimal::new(1_000, 2),
};
assert_eq!(
alert_sentence(&budget),
"total budget over by ~$10.00, spent ~$110.00 of ~$100.00"
);
}
#[test]
fn forecast_alert_copy_is_framed_as_a_projection() {
let forecast = Alert::Forecast {
projected_month_usd: Decimal::new(15_000, 2),
target_usd: Decimal::new(10_000, 2),
projected_over_by_usd: Decimal::new(5_000, 2),
};
let sentence = alert_sentence(&forecast);
assert_eq!(
sentence,
"total budget projected over by ~$50.00, ~$150.00 projected of ~$100.00"
);
assert!(
sentence.contains("projected"),
"forecast copy must read as a projection: {sentence}"
);
assert!(sentence.is_ascii(), "{sentence}");
}
#[test]
fn spend_spike_alert_copy_cites_the_norm() {
let spike = Alert::SpendSpike {
date: anomaly_date(2026, 6, 16),
value_usd: Decimal::new(2_000, 2),
baseline_median_usd: Decimal::new(250, 2),
magnitude: Some(Decimal::new(80, 1)), };
let sentence = alert_sentence(&spike);
assert_eq!(
sentence,
"daily spend spike: ~$20.00 on Jun 16, ~8.0x your ~$2.50 norm"
);
assert!(sentence.is_ascii(), "{sentence}");
}
#[test]
fn spend_spike_alert_copy_is_descriptive_when_the_median_displays_zero() {
let spike = Alert::SpendSpike {
date: anomaly_date(2026, 6, 16),
value_usd: Decimal::new(500, 2),
baseline_median_usd: Decimal::ZERO,
magnitude: None,
};
let sentence = alert_sentence(&spike);
assert_eq!(
sentence,
"daily spend spike: ~$5.00 on Jun 16, well above your ~$0.00 norm"
);
}
#[test]
fn advisory_alerts_are_non_color_cued_and_ascii_in_plain() {
let advisory = vec![
Alert::Forecast {
projected_month_usd: Decimal::new(15_000, 2),
target_usd: Decimal::new(10_000, 2),
projected_over_by_usd: Decimal::new(5_000, 2),
},
Alert::SpendSpike {
date: anomaly_date(2026, 6, 16),
value_usd: Decimal::new(2_000, 2),
baseline_median_usd: Decimal::new(250, 2),
magnitude: Some(Decimal::new(80, 1)),
},
];
assert_eq!(alert_plain_cue(&advisory[0]), " (projected over budget)");
assert_eq!(alert_plain_cue(&advisory[1]), " (spend spike)");
assert!(!advisory[0].is_critical() && !advisory[1].is_critical());
let plain = render_alerts(&advisory, RenderOptions::plain());
assert!(plain.contains("(projected over budget)"), "{plain}");
assert!(plain.contains("(spend spike)"), "{plain}");
assert!(
plain.is_ascii(),
"plain advisory output must be ASCII: {plain}"
);
}
#[test]
fn alert_check_line_and_exit_code() {
assert_eq!(alerts_check_exit_code(&[]), 0);
assert_eq!(alert_check_line(&[]), "");
let one = vec![alerts_fixture().remove(0)];
assert_eq!(alerts_check_exit_code(&one), 1);
assert_eq!(
alert_check_line(&one),
"costroid: claude code 5-hour limit at 97%, resets in 41m"
);
let several = alerts_fixture();
assert_eq!(alerts_check_exit_code(&several), 1);
let line = alert_check_line(&several);
assert!(line.contains("3 active alerts"), "{line}");
assert!(
line.contains("most pressing: claude code 5-hour limit at 97%"),
"{line}"
);
assert!(line.is_ascii(), "the cron line must be pure ASCII: {line}");
}
#[test]
fn alerts_banner_pairs_color_with_a_textual_cue() {
let mut doc = StyledDocument::new();
push_alert_banner(&mut doc, &alerts_fixture(), RenderOptions::braille(true));
let mut saw_colored = false;
for line in &doc.lines {
let colored = line
.spans
.iter()
.any(|span| matches!(span.style, SemanticStyle::Warn | SemanticStyle::Critical));
if colored {
saw_colored = true;
let text: String = line
.spans
.iter()
.map(|span| span.content.as_str())
.collect();
assert!(
text.contains('!'),
"a colored alert line must carry a non-color cue: {text:?}"
);
}
}
assert!(
saw_colored,
"the fixture should exercise an amber/red alert state"
);
}
fn forecast_day(year: i32, month: u32, day: u32, cents: i64) -> ForecastDay {
ForecastDay {
date: match NaiveDate::from_ymd_opt(year, month, day) {
Some(date) => date,
None => panic!("test date should be valid"),
},
spent_usd: Decimal::new(cents, 2),
}
}
fn forecast_view_projected_fixture() -> ForecastView {
ForecastView {
generated_at: utc(2026, 6, 16, 12, 0),
no_api_usage: false,
spend: SpendForecast::Projected {
projected_month_usd: Decimal::new(3_750, 2),
spend_to_date_usd: Decimal::new(2_000, 2),
days_elapsed: 16,
days_in_month: 30,
},
daily_actuals: vec![
forecast_day(2026, 6, 5, 500),
forecast_day(2026, 6, 10, 1_000),
forecast_day(2026, 6, 14, 500),
],
quota_etas: vec![
QuotaEta {
tool: ProviderId::ClaudeCode,
kind: LimitKind::Weekly,
outcome: QuotaEtaOutcome::ProjectedHit {
at: utc(2026, 6, 19, 8, 0),
fraction: 0.85,
},
},
QuotaEta {
tool: ProviderId::ClaudeCode,
kind: LimitKind::FiveHour,
outcome: QuotaEtaOutcome::ResetsFirst {
resets_at: utc(2026, 6, 16, 14, 0),
fraction: 0.2,
},
},
QuotaEta {
tool: ProviderId::Codex,
kind: LimitKind::Weekly,
outcome: QuotaEtaOutcome::Unavailable {
reason: QuotaEtaUnavailable::ReadingNotProjectable,
},
},
],
}
}
fn forecast_view_insufficient_fixture() -> ForecastView {
ForecastView {
generated_at: utc(2026, 6, 2, 12, 0),
no_api_usage: false,
spend: SpendForecast::InsufficientData {
spend_to_date_usd: Decimal::new(500, 2),
days_elapsed: 2,
days_in_month: 30,
min_days: 3,
},
daily_actuals: vec![forecast_day(2026, 6, 1, 500)],
quota_etas: vec![QuotaEta {
tool: ProviderId::Codex,
kind: LimitKind::Weekly,
outcome: QuotaEtaOutcome::ResetsFirst {
resets_at: utc(2026, 6, 5, 0, 0),
fraction: 0.1,
},
}],
}
}
fn forecast_view_eta_unavailable_fixture() -> ForecastView {
ForecastView {
generated_at: utc(2026, 6, 16, 12, 0),
no_api_usage: true,
spend: SpendForecast::Projected {
projected_month_usd: Decimal::ZERO,
spend_to_date_usd: Decimal::ZERO,
days_elapsed: 16,
days_in_month: 30,
},
daily_actuals: Vec::new(),
quota_etas: vec![
QuotaEta {
tool: ProviderId::ClaudeCode,
kind: LimitKind::Weekly,
outcome: QuotaEtaOutcome::Unavailable {
reason: QuotaEtaUnavailable::ReadingNotProjectable,
},
},
QuotaEta {
tool: ProviderId::Codex,
kind: LimitKind::FiveHour,
outcome: QuotaEtaOutcome::Unavailable {
reason: QuotaEtaUnavailable::WindowJustStarted,
},
},
],
}
}
#[test]
fn snapshot_forecast_braille() {
insta::assert_snapshot!(render_forecast(
&forecast_view_projected_fixture(),
RenderOptions::braille(false)
));
}
#[test]
fn snapshot_forecast_ascii() {
insta::assert_snapshot!(render_forecast(
&forecast_view_projected_fixture(),
RenderOptions::ascii(false)
));
}
#[test]
fn snapshot_forecast_plain() {
insta::assert_snapshot!(render_forecast(
&forecast_view_projected_fixture(),
RenderOptions::plain()
));
}
#[test]
fn snapshot_forecast_insufficient_data_plain() {
insta::assert_snapshot!(render_forecast(
&forecast_view_insufficient_fixture(),
RenderOptions::plain()
));
}
#[test]
fn snapshot_forecast_eta_unavailable_plain() {
insta::assert_snapshot!(render_forecast(
&forecast_view_eta_unavailable_fixture(),
RenderOptions::plain()
));
}
#[test]
fn snapshot_forecast_no_usage_ascii() {
insta::assert_snapshot!(render_forecast(
&forecast_view_eta_unavailable_fixture(),
RenderOptions::ascii(false)
));
}
#[test]
fn forecast_projected_line_is_hedged_and_names_a_weekday() {
let output = render_forecast(&forecast_view_projected_fixture(), RenderOptions::plain());
assert!(
output.contains("projected ~$37.50 by Jun 30 (estimated)"),
"{output}"
);
assert!(
output.contains("spend so far ~$20.00 over 16 of 30 days (estimated)"),
"{output}"
);
assert!(
output.contains("claude code weekly: projected to hit ~Friday (UTC, estimated)"),
"{output}"
);
assert!(
output.contains("claude code 5h: resets before you hit it (estimated)"),
"{output}"
);
assert!(
output.contains("codex weekly: ETA unavailable (no fresh verified usage reading)"),
"{output}"
);
}
#[test]
fn forecast_below_floor_shows_insufficient_data_not_a_projection() {
let output = render_forecast(
&forecast_view_insufficient_fixture(),
RenderOptions::plain(),
);
assert!(
output.contains("insufficient data to project - 2 of 30 days elapsed (need 3+)"),
"{output}"
);
assert!(!output.contains("projected ~$"), "{output}");
}
#[test]
fn forecast_no_api_usage_renders_empty_state() {
let output = render_forecast(
&forecast_view_eta_unavailable_fixture(),
RenderOptions::plain(),
);
assert!(
output.contains("no API usage recorded - nothing to forecast yet"),
"{output}"
);
assert!(
output.contains("codex 5h: ETA unavailable (window just started)"),
"{output}"
);
let visual = render_forecast(
&forecast_view_eta_unavailable_fixture(),
RenderOptions::ascii(false),
);
assert!(
!visual.contains("$0.00"),
"no-usage visual header must carry no fabricated $0.00: {visual}"
);
assert!(
visual.contains("no API usage recorded - nothing to forecast yet"),
"{visual}"
);
}
#[test]
fn forecast_document_is_monochrome() {
for view in [
forecast_view_projected_fixture(),
forecast_view_insufficient_fixture(),
forecast_view_eta_unavailable_fixture(),
] {
let doc = render_forecast_document(&view, RenderOptions::braille(true));
for line in &doc.lines {
for span in &line.spans {
assert!(
!matches!(span.style, SemanticStyle::Warn | SemanticStyle::Critical),
"forecast tab must be monochrome (amber/red are reserved): {span:?}"
);
}
}
}
}
fn anomaly_date(year: i32, month: u32, day: u32) -> NaiveDate {
match NaiveDate::from_ymd_opt(year, month, day) {
Some(date) => date,
None => panic!("test date should be valid"),
}
}
fn anomalies_view_flagged_fixture() -> AnomaliesView {
AnomaliesView {
generated_at: utc(2026, 6, 16, 12, 0),
history_days: 12,
min_history_days: 7,
baseline_days: 14,
enough_history: true,
no_usage: false,
anomalies: vec![
Anomaly {
signal: AnomalySignal::SpendSpike {
date: anomaly_date(2026, 6, 16),
},
value: Decimal::new(2_000, 2),
baseline_median: Decimal::new(250, 2),
deviation: Decimal::new(1_750, 2),
magnitude: Some(Decimal::new(80, 1)),
baseline_days: 12,
},
Anomaly {
signal: AnomalySignal::ModelMixShift {
model: "gpt-5.5".to_string(),
},
value: Decimal::new(70, 2),
baseline_median: Decimal::new(20, 2),
deviation: Decimal::new(50, 2),
magnitude: Some(Decimal::new(35, 1)),
baseline_days: 12,
},
],
}
}
fn anomalies_view_collapse_fixture() -> AnomaliesView {
AnomaliesView {
generated_at: utc(2026, 6, 16, 12, 0),
history_days: 9,
min_history_days: 7,
baseline_days: 14,
enough_history: true,
no_usage: false,
anomalies: vec![Anomaly {
signal: AnomalySignal::ModelMixShift {
model: "claude-opus-4.7".to_string(),
},
value: Decimal::ZERO,
baseline_median: Decimal::new(50, 2),
deviation: Decimal::new(50, 2),
magnitude: Some(Decimal::ZERO),
baseline_days: 9,
}],
}
}
fn anomalies_view_clean_fixture() -> AnomaliesView {
AnomaliesView {
generated_at: utc(2026, 6, 16, 12, 0),
history_days: 10,
min_history_days: 7,
baseline_days: 14,
enough_history: true,
no_usage: false,
anomalies: Vec::new(),
}
}
fn anomalies_view_insufficient_fixture() -> AnomaliesView {
AnomaliesView {
generated_at: utc(2026, 6, 16, 12, 0),
history_days: 3,
min_history_days: 7,
baseline_days: 14,
enough_history: false,
no_usage: false,
anomalies: Vec::new(),
}
}
fn anomalies_view_no_usage_fixture() -> AnomaliesView {
AnomaliesView {
generated_at: utc(2026, 6, 16, 12, 0),
history_days: 0,
min_history_days: 7,
baseline_days: 14,
enough_history: false,
no_usage: true,
anomalies: Vec::new(),
}
}
#[test]
fn snapshot_anomalies_no_usage_plain() {
insta::assert_snapshot!(render_anomalies(
&anomalies_view_no_usage_fixture(),
RenderOptions::plain()
));
}
#[test]
fn anomalies_no_usage_is_a_transient_zero_state_not_a_permanent_no_coverage() {
for options in [RenderOptions::plain(), RenderOptions::ascii(false)] {
let zero = render_anomalies(&anomalies_view_no_usage_fixture(), options);
assert!(zero.contains("no usage recorded yet"), "{zero}");
assert!(zero.contains("need a few days of history"), "{zero}");
assert!(!zero.contains("no API-billed usage"), "{zero}");
assert!(!zero.contains("API-lane only"), "{zero}");
assert!(!zero.contains("0 of 7 days"), "{zero}");
let flagged = render_anomalies(&anomalies_view_flagged_fixture(), options);
assert!(!flagged.contains("no usage recorded yet"), "{flagged}");
assert!(flagged.contains("model mix shift"), "{flagged}");
}
}
#[test]
fn snapshot_anomalies_braille() {
insta::assert_snapshot!(render_anomalies(
&anomalies_view_flagged_fixture(),
RenderOptions::braille(false)
));
}
#[test]
fn snapshot_anomalies_ascii() {
insta::assert_snapshot!(render_anomalies(
&anomalies_view_flagged_fixture(),
RenderOptions::ascii(false)
));
}
#[test]
fn snapshot_anomalies_plain() {
insta::assert_snapshot!(render_anomalies(
&anomalies_view_flagged_fixture(),
RenderOptions::plain()
));
}
#[test]
fn snapshot_anomalies_clean_plain() {
insta::assert_snapshot!(render_anomalies(
&anomalies_view_clean_fixture(),
RenderOptions::plain()
));
}
#[test]
fn snapshot_anomalies_insufficient_history_plain() {
insta::assert_snapshot!(render_anomalies(
&anomalies_view_insufficient_fixture(),
RenderOptions::plain()
));
}
#[test]
fn anomalies_callouts_are_hedged_and_name_the_compared_window() {
let output = render_anomalies(&anomalies_view_flagged_fixture(), RenderOptions::plain());
assert!(
output.contains(
"* spend spike: ~$20.00 on Jun 16, ~8.0x your ~$2.50 12-day median (estimated)"
),
"{output}"
);
assert!(
output.contains(
"* model mix shift: gpt-5.5 at 70% of tokens, ~3.5x your 20% 12-day median (estimated)"
),
"{output}"
);
let braille = render_anomalies(
&anomalies_view_flagged_fixture(),
RenderOptions::braille(false),
);
assert!(braille.contains("2 flagged"), "{braille}");
}
#[test]
fn anomalies_collapse_reads_down_from_the_norm() {
let output = render_anomalies(&anomalies_view_collapse_fixture(), RenderOptions::plain());
assert!(
output.contains(
"* model mix shift: claude-opus-4.7 at 0% of tokens, down from your 50% 9-day median (estimated)"
),
"{output}"
);
}
fn anomalies_view_suppressed_multiples_fixture() -> AnomaliesView {
AnomaliesView {
generated_at: utc(2026, 6, 16, 12, 0),
history_days: 8,
min_history_days: 7,
baseline_days: 14,
enough_history: true,
no_usage: false,
anomalies: vec![
Anomaly {
signal: AnomalySignal::SpendSpike {
date: anomaly_date(2026, 6, 16),
},
value: Decimal::new(500, 2),
baseline_median: Decimal::ZERO,
deviation: Decimal::new(500, 2),
magnitude: None,
baseline_days: 8,
},
Anomaly {
signal: AnomalySignal::ModelMixShift {
model: "gpt-5.5".to_string(),
},
value: Decimal::new(45, 2),
baseline_median: Decimal::new(3, 3), deviation: Decimal::new(447, 3),
magnitude: Some(Decimal::from(150)),
baseline_days: 8,
},
Anomaly {
signal: AnomalySignal::SpendSpike {
date: anomaly_date(2026, 6, 15),
},
value: Decimal::new(5_120, 2),
baseline_median: Decimal::new(5_000, 2),
deviation: Decimal::new(120, 2),
magnitude: Some(Decimal::new(10, 1)), baseline_days: 8,
},
],
}
}
#[test]
fn anomalies_suppress_misleading_multiples() {
let output = render_anomalies(
&anomalies_view_suppressed_multiples_fixture(),
RenderOptions::plain(),
);
assert!(
output.contains(
"* spend spike: ~$5.00 on Jun 16, well above your ~$0.00 8-day median (estimated)"
),
"magnitude None spend spike must read 'well above', not a multiple: {output}"
);
assert!(
output.contains(
"* model mix shift: gpt-5.5 at 45% of tokens, up from your 0% 8-day median (estimated)"
),
"a surge off a displayed-0% median must read 'up from', never '~150.0x your 0%': {output}"
);
assert!(
output.contains("* spend spike: ~$51.20 on Jun 15, well above your ~$50.00 8-day median (estimated)"),
"a ~1.0x bump must read 'well above', not the misleading '~1.0x': {output}"
);
assert!(
!output.contains("x your 0%"),
"no multiple over a displayed 0%: {output}"
);
assert!(
!output.contains("x your ~$0.00"),
"no multiple over a displayed $0.00: {output}"
);
assert!(
!output.contains("~1.0x"),
"no bare ~1.0x multiple: {output}"
);
}
#[test]
fn anomalies_clean_and_insufficient_states_are_honest() {
let clean = render_anomalies(&anomalies_view_clean_fixture(), RenderOptions::plain());
assert!(
clean.contains("no anomalies - usage in line with your 10-day norm (estimated)"),
"{clean}"
);
let thin = render_anomalies(
&anomalies_view_insufficient_fixture(),
RenderOptions::plain(),
);
assert!(
thin.contains("not enough history yet - 3 of 7 days (estimated)"),
"{thin}"
);
assert!(thin.contains("quota burn-rate anomalies need multi-day quota history"));
}
#[test]
fn anomalies_document_is_monochrome() {
for view in [
anomalies_view_flagged_fixture(),
anomalies_view_collapse_fixture(),
anomalies_view_suppressed_multiples_fixture(),
anomalies_view_clean_fixture(),
anomalies_view_insufficient_fixture(),
] {
let doc = render_anomalies_document(&view, RenderOptions::braille(true));
for line in &doc.lines {
for span in &line.spans {
assert!(
!matches!(span.style, SemanticStyle::Warn | SemanticStyle::Critical),
"anomalies tab must be monochrome (amber/red are reserved): {span:?}"
);
}
}
}
}
#[cfg(feature = "connect")]
#[test]
fn connection_lane_renders_state_without_key_material_and_folds_ascii() {
use costroid_core::GEMINI_UNAVAILABLE_MESSAGE;
let connections = vec![
ConnectionEntry {
vendor: "anthropic".to_string(),
state: ConnectionState::Connected {
org: Some("Acme (org-123)".to_string()),
},
},
ConnectionEntry {
vendor: "openai".to_string(),
state: ConnectionState::NotConnected,
},
ConnectionEntry {
vendor: "gemini".to_string(),
state: ConnectionState::Unavailable(GEMINI_UNAVAILABLE_MESSAGE.to_string()),
},
];
let capabilities = provider_capabilities_fixture();
let statuses = provider_statuses_for_tab();
let braille_opts = RenderOptions::braille(false);
let mut braille = render_providers_document(&capabilities, &statuses, braille_opts);
push_provider_connection_lane(&mut braille, &connections, braille_opts);
let braille = braille.render(braille_opts);
assert!(braille.contains("connections (your own usage API keys)"));
assert!(braille.contains("connected — organization Acme (org-123)"));
assert!(braille.contains("openai not connected"));
assert!(braille.contains(GEMINI_UNAVAILABLE_MESSAGE));
for options in [RenderOptions::plain(), RenderOptions::ascii(false)] {
let mut doc = render_providers_document(&capabilities, &statuses, options);
push_provider_connection_lane(&mut doc, &connections, options);
let rendered = doc.render(options);
assert!(
rendered.is_ascii(),
"connection lane must be pure ASCII: {rendered}"
);
assert!(rendered.contains("connected - organization Acme (org-123)"));
assert!(rendered.contains("unavailable - no sanctioned static-key usage API"));
}
}
#[test]
fn snapshot_statusline_braille_with_ansi() {
insta::assert_snapshot!(render_statusline(
&priced_now(),
RenderOptions::braille(true)
));
}
#[test]
fn snapshot_statusline_braille() {
insta::assert_snapshot!(render_statusline(
&priced_now(),
RenderOptions::braille(false)
));
}
#[test]
fn snapshot_statusline_ascii() {
insta::assert_snapshot!(render_statusline(
&priced_now(),
RenderOptions::ascii(false)
));
}
#[test]
fn snapshot_statusline_plain() {
insta::assert_snapshot!(render_statusline(&priced_now(), RenderOptions::plain()));
}
#[test]
fn snapshot_statusline_plain_empty_api() {
insta::assert_snapshot!(render_statusline(
&subscription_only_now(),
RenderOptions::plain()
));
}
fn frontier_event(
model: &str,
access: costroid_providers::AccessPath,
input: u64,
output: u64,
) -> costroid_providers::UsageEvent {
costroid_providers::UsageEvent {
tool: costroid_providers::ProviderId::Codex,
model: model.to_string(),
timestamp: utc(2026, 6, 2, 9, 0),
input_tokens: input,
output_tokens: output,
cache_read_tokens: 0,
cache_write_tokens: 0,
project: Some("/work/proj".to_string()),
access_path: access,
}
}
fn bench_view_for(events: &[costroid_providers::UsageEvent]) -> BenchView {
let focus_rows = match costroid_core::focus_records_from_usage(events) {
Ok(rows) => rows,
Err(err) => panic!("events should price: {err}"),
};
let snapshot = costroid_core::EngineSnapshot {
generated_at: utc(2026, 6, 2, 9, 0),
usage_events: Vec::new(),
focus_rows,
limit_windows: Vec::new(),
providers: Vec::new(),
capabilities: Vec::new(),
};
match costroid_core::bench_view(&snapshot) {
Ok(view) => view,
Err(err) => panic!("bench view should build: {err}"),
}
}
fn used_gpt() -> BenchView {
bench_view_for(&[frontier_event(
"gpt-5.5",
costroid_providers::AccessPath::Api,
1_000_000,
1_000_000,
)])
}
#[test]
fn snapshot_frontier_plain() {
insta::assert_snapshot!(render_frontier(&used_gpt(), RenderOptions::plain()));
}
#[test]
fn snapshot_frontier_plain_no_api() {
insta::assert_snapshot!(render_frontier(
&bench_view_for(&[]),
RenderOptions::plain()
));
}
#[test]
fn snapshot_frontier_braille() {
insta::assert_snapshot!(render_frontier(&used_gpt(), RenderOptions::braille(true)));
}
#[test]
fn plain_frontier_has_no_ansi() {
let output = render_frontier(&used_gpt(), RenderOptions::plain());
assert!(
!output.contains('\u{1b}'),
"plain frontier must not contain ANSI escapes: {output}"
);
}
#[test]
fn braille_scatter_maps_known_points() {
let top_left = PlotPoint {
x: 0.0,
y: 100.0,
on_frontier: false,
};
let bottom_right = PlotPoint {
x: 10.0,
y: 0.0,
on_frontier: false,
};
assert_eq!(
braille_scatter(&[top_left, bottom_right], 2, 1, false),
vec!["⠁⢀".to_string()]
);
let same_x = vec![
PlotPoint {
x: 5.0,
y: 1.0,
on_frontier: false,
},
PlotPoint {
x: 5.0,
y: 2.0,
on_frontier: false,
},
];
let rows = braille_scatter(&same_x, 3, 2, false);
assert_eq!(rows.len(), 2);
assert!(rows.iter().all(|row| row.chars().count() == 3));
}
#[test]
fn frontier_document_is_monochrome() {
let doc = render_frontier_document(&used_gpt(), RenderOptions::braille(true));
for line in &doc.lines {
for span in &line.spans {
assert!(
!matches!(span.style, SemanticStyle::Warn | SemanticStyle::Critical),
"frontier must be monochrome (amber/red are reserved): {span:?}"
);
}
}
}
#[test]
fn frontier_insight_uses_advisory_voice() {
let line = frontier_insight_line(&used_gpt());
assert!(
line.starts_with('◆'),
"insight should use the ◆ marker: {line}"
);
assert!(
line.contains("(estimated)") || line.contains('~'),
"insight should hedge: {line}"
);
let lower = line.to_lowercase();
assert!(
!lower.contains("switch to"),
"insight must not prescribe: {line}"
);
assert!(
!lower.contains("you should"),
"insight must not prescribe: {line}"
);
assert!(plain_frontier_insight_line(&used_gpt()).starts_with("insight:"));
}
#[test]
fn frontier_no_api_states_nothing_to_compare() {
let output = render_frontier(&bench_view_for(&[]), RenderOptions::plain());
assert!(output.contains("no API-billed usage to compare"));
assert!(output.contains("dominated by gpt-5.5"));
}
#[track_caller]
fn dec(literal: &str) -> Decimal {
match Decimal::from_str_exact(literal) {
Ok(value) => value,
Err(err) => panic!("bad fixture decimal {literal:?}: {err:?}"),
}
}
fn usd(literal: &str) -> UsdAmount {
UsdAmount::from_usd(dec(literal))
}
fn naive(year: i32, month: u32, day: u32) -> NaiveDate {
match NaiveDate::from_ymd_opt(year, month, day) {
Some(date) => date,
None => panic!("bad fixture date"),
}
}
fn local_with(entries: &[(NaiveDate, &str, &str)]) -> LocalCostEstimate {
let mut estimate = LocalCostEstimate::new();
for (date, model, dollars) in entries {
if let Err(err) = estimate.add(*date, model, usd(dollars)) {
panic!("fixture add failed: {err:?}");
}
}
estimate
}
fn available(days: Vec<VendorCostDay>, caveats: CostReportCaveats) -> CostReportOutcome {
CostReportOutcome::Available(VendorCostReport { days, caveats })
}
fn vendor_day(date: NaiveDate, items: Vec<CostLineItem>) -> VendorCostDay {
match VendorCostDay::from_line_items(date, items) {
Ok(day) => day,
Err(err) => panic!("fixture vendor day failed: {err:?}"),
}
}
fn line_item(model: &str, dollars: &str, confidence: AmountConfidence) -> CostLineItem {
CostLineItem {
label: model.to_string(),
amount: usd(dollars),
model: Some(model.to_string()),
cost_type: Some("tokens".to_string()),
service_tier: Some("standard".to_string()),
confidence,
}
}
fn anthropic_reconciliation() -> CostReconciliation {
let d_covered = naive(2026, 6, 14);
let d_uncovered = naive(2026, 6, 13);
let local = local_with(&[
(d_covered, "claude-opus-4-8", "3.00"),
(d_covered, "claude-sonnet-4-6", "1.20"),
(d_uncovered, "claude-opus-4-8", "1.00"),
]);
let outcome = available(
vec![vendor_day(
d_covered,
vec![
line_item("claude-opus-4-8", "3.00", AmountConfidence::Exact),
line_item("claude-sonnet-4-6", "1.00", AmountConfidence::Exact),
line_item("claude-ghost-9", "0.50", AmountConfidence::Exact),
],
)],
CostReportCaveats {
priority_tier_absent: true,
per_model_derived_best_effort: false,
},
);
reconcile_cost(&local, &outcome)
}
fn openai_reconciliation() -> CostReconciliation {
let day = naive(2026, 6, 14);
let local = local_with(&[(day, "gpt-5.5", "1.00")]);
let outcome = available(
vec![vendor_day(
day,
vec![line_item(
"gpt-5.5",
"1.50",
AmountConfidence::DerivedBestEffort,
)],
)],
CostReportCaveats {
priority_tier_absent: false,
per_model_derived_best_effort: true,
},
);
reconcile_cost(&local, &outcome)
}
fn not_connected_reconciliation() -> CostReconciliation {
let local = local_with(&[(naive(2026, 6, 14), "claude-opus-4-8", "2.00")]);
reconcile_cost(
&local,
&CostReportOutcome::Unavailable(VendorReportUnavailable::NotConnected),
)
}
fn gemini_reconciliation() -> CostReconciliation {
reconcile_cost(
&LocalCostEstimate::new(),
&CostReportOutcome::Unavailable(VendorReportUnavailable::NoSanctionedStaticKeyApi),
)
}
fn mixed_states_reconciliation() -> CostReconciliation {
let d = naive(2026, 6, 14);
let local = local_with(&[
(d, "billed-zero", "1.00"),
(d, "unattributed", "0.40"),
(d, "sub-cent", "0.001"),
]);
let outcome = available(
vec![vendor_day(
d,
vec![
line_item("billed-zero", "0", AmountConfidence::Exact),
line_item("sub-cent", "0.002", AmountConfidence::Exact),
],
)],
CostReportCaveats::default(),
);
reconcile_cost(&local, &outcome)
}
const WINDOW: &str = "2026-06-08 to 2026-06-14 (UTC, completed days)";
#[test]
fn snapshot_reconcile_anthropic_braille() {
insta::assert_snapshot!(render_reconciliation(
"anthropic",
WINDOW,
&anthropic_reconciliation(),
RenderOptions::braille(false),
));
}
#[test]
fn snapshot_reconcile_anthropic_ascii() {
insta::assert_snapshot!(render_reconciliation(
"anthropic",
WINDOW,
&anthropic_reconciliation(),
RenderOptions::ascii(false),
));
}
#[test]
fn snapshot_reconcile_anthropic_plain() {
insta::assert_snapshot!(render_reconciliation(
"anthropic",
WINDOW,
&anthropic_reconciliation(),
RenderOptions::plain(),
));
}
#[test]
fn snapshot_reconcile_openai_plain() {
insta::assert_snapshot!(render_reconciliation(
"openai",
WINDOW,
&openai_reconciliation(),
RenderOptions::plain(),
));
}
#[test]
fn snapshot_reconcile_not_connected_plain() {
insta::assert_snapshot!(render_reconciliation(
"openai",
WINDOW,
¬_connected_reconciliation(),
RenderOptions::plain(),
));
}
#[test]
fn snapshot_reconcile_gemini_plain() {
insta::assert_snapshot!(render_reconciliation(
"gemini",
WINDOW,
&gemini_reconciliation(),
RenderOptions::plain(),
));
}
#[test]
fn snapshot_reconcile_mixed_states_plain() {
insta::assert_snapshot!(render_reconciliation(
"openai",
WINDOW,
&mixed_states_reconciliation(),
RenderOptions::plain(),
));
}
#[test]
fn reconcile_renders_the_zero_billed_unattributed_and_subcent_states() {
let output = render_reconciliation(
"openai",
WINDOW,
&mixed_states_reconciliation(),
RenderOptions::plain(),
);
assert!(
output.contains("+$1.00 over (vs $0 billed)"),
"zero-billed model: {output}"
);
assert!(output.contains("not attributed by the vendor"));
assert!(
output.contains("<$0.01 under (-50.0%)"),
"sub-cent variance: {output}"
);
assert!(
!output.contains("$0.00 under") && !output.contains("$0.00 over"),
"no misleading $0.00 direction cell: {output}"
);
}
#[test]
fn reconcile_renders_honest_states_in_plain() {
let output = render_reconciliation(
"anthropic",
WINDOW,
&anthropic_reconciliation(),
RenderOptions::plain(),
);
assert!(output.contains("est ~$4.20"));
assert!(
output.contains("over"),
"expected an over-estimate row: {output}"
);
assert!(output.contains("exact"));
assert!(output.contains("report doesn't cover this day"));
assert!(!output.contains("$0.00 under") && !output.contains("inv $0.00 "));
assert!(output.contains("claude-ghost-9"));
assert!(output.contains("est ~$0.00"));
assert!(output.contains("Priority-Tier spend isn't in this report"));
}
#[test]
fn reconcile_openai_marks_and_footnotes_best_effort() {
let output = render_reconciliation(
"openai",
WINDOW,
&openai_reconciliation(),
RenderOptions::plain(),
);
assert!(output.contains("under"));
assert!(output.contains(" *"));
assert!(output.contains("OpenAI per-model figures are best-effort"));
}
#[test]
fn reconcile_not_connected_surfaces_estimate_and_remediation() {
let output = render_reconciliation(
"openai",
WINDOW,
¬_connected_reconciliation(),
RenderOptions::plain(),
);
assert!(output.contains("vendor invoice unavailable: connect openai first"));
assert!(output.contains("est ~$2.00"));
assert!(!output.contains("over") && !output.contains("under"));
}
#[test]
fn reconcile_gemini_uses_the_pinned_unavailable_message() {
let output = render_reconciliation(
"gemini",
WINDOW,
&gemini_reconciliation(),
RenderOptions::plain(),
);
assert!(output.contains(&costroid_core::GEMINI_UNAVAILABLE_MESSAGE.replace('—', "-")));
assert!(output.contains("No local usage recorded"));
}
#[test]
fn reconcile_ascii_and_plain_output_is_pure_ascii() {
for recon in [
anthropic_reconciliation(),
openai_reconciliation(),
not_connected_reconciliation(),
gemini_reconciliation(),
mixed_states_reconciliation(),
] {
let ascii =
render_reconciliation("anthropic", WINDOW, &recon, RenderOptions::ascii(false));
assert!(
ascii.is_ascii(),
"ascii reconcile must be pure ASCII: {ascii}"
);
let plain = render_reconciliation("anthropic", WINDOW, &recon, RenderOptions::plain());
assert!(
plain.is_ascii(),
"plain reconcile must be pure ASCII: {plain}"
);
assert!(
!plain.contains('\u{1b}'),
"plain reconcile must carry no ANSI escapes: {plain}"
);
}
}
}