diff --git a/rust/src/cli/serve/dashboard/snapshot.rs b/rust/src/cli/serve/dashboard/snapshot.rs index 9809aac43c..d24146e8ac 100644 --- a/rust/src/cli/serve/dashboard/snapshot.rs +++ b/rust/src/cli/serve/dashboard/snapshot.rs @@ -33,7 +33,7 @@ pub use crate::cli::serve::collection::{ }; #[cfg(test)] use crate::core::ProviderFetchResult; -use crate::core::{RateWindow, UsagePace, UsageSnapshot}; +use crate::core::{CostSnapshot, RateWindow, UsagePace, UsageSnapshot}; /// How much account identity a snapshot exposes. Upstream 0.48.0 exposes two /// CLI modes (`redacted` default, `full` opt-in); upstream's internal `none` @@ -126,6 +126,30 @@ pub struct CostPayload { pub last_30_days_usd: Option, } +/// Project a provider-owned 30-day history into the dashboard cost shape. +/// +/// Provider activity can use completed UTC buckets, so it must not be +/// relabeled as the host's local Today value. `always_visible` is the core +/// marker used by provider-owned history (currently OpenRouter activity), +/// while ordinary billing/balance snapshots remain out of this fallback. +fn reported_cost_payload(cost: Option<&CostSnapshot>) -> Option { + let cost = cost?; + if !cost.always_visible + || cost.currency_code != "USD" + || cost.period != "Last 30 days (UTC)" + || !cost.used.is_finite() + { + return None; + } + + Some(CostPayload { + today_usd: None, + // Preserve a reported zero as known data instead of treating it as + // missing and falling through to a different source. + last_30_days_usd: Some(cost.used), + }) +} + #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct DisplayPayload { @@ -251,11 +275,16 @@ fn build_provider( sort_key: u32, claude: Option<&ClaudeAccountsInput>, ) -> SnapshotProvider { - let cost = costs.get(&envelope.id).and_then(|raw| { - (raw.today_usd.is_some() || raw.last_30_days_usd.is_some()).then_some(CostPayload { - today_usd: raw.today_usd, - last_30_days_usd: raw.last_30_days_usd, - }) + let local_cost = costs.get(&envelope.id).map(|raw| CostPayload { + today_usd: raw.today_usd, + last_30_days_usd: raw.last_30_days_usd, + }); + let cost = local_cost.or_else(|| { + envelope + .fetch + .as_ref() + .ok() + .and_then(|result| reported_cost_payload(result.cost.as_ref())) }); let (source, identity, windows, updated_at, error) = match &envelope.fetch { diff --git a/rust/src/cli/serve/dashboard/snapshot_tests.rs b/rust/src/cli/serve/dashboard/snapshot_tests.rs index 7aeefe3320..26072ffb2e 100644 --- a/rust/src/cli/serve/dashboard/snapshot_tests.rs +++ b/rust/src/cli/serve/dashboard/snapshot_tests.rs @@ -414,6 +414,62 @@ mod tests { assert_eq!(row["cost"]["last30DaysUSD"], 40.5); } + #[test] + fn provider_reported_30_day_cost_falls_back_without_local_today() { + for reported in [0.0, 12.5] { + let result = fetch_result(5.0, None, None).with_cost( + CostSnapshot::new(reported, "USD", "Last 30 days (UTC)").always_visible(), + ); + let row = &serde_json::to_value(build_snapshot(&input( + vec![provider_envelope(Ok(result))], + DashboardIdentity::Redacted, + ))) + .unwrap()["providers"][0]; + assert!(row["cost"]["todayUSD"].is_null()); + assert_eq!(row["cost"]["last30DaysUSD"], reported); + } + } + + #[test] + fn unsupported_provider_reported_cost_is_not_projected() { + for cost in [ + CostSnapshot::new(12.5, "EUR", "Last 30 days (UTC)").always_visible(), + CostSnapshot::new(12.5, "USD", "This month").always_visible(), + CostSnapshot::new(12.5, "USD", "Last 30 days (UTC)"), + ] { + let row = &serde_json::to_value(build_snapshot(&input( + vec![provider_envelope(Ok( + fetch_result(5.0, None, None).with_cost(cost) + ))], + DashboardIdentity::Redacted, + ))) + .unwrap()["providers"][0]; + assert!(row["cost"].is_null()); + } + } + + #[test] + fn local_cost_payload_keeps_precedence_when_amount_is_unavailable() { + let mut input = input( + vec![provider_envelope(Ok(fetch_result(5.0, None, None) + .with_cost( + CostSnapshot::new(99.0, "USD", "Last 30 days (UTC)").always_visible(), + )))], + DashboardIdentity::Redacted, + ); + input.collection.costs.insert( + "claude".to_string(), + RawCostPayload { + today_usd: None, + last_30_days_usd: None, + }, + ); + + let row = &serde_json::to_value(build_snapshot(&input)).unwrap()["providers"][0]; + assert!(row["cost"]["todayUSD"].is_null()); + assert!(row["cost"]["last30DaysUSD"].is_null()); + } + #[test] fn claude_accounts_attach_to_first_claude_row_only() { let second = ProviderFetchEnvelope {