use bitrouter_mcp::actions::route::{ResolvedVia, RouteReport};
use bitrouter_mcp::actions::status::StatusReport;
use serde::Serialize;
use crate::output::CliReport;
use crate::output::human::{Health, Human};
#[derive(Serialize)]
pub struct DaemonActionReport {
pub action: &'static str,
pub status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub listen: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub models: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub log: Option<String>,
}
impl DaemonActionReport {
pub fn started(
action: &'static str,
status: &'static str,
pid: u32,
listen: String,
models: usize,
log: String,
) -> Self {
Self {
action,
status,
pid: Some(pid),
listen: Some(listen),
models: Some(models),
log: Some(log),
}
}
pub fn not_ready(action: &'static str, pid: u32, log: String) -> Self {
Self {
action,
status: "not_ready",
pid: Some(pid),
listen: None,
models: None,
log: Some(log),
}
}
pub fn simple(action: &'static str, status: &'static str) -> Self {
Self {
action,
status,
pid: None,
listen: None,
models: None,
log: None,
}
}
}
impl CliReport for DaemonActionReport {
fn render(&self, h: &mut Human<'_>) -> std::io::Result<()> {
match self.pid {
Some(pid) => {
let health = if self.status == "not_ready" {
Health::Unknown
} else {
Health::Up
};
h.status_block(health, &format!("bitrouter daemon {}", self.status))?;
h.field("pid", pid)?;
if let Some(listen) = &self.listen {
h.field("listen", listen)?;
}
if let Some(models) = self.models {
h.field("models", format!("{models} routable"))?;
}
if let Some(log) = &self.log {
h.field("log", log)?;
}
Ok(())
}
None => h.line(&format!("daemon {}", self.status)),
}
}
}
impl CliReport for StatusReport {
fn render(&self, h: &mut Human<'_>) -> std::io::Result<()> {
if !self.running {
h.status_block(Health::Down, "bitrouter is stopped")?;
if let Some(socket) = &self.socket {
h.field("socket", socket)?;
}
render_spend(self.spend.as_ref(), h)?;
return h.note("Run `bitrouter start` to launch the daemon.");
}
h.status_block(Health::Up, "bitrouter is running")?;
if let Some(pid) = self.pid {
h.field("pid", pid)?;
}
if let Some(listen) = &self.listen {
h.field("listen", listen)?;
}
if let Some(models) = self.models {
h.field("models", format!("{models} routable"))?;
}
if !self.providers.is_empty() {
h.field("providers", self.providers.join(", "))?;
}
if let Some(socket) = &self.socket {
h.field("socket", socket)?;
}
render_spend(self.spend.as_ref(), h)
}
}
fn render_spend(
spend: Option<&bitrouter_mcp::actions::status::Spend>,
h: &mut Human<'_>,
) -> std::io::Result<()> {
let Some(spend) = spend else {
return Ok(());
};
if let Some(spent) = &spend.spent {
let amount = crate::metering::fmt_usd(spent.estimated_micro_usd);
let line = match spent.unpriced {
0 => format!("{amount} {} ({} requests)", spent.window, spent.requests),
unpriced => format!(
"{amount}+ {} ({} requests, {unpriced} unpriced — floor, not a total)",
spent.window, spent.requests
),
};
h.field("spend", line)?;
}
if let Some(limit) = &spend.limit {
h.field(
"credits",
format!(
"{:.2} {} remaining",
limit.remaining_micro_usd as f64 / 1_000_000.0,
spend.currency
),
)?;
}
Ok(())
}
impl CliReport for RouteReport {
fn render(&self, h: &mut Human<'_>) -> std::io::Result<()> {
let via = match self.resolved_via {
ResolvedVia::Live => "live",
ResolvedVia::Config => "config",
ResolvedVia::ZeroConfig => "zero_config",
};
h.line(&format!(
"model: {} (resolved via: {via})",
self.requested_model
))?;
if self.effective_model != self.requested_model || self.effective_effort.is_some() {
let effort = match self.effective_effort {
Some(effort) => format!(" (effort: {effort})"),
None => String::new(),
};
h.line(&format!(" policy → {}{effort}", self.effective_model))?;
}
if self.provider_chain.is_empty() {
return h.line(" (empty chain — no provider declares this model)");
}
for (i, hop) in self.provider_chain.iter().enumerate() {
h.line(&format!(
" {}. {} → {} ({})",
i + 1,
hop.provider,
hop.service_id,
hop.api_protocol
))?;
}
if let Some(cost) = &self.estimated_cost {
h.line(&format!(
" rates: in {} / out {} µUSD per token{}",
rate(cost.input_micro_usd_per_token),
rate(cost.output_micro_usd_per_token),
match cost.context_tiers.len() {
0 => String::new(),
n => format!(" (+{n} long-context bracket(s))"),
}
))?;
}
Ok(())
}
}
fn rate(value: Option<f64>) -> String {
match value {
Some(v) => format!("{v}"),
None => "?".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::{Format, Output};
fn json(r: &dyn CliReport) -> serde_json::Value {
serde_json::from_slice(&Output::new(Format::Json).render_to_vec(r)).unwrap()
}
use bitrouter_mcp::actions::status::{Spend, SpendLimit, Spent};
fn local_spend(estimated_micro_usd: u64, requests: u64, unpriced: u64) -> Spend {
Spend {
currency: "USD".into(),
spent: Some(Spent {
window: "today".into(),
estimated_micro_usd,
requests,
unpriced,
}),
limit: None,
}
}
#[test]
fn status_running_json_and_human() {
let r = StatusReport::running(
7,
"127.0.0.1:4356".into(),
42,
vec!["anthropic".into(), "openai".into()],
"/x.sock".into(),
Some(local_spend(1_230_000, 9, 0)),
);
assert_eq!(
json(&r),
serde_json::json!({
"running": true, "pid": 7, "listen": "127.0.0.1:4356", "models": 42,
"providers": ["anthropic", "openai"], "socket": "/x.sock",
"spend": {
"currency": "USD",
"spent": {
"window": "today", "estimated_micro_usd": 1_230_000,
"requests": 9, "unpriced": 0
}
}
})
);
let h = String::from_utf8(Output::new(Format::Human).render_to_vec(&r)).unwrap();
assert!(h.contains("● bitrouter is running"), "{h:?}");
assert!(h.contains(" models 42 routable"), "{h:?}");
assert!(h.contains("anthropic, openai"), "{h:?}");
assert!(h.contains("$1.23 today (9 requests)"), "{h:?}");
}
#[test]
fn status_spend_never_hides_unpriced_requests() {
let r = StatusReport::running(
7,
"127.0.0.1:4356".into(),
1,
vec!["openai".into()],
"/x.sock".into(),
Some(local_spend(500_000, 10, 4)),
);
assert_eq!(json(&r)["spend"]["spent"]["unpriced"], 4);
let h = String::from_utf8(Output::new(Format::Human).render_to_vec(&r)).unwrap();
assert!(h.contains("4 unpriced"), "{h:?}");
assert!(h.contains("floor, not a total"), "{h:?}");
}
#[test]
fn status_stopped_still_reports_spend() {
let r = StatusReport::stopped("/x.sock".into(), Some(local_spend(0, 0, 0)));
assert_eq!(
json(&r),
serde_json::json!({
"running": false, "providers": [], "socket": "/x.sock",
"spend": {
"currency": "USD",
"spent": {
"window": "today", "estimated_micro_usd": 0,
"requests": 0, "unpriced": 0
}
}
})
);
let h = String::from_utf8(Output::new(Format::Human).render_to_vec(&r)).unwrap();
assert!(h.contains("○ bitrouter is stopped"), "{h:?}");
assert!(h.contains("$0.00 today (0 requests)"), "{h:?}");
}
#[test]
fn status_stopped_omits_optional_fields() {
let r = StatusReport::stopped("/x.sock".into(), None);
assert_eq!(
json(&r),
serde_json::json!({"running": false, "providers": [], "socket": "/x.sock"})
);
}
#[test]
fn status_renders_a_limit_without_a_spent_figure() {
let r = StatusReport::metered(Spend {
currency: "USD".into(),
spent: None,
limit: Some(SpendLimit {
balance_micro_usd: 5_000_000,
pending_micro_usd: 769_000,
remaining_micro_usd: 4_231_000,
}),
});
let h = String::from_utf8(Output::new(Format::Human).render_to_vec(&r)).unwrap();
assert!(h.contains("credits 4.23 USD remaining"), "{h:?}");
assert!(!h.contains("spend"), "{h:?}");
}
#[test]
fn status_json_round_trips_through_the_shared_type() {
let r = StatusReport::running(
7,
"127.0.0.1:4356".into(),
42,
vec!["openai".into()],
"/x.sock".into(),
Some(local_spend(42, 1, 1)),
);
let back: StatusReport = serde_json::from_value(json(&r)).expect("round trip");
assert_eq!(serde_json::to_value(&back).unwrap(), json(&r));
}
#[test]
fn route_empty_chain_is_empty_array() {
let r = RouteReport {
requested_model: "m".into(),
effective_model: "m".into(),
effective_effort: None,
resolved_via: ResolvedVia::Config,
policy_decision: None,
provider_chain: vec![],
estimated_cost: None,
};
assert_eq!(json(&r)["provider_chain"], serde_json::json!([]));
let h = String::from_utf8(Output::new(Format::Human).render_to_vec(&r)).unwrap();
assert!(h.contains("no provider declares this model"), "{h}");
}
#[test]
fn route_human_names_the_effective_model_when_policy_moved_it() {
use bitrouter_mcp::actions::route::ProviderHop;
let r = RouteReport {
requested_model: "small".into(),
effective_model: "big".into(),
effective_effort: None,
resolved_via: ResolvedVia::Config,
policy_decision: None,
provider_chain: vec![ProviderHop {
provider: "demo".into(),
service_id: "big".into(),
api_protocol: "openai".into(),
}],
estimated_cost: None,
};
let h = String::from_utf8(Output::new(Format::Human).render_to_vec(&r)).unwrap();
assert!(h.contains("policy → big"), "{h}");
}
#[test]
fn route_json_round_trips_through_the_shared_type() {
use bitrouter_mcp::actions::route::{ContextTierRates, EstimatedCost, ProviderHop};
use bitrouter_sdk::language_model::types::ReasoningEffort;
for (via, wire) in [
(ResolvedVia::Live, "live"),
(ResolvedVia::Config, "config"),
(ResolvedVia::ZeroConfig, "zero_config"),
] {
let r = RouteReport {
requested_model: "small".into(),
effective_model: "big".into(),
effective_effort: Some(ReasoningEffort::High),
resolved_via: via,
policy_decision: None,
provider_chain: vec![ProviderHop {
provider: "demo".into(),
service_id: "big".into(),
api_protocol: "openai".into(),
}],
estimated_cost: Some(EstimatedCost::new(
Some(1.0),
Some(2.0),
vec![ContextTierRates {
above_input_tokens: 200_000,
input_micro_usd_per_token: Some(2.0),
output_micro_usd_per_token: Some(4.0),
}],
)),
};
let emitted = json(&r);
assert_eq!(emitted["resolved_via"], wire, "{via:?}");
let back: RouteReport = serde_json::from_value(emitted.clone()).expect("round trip");
assert_eq!(serde_json::to_value(&back).unwrap(), emitted);
assert_eq!(
emitted["estimated_cost"]["context_tiers"][0]["above_input_tokens"],
200_000
);
}
}
#[test]
fn daemon_action_simple_one_liner() {
let r = DaemonActionReport::simple("stop", "stopped");
assert_eq!(
json(&r),
serde_json::json!({"action": "stop", "status": "stopped"})
);
let h = String::from_utf8(Output::new(Format::Human).render_to_vec(&r)).unwrap();
assert_eq!(h, "daemon stopped\n");
}
}