From 5358eae0d5ec3f502e1fc7c083910752e871640e Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:38:21 +0700 Subject: [PATCH] Port upstream 0.61.0: show provider history provenance and token totals in CLI usage text --- CHANGELOG.md | 1 + rust/src/cli/usage/render.rs | 57 +++++++++++++- rust/src/cli/usage_tests.rs | 92 +++++++++++++++++++++++ rust/src/core/usage_snapshot.rs | 26 +++++++ rust/src/providers/openrouter/activity.rs | 62 ++++++++++++++- 5 files changed, 233 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c054768f2c..d9a864359b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ menu-bar layout. - Grok: Settings and tray **Add account** flow matching Codex/Claude — isolated `grok login --oauth`, save current CLI login, switch, and remove without logging out the active session. - Replicate: cookie-authenticated monthly spend and optional prepaid credit balance from the billing page, with user and organization account isolation. - DeepSeek: show reported per-model spend in the provider details while preserving the billing currency, reporting period, zero values, and incomplete-total safeguards. +- CLI: show provider-supplied history in usage text with its source period, known zero, and cost provenance (reported, estimated, or includes estimates), plus input-plus-output token totals for OpenRouter Activity. ### Fixed - Claude: when Hide Personal Info is enabled, keep saved account rows distinguishable with stable localized `Account N` labels and matching redacted tooltips. diff --git a/rust/src/cli/usage/render.rs b/rust/src/cli/usage/render.rs index 62fd50310d..e78c5e8f2d 100644 --- a/rust/src/cli/usage/render.rs +++ b/rust/src/cli/usage/render.rs @@ -10,6 +10,7 @@ use crate::core::{ CostSnapshot, ProviderDisplayDetail, ProviderFetchResult, ProviderId, ProviderInventoryItem, RateWindow, UsagePace, UsageSnapshot, instantiate_provider, }; +use crate::spend_contract::CostProvenance; use crate::status::{ProviderStatus as StatusInfo, StatusLevel}; pub fn render_text_error(provider_id: ProviderId, error_msg: &str, use_color: bool) -> String { @@ -427,11 +428,12 @@ fn append_cost_line(lines: &mut Vec, cost: Option<&CostSnapshot>) { // Provider-supplied Activity history is a completed reporting window, // rather than the ordinary current-cost meter. Providers mark such - // snapshots `always_visible`; keep their source period and known zero - // visible in text output without adding a second generic cost line. The - // daily points remain available in the JSON cost payload. + // snapshots `always_visible`; keep their source period, known zero, token + // total and cost provenance visible in text output without adding a second + // generic cost line or recomputing totals from the daily points. The daily + // points remain available in the JSON cost payload. if cost.limit.is_none() && cost.always_visible { - lines.push(format!(" {}: {}", cost.period, cost.format_used())); + lines.push(format_history_line(cost)); return; } @@ -451,6 +453,53 @@ fn append_cost_line(lines: &mut Vec, cost: Option<&CostSnapshot>) { } } +/// One provider-history line: `: () · tokens`. +/// +/// Mirrors upstream `CLIRenderer.liveHistoryLine` (0.61.0). The spend and token +/// totals are the provider's own; nothing is summed from daily points. +fn format_history_line(cost: &CostSnapshot) -> String { + let spend = cost.format_used(); + let spend = match cost.provenance { + Some(CostProvenance::VendorMetered) => format!("{spend} (reported)"), + Some(CostProvenance::ListPriceEstimate) => format!("{spend} (estimated)"), + Some(CostProvenance::Mixed) => format!("{spend} (includes estimates)"), + Some(CostProvenance::Unknown) | None => spend, + }; + let mut values = vec![spend]; + if let Some(tokens) = cost.history_tokens { + let unit = if tokens == 1 { "token" } else { "tokens" }; + values.push(format!("{} {unit}", format_token_count(tokens))); + } + format!(" {}: {}", cost.period, values.join(" · ")) +} + +/// Compact token count: `999`, `1.2K`, `15K`, `2.5M`, `1B`. A unit is promoted +/// once the lower unit would round to 1000 (upstream `tokenCountString`). +fn format_token_count(tokens: u64) -> String { + const UNITS: [(u64, f64, &str); 3] = [ + (999_500_000, 1_000_000_000.0, "B"), + (999_500, 1_000_000.0, "M"), + (1_000, 1_000.0, "K"), + ]; + for (threshold, divisor, suffix) in UNITS { + if tokens >= threshold { + #[expect( + clippy::cast_precision_loss, + reason = "display rounding to at most two significant digits" + )] + let scaled = tokens as f64 / divisor; + let formatted = if scaled >= 10.0 { + format!("{scaled:.0}") + } else { + let one = format!("{scaled:.1}"); + one.strip_suffix(".0").unwrap_or(&one).to_string() + }; + return format!("{formatted}{suffix}"); + } + } + tokens.to_string() +} + /// Render usage as text (backwards compatible version) pub fn render_text(provider: ProviderId, result: &ProviderFetchResult, use_color: bool) -> String { render_text_with_status(provider, result, None, use_color) diff --git a/rust/src/cli/usage_tests.rs b/rust/src/cli/usage_tests.rs index d332131b81..44211d4028 100644 --- a/rust/src/cli/usage_tests.rs +++ b/rust/src/cli/usage_tests.rs @@ -324,3 +324,95 @@ fn json_inventory_is_additive_and_contains_no_redemption_token() { .contains("coupon-token-secret") ); } + +fn history_output(cost: CostSnapshot) -> String { + let result = fetch_result(UsageSnapshot::new(RateWindow::new(0.0))).with_cost(cost); + render_text_with_status(ProviderId::OpenRouter, &result, None, false) +} + +#[test] +fn history_line_shows_provenance_and_token_total() { + use crate::spend_contract::CostProvenance; + + let cases = [ + (CostProvenance::VendorMetered, "$1.25 (reported)"), + (CostProvenance::ListPriceEstimate, "$1.25 (estimated)"), + (CostProvenance::Mixed, "$1.25 (includes estimates)"), + (CostProvenance::Unknown, "$1.25"), + ]; + for (provenance, spend) in cases { + let output = history_output( + CostSnapshot::new(1.25, "USD", "Last 30 days (UTC)") + .with_history_tokens(15) + .with_provenance(provenance) + .always_visible(), + ); + let expected = format!(" Last 30 days (UTC): {spend} · 15 tokens"); + assert_eq!(output.matches(&expected).count(), 1, "{output}"); + } +} + +#[test] +fn history_line_preserves_known_zero_singular_token_and_currency() { + use crate::spend_contract::CostProvenance; + + let zero = history_output( + CostSnapshot::new(0.0, "USD", "Last 30 days (UTC)") + .with_history_tokens(0) + .with_provenance(CostProvenance::VendorMetered) + .always_visible(), + ); + assert!(zero.contains("Last 30 days (UTC): $0.00 (reported) · 0 tokens")); + + let one = history_output( + CostSnapshot::new(0.0, "USD", "Last 1 day") + .with_history_tokens(1) + .always_visible(), + ); + assert!(one.ends_with("Last 1 day: $0.00 · 1 token"), "{one}"); + + let euro = history_output(CostSnapshot::new(2.5, "EUR", "Billing period").always_visible()); + assert!(euro.contains("Billing period: €2.50")); + assert!(!euro.contains("token")); +} + +#[test] +fn history_line_compacts_large_token_totals() { + let cases = [ + (999, "999 tokens"), + (1_000, "1K tokens"), + (1_250, "1.2K tokens"), + (15_400, "15K tokens"), + (999_499, "999K tokens"), + (999_500, "1M tokens"), + (2_500_000, "2.5M tokens"), + (999_500_000, "1B tokens"), + ]; + for (tokens, expected) in cases { + let output = history_output( + CostSnapshot::new(1.0, "USD", "Last 30 days (UTC)") + .with_history_tokens(tokens) + .always_visible(), + ); + assert!( + output.contains(&format!("$1.00 · {expected}")), + "{tokens}: {output}" + ); + } +} + +#[test] +fn history_fields_do_not_change_the_cost_json_contract() { + use crate::spend_contract::CostProvenance; + + let result = fetch_result(UsageSnapshot::new(RateWindow::new(0.0))).with_cost( + CostSnapshot::new(1.25, "USD", "Last 30 days (UTC)") + .with_history_tokens(15) + .with_provenance(CostProvenance::VendorMetered) + .always_visible(), + ); + let json = render_json_result(ProviderId::OpenRouter, result, None); + let cost = json.get("cost").and_then(|cost| cost.as_object()).unwrap(); + assert!(!cost.contains_key("historyTokens") && !cost.contains_key("history_tokens")); + assert!(!cost.contains_key("provenance")); +} diff --git a/rust/src/core/usage_snapshot.rs b/rust/src/core/usage_snapshot.rs index ebbdcb8c20..09db5dfdaa 100755 --- a/rust/src/core/usage_snapshot.rs +++ b/rust/src/core/usage_snapshot.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use super::ProviderDisplayDetail; use super::RateWindow; +use crate::spend_contract::CostProvenance; /// Subscription dates explicitly reported by an authenticated provider /// dashboard or subscription endpoint. /// @@ -414,6 +415,17 @@ pub struct CostSnapshot { /// usage signal and must remain visible when optional local summaries are hidden. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub always_visible: bool, + + /// Input-plus-output token total for the same reporting window as `used`, + /// when the provider supplies one. Text rendering only: it is not part of + /// the serialized cost payload, which keeps the ordinary usage JSON contract. + #[serde(skip)] + pub history_tokens: Option, + + /// How the `used` figure was produced, when the provider states it. Text + /// rendering only; not serialized. + #[serde(skip)] + pub provenance: Option, } impl CostSnapshot { @@ -432,6 +444,8 @@ impl CostSnapshot { account_id: None, daily: Vec::new(), always_visible: false, + history_tokens: None, + provenance: None, } } @@ -541,6 +555,18 @@ impl CostSnapshot { self } + /// Attach the provider-supplied token total for this spend window. + pub fn with_history_tokens(mut self, tokens: u64) -> Self { + self.history_tokens = Some(tokens); + self + } + + /// Record how the spend figure was produced (reported, estimated, mixed). + pub fn with_provenance(mut self, provenance: CostProvenance) -> Self { + self.provenance = Some(provenance); + self + } + /// Builder pattern: set currency symbol for localized rendering. pub fn with_currency_symbol(mut self, symbol: impl Into) -> Self { self.currency_symbol = Some(symbol.into()); diff --git a/rust/src/providers/openrouter/activity.rs b/rust/src/providers/openrouter/activity.rs index 9fcd2bec9f..67d6cf48e1 100644 --- a/rust/src/providers/openrouter/activity.rs +++ b/rust/src/providers/openrouter/activity.rs @@ -4,6 +4,7 @@ use chrono::{DateTime, Duration, Utc}; use serde_json::Value; use crate::core::{CostDailyPoint, CostSnapshot, ProviderError}; +use crate::spend_contract::CostProvenance; const MAX_ACTIVITY_ROWS: usize = 20_000; /// Distinct identity rows tracked for dedupe; bounds the `seen` map. @@ -19,6 +20,8 @@ pub(super) fn parse_activity_cost( let mut seen: HashMap = HashMap::new(); let mut daily: BTreeMap = BTreeMap::new(); let mut total = 0.0; + let mut estimated_total = 0.0; + let mut tokens = 0u64; let mut rows_seen = 0usize; for payload in payloads { @@ -139,16 +142,32 @@ pub(super) fn parse_activity_cost( )); } total += cost; + estimated_total += estimated; + // Prompt plus completion only: reasoning tokens are a separate counter. + tokens = tokens.saturating_add(prompt + completion); *daily.entry(day.to_string()).or_default() += cost; } } - if !total.is_finite() { + if !total.is_finite() || !estimated_total.is_finite() { return Err(ProviderError::Parse( "OpenRouter Activity spend overflowed".into(), )); } + // Matches upstream's plugin snapshot mapper: any BYOK estimate makes the + // window estimated, or mixed when metered spend is also present. + let provenance = if estimated_total > 0.0 { + if total - estimated_total > 0.0 { + CostProvenance::Mixed + } else { + CostProvenance::ListPriceEstimate + } + } else { + CostProvenance::VendorMetered + }; Ok(CostSnapshot::new(total, "USD", "Last 30 days (UTC)") + .with_history_tokens(tokens) + .with_provenance(provenance) .with_daily( daily .into_iter() @@ -314,4 +333,45 @@ mod tests { assert!(error.to_string().contains("completed UTC day")); } + + #[test] + fn records_token_total_and_cost_provenance() { + use crate::spend_contract::CostProvenance; + + let cases = [ + (1.25, 0.0, CostProvenance::VendorMetered), + (0.0, 0.75, CostProvenance::ListPriceEstimate), + (1.25, 0.75, CostProvenance::Mixed), + ]; + for (usage, byok, expected) in cases { + let history = serde_json::json!({"data":[ + {"date":"2026-08-17","model":"m","prompt_tokens":10,"completion_tokens":5, + "reasoning_tokens":2,"requests":1,"usage":usage,"byok_usage_inference":byok} + ]}); + let cost = parse_activity_cost(&[history], now()).unwrap(); + assert_eq!(cost.history_tokens, Some(15)); + assert_eq!(cost.provenance, Some(expected)); + assert_eq!(cost.used, usage + byok); + } + } + + #[test] + fn empty_activity_is_a_reported_zero_with_zero_tokens() { + use crate::spend_contract::CostProvenance; + + let cost = parse_activity_cost(&[serde_json::json!({"data":[]})], now()).unwrap(); + assert_eq!(cost.used, 0.0); + assert_eq!(cost.history_tokens, Some(0)); + assert_eq!(cost.provenance, Some(CostProvenance::VendorMetered)); + } + + #[test] + fn duplicate_rows_do_not_double_count_tokens() { + let row = serde_json::json!({"date":"2026-08-21","model":"m","prompt_tokens":10, + "completion_tokens":5,"requests":1,"usage":1.0}); + let history = serde_json::json!({"data":[row.clone()]}); + let latest = serde_json::json!({"data":[row]}); + let cost = parse_activity_cost(&[history, latest], now()).unwrap(); + assert_eq!(cost.history_tokens, Some(15)); + } }