use serde::Serialize;
use crate::metering::fmt_usd;
use crate::metering::pricing::ChargeStatus;
use crate::metering::store::{RateMetrics, RequestRow, SpendSummary, TimeWindow};
use crate::output::CliReport;
use crate::output::human::{Health, Human, Table};
const HEADERS: [&str; 8] = [
"time", "model", "provider", "in", "out", "cost", "latency", "status",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
Live,
HistoryOnly,
Empty,
}
#[derive(Debug, Clone, Serialize)]
pub struct DaemonView {
pub pid: u32,
pub listen: String,
pub models: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct RequestView {
pub request_id: String,
pub created_at: String,
pub model: String,
pub provider: String,
pub prompt_tokens: i64,
pub completion_tokens: i64,
pub cache_read_tokens: i64,
pub cache_write_tokens: i64,
pub charge_micro_usd: i64,
pub charge_status: &'static str,
pub latency_ms: i64,
pub error: Option<String>,
pub episode_id: Option<String>,
}
impl From<RequestRow> for RequestView {
fn from(row: RequestRow) -> Self {
Self {
request_id: row.request_id,
created_at: row.created_at,
model: row.model_id,
provider: row.provider_id,
prompt_tokens: row.prompt_tokens,
completion_tokens: row.completion_tokens,
cache_read_tokens: row.cache_read_tokens,
cache_write_tokens: row.cache_write_tokens,
charge_micro_usd: row.estimated_charge_micro_usd,
charge_status: row.charge_status.as_str(),
latency_ms: row.latency_ms,
error: row.error,
episode_id: row.episode_id,
}
}
}
impl RequestView {
fn cells(&self) -> [String; 8] {
[
clock(&self.created_at),
self.model.clone(),
self.provider.clone(),
tokens(self.prompt_tokens),
tokens(self.completion_tokens),
charge(self.charge_micro_usd, self.charge_status),
latency(self.latency_ms),
status(self.error.as_deref()),
]
}
}
#[derive(Debug, Clone, Serialize)]
pub struct RequestsReport {
pub mode: Mode,
pub daemon: Option<DaemonView>,
pub window: String,
pub scope: &'static str,
pub spend_micro_usd: Option<u64>,
pub requests: u64,
pub unpriced_requests: u64,
pub requests_per_minute: f64,
pub tokens_per_minute: f64,
pub rows: Vec<RequestView>,
}
const SCOPE: &str = "all callers";
impl RequestsReport {
pub fn new(
daemon: Option<DaemonView>,
window: TimeWindow,
summary: SpendSummary,
rate: RateMetrics,
rows: Vec<RequestRow>,
) -> Self {
let mode = match (daemon.is_some(), rows.is_empty()) {
(true, _) => Mode::Live,
(false, false) => Mode::HistoryOnly,
(false, true) => Mode::Empty,
};
Self {
mode,
daemon,
window: window_label(window).to_string(),
scope: SCOPE,
spend_micro_usd: (summary.unpriced < summary.requests || summary.requests == 0)
.then_some(summary.spend_micro_usd),
requests: summary.requests,
unpriced_requests: summary.unpriced,
requests_per_minute: rate.requests_per_minute,
tokens_per_minute: rate.tokens_per_minute,
rows: rows.into_iter().map(RequestView::from).collect(),
}
}
fn health(&self) -> Health {
match self.mode {
Mode::Live => Health::Up,
Mode::HistoryOnly | Mode::Empty => Health::Down,
}
}
fn headline(&self) -> String {
match (&self.daemon, self.mode) {
(Some(d), _) => format!("live · pid {} · {} · {} models", d.pid, d.listen, d.models),
(None, Mode::HistoryOnly) => "history only — daemon not running".to_string(),
(None, _) => "nothing recorded yet — try bitrouter serve".to_string(),
}
}
fn rollup(&self) -> String {
let spend = match self.spend_micro_usd {
Some(micro_usd) => fmt_usd(micro_usd),
None => "unreported".to_string(),
};
let mut line = format!(
"{} {spend} · {} req · {:.1} req/min · {} tok/min · {}",
self.window,
self.requests,
self.requests_per_minute,
tokens(self.tokens_per_minute as i64),
self.scope,
);
if self.requests == 0 {
line.push_str(" · no requests in this window");
}
line
}
fn caveat(&self) -> Option<String> {
match (self.unpriced_requests, self.spend_micro_usd) {
(0, _) => None,
(n, None) => Some(format!(
"no charge evidence for any of these {n} requests — the daemon \
recorded them but could not price them"
)),
(n, Some(_)) => Some(format!(
"{n} of {} requests have no charge evidence; the total above is \
a floor, not a price",
self.requests
)),
}
}
}
impl CliReport for RequestsReport {
fn render(&self, h: &mut Human<'_>) -> std::io::Result<()> {
h.status_block(self.health(), &self.headline())?;
h.line(&self.rollup())?;
if let Some(caveat) = self.caveat() {
h.note(&caveat)?;
}
if self.rows.is_empty() {
return Ok(());
}
h.blank()?;
let mut table = Table::new(HEADERS);
for row in &self.rows {
table.push(row.cells());
}
h.table(&table)
}
}
fn window_label(window: TimeWindow) -> &'static str {
match window {
TimeWindow::LastMinute => "last minute",
TimeWindow::LastHour => "last hour",
TimeWindow::Today => "today",
TimeWindow::ThisWeek => "this week",
TimeWindow::ThisMonth => "this month",
TimeWindow::Custom { .. } => "window",
}
}
fn clock(created_at: &str) -> String {
match chrono::DateTime::parse_from_rfc3339(created_at) {
Ok(t) => t
.with_timezone(&chrono::Local)
.format("%H:%M:%S")
.to_string(),
Err(_) => created_at.to_string(),
}
}
fn tokens(n: i64) -> String {
let n = n.max(0);
if n >= 1000 {
format!("{:.1}k", n as f64 / 1000.0)
} else {
n.to_string()
}
}
fn charge(micro_usd: i64, status: &str) -> String {
match ChargeStatus::from_persisted(status) {
ChargeStatus::Computed if micro_usd > 0 => fmt_usd(micro_usd as u64),
ChargeStatus::Computed | ChargeStatus::NotCharged => "—".to_string(),
ChargeStatus::Unknown | ChargeStatus::LegacyUnknown => "?".to_string(),
}
}
fn latency(ms: i64) -> String {
if ms <= 0 {
"—".to_string()
} else if ms < 1000 {
format!("{ms}ms")
} else {
format!("{:.1}s", ms as f64 / 1000.0)
}
}
const ERROR_CHARS: usize = 24;
fn status(error: Option<&str>) -> String {
match error {
None => "ok".to_string(),
Some(e) => {
let one_line = e.replace('\n', " ");
let mut short: String = one_line.chars().take(ERROR_CHARS).collect();
if one_line.chars().count() > ERROR_CHARS {
short.push('…');
}
short
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::{Format, Output};
fn row() -> RequestRow {
RequestRow {
request_id: "r1".into(),
created_at: "2026-08-10T12:00:00Z".into(),
model_id: "gpt-5".into(),
provider_id: "openai".into(),
prompt_tokens: 12_431,
completion_tokens: 891,
cache_read_tokens: 0,
cache_write_tokens: 0,
estimated_charge_micro_usd: 42_000,
latency_ms: 1_800,
error: None,
charge_status: ChargeStatus::Computed,
episode_id: None,
}
}
fn unpriced() -> RequestRow {
RequestRow {
estimated_charge_micro_usd: 0,
charge_status: ChargeStatus::LegacyUnknown,
..row()
}
}
fn daemon() -> DaemonView {
DaemonView {
pid: 4412,
listen: "127.0.0.1:4356".to_string(),
models: 47,
}
}
fn report(daemon: Option<DaemonView>, rows: Vec<RequestRow>) -> RequestsReport {
RequestsReport::new(
daemon,
TimeWindow::Today,
SpendSummary::default(),
RateMetrics::default(),
rows,
)
}
fn spend(priced: u64, unpriced: u64, micro_usd: u64) -> RequestsReport {
RequestsReport::new(
None,
TimeWindow::Today,
SpendSummary {
spend_micro_usd: micro_usd,
requests: priced + unpriced,
unpriced,
},
RateMetrics::default(),
Vec::new(),
)
}
fn human(report: &RequestsReport) -> String {
String::from_utf8(Output::new(Format::Human).render_to_vec(report))
.unwrap_or_else(|e| format!("not utf-8: {e}"))
}
fn json(report: &RequestsReport) -> serde_json::Value {
let bytes = Output::new(Format::Json).render_to_vec(report);
serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null)
}
#[test]
fn a_row_renders_every_column_the_table_promises() {
let cells = RequestView::from(row()).cells();
assert_eq!(cells[1], "gpt-5");
assert_eq!(cells[2], "openai");
assert_eq!(cells[3], "12.4k");
assert_eq!(cells[4], "891");
assert_eq!(cells[5], "$0.04");
assert_eq!(cells[6], "1.8s");
assert_eq!(cells[7], "ok");
}
#[test]
fn an_uncharged_request_shows_a_dash_not_a_zero() {
let mut r = row();
r.estimated_charge_micro_usd = 0;
r.latency_ms = 0;
r.charge_status = ChargeStatus::NotCharged;
let cells = RequestView::from(r).cells();
assert_eq!(cells[5], "—");
assert_eq!(cells[6], "—");
}
#[test]
fn an_unpriced_request_is_not_rendered_as_free() {
let cells = RequestView::from(unpriced()).cells();
assert_eq!(cells[5], "?", "unknown evidence must not read as a price");
assert_ne!(cells[5], "—");
assert_ne!(cells[5], "$0.00");
}
#[test]
fn a_window_with_no_charge_evidence_reports_unreported() {
let r = spend(0, 264, 0);
assert!(r.rollup().contains("unreported"), "{}", r.rollup());
assert!(!r.rollup().contains("$0.00"), "{}", r.rollup());
assert_eq!(
json(&r)["spend_micro_usd"],
serde_json::Value::Null,
"a zero here would be believed by an agent that did not check"
);
assert_eq!(json(&r)["unpriced_requests"], 264);
}
#[test]
fn a_partial_total_says_it_is_a_floor() {
let r = spend(3, 2, 110_450);
assert!(r.rollup().contains("$0.11"), "{}", r.rollup());
let caveat = r.caveat().unwrap_or_default();
assert!(caveat.contains("2 of 5"), "{caveat}");
assert!(caveat.contains("floor"), "{caveat}");
assert!(
human(&r).contains("floor"),
"the caveat must reach the page"
);
}
#[test]
fn a_fully_priced_window_has_no_caveat() {
assert!(spend(5, 0, 110_450).caveat().is_none());
}
#[test]
fn a_row_carries_its_episode_id_when_one_exists() {
let mut r = row();
r.episode_id = Some("ep_7f3a".into());
let value = json(&report(None, vec![r]));
assert_eq!(value["rows"][0]["episode_id"], "ep_7f3a");
let without = json(&report(None, vec![row()]));
assert_eq!(without["rows"][0]["episode_id"], serde_json::Value::Null);
}
#[test]
fn each_row_carries_its_charge_evidence() {
let value = json(&report(None, vec![row(), unpriced()]));
assert_eq!(value["rows"][0]["charge_status"], "computed");
assert_eq!(value["rows"][1]["charge_status"], "legacy_unknown");
}
#[test]
fn a_long_upstream_error_cannot_push_other_columns_off_screen() {
let mut r = row();
r.error =
Some("upstream refused the request\nwith a very long multi-line explanation".into());
let cells = RequestView::from(r).cells();
assert!(!cells[7].contains('\n'), "newlines would break the row");
assert!(cells[7].chars().count() <= ERROR_CHARS + 1, "{}", cells[7]);
}
#[test]
fn a_dead_daemon_with_history_is_not_the_same_as_a_fresh_install() {
let history = report(None, vec![row()]);
assert_eq!(history.mode, Mode::HistoryOnly);
assert!(history.headline().contains("history only"));
let fresh = report(None, Vec::new());
assert_eq!(fresh.mode, Mode::Empty);
assert!(
fresh.headline().contains("bitrouter serve"),
"an empty view must say what to do, not just show nothing"
);
}
#[test]
fn a_live_daemon_with_no_traffic_still_reads_as_live() {
let idle = report(Some(daemon()), Vec::new());
assert_eq!(idle.mode, Mode::Live);
let line = idle.headline();
assert!(line.contains("live"), "{line}");
assert!(line.contains("pid 4412"), "{line}");
assert!(line.contains("47 models"), "{line}");
}
#[test]
fn an_empty_window_says_so_rather_than_showing_a_bare_zero() {
let empty = report(None, Vec::new());
assert!(empty.rollup().contains("no requests"), "{}", empty.rollup());
assert!(empty.caveat().is_none());
}
#[test]
fn the_rollup_states_whose_spend_it_is() {
let r = report(Some(daemon()), vec![row()]);
assert!(r.rollup().contains("all callers"), "{}", r.rollup());
assert_eq!(json(&r)["scope"], "all callers");
}
#[test]
fn the_human_view_is_a_padded_table_with_no_trailing_whitespace() {
let text = human(&report(None, vec![row()]));
assert!(text.contains("provider"), "header row present");
assert!(text.contains("openai"), "data row present");
for line in text.lines() {
assert_eq!(line, line.trim_end(), "output must not pad line ends");
}
}
#[test]
fn the_human_view_still_reports_state_when_there_is_nothing_to_list() {
let text = human(&report(None, Vec::new()));
assert!(text.contains("nothing recorded yet"), "{text}");
assert!(
!text.contains("provider"),
"an empty table header is noise, not information"
);
}
#[test]
fn the_json_view_carries_the_rows_an_agent_needs() {
let value = json(&report(Some(daemon()), vec![row()]));
assert_eq!(value["mode"], "live");
assert_eq!(value["daemon"]["pid"], 4412);
assert_eq!(value["rows"][0]["model"], "gpt-5");
assert_eq!(value["rows"][0]["provider"], "openai");
assert_eq!(value["rows"][0]["charge_micro_usd"], 42_000);
assert_eq!(value["rows"][0]["request_id"], "r1");
}
#[test]
fn an_unparseable_timestamp_survives_to_the_cell() {
let mut r = row();
r.created_at = "not a timestamp".into();
assert_eq!(RequestView::from(r).cells()[0], "not a timestamp");
}
}