use crate::actions::route::{ResolvedVia, RouteReport};
use crate::actions::status::StatusReport;
use bitrouter_sdk::invocation;
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(version) = &self.installed_version {
h.field("installed version", version)?;
}
if let Some(socket) = &self.socket {
h.field("socket", socket)?;
}
render_configuration_state(self, h)?;
render_router_state(self, h)?;
render_spend(self.spend.as_ref(), h)?;
return h.note(&format!(
"Run `{} start` to launch the daemon.",
invocation::name()
));
}
h.status_block(Health::Up, "bitrouter is running")?;
if let Some(pid) = self.pid {
h.field("pid", pid)?;
}
if let Some(version) = &self.installed_version {
h.field("installed version", version)?;
}
if self.socket.is_some() {
h.field(
"daemon version",
self.daemon_version.as_deref().unwrap_or("unknown"),
)?;
}
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)?;
}
if let Some(compatibility) = &self.compatibility {
h.field("compatibility", compatibility)?;
if let Some(activity) = &self.handoff_activity {
h.field("active operations", activity.operations)?;
h.field(
"supervised runs",
activity
.supervised_runs
.map_or_else(|| "unknown".to_string(), |runs| runs.to_string()),
)?;
h.field("ACP route leases", activity.route_leases)?;
h.field(
"ACP recordings",
activity.acp_recordings.map_or_else(
|| "unknown".to_string(),
|recordings| recordings.to_string(),
),
)?;
} else {
h.field("active work", "unknown")?;
}
match compatibility.as_str() {
"legacy_unknown" => h.note("Finish active work, then run `bro restart`; this daemon cannot prove it is idle.")?,
"externally_managed" => h.note("Restart this daemon with its service manager after active work finishes.")?,
"handoff_required" => h.note("The next supervised command attempts a safe handoff if idle and the database preflight passes.")?,
"daemon_newer" => h.note("Install a matching or newer bro binary before using this daemon.")?,
_ => {}
}
}
render_configuration_state(self, h)?;
render_router_state(self, h)?;
render_spend(self.spend.as_ref(), h)
}
}
fn render_configuration_state(report: &StatusReport, h: &mut Human<'_>) -> std::io::Result<()> {
use crate::reload::{
AuxiliaryConfigState, ConfigSourceKind, RunningConfigState, SavedConfigState,
};
let Some(state) = &report.config_state else {
if report.pid.is_some() || report.socket.is_some() {
return h.field("configuration", "saved/running state unavailable");
}
return Ok(());
};
h.field(
"config source",
match state.source {
ConfigSourceKind::File => "file",
ConfigSourceKind::Default => "generated default",
},
)?;
h.field(
"config saved",
match state.saved {
SavedConfigState::Available => "available",
SavedConfigState::Generated => "generated",
SavedConfigState::Missing => "missing",
SavedConfigState::Invalid => "invalid",
SavedConfigState::Unavailable => "unavailable",
},
)?;
h.field(
"config running",
match state.running {
RunningConfigState::InSync => "saved configuration inputs in sync",
RunningConfigState::ReloadRequired => "reload required",
RunningConfigState::RestartRequired => "restart required",
RunningConfigState::Mixed => "mixed; inspect last reload",
RunningConfigState::Unknown => "unknown",
},
)?;
if !state.reload_required_fields.is_empty() {
h.field("reload fields", state.reload_required_fields.join(", "))?;
}
if !state.restart_required_fields.is_empty() {
h.field("restart fields", state.restart_required_fields.join(", "))?;
}
let auxiliary = |state| match state {
AuxiliaryConfigState::InSync => "in sync",
AuxiliaryConfigState::ReloadRequired => "reload required",
AuxiliaryConfigState::NotConfigured => "not configured",
AuxiliaryConfigState::Missing => "missing",
AuxiliaryConfigState::Invalid => "invalid",
AuxiliaryConfigState::Unavailable => "unavailable",
AuxiliaryConfigState::Unknown => "unknown",
};
h.field("named policy", auxiliary(state.named_policy))?;
h.field("access policies", auxiliary(state.access_policies))?;
if matches!(state.running, RunningConfigState::Mixed)
&& let Some(latest) = state.mixed_state_history.last()
{
let outcome = match latest.outcome {
crate::reload::ReloadOutcome::Succeeded => "succeeded",
crate::reload::ReloadOutcome::Failed => "failed",
crate::reload::ReloadOutcome::PartiallyApplied => "partially applied",
crate::reload::ReloadOutcome::Unknown => "unknown",
};
h.field(
"mixed reload history",
format!(
"{} retained; latest generation {} {outcome}; inspect JSON participant results",
state.mixed_state_history.len(),
latest.generation
),
)?;
}
if let Some(generation) = state.generation {
h.field("config generation", generation)?;
}
Ok(())
}
fn render_router_state(report: &StatusReport, h: &mut Human<'_>) -> std::io::Result<()> {
match (&report.saved_routers, &report.running_routers) {
(Some(saved), Some(running)) => {
h.field(
"routers",
format!("{} saved, {} running", saved.len(), running.len()),
)?;
}
(Some(saved), None) => {
h.field(
"routers",
format!("{} saved; running view unavailable", saved.len()),
)?;
}
(None, Some(running)) => {
h.field(
"routers",
format!("{} running; saved view unavailable", running.len()),
)?;
}
(None, None) => {}
}
match (
report.config_state.is_none(),
report.router_restart_required,
) {
(true, Some(true)) => h.field("router config", "restart required"),
(true, Some(false)) => h.field("router config", "saved and running match"),
_ => Ok(()),
}
}
fn render_spend(
spend: Option<&crate::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 let Some(router) = &self.router {
let source = match self.router_source {
Some(crate::actions::models::RouterSource::User) => "user",
Some(crate::actions::models::RouterSource::Legacy) => "legacy",
Some(crate::actions::models::RouterSource::Default) => "default",
None => "unknown",
};
h.line(&format!(
" router: {} ({source}, {})",
router.router_id, router.binding_digest
))?;
}
if let Some(policy) = &self.bound_policy {
h.line(&format!(
" policy: {policy} (decision not executed; base model: {})",
self.effective_model
))?;
if !self.candidate_models.is_empty() {
h.line(&format!(
" candidates: {}",
self.candidate_models.join(", ")
))?;
}
} else 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() {
if self.bound_policy.is_some() && self.policy_decision_executed == Some(false) {
return h.line(" (no provider chain selected without a policy decision)");
}
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 crate::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("configuration saved/running state unavailable"),
"{h:?}"
);
assert!(h.contains("$1.23 today (9 requests)"), "{h:?}");
}
#[test]
fn status_renders_whole_configuration_state_without_values() -> anyhow::Result<()> {
use crate::reload::{
AuxiliaryConfigState, ConfigSourceKind, ConfigurationState, RunningConfigState,
SavedConfigState,
};
let report = StatusReport::running(
7,
"127.0.0.1:4356".into(),
1,
vec!["demo".into()],
"/x.sock".into(),
None,
)
.with_config_state(Some(ConfigurationState {
server_instance_id: Some("fixture-instance".into()),
generation: Some(4),
source: ConfigSourceKind::File,
saved: SavedConfigState::Available,
running: RunningConfigState::RestartRequired,
reload_required_fields: vec!["providers".into()],
restart_required_fields: vec!["server".into()],
named_policy: AuxiliaryConfigState::ReloadRequired,
access_policies: AuxiliaryConfigState::Unknown,
last_reload: None,
mixed_state_history: Vec::new(),
}));
let value = serde_json::to_value(&report)?;
assert_eq!(value["config_state"]["running"], "restart_required");
assert_eq!(
value["config_state"]["restart_required_fields"],
serde_json::json!(["server"])
);
let human = String::from_utf8(Output::new(Format::Human).render_to_vec(&report))?;
assert!(
human.contains("config running restart required"),
"{human}"
);
assert!(human.contains("reload fields providers"), "{human}");
assert!(human.contains("restart fields server"), "{human}");
assert!(human.contains("named policy reload required"), "{human}");
assert!(human.contains("access policies unknown"), "{human}");
assert!(!human.contains("fixture-instance"), "{human}");
Ok(())
}
#[test]
fn mixed_status_points_to_the_retained_participant_cause() -> anyhow::Result<()> {
use crate::reload::{
AuxiliaryConfigState, ConfigSourceKind, ConfigurationState, ReloadOutcome,
ReloadReport, RunningConfigState, SavedConfigState,
};
let report = StatusReport::running(
7,
"127.0.0.1:4356".into(),
1,
vec!["demo".into()],
"/x.sock".into(),
None,
)
.with_config_state(Some(ConfigurationState {
server_instance_id: Some("fixture-instance".into()),
generation: Some(4),
source: ConfigSourceKind::File,
saved: SavedConfigState::Available,
running: RunningConfigState::Mixed,
reload_required_fields: Vec::new(),
restart_required_fields: Vec::new(),
named_policy: AuxiliaryConfigState::InSync,
access_policies: AuxiliaryConfigState::InSync,
last_reload: None,
mixed_state_history: vec![ReloadReport {
server_instance_id: "fixture-instance".into(),
generation: 3,
outcome: ReloadOutcome::PartiallyApplied,
participants: Vec::new(),
restart_required_fields: Vec::new(),
started_at: "2026-09-15T00:00:00Z".into(),
completed_at: "2026-09-15T00:00:01Z".into(),
}],
}));
let value = serde_json::to_value(&report)?;
assert_eq!(
value["config_state"]["mixed_state_history"][0]["outcome"],
"partially_applied"
);
let human = String::from_utf8(Output::new(Format::Human).render_to_vec(&report))?;
assert!(
human.contains(
"mixed reload history 1 retained; latest generation 3 partially applied"
),
"{human}"
);
assert!(
human.contains("inspect JSON participant results"),
"{human}"
);
Ok(())
}
#[test]
fn in_sync_human_label_is_scoped_to_saved_configuration_inputs() -> anyhow::Result<()> {
let report: StatusReport = serde_json::from_value(serde_json::json!({
"running": true,
"providers": [],
"config_state": {
"source": "file",
"saved": "available",
"running": "in_sync",
"named_policy": "in_sync",
"access_policies": "in_sync"
}
}))?;
let human = String::from_utf8(Output::new(Format::Human).render_to_vec(&report))?;
assert!(
human.contains("config running saved configuration inputs in sync"),
"{human}"
);
Ok(())
}
#[test]
fn status_distinguishes_saved_and_running_router_state() -> anyhow::Result<()> {
use crate::actions::models::{RouterReadiness, RouterSource, RouterStatus};
let router = |id: &str| RouterStatus {
id: id.to_string(),
address: format!("bitrouter/{id}"),
source: RouterSource::User,
readiness: RouterReadiness::Ready,
reason: None,
selection: None,
system_prompt_default: true,
parameter_defaults: vec!["temperature".to_string()],
binding_digest: Some("router-v1:sha256:test".to_string()),
};
let report = StatusReport::running(
7,
"127.0.0.1:4356".into(),
1,
vec!["demo".into()],
"/x.sock".into(),
None,
)
.with_router_views(
Some(vec![router("coding"), router("review")]),
Some(vec![router("coding")]),
Some(true),
);
let value = json(&report);
assert_eq!(value["router_restart_required"], true);
assert_eq!(value["saved_routers"].as_array().map(Vec::len), Some(2));
assert_eq!(value["running_routers"].as_array().map(Vec::len), Some(1));
let human = String::from_utf8(Output::new(Format::Human).render_to_vec(&report))?;
assert!(human.contains("2 saved, 1 running"), "{human}");
assert!(human.contains("restart required"), "{human}");
Ok(())
}
#[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,
router: None,
router_source: None,
bound_policy: None,
policy_decision_executed: None,
candidate_models: Vec::new(),
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 crate::actions::route::ProviderHop;
let r = RouteReport {
requested_model: "small".into(),
effective_model: "big".into(),
effective_effort: None,
resolved_via: ResolvedVia::Config,
policy_decision: None,
router: None,
router_source: None,
bound_policy: None,
policy_decision_executed: None,
candidate_models: Vec::new(),
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 crate::actions::route::{ContextTierRates, EstimatedCost, ProviderHop};
use bitrouter_ai::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,
router: None,
router_source: None,
bound_policy: None,
policy_decision_executed: None,
candidate_models: Vec::new(),
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");
}
}