use chrono::Datelike;
use serde::{Deserialize, 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; 9] = [
"time", "router", "model", "provider", "in", "out", "cost", "latency", "status",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
Live,
HistoryOnly,
Empty,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct DaemonView {
pub pid: u32,
pub listen: String,
pub models: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct RequestView {
pub request_id: String,
pub created_at: String,
#[serde(default)]
pub router_id: Option<String>,
#[serde(default)]
pub binding_digest: Option<String>,
#[serde(default)]
pub original_selector: Option<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: String,
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,
router_id: row.router_id,
binding_digest: row.binding_digest,
original_selector: row.original_selector,
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().to_string(),
latency_ms: row.latency_ms,
error: row.error,
episode_id: row.episode_id,
}
}
}
impl RequestView {
pub fn display_cells(&self) -> [String; 9] {
[
clock(&self.created_at),
self.router_id.clone().unwrap_or_else(|| "—".to_string()),
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()),
]
}
#[cfg(test)]
fn cells(&self) -> [String; 9] {
self.display_cells()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct RequestFilterView {
pub since: String,
pub until: String,
pub model: Option<String>,
pub provider: Option<String>,
}
impl RequestFilterView {
pub fn new(
since: String,
until: String,
model: Option<String>,
provider: Option<String>,
) -> Self {
Self {
since,
until,
model,
provider,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MeteringComponentAvailability {
Available,
Unavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct MeteringAvailability {
pub summary: MeteringComponentAvailability,
pub rate: MeteringComponentAvailability,
pub rows: MeteringComponentAvailability,
}
impl MeteringAvailability {
pub const fn available() -> Self {
Self {
summary: MeteringComponentAvailability::Available,
rate: MeteringComponentAvailability::Available,
rows: MeteringComponentAvailability::Available,
}
}
pub const fn unavailable() -> Self {
Self {
summary: MeteringComponentAvailability::Unavailable,
rate: MeteringComponentAvailability::Unavailable,
rows: MeteringComponentAvailability::Unavailable,
}
}
}
#[derive(Debug, Clone)]
pub struct RequestReportData {
pub window: String,
pub filters: RequestFilterView,
pub summary: SpendSummary,
pub rate: RateMetrics,
pub rows: Vec<RequestRow>,
pub metering: MeteringAvailability,
pub truncated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct RequestsReport {
pub mode: Mode,
pub daemon: Option<DaemonView>,
pub window: String,
pub scope: String,
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>,
pub filters: RequestFilterView,
pub truncated: bool,
pub metering: MeteringAvailability,
pub rate_scope: String,
}
const SCOPE: &str = "all callers";
const RATE_SCOPE: &str = "all callers, trailing minute";
impl RequestsReport {
pub fn new(
daemon: Option<DaemonView>,
window: TimeWindow,
summary: SpendSummary,
rate: RateMetrics,
rows: Vec<RequestRow>,
) -> Self {
let now = chrono::Utc::now();
let until = match window {
TimeWindow::Custom { end, .. } => end,
_ => now,
};
let filters = RequestFilterView::new(
window_start(window, now).to_rfc3339(),
until.to_rfc3339(),
None,
None,
);
Self::new_filtered(
daemon,
RequestReportData {
window: window_label(window).to_string(),
filters,
summary,
rate,
rows,
metering: MeteringAvailability::available(),
truncated: false,
},
)
}
pub fn new_filtered(daemon: Option<DaemonView>, data: RequestReportData) -> Self {
let mode = match (daemon.is_some(), data.rows.is_empty()) {
(true, _) => Mode::Live,
(false, false) => Mode::HistoryOnly,
(false, true) => Mode::Empty,
};
Self {
mode,
daemon,
window: data.window,
scope: SCOPE.to_string(),
spend_micro_usd: (data.metering.summary == MeteringComponentAvailability::Available
&& (data.summary.unpriced < data.summary.requests || data.summary.requests == 0))
.then_some(data.summary.spend_micro_usd),
requests: data.summary.requests,
unpriced_requests: data.summary.unpriced,
requests_per_minute: data.rate.requests_per_minute,
tokens_per_minute: data.rate.tokens_per_minute,
rows: data.rows.into_iter().map(RequestView::from).collect(),
filters: data.filters,
truncated: data.truncated,
metering: data.metering,
rate_scope: RATE_SCOPE.to_string(),
}
}
pub fn sanitize_for_remote(&mut self) {
for row in &mut self.rows {
if let Some(error) = row.error.as_deref() {
row.error = Some(remote_error_summary(error).to_string());
}
}
}
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, _) if self.metering.rows == MeteringComponentAvailability::Unavailable => {
"metering unavailable — daemon not running".to_string()
}
(None, Mode::HistoryOnly) => "history only — daemon not running".to_string(),
(None, _) => format!(
"nothing recorded yet — try {} serve",
bitrouter_sdk::invocation::name()
),
}
}
fn rollup(&self) -> String {
let spend = match self.spend_micro_usd {
Some(micro_usd) => fmt_usd(micro_usd),
None => "unreported".to_string(),
};
let summary = match self.metering.summary {
MeteringComponentAvailability::Available => format!("{} req", self.requests),
MeteringComponentAvailability::Unavailable => "summary unavailable".to_string(),
};
let rate = match self.metering.rate {
MeteringComponentAvailability::Available => format!(
"{:.1} req/min · {} tok/min ({})",
self.requests_per_minute,
tokens(self.tokens_per_minute as i64),
self.rate_scope,
),
MeteringComponentAvailability::Unavailable => "rate unavailable".to_string(),
};
let mut line = format!(
"{} {spend} · {summary} · {rate} · {}",
self.window, self.scope,
);
if self.metering.summary == MeteringComponentAvailability::Available && self.requests == 0 {
line.push_str(" · no requests in this window");
}
line
}
fn caveat(&self) -> Option<String> {
let mut caveats = Vec::new();
if self.metering.summary == MeteringComponentAvailability::Unavailable {
caveats.push("metering summary unavailable".to_string());
}
if self.metering.rate == MeteringComponentAvailability::Unavailable {
caveats.push("metering rate unavailable".to_string());
}
if self.metering.rows == MeteringComponentAvailability::Unavailable {
caveats.push("metering request rows unavailable".to_string());
}
if self.metering.summary == MeteringComponentAvailability::Available {
let pricing = 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
)),
};
if let Some(pricing) = pricing {
caveats.push(pricing);
}
}
if caveats.is_empty() {
None
} else {
Some(caveats.join("; "))
}
}
}
fn remote_error_summary(error: &str) -> &'static str {
let category = error
.chars()
.take(256)
.collect::<String>()
.to_ascii_lowercase();
if category.contains("rate limit") || category.contains("rate_limited") {
"upstream rate limited"
} else if category.contains("timeout") || category.contains("timed out") {
"upstream timed out"
} else if category.contains("policy") || category.contains("content filter") {
"upstream rejected request"
} else {
"upstream request failed"
}
}
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.display_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 window_start(
window: TimeWindow,
now: chrono::DateTime<chrono::Utc>,
) -> chrono::DateTime<chrono::Utc> {
let midnight = now.date_naive().and_time(chrono::NaiveTime::MIN).and_utc();
match window {
TimeWindow::LastMinute => now - chrono::Duration::minutes(1),
TimeWindow::LastHour => now - chrono::Duration::hours(1),
TimeWindow::Today => midnight,
TimeWindow::ThisWeek => {
midnight - chrono::Duration::days(now.weekday().num_days_from_monday().into())
}
TimeWindow::ThisMonth => {
let first = chrono::NaiveDate::from_ymd_opt(now.year(), now.month(), 1);
match first {
Some(first) => first.and_time(chrono::NaiveTime::MIN).and_utc(),
None => midnight,
}
}
TimeWindow::Custom { start, .. } => start,
}
}
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(),
router_id: None,
binding_digest: None,
original_selector: None,
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], "—");
assert_eq!(cells[2], "gpt-5");
assert_eq!(cells[3], "openai");
assert_eq!(cells[4], "12.4k");
assert_eq!(cells[5], "891");
assert_eq!(cells[6], "$0.04");
assert_eq!(cells[7], "1.8s");
assert_eq!(cells[8], "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[6], "—");
assert_eq!(cells[7], "—");
}
#[test]
fn an_unpriced_request_is_not_rendered_as_free() {
let cells = RequestView::from(unpriced()).cells();
assert_eq!(cells[6], "?", "unknown evidence must not read as a price");
assert_ne!(cells[6], "—");
assert_ne!(cells[6], "$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 a_row_carries_router_identity_without_inventing_it_for_legacy_rows() {
let mut routed = row();
routed.router_id = Some("coding".into());
routed.binding_digest = Some("router-v1:sha256:abc".into());
routed.original_selector = Some("bitrouter/coding".into());
let value = json(&report(None, vec![routed]));
assert_eq!(value["rows"][0]["router_id"], "coding");
assert_eq!(value["rows"][0]["binding_digest"], "router-v1:sha256:abc");
assert_eq!(value["rows"][0]["original_selector"], "bitrouter/coding");
let legacy = json(&report(None, vec![row()]));
assert_eq!(legacy["rows"][0]["router_id"], serde_json::Value::Null);
assert_eq!(legacy["rows"][0]["binding_digest"], serde_json::Value::Null);
assert_eq!(
legacy["rows"][0]["original_selector"],
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[8].contains('\n'), "newlines would break the row");
assert!(cells[8].chars().count() <= ERROR_CHARS + 1, "{}", cells[8]);
}
#[test]
fn remote_reports_replace_raw_upstream_errors_with_safe_categories() {
let secret = "https://provider.example/v1?token=brk_do-not-disclose";
let mut failed = row();
failed.error = Some(format!("request timed out at {secret}"));
let mut report = report(None, vec![failed]);
assert!(
report.rows[0]
.error
.as_deref()
.is_some_and(|error| error.contains(secret))
);
report.sanitize_for_remote();
assert_eq!(report.rows[0].error.as_deref(), Some("upstream timed out"));
assert!(!json(&report).to_string().contains(secret));
}
#[test]
fn unavailable_metering_is_not_rendered_as_observed_zero_usage() {
let report = RequestsReport::new_filtered(
None,
RequestReportData {
window: "today".to_string(),
filters: RequestFilterView::new(
"2026-09-08T00:00:00+00:00".to_string(),
"2026-09-08T10:30:00+00:00".to_string(),
None,
None,
),
summary: SpendSummary::default(),
rate: RateMetrics::default(),
rows: Vec::new(),
metering: MeteringAvailability::unavailable(),
truncated: false,
},
);
let value = json(&report);
assert_eq!(value["spend_micro_usd"], serde_json::Value::Null);
assert_eq!(value["metering"]["summary"], "unavailable");
assert_eq!(value["metering"]["rate"], "unavailable");
assert!(report.rollup().contains("summary unavailable"));
assert!(report.rollup().contains("rate unavailable"));
assert!(report.headline().contains("metering unavailable"));
assert!(
report
.caveat()
.is_some_and(|note| note.contains("request rows unavailable"))
);
}
#[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("bro 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");
}
}