From a9884c7d4809c208c7ce10630da8b41cec713e8d Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:33:53 +0100 Subject: [PATCH 01/75] Keep unknown windows and reset times unknown (OpenCode, OpenCode Go, Sub2API, MiniMax, Antigravity) - Antigravity: export NamedRateWindow.usage_known as usageKnown so an unknown lane is no longer read as a real 0 %. - OpenCode / OpenCode Go: a percent without resetInSec has no reset time instead of resets_at = now. - OpenCode Go local estimate: no usage in the last five hours means no session reset, not a made-up 5 h countdown. - Sub2API: the subscription expiry is an informational row, not a 0 % window whose reset is the contract end. - MiniMax: a coding-plan clock time that has passed rolls the parsed time by one day instead of becoming now + 1 day. Each item has a test that failed before; the explicit resetInSec: 0 case stays a reset (VibeTV #289 section C). --- rust/src/core/usage_snapshot.rs | 9 ++- rust/src/providers/antigravity/tests.rs | 25 +++++++ rust/src/providers/minimax/coding_plan.rs | 18 ++++- .../src/providers/minimax/coding_plan_html.rs | 15 +++- rust/src/providers/opencode/mod.rs | 68 +++++++++++++++---- rust/src/providers/opencodego/local.rs | 40 +++++++++-- rust/src/providers/opencodego/mod.rs | 52 ++++++++------ rust/src/providers/sub2api/mod.rs | 38 +++++++++-- 8 files changed, 215 insertions(+), 50 deletions(-) diff --git a/rust/src/core/usage_snapshot.rs b/rust/src/core/usage_snapshot.rs index 15bf72fb700..2502d2796e4 100755 --- a/rust/src/core/usage_snapshot.rs +++ b/rust/src/core/usage_snapshot.rs @@ -79,9 +79,12 @@ pub struct NamedRateWindow { pub id: String, pub title: String, pub window: RateWindow, - /// Whether the provider explicitly reported usage for this lane. - /// In-memory presentation metadata only; external snapshot JSON stays stable. - #[serde(default = "named_rate_window_usage_known_default", skip_serializing)] + /// Whether the provider explicitly reported usage for this lane. Exported + /// so consumers can tell an unknown lane from a real 0 %. + #[serde( + default = "named_rate_window_usage_known_default", + rename = "usageKnown" + )] pub usage_known: bool, } diff --git a/rust/src/providers/antigravity/tests.rs b/rust/src/providers/antigravity/tests.rs index d74ae791e7c..8840ef6f01e 100644 --- a/rust/src/providers/antigravity/tests.rs +++ b/rust/src/providers/antigravity/tests.rs @@ -162,6 +162,31 @@ fn antigravity_extra_windows_preserve_usage_known() { assert_eq!(claude.window.used_percent, 0.0); } +#[test] +fn antigravity_unknown_window_exports_usage_known_false() { + let resp: UserStatusResponse = serde_json::from_value(serde_json::json!({ + "userStatus": { + "cascadeModelConfigData": { + "clientModelConfigs": [ + {"label": "Claude 4 Sonnet", "quotaInfo": {"remainingFraction": 0.8}}, + {"label": "Gemini 2.5 Pro", "quotaInfo": {"remainingFraction": 0.6}}, + {"label": "Gemini 2.5 Flash", "quotaInfo": {"remainingFraction": 0.4}}, + {"label": "Claude 3.5 Sonnet", "quotaInfo": {"remainingFraction": null}} + ] + } + } + })) + .unwrap(); + let snap = AntigravityProvider::new().parse_user_status(resp).unwrap(); + let exported = serde_json::to_value(&snap).unwrap(); + let windows = exported["extra_rate_windows"].as_array().unwrap(); + assert_eq!(windows.len(), snap.extra_rate_windows.len()); + assert!(windows.iter().any(|window| window["usageKnown"] == false)); + for (json, window) in windows.iter().zip(&snap.extra_rate_windows) { + assert_eq!(json["usageKnown"], window.usage_known); + } +} + #[test] fn test_parse_user_status_standard() { let resp = make_response(vec![ diff --git a/rust/src/providers/minimax/coding_plan.rs b/rust/src/providers/minimax/coding_plan.rs index 5169101e716..243672864cd 100644 --- a/rust/src/providers/minimax/coding_plan.rs +++ b/rust/src/providers/minimax/coding_plan.rs @@ -457,7 +457,7 @@ fn parse_resets_at_from_time_range( let dt = today.and_time(time).and_local_timezone(tz).single()?; let mut candidate = dt.with_timezone(&Utc); if candidate < now { - candidate = now + Duration::days(1); + candidate += Duration::days(1); } return Some(candidate); } @@ -697,6 +697,22 @@ mod tests { Utc.with_ymd_and_hms(2026, 8, 3, 12, 0, 0).unwrap() } + #[test] + fn past_clock_time_rolls_the_parsed_time_one_day() { + let now = Utc.with_ymd_and_hms(2026, 10, 28, 12, 34, 0).unwrap(); + let end = parse_resets_at_from_time_range("16:00-18:00(UTC+8)", "5 hours", now); + assert_eq!( + end, + Some(Utc.with_ymd_and_hms(2026, 10, 29, 10, 0, 0).unwrap()) + ); + + let later = parse_resets_at_from_time_range("16:00-22:00(UTC+8)", "5 hours", now); + assert_eq!( + later, + Some(Utc.with_ymd_and_hms(2026, 10, 28, 14, 0, 0).unwrap()) + ); + } + #[test] fn parses_remains_json_snake_case() { let json = serde_json::json!({ diff --git a/rust/src/providers/minimax/coding_plan_html.rs b/rust/src/providers/minimax/coding_plan_html.rs index 35bc47f205b..341d461d32a 100644 --- a/rust/src/providers/minimax/coding_plan_html.rs +++ b/rust/src/providers/minimax/coding_plan_html.rs @@ -269,7 +269,7 @@ fn date_for_time(time: &str, tz_hint: Option<&str>, now: DateTime) -> Optio let dt = today.and_time(time_only).and_local_timezone(tz).single()?; let mut candidate = dt.with_timezone(&Utc); if candidate < now { - candidate = now + Duration::days(1); + candidate += Duration::days(1); } Some(candidate) } @@ -479,6 +479,19 @@ mod tests { Utc.with_ymd_and_hms(2026, 8, 3, 12, 0, 0).unwrap() } + #[test] + fn past_clock_time_rolls_the_parsed_time_one_day() { + let now = Utc.with_ymd_and_hms(2026, 10, 28, 12, 34, 0).unwrap(); + assert_eq!( + date_for_time("10:00", Some("UTC"), now), + Some(Utc.with_ymd_and_hms(2026, 10, 29, 10, 0, 0).unwrap()) + ); + assert_eq!( + date_for_time("14:00", Some("UTC"), now), + Some(Utc.with_ymd_and_hms(2026, 10, 28, 14, 0, 0).unwrap()) + ); + } + #[test] fn parses_camelcase_next_data_html() { let entry = serde_json::json!({ diff --git a/rust/src/providers/opencode/mod.rs b/rust/src/providers/opencode/mod.rs index 5e714f024ba..edb1948807c 100755 --- a/rust/src/providers/opencode/mod.rs +++ b/rust/src/providers/opencode/mod.rs @@ -233,22 +233,24 @@ impl OpenCodeProvider { /// Rolling is preferred as primary; weekly-only becomes primary. fn snapshot_from_windows( &self, - rolling: Option<(f64, i64)>, - weekly: Option<(f64, i64)>, + rolling: Option<(f64, Option)>, + weekly: Option<(f64, Option)>, now: DateTime, renews_at: Option>, ) -> Option { + let reset_at = + |seconds: Option| seconds.map(|seconds| now + chrono::Duration::seconds(seconds)); let primary = match (rolling, weekly) { (Some(r), _) => RateWindow::with_details( r.0, Some(300), // 5 hours rolling - Some(now + chrono::Duration::seconds(r.1)), + reset_at(r.1), None, ), (None, Some(w)) => RateWindow::with_details( w.0, Some(10080), // weekly used as sole window - Some(now + chrono::Duration::seconds(w.1)), + reset_at(w.1), None, ), (None, None) => return None, @@ -261,7 +263,7 @@ impl OpenCodeProvider { usage = usage.with_secondary(RateWindow::with_details( w.0, Some(10080), - Some(now + chrono::Duration::seconds(w.1)), + reset_at(w.1), None, )); } @@ -321,7 +323,7 @@ impl OpenCodeProvider { } /// Find usage window in JSON by keys - fn find_usage_window(&self, json: &Value, keys: &[&str]) -> Option<(f64, i64)> { + fn find_usage_window(&self, json: &Value, keys: &[&str]) -> Option<(f64, Option)> { for key in keys { if let Some(obj) = json.get(key) && let Some(window) = self.parse_window(obj) @@ -350,15 +352,15 @@ impl OpenCodeProvider { } /// Parse a usage window object - fn parse_window(&self, obj: &Value) -> Option<(f64, i64)> { + fn parse_window(&self, obj: &Value) -> Option<(f64, Option)> { // Bare number: treat as percent used (0–1 fractions scaled). if let Some(n) = obj.as_f64() { let percent = if n <= 1.0 { n * 100.0 } else { n }; - return Some((percent.clamp(0.0, 100.0), 0)); + return Some((percent.clamp(0.0, 100.0), None)); } let percent = Self::window_percent(obj)?; - let reset_sec = Self::window_reset_seconds(obj).unwrap_or(0); - Some((percent.clamp(0.0, 100.0), reset_sec.max(0))) + let reset_sec = Self::window_reset_seconds(obj).map(|seconds| seconds.max(0)); + Some((percent.clamp(0.0, 100.0), reset_sec)) } fn window_percent(obj: &Value) -> Option { @@ -508,7 +510,11 @@ impl OpenCodeProvider { } /// Extract usage via regex patterns (JS-object and JSON-ish payloads). - fn extract_usage_regex(&self, text: &str, prefix: &str) -> Result<(f64, i64), ProviderError> { + fn extract_usage_regex( + &self, + text: &str, + prefix: &str, + ) -> Result<(f64, Option), ProviderError> { // Allow optional quotes around the window key and percent field names, // and accept the same percent aliases used by `window_percent`. let percent_pattern = format!( @@ -525,9 +531,7 @@ impl OpenCodeProvider { clippy::cast_possible_truncation, reason = "resetInSec values are whole-second counts scraped as integral numbers" )] - let reset = super::extract_number(&reset_pattern, text) - .map(|n| n as i64) - .unwrap_or(0); + let reset = super::extract_number(&reset_pattern, text).map(|n| n as i64); Ok((percent, reset)) } @@ -667,6 +671,42 @@ mod tests { ); } + #[test] + fn percent_without_reset_keeps_reset_unknown() { + let provider = OpenCodeProvider::new(); + let now = DateTime::::from_timestamp(1_700_000_000, 0).unwrap(); + let json = serde_json::json!({ + "rollingUsage": { "usagePercent": 10 }, + "weeklyUsage": { "usagePercent": 50, "resetInSec": 3600 } + }); + let snap = provider.parse_usage_json(&json, now).expect("snapshot"); + assert_eq!(snap.primary.used_percent, 10.0); + assert_eq!(snap.primary.resets_at, None); + assert_eq!( + snap.secondary.as_ref().unwrap().resets_at, + Some(now + chrono::Duration::seconds(3600)) + ); + + let bare = serde_json::json!({ "rollingUsage": 0.25 }); + let snap = provider.parse_usage_json(&bare, now).expect("snapshot"); + assert_eq!(snap.primary.used_percent, 25.0); + assert_eq!(snap.primary.resets_at, None); + + let text = "rollingUsage: { usagePercent: 10 }, weeklyUsage: { usagePercent: 50 }"; + let snap = provider.parse_subscription(text).expect("snapshot"); + assert_eq!(snap.primary.resets_at, None); + assert_eq!(snap.secondary.as_ref().unwrap().resets_at, None); + } + + #[test] + fn explicit_zero_reset_stays_a_reset() { + let provider = OpenCodeProvider::new(); + let now = DateTime::::from_timestamp(1_700_000_000, 0).unwrap(); + let json = serde_json::json!({ "rollingUsage": { "usagePercent": 10, "resetInSec": 0 } }); + let snap = provider.parse_usage_json(&json, now).expect("snapshot"); + assert_eq!(snap.primary.resets_at, Some(now)); + } + #[test] fn ignores_out_of_range_reset_timestamps() { let payload = serde_json::json!({ "resetAt": i64::MAX }); diff --git a/rust/src/providers/opencodego/local.rs b/rust/src/providers/opencodego/local.rs index 57777068642..09013973840 100644 --- a/rust/src/providers/opencodego/local.rs +++ b/rust/src/providers/opencodego/local.rs @@ -83,7 +83,8 @@ pub struct LocalUsageSnapshot { pub rolling_usage_percent: f64, pub weekly_usage_percent: f64, pub monthly_usage_percent: f64, - pub rolling_reset_in_sec: i64, + /// `None` when no usage falls inside the last five hours: no active session, no reset. + pub rolling_reset_in_sec: Option, pub weekly_reset_in_sec: i64, pub monthly_reset_in_sec: i64, } @@ -94,7 +95,8 @@ impl LocalUsageSnapshot { let primary = RateWindow::with_details( self.rolling_usage_percent, Some(300), - Some(now + Duration::seconds(self.rolling_reset_in_sec)), + self.rolling_reset_in_sec + .map(|seconds| now + Duration::seconds(seconds)), None, ); let mut snap = UsageSnapshot::new(primary).with_login_method("OpenCode Go"); @@ -300,8 +302,8 @@ fn snapshot_from_rows(rows: &[UsageRow], now: DateTime) -> LocalUsageSnapsh } } - let oldest = oldest_session_ms.unwrap_or(now_ms); - let rolling_reset_in_sec = ((oldest + FIVE_HOURS_MS - now_ms) / 1000).max(0); + let rolling_reset_in_sec = + oldest_session_ms.map(|oldest| ((oldest + FIVE_HOURS_MS - now_ms) / 1000).max(0)); LocalUsageSnapshot { rolling_usage_percent: percent(session_cost, SESSION_LIMIT_USD), @@ -612,7 +614,7 @@ mod tests { rolling_usage_percent: 12.0, weekly_usage_percent: 23.0, monthly_usage_percent: 34.0, - rolling_reset_in_sec: 300, + rolling_reset_in_sec: Some(300), weekly_reset_in_sec: 1_000, monthly_reset_in_sec: 2_000, } @@ -630,6 +632,34 @@ mod tests { assert!(result.usage.tertiary.as_ref().unwrap().resets_at.is_some()); } + #[test] + fn local_estimate_without_active_session_has_no_session_reset() { + let now = Utc::now(); + let old = now.timestamp_millis() - 90 * 24 * 60 * 60 * 1000; + let rows = [UsageRow { + created_ms: old, + cost: 3.0, + request_count: 1, + model: String::new(), + }]; + let result = snapshot_from_rows(&rows, now).to_fetch_result(); + assert_eq!( + result.source_label, + super::super::LOCAL_ESTIMATE_SOURCE_LABEL + ); + assert_eq!(result.usage.primary.used_percent, 0.0); + assert_eq!(result.usage.primary.resets_at, None); + + let recent = [UsageRow { + created_ms: now.timestamp_millis() - 60 * 60 * 1000, + cost: 3.0, + request_count: 1, + model: String::new(), + }]; + let result = snapshot_from_rows(&recent, now).to_fetch_result(); + assert!(result.usage.primary.resets_at.is_some()); + } + fn temp_db_path(label: &str) -> PathBuf { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/rust/src/providers/opencodego/mod.rs b/rust/src/providers/opencodego/mod.rs index 62d0ca09d89..42c5764df56 100644 --- a/rust/src/providers/opencodego/mod.rs +++ b/rust/src/providers/opencodego/mod.rs @@ -160,35 +160,32 @@ impl OpenCodeGoProvider { fn parse_usage_text(text: &str) -> Result { let now = Utc::now(); + let reset_at = + |seconds: Option| seconds.map(|seconds| now + chrono::Duration::seconds(seconds)); let rolling = Self::extract_window(text, &["rollingUsage", "rolling_usage", "rolling"]) .ok_or_else(|| ProviderError::Parse("Missing rolling usage window".to_string()))?; let weekly = Self::extract_window(text, &["weeklyUsage", "weekly_usage", "weekly"]); let monthly = Self::extract_window(text, &["monthlyUsage", "monthly_usage", "monthly"]); - let primary = RateWindow::with_details( - rolling.0, - Some(300), - Some(now + chrono::Duration::seconds(rolling.1)), - None, - ); + let primary = RateWindow::with_details(rolling.0, Some(300), reset_at(rolling.1), None); let mut snap = UsageSnapshot::new(primary).with_login_method("OpenCode Go"); if let Some((pct, reset)) = weekly { snap = snap.with_secondary(RateWindow::with_details( pct, Some(10080), - Some(now + chrono::Duration::seconds(reset)), + reset_at(reset), None, )); } if let Some((pct, reset)) = monthly { - let resets_at = now + chrono::Duration::seconds(reset); + let resets_at = reset_at(reset); snap = snap.with_tertiary(RateWindow::with_details( pct, - RateWindow::monthly_window_minutes(Some(resets_at)).or(Some(43200)), - Some(resets_at), + RateWindow::monthly_window_minutes(resets_at).or(Some(43200)), + resets_at, None, )); } @@ -204,8 +201,8 @@ impl OpenCodeGoProvider { Ok(snap) } - /// Extract `(percent, resetInSec)` for a usage block by name. - fn extract_window(text: &str, names: &[&str]) -> Option<(f64, i64)> { + /// Extract `(percent, resetInSec if present)` for a usage block by name. + fn extract_window(text: &str, names: &[&str]) -> Option<(f64, Option)> { for name in names { let percent_pattern = format!( r#"{}[^}}]*?(?:usagePercent|usedPercent|percentUsed|percent)\s*[:=]\s*([0-9]+(?:\.[0-9]+)?)"#, @@ -222,11 +219,9 @@ impl OpenCodeGoProvider { clippy::cast_possible_truncation, reason = "resetInSec values are whole-second counts scraped as integral numbers" )] - let reset = super::extract_number(&reset_pattern, text) - .map(|n| n as i64) - .unwrap_or(0); + let reset = super::extract_number(&reset_pattern, text).map(|n| n as i64); // Direct percent fields arrive as integer percent in the serialized payload; no fraction scaling (upstream parseSubscription parity; win-fork #247). - return Some((p.clamp(0.0, 100.0), reset.max(0))); + return Some((p.clamp(0.0, 100.0), reset.map(|seconds| seconds.max(0)))); } // Computed used/limit (already 0..100) — do not apply fraction *100. @@ -247,11 +242,9 @@ impl OpenCodeGoProvider { clippy::cast_possible_truncation, reason = "resetInSec values are whole-second counts scraped as integral numbers" )] - let reset = super::extract_number(&reset_pattern, text) - .map(|n| n as i64) - .unwrap_or(0); + let reset = super::extract_number(&reset_pattern, text).map(|n| n as i64); let p = (used / limit) * 100.0; - return Some((p.clamp(0.0, 100.0), reset.max(0))); + return Some((p.clamp(0.0, 100.0), reset.map(|seconds| seconds.max(0)))); } } None @@ -793,6 +786,25 @@ fn billing_numeric_value(value: &serde_json::Value) -> Option { mod tests { use super::*; + #[test] + fn percent_without_reset_keeps_reset_unknown() { + let text = "rollingUsage: { usagePercent: 10 }, weeklyUsage: { usagePercent: 50, resetInSec: 3600 }, monthlyUsage: { usagePercent: 5 }"; + let snap = OpenCodeGoProvider::parse_usage_text(text).expect("snapshot"); + assert_eq!(snap.primary.used_percent, 10.0); + assert_eq!(snap.primary.resets_at, None); + assert!(snap.secondary.as_ref().unwrap().resets_at.is_some()); + let monthly = snap.tertiary.as_ref().unwrap(); + assert_eq!(monthly.used_percent, 5.0); + assert_eq!(monthly.resets_at, None); + } + + #[test] + fn explicit_zero_reset_stays_a_reset() { + let text = "rollingUsage: { usagePercent: 10, resetInSec: 0 }"; + let snap = OpenCodeGoProvider::parse_usage_text(text).expect("snapshot"); + assert!(snap.primary.resets_at.is_some()); + } + #[test] fn parses_workspace_ids() { let text = r#"{ id: "wrk_abc123", name: "x" } { id: "wrk_def456" }"#; diff --git a/rust/src/providers/sub2api/mod.rs b/rust/src/providers/sub2api/mod.rs index e738e8700bc..d0207b84136 100644 --- a/rust/src/providers/sub2api/mod.rs +++ b/rust/src/providers/sub2api/mod.rs @@ -660,12 +660,7 @@ fn snapshot_from_parsed(parsed: ParsedUsage) -> ProviderFetchResult { snapshot = snapshot.with_extra_rate_window( "expires", "Expires", - RateWindow::with_details( - 0.0, - None, - Some(expires_at), - Some(expires_at.format("%Y-%m-%d").to_string()), - ), + RateWindow::informational(expires_at.format("%Y-%m-%d").to_string()), ); } @@ -912,6 +907,37 @@ mod tests { ); } + #[test] + fn expiry_is_information_not_a_quota_window() { + let json = r#" + { + "mode": "unrestricted", + "isValid": true, + "subscription": { + "daily_usage_usd": 2, + "daily_limit_usd": 10, + "expires_at": "2026-08-15T00:00:00Z" + } + } + "#; + + let result = snapshot_from_parsed(parse_usage_body(json).unwrap()); + let expires = result + .usage + .extra_rate_windows + .iter() + .find(|w| w.id == "expires") + .expect("expires window"); + assert!(expires.window.is_informational); + assert_eq!(expires.window.resets_at, None); + assert_eq!( + expires.window.reset_description.as_deref(), + Some("2026-08-15") + ); + assert!(!result.usage.primary.is_informational); + assert!((result.usage.primary.used_percent - 20.0).abs() < f64::EPSILON); + } + #[test] fn subscription_without_limits_shows_usage_not_empty() { let json = r#" From 50b9e9bd11bd84553a77be6765be77789bebd6fc Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:33:55 +0100 Subject: [PATCH 02/75] Mark Azure OpenAI deployment status as informational --- rust/src/providers/azureopenai.rs | 35 ++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/rust/src/providers/azureopenai.rs b/rust/src/providers/azureopenai.rs index 35585618f4f..eb2bf50e9b5 100644 --- a/rust/src/providers/azureopenai.rs +++ b/rust/src/providers/azureopenai.rs @@ -89,15 +89,20 @@ impl AzureOpenAIProvider { let parsed: ChatCompletionResponse = serde_json::from_slice(&bytes) .map_err(|e| ProviderError::Parse(format!("Azure OpenAI response parse error: {e}")))?; - let mut primary = RateWindow::new(0.0); - primary.reset_description = Some(Self::detail_text( + Ok(Self::snapshot_from_response(&config, &parsed)) + } + + fn snapshot_from_response( + config: &AzureOpenAIConfig, + parsed: &ChatCompletionResponse, + ) -> UsageSnapshot { + let primary = RateWindow::informational(Self::detail_text( &config.deployment, parsed.model.as_deref(), )); - let usage = UsageSnapshot::new(primary) + UsageSnapshot::new(primary) .with_organization(config.endpoint.host_str().unwrap_or_default().to_string()) - .with_login_method(format!("Deployment: {}", config.deployment)); - Ok(usage) + .with_login_method(format!("Deployment: {}", config.deployment)) } fn resolve_config(ctx: &FetchContext) -> Result { @@ -364,3 +369,23 @@ mod tests { assert!(AzureOpenAIProvider::parse_endpoint("https://example.com%2f.evil.test").is_err()); } } + +#[cfg(test)] +mod cx289b_tests { + use super::*; + + #[test] + fn deployment_without_quota_is_informational() { + let config = AzureOpenAIProvider::parse_saved_config( + "synthetic-key|https://example.openai.azure.com|test-deployment", + ) + .unwrap(); + for json in ["{}", r#"{"model":"test-model"}"#] { + let parsed = serde_json::from_str(json).unwrap(); + let usage = AzureOpenAIProvider::snapshot_from_response(&config, &parsed); + assert!(usage.primary.is_informational); + assert!(usage.primary.window_minutes.is_none()); + assert!(usage.primary.resets_at.is_none()); + } + } +} From 0376c2fb7eea62592215ce7dd72fb446e782b30c Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:33:55 +0100 Subject: [PATCH 03/75] Keep Bedrock spend without a budget informational --- rust/src/providers/bedrock/mod.rs | 77 +++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/rust/src/providers/bedrock/mod.rs b/rust/src/providers/bedrock/mod.rs index ddf613628f9..60b05ca54d0 100644 --- a/rust/src/providers/bedrock/mod.rs +++ b/rust/src/providers/bedrock/mod.rs @@ -429,6 +429,29 @@ impl BedrockProvider { } } + fn spend_window( + spend: f64, + budget: Option, + resets_at: Option>, + ) -> RateWindow { + match budget { + Some(limit) if limit > 0.0 => RateWindow::with_details( + (spend / limit) * 100.0, + None, + resets_at, + Some("Monthly budget".to_string()), + ), + _ => RateWindow::informational(format!("Monthly spend ${spend:.2}")), + } + } + + fn activity_window(activity: &BedrockClaudeActivity) -> RateWindow { + RateWindow::informational(format!( + "Claude 14d: {:.0} input, {:.0} output tokens, {:.0} requests", + activity.input_tokens, activity.output_tokens, activity.request_count + )) + } + async fn fetch_via_api( &self, ctx: &FetchContext, @@ -438,25 +461,7 @@ impl BedrockProvider { let spend = self.fetch_monthly_spend(&credentials).await?; let resets_at = end_of_current_month(); - let used_percent = budget - .map(|limit| { - if limit > 0.0 { - (spend / limit) * 100.0 - } else { - 0.0 - } - }) - .unwrap_or(0.0); - - let mut primary = RateWindow::with_details( - used_percent, - None, - resets_at, - budget.map(|_| "Monthly budget".to_string()), - ); - if budget.is_none() { - primary.reset_description = Some(format!("Monthly spend ${spend:.2}")); - } + let primary = Self::spend_window(spend, budget, resets_at); let mut cost = CostSnapshot::new(spend, "USD", "Monthly"); if let Some(limit) = budget { @@ -482,11 +487,7 @@ impl BedrockProvider { if let Ok(activity) = self.fetch_claude_activity(&credentials, ®ion).await && activity.request_count > 0.0 { - let mut window = RateWindow::new(0.0); - window.reset_description = Some(format!( - "Claude 14d: {:.0} input, {:.0} output tokens, {:.0} requests", - activity.input_tokens, activity.output_tokens, activity.request_count - )); + let window = Self::activity_window(&activity); usage = usage.with_extra_rate_window("claude-14d", "Claude 14d activity", window); } Ok(ProviderFetchResult::new(usage, "api").with_cost(cost)) @@ -911,3 +912,31 @@ mod tests { ); } } + +#[cfg(test)] +mod cx289b_tests { + use super::*; + #[test] + fn spend_without_budget_is_informational() { + for spend in [0.0, 12.5] { + let window = BedrockProvider::spend_window(spend, None, end_of_current_month()); + assert!(window.is_informational); + assert!(window.resets_at.is_none()); + } + } + #[test] + fn activity_without_denominator_is_informational() { + let activity = BedrockClaudeActivity { + input_tokens: 10.0, + output_tokens: 20.0, + request_count: 1.0, + }; + assert!(BedrockProvider::activity_window(&activity).is_informational); + } + #[test] + fn real_zero_spend_with_budget_stays_quota() { + let window = BedrockProvider::spend_window(0.0, Some(100.0), end_of_current_month()); + assert!(!window.is_informational); + assert_eq!(window.used_percent, 0.0); + } +} From 1903bb14f0a4b2ebcc8a7d960d45ee89b81bb8a2 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:33:55 +0100 Subject: [PATCH 04/75] Keep Claude Admin costs and token counts informational --- rust/src/providers/claude/admin_api.rs | 80 ++++++++++++++++---------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/rust/src/providers/claude/admin_api.rs b/rust/src/providers/claude/admin_api.rs index e56f0cf907c..1df4b1c4e72 100644 --- a/rust/src/providers/claude/admin_api.rs +++ b/rust/src/providers/claude/admin_api.rs @@ -138,7 +138,6 @@ struct CostReportResponse { #[derive(Debug, Deserialize)] struct CostBucket { - starting_at: String, #[allow( dead_code, reason = "admin API response fields are deserialized for parsing but not all are read" @@ -161,7 +160,6 @@ struct MessagesUsageResponse { #[derive(Debug, Deserialize)] struct MessageBucket { - starting_at: String, #[allow( dead_code, reason = "admin API response fields are deserialized for parsing but not all are read" @@ -215,39 +213,19 @@ fn result_from_admin_usage( .map(|r| r.output_tokens.unwrap_or(0)) .sum(); let total_tokens = input_tokens + output_tokens; - let start = costs - .data - .first() - .and_then(|b| DateTime::parse_from_rfc3339(&b.starting_at).ok()) - .or_else(|| { - messages - .data - .first() - .and_then(|b| DateTime::parse_from_rfc3339(&b.starting_at).ok()) - }) - .map(|dt| dt.with_timezone(&Utc)); - - let mut usage = UsageSnapshot::new(RateWindow::with_details( - 0.0, - None, - start, - Some(format!("${cost_total:.2} over last 30 days")), - )) - .with_secondary(RateWindow::with_details( - 0.0, - None, - None, - Some(format!("{total_tokens} tokens")), - )) + let mut usage = UsageSnapshot::new(RateWindow::informational(format!( + "${cost_total:.2} over last 30 days" + ))) + .with_secondary(RateWindow::informational(format!("{total_tokens} tokens"))) .with_extra_rate_window( "input-tokens", "Input tokens", - RateWindow::with_details(0.0, None, None, Some(format!("{input_tokens}"))), + RateWindow::informational(format!("{input_tokens}")), ) .with_extra_rate_window( "output-tokens", "Output tokens", - RateWindow::with_details(0.0, None, None, Some(format!("{output_tokens}"))), + RateWindow::informational(format!("{output_tokens}")), ) .with_login_method("Admin API"); usage.updated_at = now; @@ -276,7 +254,7 @@ fn result_from_admin_usage( usage = usage.with_extra_rate_window( format!("model-{idx}"), format!("Model: {model}"), - RateWindow::with_details(0.0, None, None, Some(format!("{tokens} tokens"))), + RateWindow::informational(format!("{tokens} tokens")), ); } @@ -297,7 +275,7 @@ fn result_from_admin_usage( usage = usage.with_extra_rate_window( format!("cost-{idx}"), format!("Cost: {item}"), - RateWindow::with_details(0.0, None, None, Some(format!("${cost:.2}"))), + RateWindow::informational(format!("${cost:.2}")), ); } @@ -324,3 +302,45 @@ mod tests { ); } } + +#[cfg(test)] +mod cx289b_tests { + use super::*; + use serde_json::json; + + #[test] + fn admin_costs_and_tokens_without_quota_are_informational() { + let costs = serde_json::from_value(json!({"data":[{ + "starting_at":"2026-10-01T00:00:00Z", "ending_at":"2026-10-02T00:00:00Z", + "results":[{"amount":"1250", "description":"API"}] + }]})) + .unwrap(); + let messages = serde_json::from_value(json!({"data":[{ + "starting_at":"2026-10-01T00:00:00Z", "ending_at":"2026-10-02T00:00:00Z", + "results":[{"model":"synthetic", "uncached_input_tokens":100, "output_tokens":50}] + }]})) + .unwrap(); + let result = result_from_admin_usage(&costs, &messages, Utc::now()); + assert!(result.usage.primary.is_informational); + assert!(result.usage.primary.resets_at.is_none()); + assert!(result.usage.secondary.unwrap().is_informational); + assert!( + result + .usage + .extra_rate_windows + .iter() + .all(|w| w.window.is_informational) + ); + assert!(result.cost.is_some()); + } + #[test] + fn empty_admin_reports_are_not_a_zero_quota() { + let result = result_from_admin_usage( + &CostReportResponse { data: vec![] }, + &MessagesUsageResponse { data: vec![] }, + Utc::now(), + ); + assert!(result.usage.primary.is_informational); + assert!(result.usage.secondary.unwrap().is_informational); + } +} From f58958b97087ea8388b675fdbed69b16107c3ac6 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:39:49 +0100 Subject: [PATCH 05/75] Dashboard: mark informational and unknown windows as unavailable The dashboard window payload carried 0 % used / 100 % remaining for an informational primary and for an Antigravity lane without usage, with no way to tell them from a real zero. Add an additive `unavailable` key (omitted when false) and let the built-in web UI skip those rows (Marcus's P3 on #15, VibeTV #289). --- rust/src/cli/serve/dashboard/antigravity.rs | 6 ++- rust/src/cli/serve/dashboard/dashboard.html | 2 +- rust/src/cli/serve/dashboard/html.rs | 2 +- .../src/cli/serve/dashboard/snapshot_tests.rs | 43 +++++++++++++++++++ rust/src/cli/serve/dashboard/window.rs | 5 +++ 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/rust/src/cli/serve/dashboard/antigravity.rs b/rust/src/cli/serve/dashboard/antigravity.rs index 6b2bb2cdf40..0d0e0ba0508 100644 --- a/rust/src/cli/serve/dashboard/antigravity.rs +++ b/rust/src/cli/serve/dashboard/antigravity.rs @@ -41,12 +41,14 @@ pub(super) fn quota_summary_windows( )); } windows.extend(usage.extra_rate_windows.iter().map(|extra| { - make_window_with_idle( + let mut window = make_window_with_idle( &extra.id, &extra.title, &extra.window, idle_ids.contains(&extra.id), - ) + ); + window.unavailable |= !extra.usage_known; + window })); windows } diff --git a/rust/src/cli/serve/dashboard/dashboard.html b/rust/src/cli/serve/dashboard/dashboard.html index 72507fe776a..709448dc518 100644 --- a/rust/src/cli/serve/dashboard/dashboard.html +++ b/rust/src/cli/serve/dashboard/dashboard.html @@ -129,7 +129,7 @@

CodexBar

return "bar"; } function visibleWindows(windows) { - return (windows || []).filter(w => w.idle !== true); + return (windows || []).filter(w => w.idle !== true && w.unavailable !== true); } function windowRow(w) { const used = Number.isFinite(w.usedPercent) ? Math.max(0, Math.min(100, w.usedPercent)) : 0; diff --git a/rust/src/cli/serve/dashboard/html.rs b/rust/src/cli/serve/dashboard/html.rs index 37aeb5dc372..57aca40ce42 100644 --- a/rust/src/cli/serve/dashboard/html.rs +++ b/rust/src/cli/serve/dashboard/html.rs @@ -58,7 +58,7 @@ mod tests { fn web_ui_skips_windows_marked_idle() { let html = render_shell(60); assert!(html.contains("function visibleWindows(windows)")); - assert!(html.contains("w.idle !== true")); + assert!(html.contains("w.idle !== true && w.unavailable !== true")); assert!(html.contains("visibleWindows(account.windows).forEach")); assert!(html.contains("visibleWindows(row.windows).forEach")); } diff --git a/rust/src/cli/serve/dashboard/snapshot_tests.rs b/rust/src/cli/serve/dashboard/snapshot_tests.rs index a4a8265c6d0..c72bfd2c8f5 100644 --- a/rust/src/cli/serve/dashboard/snapshot_tests.rs +++ b/rust/src/cli/serve/dashboard/snapshot_tests.rs @@ -317,6 +317,49 @@ mod tests { assert!(windows.iter().all(|window| window.get("idle").is_none())); } + #[test] + fn antigravity_dashboard_marks_unknown_lane_unavailable() { + let windows = vec![ + NamedRateWindow::new("model-gemini", "Gemini Pro", RateWindow::new(20.0)), + NamedRateWindow::new("model-claude", "Claude Sonnet", RateWindow::new(0.0)) + .with_usage_known(false), + NamedRateWindow::new("model-gpt", "GPT", RateWindow::new(0.0)), + ]; + let json = serde_json::to_value(build_snapshot(&input( + vec![antigravity_envelope(windows)], + DashboardIdentity::Redacted, + ))) + .unwrap(); + let windows = json["providers"][0]["windows"].as_array().unwrap(); + let unavailable: Vec<&str> = windows + .iter() + .filter(|window| window["unavailable"] == true) + .map(|window| window["label"].as_str().unwrap()) + .collect(); + assert_eq!(unavailable, vec!["Claude Sonnet"]); + } + + #[test] + fn informational_window_is_unavailable_and_a_real_zero_is_not() { + let usage = UsageSnapshot::new(RateWindow::informational("No primary quota")) + .with_secondary(RateWindow::new(0.0)); + let json = serde_json::to_value(build_snapshot(&input( + vec![ProviderFetchEnvelope { + id: "windsurf".to_string(), + display_name: "Windsurf".to_string(), + session_label: "Daily".to_string(), + weekly_label: "Weekly".to_string(), + fetch: Ok(ProviderFetchResult::new(usage, "local")), + }], + DashboardIdentity::Redacted, + ))) + .unwrap(); + let windows = json["providers"][0]["windows"].as_array().unwrap(); + assert_eq!(windows[0]["unavailable"], true); + assert!(windows[1].get("unavailable").is_none()); + assert_eq!(windows[1]["usedPercent"], 0.0); + } + #[test] fn antigravity_dashboard_keeps_all_families_after_global_reset() { let windows = vec![ diff --git a/rust/src/cli/serve/dashboard/window.rs b/rust/src/cli/serve/dashboard/window.rs index b8eebb26acc..790a19cc11a 100644 --- a/rust/src/cli/serve/dashboard/window.rs +++ b/rust/src/cli/serve/dashboard/window.rs @@ -13,6 +13,10 @@ pub struct WindowPayload { /// Display-only hint. Script clients can ignore this additive schema-v1 key. #[serde(skip_serializing_if = "is_false")] pub idle: bool, + /// Informational or unknown lane: `used_percent` / `remaining_percent` are + /// placeholders, not a measurement. Additive schema-v1 key. + #[serde(skip_serializing_if = "is_false")] + pub unavailable: bool, } fn is_false(value: &bool) -> bool { @@ -33,5 +37,6 @@ pub(super) fn make_window_with_idle( remaining_percent: (100.0 - used).clamp(0.0, 100.0), reset_at: window.resets_at, idle, + unavailable: window.is_informational, } } From 822cefe35ca131ca340a1507e57838cd48735dcb Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:33:55 +0100 Subject: [PATCH 06/75] Report Crof balance as informational --- rust/src/providers/crof/mod.rs | 61 ++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/rust/src/providers/crof/mod.rs b/rust/src/providers/crof/mod.rs index 25b69904886..9f570bb9793 100644 --- a/rust/src/providers/crof/mod.rs +++ b/rust/src/providers/crof/mod.rs @@ -104,20 +104,22 @@ fn snapshot_from_usage(usage: &CrofUsageResponse) -> UsageSnapshot { } else { format!("${credits:.4}") }; - let mut primary = RateWindow::new(if credits > 0.0 { 0.0 } else { 100.0 }); - primary.reset_description = Some(display.clone()); + let primary = RateWindow::informational(display.clone()); let mut snapshot = UsageSnapshot::new(primary).with_login_method(format!("{display} balance")); if let (Some(plan), Some(usable)) = (usage.requests_plan, usage.usable_requests) { - let remaining = usable.max(0.0).min(plan.max(0.0)); - let remaining_percent = if plan > 0.0 { - ((remaining / plan) * 100.0).clamp(0.0, 100.0) + let requests = if plan > 0.0 { + let remaining = usable.max(0.0).min(plan); + RateWindow::with_details( + 100.0 - ((remaining / plan) * 100.0).clamp(0.0, 100.0), + None, + None, + Some(format!("{remaining:.0} requests left")), + ) } else { - 0.0 + RateWindow::informational(format!("{:.0} requests left", usable.max(0.0))) }; - let mut requests = RateWindow::new(100.0 - remaining_percent); - requests.reset_description = Some(format!("{remaining:.0} requests left")); snapshot = snapshot.with_secondary(requests); } @@ -236,13 +238,52 @@ mod tests { } #[test] - fn crof_zero_balance_is_exhausted() { + fn crof_zero_balance_is_informational() { let snapshot = snapshot_from_usage(&CrofUsageResponse { credits: 0.0, requests_plan: None, usable_requests: None, }); - assert_eq!(snapshot.primary.used_percent, 100.0); + assert!(snapshot.primary.is_informational); assert_eq!(snapshot.primary.reset_description.as_deref(), Some("$0.00")); } } + +#[cfg(test)] +mod cx289b_tests { + use super::*; + #[test] + fn balance_without_quota_is_informational() { + for credits in [0.0, 12.5] { + let snapshot = snapshot_from_usage(&CrofUsageResponse { + credits, + requests_plan: None, + usable_requests: None, + }); + assert!(snapshot.primary.is_informational); + assert!(snapshot.secondary.is_none()); + } + } + #[test] + fn request_count_without_positive_plan_is_informational() { + for plan in [0.0, -1.0] { + let snapshot = snapshot_from_usage(&CrofUsageResponse { + credits: 12.5, + requests_plan: Some(plan), + usable_requests: Some(10.0), + }); + assert!(snapshot.secondary.unwrap().is_informational); + } + } + #[test] + fn real_zero_requests_stays_quota() { + let snapshot = snapshot_from_usage(&CrofUsageResponse { + credits: 12.5, + requests_plan: Some(100.0), + usable_requests: Some(100.0), + }); + let window = snapshot.secondary.unwrap(); + assert!(!window.is_informational); + assert_eq!(window.used_percent, 0.0); + } +} From f491e4118ae044e65daf70298b41c9b0023771ba Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:33:55 +0100 Subject: [PATCH 07/75] Keep CrossModel balances and costs informational --- rust/src/providers/crossmodel/mod.rs | 59 +++++++++++++++++++--------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/rust/src/providers/crossmodel/mod.rs b/rust/src/providers/crossmodel/mod.rs index 0f23cdb7305..0f0fd536f12 100644 --- a/rust/src/providers/crossmodel/mod.rs +++ b/rust/src/providers/crossmodel/mod.rs @@ -158,22 +158,13 @@ fn snapshot_from_parts( let currency = normalize_currency(&credits.currency); let balance = major_units(credits.balance_micro); let uncollected = major_units(credits.uncollected_micro); - let mut primary = usage + let primary = usage .as_ref() .and_then(|u| usage_window_rate(u.daily.as_ref(), "Daily")) .unwrap_or_else(|| { - RateWindow::with_details( - 0.0, - Some(24 * 60), - None, - Some(format!("{} balance", format_amount(balance, ¤cy))), - ) + RateWindow::informational(format!("{} balance", format_amount(balance, ¤cy))) }); - if primary.reset_description.is_none() { - primary.reset_description = Some("Daily usage".to_string()); - } - let mut snapshot = UsageSnapshot::new(primary).with_login_method("API key"); if let Some(usage) = usage .as_ref() @@ -190,7 +181,7 @@ fn snapshot_from_parts( snapshot = snapshot.with_extra_rate_window( "uncollected", "Uncollected", - RateWindow::with_details(0.0, None, None, Some(format_amount(uncollected, ¤cy))), + RateWindow::informational(format_amount(uncollected, ¤cy)), ); } @@ -217,12 +208,7 @@ fn usage_window_rate(window: Option<&UsageWindow>, label: &str) -> Option f64 { @@ -315,3 +301,40 @@ mod tests { ); } } + +#[cfg(test)] +mod cx289b_tests { + use super::*; + use serde_json::json; + #[test] + fn balance_without_quota_is_informational() { + let credits = serde_json::from_value( + json!({"currency":"USD", "balanceMicro":12500000, "uncollectedMicro":1000000}), + ) + .unwrap(); + let result = snapshot_from_parts(credits, None); + assert!(result.usage.primary.is_informational); + assert!(result.usage.primary.window_minutes.is_none()); + assert!( + result + .usage + .extra_rate_windows + .iter() + .all(|w| w.window.is_informational) + ); + } + #[test] + fn costs_and_zero_counters_without_quota_are_informational() { + let credits = serde_json::from_value(json!({"currency":"USD", "balanceMicro":0})).unwrap(); + let usage = serde_json::from_value(json!({"currency":"USD", + "daily":{"costMicro":0,"requestCount":0}, + "weekly":{"costMicro":1000000,"totalTokens":50}, + "monthly":{"costMicro":2000000} + })) + .unwrap(); + let result = snapshot_from_parts(credits, Some(usage)); + assert!(result.usage.primary.is_informational); + assert!(result.usage.secondary.unwrap().is_informational); + assert!(result.usage.tertiary.unwrap().is_informational); + } +} From ff69ea7c1a6df7ac84db3be565ba48c134fb07bf Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:33:55 +0100 Subject: [PATCH 08/75] Keep Deepgram usage counters informational --- rust/src/providers/deepgram/mod.rs | 32 ++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/rust/src/providers/deepgram/mod.rs b/rust/src/providers/deepgram/mod.rs index eed4bfec4af..8eb21b0577e 100644 --- a/rust/src/providers/deepgram/mod.rs +++ b/rust/src/providers/deepgram/mod.rs @@ -282,18 +282,15 @@ fn aggregate_summaries( } fn snapshot_from_summary(summary: &DeepgramUsageSummary) -> UsageSnapshot { - let mut primary = RateWindow::new(0.0); - primary.reset_description = Some(format!("{} requests", format_count(summary.requests))); + let primary = RateWindow::informational(format!("{} requests", format_count(summary.requests))); - let mut secondary = RateWindow::new(0.0); - secondary.reset_description = Some(format!( + let secondary = RateWindow::informational(format!( "{} audio hours / {} billable hours", format_decimal(summary.hours), format_decimal(summary.total_hours) )); - let mut tertiary = RateWindow::new(0.0); - tertiary.reset_description = Some(format!( + let tertiary = RateWindow::informational(format!( "{} tokens / {} TTS chars", format_count(summary.tokens_in + summary.tokens_out), format_count(summary.tts_characters) @@ -417,3 +414,26 @@ mod tests { assert_eq!(snapshot.login_method.as_deref(), Some("Project: Prod")); } } + +#[cfg(test)] +mod cx289b_tests { + use super::*; + #[test] + fn counters_without_quota_are_informational() { + let project = Project { + project_id: "synthetic".into(), + name: None, + }; + for raw in [ + "{}", + r#"{"results":[{"requests":0,"tokens_in":0,"tokens_out":0,"hours":0,"total_hours":0,"tts_characters":0}]}"#, + r#"{"results":[{"requests":10,"tokens_in":100,"tokens_out":50,"hours":2,"total_hours":3,"tts_characters":1000}]}"#, + ] { + let usage: UsageResponse = serde_json::from_str(raw).unwrap(); + let snapshot = snapshot_from_summary(&summary_from_usage(&project, &usage)); + assert!(snapshot.primary.is_informational); + assert!(snapshot.secondary.unwrap().is_informational); + assert!(snapshot.tertiary.unwrap().is_informational); + } + } +} From fc638f525730caae92555540e4c4d6e5ce8af52e Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:42:19 +0100 Subject: [PATCH 09/75] Keep Ollama model lists and missing primary informational --- rust/src/providers/ollama/mod.rs | 48 +++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/rust/src/providers/ollama/mod.rs b/rust/src/providers/ollama/mod.rs index bd33693ec66..c6df89f7594 100755 --- a/rust/src/providers/ollama/mod.rs +++ b/rust/src/providers/ollama/mod.rs @@ -203,9 +203,8 @@ impl OllamaProvider { let response: TagsResponse = serde_json::from_slice(bytes) .map_err(|e| ProviderError::Parse(format!("Could not parse Ollama API tags: {e}")))?; - let mut primary = RateWindow::new(0.0); - primary.reset_description = - Some(format!("{} cloud models available", response.models.len())); + let primary = + RateWindow::informational(format!("{} cloud models available", response.models.len())); Ok(UsageSnapshot::new(primary).with_login_method("API key")) } } @@ -464,7 +463,7 @@ fn rate_window_from_usage_block(block: Option<&UsageBlock>) -> RateWindow { block.reset_description.clone(), ) }) - .unwrap_or_else(|| RateWindow::new(0.0)) + .unwrap_or_else(|| RateWindow::informational("No Ollama primary usage reported")) } fn parse_first_datetime(html: &str) -> Option> { @@ -840,3 +839,44 @@ mod tests { ); } } + +#[cfg(test)] +mod informational_tests { + use super::*; + #[test] + fn model_list_is_information() { + for body in [ + br#"{"models":[]}"#.as_slice(), + br#"{"models":[{"name":"test"}]}"#.as_slice(), + ] { + assert!( + OllamaProvider::parse_api_tags(body) + .unwrap() + .primary + .is_informational + ); + } + assert!(OllamaProvider::parse_api_tags(b"{}").is_err()); + } + #[test] + fn weekly_only_does_not_invent_primary() { + let usage = OllamaProvider::new() + .parse_usage_html("Weekly usage 25% used") + .unwrap(); + assert!(usage.primary.is_informational); + assert_eq!(usage.secondary.unwrap().used_percent, 25.0); + } + #[test] + fn real_zero_dollar_quota_stays_available() { + let usage = OllamaProvider::new() + .parse_usage_html("Session usage $0 of $100 used") + .unwrap(); + assert!(!usage.primary.is_informational); + assert_eq!(usage.primary.used_percent, 0.0); + assert!( + OllamaProvider::new() + .parse_usage_html("") + .is_err() + ); + } +} From e1c2573f52f00324f5dc0f25c3b05d68dd9ac43f Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:42:19 +0100 Subject: [PATCH 10/75] Keep OpenAI API balances without totals and costs informational --- rust/src/providers/openaiapi/mod.rs | 104 ++++++++++++++++------------ 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/rust/src/providers/openaiapi/mod.rs b/rust/src/providers/openaiapi/mod.rs index 004025eace6..8f304163ca1 100644 --- a/rust/src/providers/openaiapi/mod.rs +++ b/rust/src/providers/openaiapi/mod.rs @@ -44,12 +44,6 @@ struct CostsResponse { #[derive(Debug, Deserialize)] struct CostBucket { - start_time: i64, - #[allow( - dead_code, - reason = "field present in the OpenAI API billing payload; kept so serde preserves it" - )] - end_time: i64, results: Vec, } @@ -71,12 +65,6 @@ struct CompletionsUsageResponse { #[derive(Debug, Deserialize)] struct CompletionsUsageBucket { - start_time: i64, - #[allow( - dead_code, - reason = "field present in the OpenAI API billing payload; kept so serde preserves it" - )] - end_time: i64, results: Vec, } @@ -246,13 +234,6 @@ impl OpenAIApiProvider { } fn result_from_grants(grants: &CreditGrantsResponse) -> ProviderFetchResult { - let used_percent = if grants.total_granted > 0.0 { - grants.total_used / grants.total_granted * 100.0 - } else if grants.total_available > 0.0 { - 0.0 - } else { - 100.0 - }; let next_expiry = grants.grants.as_ref().and_then(|list| { list.data .iter() @@ -262,15 +243,17 @@ fn result_from_grants(grants: &CreditGrantsResponse) -> ProviderFetchResult { .min() }); - let mut primary = RateWindow::with_details( - used_percent, - None, - next_expiry, - Some(format!("${:.2} available", grants.total_available.max(0.0))), - ); - if grants.total_granted <= 0.0 && grants.total_available > 0.0 { - primary.used_percent = 0.0; - } + let detail = format!("${:.2} available", grants.total_available.max(0.0)); + let primary = if grants.total_granted > 0.0 { + RateWindow::with_details( + grants.total_used / grants.total_granted * 100.0, + None, + next_expiry, + Some(detail), + ) + } else { + RateWindow::informational(detail) + }; let usage = UsageSnapshot::new(primary).with_login_method(format!( "API balance: ${:.2}", @@ -323,13 +306,6 @@ fn result_from_admin_usage( }) .sum(); - let first_bucket = costs - .data - .first() - .map(|b| b.start_time) - .or_else(|| completions.data.first().map(|b| b.start_time)); - let start = first_bucket.and_then(|ts| Utc.timestamp_opt(ts, 0).single()); - let mut model_tokens: HashMap = HashMap::new(); for result in completions.data.iter().flat_map(|bucket| &bucket.results) { let model = result @@ -361,12 +337,9 @@ fn result_from_admin_usage( .unwrap_or(0.0); } - let mut usage = UsageSnapshot::new(RateWindow::with_details( - 0.0, - None, - start, - Some(format!("${cost_total:.2} over last 30 days")), - )) + let mut usage = UsageSnapshot::new(RateWindow::informational(format!( + "${cost_total:.2} over last 30 days" + ))) .with_extra_rate_window( "requests", "Requests", @@ -579,8 +552,6 @@ mod tests { fn openai_admin_usage_accepts_numeric_string_cost_amounts() { let costs = CostsResponse { data: vec![CostBucket { - start_time: 1_797_638_400, - end_time: 1_797_724_800, results: vec![CostResult { amount: Some(CostAmount { value: serde_json::json!("12.50"), @@ -591,8 +562,6 @@ mod tests { }; let completions = CompletionsUsageResponse { data: vec![CompletionsUsageBucket { - start_time: 1_797_638_400, - end_time: 1_797_724_800, results: vec![CompletionsUsageResult { model: Some("gpt-5.1".to_string()), input_tokens: Some(100), @@ -677,3 +646,48 @@ mod tests { assert_eq!(response_error_detail(" "), ""); } } + +#[cfg(test)] +mod informational_tests { + use super::*; + #[test] + fn balance_without_denominator_is_information() { + for available in [0.0, 20.0] { + let result = result_from_grants(&CreditGrantsResponse { + total_granted: 0.0, + total_used: 0.0, + total_available: available, + grants: None, + }); + assert!(result.usage.primary.is_informational); + } + assert!(serde_json::from_str::("{}").is_err()); + } + #[test] + fn admin_costs_are_information_without_quota_reset() { + let costs = serde_json::from_str( + r#"{"data":[{"start_time":1000,"end_time":2000,"results":[{"amount":{"value":5}}]}]}"#, + ) + .unwrap(); + let result = result_from_admin_usage( + &costs, + &CompletionsUsageResponse { data: vec![] }, + Utc::now(), + None, + ); + assert!(result.usage.primary.is_informational); + assert!(result.usage.primary.resets_at.is_none()); + assert_eq!(result.cost.unwrap().used, 5.0); + } + #[test] + fn real_zero_grant_usage_stays_available() { + let result = result_from_grants(&CreditGrantsResponse { + total_granted: 100.0, + total_used: 0.0, + total_available: 100.0, + grants: None, + }); + assert!(!result.usage.primary.is_informational); + assert_eq!(result.usage.primary.used_percent, 0.0); + } +} From d700bb2d46402917ca74029a2a1bbd0d7c8f9fb6 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:42:19 +0100 Subject: [PATCH 11/75] Keep OpenRouter costs and invalid credit totals informational --- rust/src/providers/openrouter/mod.rs | 63 ++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/rust/src/providers/openrouter/mod.rs b/rust/src/providers/openrouter/mod.rs index 457e209b579..96dcf34e923 100755 --- a/rust/src/providers/openrouter/mod.rs +++ b/rust/src/providers/openrouter/mod.rs @@ -50,11 +50,11 @@ impl CreditsData { (self.total_credits - self.total_usage).max(0.0) } - fn used_percent(&self) -> f64 { + fn used_percent(&self) -> Option { if self.total_credits > 0.0 { - ((self.total_usage / self.total_credits) * 100.0).min(100.0) + Some(((self.total_usage / self.total_credits) * 100.0).min(100.0)) } else { - 0.0 + None } } } @@ -269,8 +269,12 @@ impl OpenRouterProvider { fn build_credits_usage(credits: &CreditsData) -> UsageSnapshot { let balance = credits.balance(); - let mut primary = RateWindow::new(credits.used_percent()); - primary.reset_description = Some(format!("${:.2} remaining", balance)); + let detail = format!("${:.2} remaining", balance); + let mut primary = credits + .used_percent() + .map(RateWindow::new) + .unwrap_or_else(|| RateWindow::informational(&detail)); + primary.reset_description = Some(detail); UsageSnapshot::new(primary).with_login_method(format!("${:.2} balance", balance)) } @@ -392,8 +396,7 @@ impl OpenRouterProvider { return; }; - let mut window = RateWindow::new(0.0); - window.reset_description = Some(format!("${spend:.2} {period}")); + let window = RateWindow::informational(format!("${spend:.2} {period}")); *usage = usage.clone().with_extra_rate_window(id, label, window); } } @@ -663,3 +666,49 @@ mod tests { assert_eq!(parsed.data.limit_reset.as_deref(), Some("monthly")); } } + +#[cfg(test)] +mod informational_tests { + use super::*; + #[test] + fn spend_windows_are_information() { + let mut usage = OpenRouterProvider::build_credits_usage(&CreditsData { + total_credits: 100.0, + total_usage: 0.0, + }); + let key: KeyData = + serde_json::from_str(r#"{"usage_daily":0,"usage_weekly":2,"usage_monthly":5}"#) + .unwrap(); + OpenRouterProvider::enrich_usage_with_key_data(&mut usage, key); + assert_eq!(usage.extra_rate_windows.len(), 3); + assert!( + usage + .extra_rate_windows + .iter() + .all(|w| w.window.is_informational) + ); + assert!(usage.secondary.is_none()); + } + #[test] + fn zero_denominator_is_not_a_quota() { + let usage = OpenRouterProvider::build_credits_usage(&CreditsData { + total_credits: 0.0, + total_usage: 5.0, + }); + assert!(usage.primary.is_informational); + assert!(serde_json::from_str::("{}").is_err()); + } + #[test] + fn real_zero_credit_and_key_usage_stays_available() { + let mut usage = OpenRouterProvider::build_credits_usage(&CreditsData { + total_credits: 100.0, + total_usage: 0.0, + }); + let key = serde_json::from_str(r#"{"limit":10,"usage":0}"#).unwrap(); + OpenRouterProvider::add_key_quota(&mut usage, &key); + for w in [usage.primary, usage.secondary.unwrap()] { + assert!(!w.is_informational); + assert_eq!(w.used_percent, 0.0); + } + } +} From 111fcab4748b4b12608563b6ca8fc04035c770fd Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:42:19 +0100 Subject: [PATCH 12/75] Report Poe balances as information and reject missing balances --- rust/src/providers/poe/mod.rs | 50 ++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/rust/src/providers/poe/mod.rs b/rust/src/providers/poe/mod.rs index d4548ced6f2..99a3b0b0e2e 100644 --- a/rust/src/providers/poe/mod.rs +++ b/rust/src/providers/poe/mod.rs @@ -112,7 +112,7 @@ impl Provider for PoeProvider { ], ); let mut result = - ProviderFetchResult::new(snapshot_from_balance_at(&value, refresh_now), "api"); + ProviderFetchResult::new(snapshot_from_balance_at(&value, refresh_now)?, "api"); if ctx.include_credits && let Ok(entries) = fetch_points_history(&self.client, &key, refresh_now).await && let Some(cost) = cost_snapshot_from_history(&entries, balance, refresh_now) @@ -192,7 +192,10 @@ async fn fetch_points_history( Ok(entries) } -fn snapshot_from_balance_at(value: &Value, updated_at: DateTime) -> UsageSnapshot { +fn snapshot_from_balance_at( + value: &Value, + updated_at: DateTime, +) -> Result { let balance = first_number( value, &[ @@ -201,16 +204,13 @@ fn snapshot_from_balance_at(value: &Value, updated_at: DateTime) -> UsageSn "balance", "points", ], - ); - let mut primary = RateWindow::new(0.0); - primary.reset_description = balance.map(|v| format!("Balance: {v:.0} points")); - let label = primary - .reset_description - .clone() - .unwrap_or_else(|| "Poe API".into()); + ) + .ok_or_else(|| ProviderError::Parse("Poe response contains no point balance".into()))?; + let label = format!("Balance: {balance:.0} points"); + let primary = RateWindow::informational(&label); let mut snapshot = UsageSnapshot::new(primary).with_login_method(label); snapshot.updated_at = updated_at; - snapshot + Ok(snapshot) } fn history_rows(root: &Value) -> Vec<&Value> { @@ -369,7 +369,8 @@ mod tests { let snapshot = snapshot_from_balance_at( &serde_json::json!({"current_point_balance": 1234}), Utc.with_ymd_and_hms(2026, 9, 1, 0, 0, 0).unwrap(), - ); + ) + .unwrap(); assert_eq!( snapshot.login_method.as_deref(), Some("Balance: 1234 points") @@ -398,7 +399,8 @@ mod tests { let balance_snapshot = snapshot_from_balance_at( &serde_json::json!({"current_point_balance": 321}), refresh_now, - ); + ) + .unwrap(); let cost = cost_snapshot_from_history(&entries, Some(321.0), refresh_now).unwrap(); assert_eq!(balance_snapshot.updated_at, refresh_now); @@ -428,3 +430,27 @@ mod tests { ); } } + +#[cfg(test)] +mod informational_tests { + use super::*; + #[test] + fn point_balance_is_information() { + for balance in [0, 1234] { + let usage = snapshot_from_balance_at( + &serde_json::json!({"current_point_balance":balance}), + Utc::now(), + ) + .unwrap(); + assert!(usage.primary.is_informational); + assert_eq!( + usage.primary.reset_description, + Some(format!("Balance: {balance} points")) + ); + } + } + #[test] + fn missing_balance_is_an_error() { + assert!(snapshot_from_balance_at(&serde_json::json!({}), Utc::now()).is_err()); + } +} From c084d8390007eb344561a0a85da829449d55470b Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:42:19 +0100 Subject: [PATCH 13/75] Distinguish Venice balances from measured allocation quotas --- rust/src/providers/venice/mod.rs | 97 ++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 35 deletions(-) diff --git a/rust/src/providers/venice/mod.rs b/rust/src/providers/venice/mod.rs index 9ff20acf441..b5e9577be5f 100644 --- a/rust/src/providers/venice/mod.rs +++ b/rust/src/providers/venice/mod.rs @@ -85,52 +85,50 @@ impl VeniceProvider { .json() .await .map_err(|e| ProviderError::Parse(format!("Failed to parse Venice balance: {e}")))?; - Ok(snapshot_from_balance(&balance)) + snapshot_from_balance(&balance) } } -fn snapshot_from_balance(balance: &VeniceBalanceResponse) -> UsageSnapshot { +fn snapshot_from_balance(balance: &VeniceBalanceResponse) -> Result { let active_currency = balance .consumption_currency .as_deref() .unwrap_or("") .to_ascii_uppercase(); - let (used_percent, detail) = if !balance.can_consume { - (100.0, "Balance unavailable for API calls".to_string()) - } else if active_currency == "USD" && balance.balances.usd.unwrap_or(0.0) > 0.0 { - ( - 0.0, - format!("${:.2} USD remaining", balance.balances.usd.unwrap_or(0.0)), + if balance.balances.diem.is_none() && balance.balances.usd.is_none() { + return Err(ProviderError::Parse( + "Venice response contains no balance".into(), + )); + } + let window = if !balance.can_consume { + RateWindow::informational("Balance unavailable for API calls") + } else if active_currency == "USD" { + let usd = balance.balances.usd.ok_or_else(|| { + ProviderError::Parse("Venice response contains no USD balance".into()) + })?; + RateWindow::informational(format!("${usd:.2} USD remaining")) + } else if let (Some(diem), Some(allocation)) = + (balance.balances.diem, balance.diem_epoch_allocation) + && allocation > 0.0 + { + RateWindow::with_details( + ((allocation - diem) / allocation * 100.0).clamp(0.0, 100.0), + None, + None, + Some(format!("DIEM {diem:.2} / {allocation:.2} epoch allocation")), ) - } else if active_currency != "USD" { - if let (Some(diem), Some(allocation)) = - (balance.balances.diem, balance.diem_epoch_allocation) - && allocation > 0.0 - { - let used = ((allocation - diem) / allocation * 100.0).clamp(0.0, 100.0); - ( - used, - format!("DIEM {:.2} / {:.2} epoch allocation", diem, allocation), - ) - } else if let Some(diem) = balance.balances.diem - && diem > 0.0 - { - (0.0, format!("DIEM {diem:.2} remaining")) - } else if let Some(usd) = balance.balances.usd - && usd > 0.0 - { - (0.0, format!("${usd:.2} USD remaining")) - } else { - (100.0, "No Venice API balance available".to_string()) - } + } else if let Some(diem) = balance.balances.diem { + RateWindow::informational(format!("DIEM {diem:.2} remaining")) + } else if let Some(usd) = balance.balances.usd { + RateWindow::informational(format!("${usd:.2} USD remaining")) } else { - (100.0, "No Venice API balance available".to_string()) + return Err(ProviderError::Parse( + "Venice response contains no balance".into(), + )); }; - - let mut window = RateWindow::new(used_percent); - window.reset_description = Some(detail.clone()); - UsageSnapshot::new(window).with_login_method(detail) + let detail = window.reset_description.clone().unwrap_or_default(); + Ok(UsageSnapshot::new(window).with_login_method(detail)) } impl Default for VeniceProvider { @@ -212,7 +210,36 @@ mod tests { usd: None, }, diem_epoch_allocation: Some(100.0), - }); + }) + .unwrap(); assert_eq!(snapshot.primary.used_percent, 75.0); } } + +#[cfg(test)] +mod informational_tests { + use super::*; + fn parse(body: &str) -> Result { + snapshot_from_balance(&serde_json::from_str(body).unwrap()) + } + #[test] + fn balances_without_allocation_are_information() { + for body in [ + r#"{"canConsume":true,"consumptionCurrency":"USD","balances":{"usd":10}}"#, + r#"{"canConsume":true,"balances":{"diem":10}}"#, + r#"{"canConsume":false,"balances":{"usd":0}}"#, + ] { + assert!(parse(body).unwrap().primary.is_informational); + } + } + #[test] + fn missing_balances_are_an_error() { + assert!(parse(r#"{"canConsume":true,"balances":{}}"#).is_err()); + } + #[test] + fn real_zero_diem_usage_stays_available() { + let usage = parse(r#"{"canConsume":true,"consumptionCurrency":"DIEM","balances":{"diem":100},"diemEpochAllocation":100}"#).unwrap(); + assert!(!usage.primary.is_informational); + assert_eq!(usage.primary.used_percent, 0.0); + } +} From 635c7c9cb81059f833e8a8c7295df3bcc0dd4f2c Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:42:19 +0100 Subject: [PATCH 14/75] Stop reporting Vertex AI metadata and failed fetches as quotas --- rust/src/providers/vertexai/mod.rs | 65 ++++++++++++++++++------------ 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/rust/src/providers/vertexai/mod.rs b/rust/src/providers/vertexai/mod.rs index 50029954e00..838b92afe66 100755 --- a/rust/src/providers/vertexai/mod.rs +++ b/rust/src/providers/vertexai/mod.rs @@ -184,10 +184,7 @@ impl VertexAIProvider { .map_err(|e| ProviderError::Other(e.to_string()))?; // Get project ID from config - let project_id = self - .get_project_id() - .await - .unwrap_or_else(|_| "unknown".to_string()); + let project_id = self.get_project_id().await?; // Vertex AI billing/quota API let resp = client @@ -197,23 +194,13 @@ impl VertexAIProvider { )) .header("Authorization", format!("Bearer {}", token)) .send() - .await; - - match resp { - Ok(r) if r.status().is_success() => { - let json: serde_json::Value = r - .json() - .await - .map_err(|e| ProviderError::Parse(e.to_string()))?; - self.parse_usage_response(&json, &project_id) - } - _ => { - // Return placeholder with project info - let usage = UsageSnapshot::new(RateWindow::new(0.0)) - .with_login_method(format!("Vertex AI ({})", project_id)); - Ok(usage) - } - } + .await? + .error_for_status()?; + let json: serde_json::Value = resp + .json() + .await + .map_err(|e| ProviderError::Parse(e.to_string()))?; + self.parse_usage_response(&json, &project_id) } async fn get_project_id(&self) -> Result { @@ -257,10 +244,15 @@ impl VertexAIProvider { let project_name = json .get("name") .and_then(|v| v.as_str()) - .unwrap_or(project_id); + .filter(|name| !name.trim().is_empty()) + .ok_or_else(|| { + ProviderError::Parse(format!("No Vertex AI project metadata for {project_id}")) + })?; - let usage = UsageSnapshot::new(RateWindow::new(0.0)) - .with_login_method(format!("Vertex AI ({})", project_name)); + let usage = UsageSnapshot::new(RateWindow::informational( + "Project metadata; quota unavailable", + )) + .with_login_method(format!("Vertex AI ({})", project_name)); Ok(usage) } @@ -281,7 +273,10 @@ impl VertexAIProvider { "Vertex AI (installed)".to_string() }; - let usage = UsageSnapshot::new(RateWindow::new(0.0)).with_login_method(&label); + let usage = UsageSnapshot::new(RateWindow::informational( + "gcloud installed; quota unavailable", + )) + .with_login_method(&label); Ok(usage) } else { Err(ProviderError::NotInstalled("gcloud not found".to_string())) @@ -397,3 +392,23 @@ mod tests { )); } } + +#[cfg(test)] +mod informational_tests { + use super::*; + #[test] + fn project_metadata_is_information() { + let usage = VertexAIProvider::new() + .parse_usage_response(&serde_json::json!({"name":"test-project"}), "test-project") + .unwrap(); + assert!(usage.primary.is_informational); + } + #[test] + fn empty_project_response_is_an_error() { + assert!( + VertexAIProvider::new() + .parse_usage_response(&serde_json::json!({}), "test-project") + .is_err() + ); + } +} From 8165456093369100e1782ebf20f66bd8234b7e89 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:42:19 +0100 Subject: [PATCH 15/75] Keep Wayfinder gateway statistics informational --- rust/src/providers/wayfinder.rs | 96 +++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 35 deletions(-) diff --git a/rust/src/providers/wayfinder.rs b/rust/src/providers/wayfinder.rs index 57ad1c0ceba..2ec7644e6d0 100644 --- a/rust/src/providers/wayfinder.rs +++ b/rust/src/providers/wayfinder.rs @@ -245,41 +245,7 @@ impl Provider for WayfinderProvider { // Metrics are best-effort enrichment; a failure must not fail the usage fetch. let _metrics = Self::fetch_metrics(&client, &endpoint_url(&base, METRICS_PATH)?).await; - let wayfinder_usage = WayfinderUsageSnapshot { - gateway_status: health.status, - offline: health.offline, - dry_run: models.dry_run, - missing_keys: health.missing_keys, - model_count: models.model_count(), - models: models.models.into_iter().map(|model| model.name).collect(), - requests: savings.requests, - estimated_requests: savings.estimated_requests, - tokens: savings.tokens, - realized: savings.realized, - baseline: savings.baseline, - saved: savings.saved, - saved_percent: savings.saved_pct, - period_days: savings.period_days, - unit: savings.unit, - priced: savings.priced, - routes: savings - .by_route - .into_iter() - .map(|(name, route)| WayfinderRouteSummary { - name, - requests: route.requests, - tokens: route.tokens, - realized: route.realized, - baseline: route.baseline, - saved: route.saved, - }) - .collect(), - }; - - Ok( - ProviderFetchResult::new(UsageSnapshot::new(RateWindow::new(0.0)), "gateway") - .with_wayfinder_usage(wayfinder_usage), - ) + Ok(result_from_gateway(health, models, savings)) } fn available_sources(&self) -> Vec { @@ -287,6 +253,49 @@ impl Provider for WayfinderProvider { } } +fn result_from_gateway( + health: HealthResponse, + models: ModelsResponse, + savings: SavingsResponse, +) -> ProviderFetchResult { + let wayfinder_usage = WayfinderUsageSnapshot { + gateway_status: health.status, + offline: health.offline, + dry_run: models.dry_run, + missing_keys: health.missing_keys, + model_count: models.model_count(), + models: models.models.into_iter().map(|model| model.name).collect(), + requests: savings.requests, + estimated_requests: savings.estimated_requests, + tokens: savings.tokens, + realized: savings.realized, + baseline: savings.baseline, + saved: savings.saved, + saved_percent: savings.saved_pct, + period_days: savings.period_days, + unit: savings.unit, + priced: savings.priced, + routes: savings + .by_route + .into_iter() + .map(|(name, route)| WayfinderRouteSummary { + name, + requests: route.requests, + tokens: route.tokens, + realized: route.realized, + baseline: route.baseline, + saved: route.saved, + }) + .collect(), + }; + + ProviderFetchResult::new( + UsageSnapshot::new(RateWindow::informational("Gateway statistics; no quota")), + "gateway", + ) + .with_wayfinder_usage(wayfinder_usage) +} + #[cfg(test)] mod tests { use super::*; @@ -353,3 +362,20 @@ mod tests { assert!(parse_savings("{\"requests\": \"many\"}").is_err()); } } + +#[cfg(test)] +mod informational_tests { + use super::*; + #[test] + fn gateway_statistics_are_information() { + let result = result_from_gateway( + parse_health(include_str!("fixtures/wayfinder/health.json")).unwrap(), + parse_models(include_str!("fixtures/wayfinder/models.json")).unwrap(), + parse_savings(include_str!("fixtures/wayfinder/savings.json")).unwrap(), + ); + assert!(result.usage.primary.is_informational); + assert_eq!(result.wayfinder_usage.unwrap().requests, 14); + assert!(parse_health("{}").is_err()); + assert!(parse_savings("{}").is_err()); + } +} From 94b4d08bd59679a1f3e67320190378d03b9494f8 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:15 +0100 Subject: [PATCH 16/75] Keep DeepInfra billing without a spending limit informational --- rust/src/providers/deepinfra/mod.rs | 79 ++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/rust/src/providers/deepinfra/mod.rs b/rust/src/providers/deepinfra/mod.rs index b8ab5e2600b..380c493e94a 100644 --- a/rust/src/providers/deepinfra/mod.rs +++ b/rust/src/providers/deepinfra/mod.rs @@ -87,22 +87,6 @@ impl DeepInfraSnapshot { } fn to_usage_snapshot(&self) -> UsageSnapshot { - // Upstream #2822: with a positive spending limit configured, the - // primary percent reflects billing-cycle spend against that limit so - // the automatic menu-bar metric tracks it; balance-depletion stays - // the hard 100% signal. - let used_percent = - if self.suspended || self.amount_owed_usd > 0.0 || self.available_balance_usd <= 0.0 { - 100.0 - } else if let Some(limit) = self - .spending_limit_usd - .filter(|limit| *limit > 0.0 && limit.is_finite()) - { - ((self.recent_cost_usd / limit) * 100.0).clamp(0.0, 100.0) - } else { - 0.0 - }; - let balance_text = if self.amount_owed_usd > 0.0 { format!("${:.2} owed", self.amount_owed_usd) } else { @@ -123,8 +107,18 @@ impl DeepInfraSnapshot { format!("{balance_text} · {spending_text}") }; - let mut primary = RateWindow::new(used_percent); - primary.reset_description = Some(detail); + let primary = match self + .spending_limit_usd + .filter(|limit| *limit > 0.0 && limit.is_finite()) + { + Some(limit) => RateWindow::with_details( + (self.recent_cost_usd / limit * 100.0).clamp(0.0, 100.0), + None, + None, + Some(detail), + ), + None => RateWindow::informational(detail), + }; UsageSnapshot::new(primary).with_login_method(balance_text) } @@ -364,7 +358,7 @@ mod tests { } #[test] - fn spend_over_limit_clamps_to_100_and_zero_limit_stays_binary() { + fn spend_over_limit_clamps_to_100_and_zero_limit_is_informational() { // Spend beyond the positive limit clamps at 100% instead of exceeding. let snapshot = parse_snapshot_for_testing( &checklist_json(-50.0, 30.0, Some(20.0), false, None), @@ -373,13 +367,13 @@ mod tests { .unwrap(); assert_eq!(snapshot.to_usage_snapshot().primary.used_percent, 100.0); - // No (or non-positive) limit keeps the legacy binary balance signal. + // Without a positive limit, balance is information rather than a quota. let snapshot = parse_snapshot_for_testing( &checklist_json(-50.0, 1.0, Some(0.0), false, None), &usage_json(100.0), ) .unwrap(); - assert_eq!(snapshot.to_usage_snapshot().primary.used_percent, 0.0); + assert!(snapshot.to_usage_snapshot().primary.is_informational); } #[test] @@ -395,7 +389,7 @@ mod tests { assert_eq!(snapshot.spending_limit_usd, None); let usage = snapshot.to_usage_snapshot(); - assert_eq!(usage.primary.used_percent, 100.0); + assert!(usage.primary.is_informational); assert_eq!( usage.primary.reset_description.as_deref(), Some("$9.75 owed · $6.50 spent this month") @@ -404,7 +398,7 @@ mod tests { } #[test] - fn suspended_account_is_marked_exhausted() { + fn suspended_account_is_reported_without_a_quota() { let snapshot = parse_snapshot_for_testing( &checklist_json(-5.0, 1.0, None, true, Some("Payment review")), &usage_json(100.0), @@ -412,7 +406,7 @@ mod tests { .unwrap() .to_usage_snapshot(); - assert_eq!(snapshot.primary.used_percent, 100.0); + assert!(snapshot.primary.is_informational); assert!( snapshot .primary @@ -459,3 +453,40 @@ mod tests { assert!(!provider.metadata().default_enabled); } } + +#[cfg(test)] +mod section_b_tests { + use super::*; + + #[test] + fn balance_without_limit_is_informational() { + for (balance, recent, suspended) in + [(-20.0, 3.0, false), (2.0, 1.0, false), (-20.0, 0.0, true)] + { + let checklist = serde_json::json!({"stripe_balance": balance, "recent": recent, "suspended": suspended}); + let snapshot = parse_snapshot_for_testing(&checklist.to_string(), r#"{"months":[]}"#) + .unwrap() + .to_usage_snapshot(); + assert!(snapshot.primary.is_informational); + assert!(snapshot.primary.window_minutes.is_none()); + assert!(snapshot.primary.resets_at.is_none()); + } + } + + #[test] + fn actual_zero_over_limit_remains_quota() { + let snapshot = parse_snapshot_for_testing( + r#"{"stripe_balance":-20,"recent":0,"limit":10}"#, + r#"{"months":[]}"#, + ) + .unwrap() + .to_usage_snapshot(); + assert!(!snapshot.primary.is_informational); + assert_eq!(snapshot.primary.used_percent, 0.0); + } + + #[test] + fn missing_billing_fields_are_rejected() { + assert!(parse_snapshot_for_testing(r#"{"limit":10}"#, r#"{"months":[]}"#).is_err()); + } +} From 5d0d00d35c8d4c9cbf6ee819213e30da90ecb1a9 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:15 +0100 Subject: [PATCH 17/75] Keep DeepSeek balances and activity counters informational --- rust/src/providers/deepseek/mod.rs | 84 +++++++++++++++++++----------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/rust/src/providers/deepseek/mod.rs b/rust/src/providers/deepseek/mod.rs index 1ff6683c4df..d89a30cd9b4 100644 --- a/rust/src/providers/deepseek/mod.rs +++ b/rust/src/providers/deepseek/mod.rs @@ -261,8 +261,7 @@ impl DeepSeekProvider { let selected = select_balance_info(&balance.balance_infos).cloned(); let Some(info) = selected else { - let mut window = RateWindow::new(100.0); - window.reset_description = Some("No balance information returned".to_string()); + let window = RateWindow::informational("No balance information returned"); return UsageSnapshot::new(window).with_login_method("Balance unavailable"); }; @@ -271,11 +270,7 @@ impl DeepSeekProvider { let topped_up = parse_money(&info.topped_up_balance); let symbol = currency_symbol(&info.currency); - let mut window = RateWindow::new(if !balance.is_available || total <= 0.0 { - 100.0 - } else { - 0.0 - }); + let mut window = RateWindow::informational("Balance unavailable"); window.reset_description = if !balance.is_available { Some("Balance unavailable for API calls".to_string()) @@ -303,57 +298,42 @@ impl DeepSeekProvider { usage = usage.with_extra_rate_window( "tokens-today", "Tokens today", - RateWindow::with_details( - 0.0, - Some(24 * 60), - None, - Some(format_count(summary.today_tokens)), - ), + RateWindow::informational(format_count(summary.today_tokens)), ); } if summary.month_tokens > 0.0 { usage = usage.with_extra_rate_window( "tokens-month", "Tokens (month)", - RateWindow::with_details(0.0, None, None, Some(format_count(summary.month_tokens))), + RateWindow::informational(format_count(summary.month_tokens)), ); } if summary.today_requests > 0.0 { usage = usage.with_extra_rate_window( "requests-today", "Requests today", - RateWindow::with_details( - 0.0, - Some(24 * 60), - None, - Some(format_count(summary.today_requests)), - ), + RateWindow::informational(format_count(summary.today_requests)), ); } if summary.month_requests > 0.0 { usage = usage.with_extra_rate_window( "requests-month", "Requests (month)", - RateWindow::with_details( - 0.0, - None, - None, - Some(format_count(summary.month_requests)), - ), + RateWindow::informational(format_count(summary.month_requests)), ); } if let Some(model) = summary.top_model.as_deref() { usage = usage.with_extra_rate_window( "top-model", "Top model", - RateWindow::with_details(0.0, None, None, Some(model.to_string())), + RateWindow::informational(model), ); } for (idx, (category, tokens)) in summary.category_tokens.iter().take(3).enumerate() { usage = usage.with_extra_rate_window( format!("category-{idx}"), format!("Category: {}", category_label(category)), - RateWindow::with_details(0.0, None, None, Some(format_count(*tokens))), + RateWindow::informational(format_count(*tokens)), ); } usage @@ -695,7 +675,7 @@ mod tests { ], }); - assert_eq!(snapshot.primary.used_percent, 100.0); + assert!(snapshot.primary.is_informational); assert_eq!( snapshot.primary.reset_description.as_deref(), Some("$0.00 — add credits at platform.deepseek.com") @@ -704,7 +684,7 @@ mod tests { } #[test] - fn exhausted_when_balance_unavailable() { + fn reports_unavailable_balance_as_information() { let snapshot = DeepSeekProvider::snapshot_from_balance(BalanceResponse { is_available: false, balance_infos: vec![BalanceInfo { @@ -715,7 +695,7 @@ mod tests { }], }); - assert_eq!(snapshot.primary.used_percent, 100.0); + assert!(snapshot.primary.is_informational); assert_eq!( snapshot.primary.reset_description.as_deref(), Some("Balance unavailable for API calls") @@ -794,3 +774,45 @@ mod tests { ); } } + +#[cfg(test)] +mod section_b_tests { + use super::*; + + #[test] + fn balances_and_missing_balance_are_not_quotas() { + for body in [ + r#"{}"#, + r#"{"is_available":true,"balance_infos":[{"currency":"USD","total_balance":"10","granted_balance":"2","topped_up_balance":"8"}]}"#, + r#"{"is_available":false,"balance_infos":[{"currency":"USD","total_balance":"0","granted_balance":"0","topped_up_balance":"0"}]}"#, + ] { + let snapshot = + DeepSeekProvider::snapshot_from_balance(serde_json::from_str(body).unwrap()); + assert!(snapshot.primary.is_informational, "{body}"); + assert!(snapshot.primary.resets_at.is_none()); + } + } + + #[test] + fn activity_and_model_rows_are_informational() { + let summary = DeepSeekUsageSummary { + today_tokens: 2.0, + month_tokens: 5.0, + today_requests: 1.0, + month_requests: 3.0, + top_model: Some("deepseek-chat".into()), + category_tokens: vec![("input".into(), 2.0)], + ..Default::default() + }; + let snapshot = DeepSeekProvider::apply_usage_summary( + UsageSnapshot::new(RateWindow::informational("Balance")), + &summary, + ); + assert_eq!(snapshot.extra_rate_windows.len(), 6); + for row in snapshot.extra_rate_windows { + assert!(row.window.is_informational, "{}", row.id); + assert!(row.window.window_minutes.is_none()); + assert!(row.window.resets_at.is_none()); + } + } +} From a10ec27ce821f35b9f1519872f501e79a37876be Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:15 +0100 Subject: [PATCH 18/75] Keep Fireworks rated spend informational --- rust/src/providers/fireworks/mod.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/rust/src/providers/fireworks/mod.rs b/rust/src/providers/fireworks/mod.rs index 7e2b7231c84..df13fbfb9ec 100644 --- a/rust/src/providers/fireworks/mod.rs +++ b/rust/src/providers/fireworks/mod.rs @@ -160,8 +160,11 @@ impl FireworksSummary { .last_30_days_spend .zip(self.currency_code.as_deref()) .map(|(spend, _)| format_money(spend)); - let mut primary = RateWindow::new(0.0); - primary.reset_description = spend_text.clone(); + let primary = RateWindow::informational( + spend_text + .clone() + .unwrap_or_else(|| "Rated spend unavailable".to_string()), + ); let mut snapshot = UsageSnapshot::new(primary); if let Some(text) = spend_text { snapshot = snapshot.with_login_method(text); @@ -597,3 +600,25 @@ mod tests { assert!(!provider.metadata().default_enabled); } } + +#[cfg(test)] +mod section_b_tests { + use super::*; + + #[test] + fn rated_spend_is_informational_and_cost_is_preserved() { + let summary = parse_summary_for_testing( + r#"{"lineItems":[{"totalCost":{"currencyCode":"USD","units":"3","nanos":500000000}}]}"#, + ) + .unwrap(); + assert!(summary.to_usage_snapshot().primary.is_informational); + assert_eq!(summary.to_cost_snapshot().unwrap().used, 3.5); + } + + #[test] + fn missing_rated_spend_is_not_zero_quota() { + let summary = parse_summary_for_testing("{}").unwrap(); + assert!(summary.to_usage_snapshot().primary.is_informational); + assert!(summary.to_cost_snapshot().is_none()); + } +} From 3f273257895119da3ae5d7666124130b0db78f17 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:15 +0100 Subject: [PATCH 19/75] Keep Groq metrics informational and reject missing scalars --- rust/src/providers/groq/mod.rs | 102 ++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/rust/src/providers/groq/mod.rs b/rust/src/providers/groq/mod.rs index fb0da5a6b33..b491ff9775b 100644 --- a/rust/src/providers/groq/mod.rs +++ b/rust/src/providers/groq/mod.rs @@ -194,18 +194,18 @@ fn parse_scalar(data: &[u8]) -> Result { .unwrap_or_else(|| "Groq metrics query failed.".to_string()), )); } - Ok(decoded + decoded .data - .map(|payload| { - payload - .result - .iter() - .filter_map(|series| series.value.as_ref()) - .filter_map(|value| value.last()) - .filter_map(PrometheusValue::as_f64) - .sum() + .into_iter() + .flat_map(|payload| payload.result) + .filter_map(|series| series.value) + .filter_map(|value| value.last().and_then(PrometheusValue::as_f64)) + .filter(|value| value.is_finite()) + .reduce(|total, value| total + value) + .filter(|value| value.is_finite()) + .ok_or_else(|| { + ProviderError::Parse("Groq metrics response contains no usable scalar".to_string()) }) - .unwrap_or(0.0)) } fn snapshot_from_metrics(metrics: &GroqMetrics) -> UsageSnapshot { @@ -214,30 +214,14 @@ fn snapshot_from_metrics(metrics: &GroqMetrics) -> UsageSnapshot { (metrics.input_token_rate_per_second + metrics.output_token_rate_per_second) * 60.0; let cache_hits_per_minute = metrics.prompt_cache_hit_rate_per_second * 60.0; - let mut primary = RateWindow::with_details( - 0.0, - Some(5), - None, - Some(format!("{} req/min", format_metric(requests_per_minute))), - ); - primary.used_percent = 0.0; - - let secondary = RateWindow::with_details( - 0.0, - Some(5), - None, - Some(format!("{} tok/min", format_metric(tokens_per_minute))), - ); - - let tertiary = RateWindow::with_details( - 0.0, - Some(5), - None, - Some(format!( - "{} cache/min", - format_metric(cache_hits_per_minute) - )), - ); + let primary = + RateWindow::informational(format!("{} req/min", format_metric(requests_per_minute))); + let secondary = + RateWindow::informational(format!("{} tok/min", format_metric(tokens_per_minute))); + let tertiary = RateWindow::informational(format!( + "{} cache/min", + format_metric(cache_hits_per_minute) + )); UsageSnapshot::new(primary) .with_secondary(secondary) @@ -328,3 +312,53 @@ mod tests { ); } } + +#[cfg(test)] +mod section_b_tests { + use super::*; + + #[test] + fn rates_without_limits_are_informational() { + for value in [0.0, 2.0] { + let snapshot = snapshot_from_metrics(&GroqMetrics { + request_rate_per_second: value, + input_token_rate_per_second: value, + output_token_rate_per_second: value, + prompt_cache_hit_rate_per_second: value, + }); + for window in [ + Some(snapshot.primary), + snapshot.secondary, + snapshot.tertiary, + ] + .into_iter() + .flatten() + { + assert!(window.is_informational); + assert!(window.window_minutes.is_none()); + assert!(window.resets_at.is_none()); + } + } + } + + #[test] + fn missing_scalar_is_an_error_not_zero() { + for body in [ + r#"{"status":"success"}"#, + r#"{"status":"success","data":{"result":[]}}"#, + r#"{"status":"success","data":{"result":[{"value":[123,"bad"]}]}}"#, + r#"{"status":"success","data":{"result":[{"value":[123,"NaN"]}]}}"#, + ] { + assert!(parse_scalar(body.as_bytes()).is_err(), "{body}"); + } + } + + #[test] + fn actual_zero_scalar_is_preserved() { + assert_eq!( + parse_scalar(br#"{"status":"success","data":{"result":[{"value":[123,"0"]}]}}"#) + .unwrap(), + 0.0 + ); + } +} From 67ce4a5dbd3991fcb21be13b3aede8ea4bd13281 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:15 +0100 Subject: [PATCH 20/75] Require a complete Kimi quota pair and keep balances informational --- rust/src/providers/kimik2/mod.rs | 122 +++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 22 deletions(-) diff --git a/rust/src/providers/kimik2/mod.rs b/rust/src/providers/kimik2/mod.rs index cc9e2b085b3..0c7934a04fa 100755 --- a/rust/src/providers/kimik2/mod.rs +++ b/rust/src/providers/kimik2/mod.rs @@ -306,42 +306,50 @@ impl KimiK2Provider { let available_balance = data .get("available_balance") .or_else(|| data.get("balance")) - .and_then(finite_json_f64) - .unwrap_or(0.0); + .and_then(finite_json_f64); // Total credits (used + available) let total_credits = data .get("total_balance") .or_else(|| data.get("total")) .and_then(finite_json_f64) - .unwrap_or(available_balance.max(0.0)); + .filter(|total| *total > 0.0); // Used credits let used_credits = data .get("used_balance") .or_else(|| data.get("used")) .and_then(finite_json_f64) - .unwrap_or(total_credits - available_balance); - - // Calculate percentage used - let used_percent = if total_credits > 0.0 { - (used_credits / total_credits) * 100.0 - } else { - 0.0 - }; + .or_else(|| { + total_credits + .zip(available_balance) + .map(|(total, available)| total - available) + }); // Cash balance (if any) let voucher_balance = data.get("voucher_balance").and_then(finite_json_f64); let cash_balance = data.get("cash_balance").and_then(finite_json_f64); // Create primary rate window (credits used) - let mut primary = RateWindow::new(used_percent); - primary.reset_description = Some(format!( - "Balance {}", - region.format_balance(available_balance) - )); + let quota = used_credits.zip(total_credits); + if available_balance.is_none() && quota.is_none() { + return Err(ProviderError::Parse( + "Moonshot response contains no balance or usable quota".to_string(), + )); + } + let detail = available_balance + .map(|balance| format!("Balance {}", region.format_balance(balance))) + .unwrap_or_else(|| "Balance unavailable".to_string()); + let primary = match quota { + Some((used, total)) => { + RateWindow::with_details(used / total * 100.0, None, None, Some(detail)) + } + None => RateWindow::informational(detail), + }; - let mut login_method = format!("Balance: {}", region.format_balance(available_balance)); + let mut login_method = available_balance + .map(|balance| format!("Balance: {}", region.format_balance(balance))) + .unwrap_or_else(|| "Balance unavailable".to_string()); if let Some(cash) = cash_balance && cash < 0.0 { @@ -364,15 +372,14 @@ impl KimiK2Provider { // Add secondary window for cash balance if available if let Some(voucher) = voucher_balance { - let mut voucher_window = RateWindow::new(0.0); - voucher_window.reset_description = - Some(format!("Voucher {}", region.format_balance(voucher))); + let voucher_window = + RateWindow::informational(format!("Voucher {}", region.format_balance(voucher))); usage = usage.with_extra_rate_window("voucher", "Voucher balance", voucher_window); } if let Some(cash) = cash_balance { - let mut cash_window = RateWindow::new(if cash < 0.0 { 100.0 } else { 0.0 }); - cash_window.reset_description = Some(format!("Cash {}", region.format_balance(cash))); + let cash_window = + RateWindow::informational(format!("Cash {}", region.format_balance(cash))); usage = usage.with_extra_rate_window("cash", "Cash balance", cash_window); } @@ -664,3 +671,74 @@ mod tests { ); } } + +#[cfg(test)] +mod section_b_tests { + use super::*; + use serde_json::json; + + #[test] + fn balances_without_total_are_informational() { + let snapshot = KimiK2Provider::new() + .parse_usage_response( + &json!({"data":{"available_balance":12,"voucher_balance":2,"cash_balance":-3}}), + MoonshotRegion::International, + ) + .unwrap(); + assert!(snapshot.primary.is_informational); + assert_eq!(snapshot.extra_rate_windows.len(), 2); + assert!( + snapshot + .extra_rate_windows + .iter() + .all(|row| row.window.is_informational) + ); + } + + #[test] + fn missing_balance_is_an_error_not_zero() { + for body in [ + json!({}), + json!({"data":{"total_balance":10}}), + json!({"data":{"available_balance":"NaN"}}), + ] { + assert!( + KimiK2Provider::new() + .parse_usage_response(&body, MoonshotRegion::International) + .is_err() + ); + } + } + + #[test] + fn explicit_used_and_total_without_balance_remain_quota() { + for used in [0.0, 2.5] { + let snapshot = KimiK2Provider::new() + .parse_usage_response( + &json!({"data":{"used_balance":used,"total_balance":10}}), + MoonshotRegion::International, + ) + .unwrap(); + assert!(!snapshot.primary.is_informational); + assert_eq!(snapshot.primary.used_percent, used * 10.0); + assert_eq!( + snapshot.primary.reset_description.as_deref(), + Some("Balance unavailable") + ); + } + } + + #[test] + fn actual_zero_over_total_remains_quota() { + for body in [ + json!({"data":{"available_balance":10,"total_balance":10}}), + json!({"data":{"available_balance":10,"total_balance":10,"used_balance":0}}), + ] { + let snapshot = KimiK2Provider::new() + .parse_usage_response(&body, MoonshotRegion::International) + .unwrap(); + assert!(!snapshot.primary.is_informational); + assert_eq!(snapshot.primary.used_percent, 0.0); + } + } +} From bb8658a15326b0cdc315764bd1e61efec7846c6e Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:15 +0100 Subject: [PATCH 21/75] Keep LLMProxy counters and missing quota informational --- rust/src/providers/llmproxy/mod.rs | 72 +++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/rust/src/providers/llmproxy/mod.rs b/rust/src/providers/llmproxy/mod.rs index 230ee0ce79e..2bcac93d607 100644 --- a/rust/src/providers/llmproxy/mod.rs +++ b/rust/src/providers/llmproxy/mod.rs @@ -306,27 +306,19 @@ fn parse_summary(data: &[u8]) -> Result { } fn snapshot_from_summary(summary: &LLMProxySummary) -> UsageSnapshot { - let used_percent = summary - .minimum_remaining_percent - .map(|remaining| (100.0 - remaining).clamp(0.0, 100.0)) - .unwrap_or(0.0); - let mut primary = RateWindow::with_details(used_percent, None, summary.next_reset_at, None); - primary.reset_description = summary - .minimum_remaining_percent - .map(|remaining| format!("{remaining:.1}% minimum remaining")); - - let secondary = RateWindow::with_details( - 0.0, - None, - None, - Some(format!("{} requests", format_count(summary.total_requests))), - ); - let tertiary = RateWindow::with_details( - 0.0, - None, - None, - Some(format!("{} tokens", format_count(summary.total_tokens))), - ); + let primary = match summary.minimum_remaining_percent { + Some(remaining) => RateWindow::with_details( + (100.0 - remaining).clamp(0.0, 100.0), + None, + summary.next_reset_at, + Some(format!("{remaining:.1}% minimum remaining")), + ), + None => RateWindow::informational("Quota unavailable"), + }; + let secondary = + RateWindow::informational(format!("{} requests", format_count(summary.total_requests))); + let tertiary = + RateWindow::informational(format!("{} tokens", format_count(summary.total_tokens))); let mut snapshot = UsageSnapshot::new(primary) .with_secondary(secondary) @@ -349,7 +341,7 @@ fn snapshot_from_summary(summary: &LLMProxySummary) -> UsageSnapshot { snapshot = snapshot.with_extra_rate_window( provider.name.clone(), provider.name.clone(), - RateWindow::with_details(0.0, None, None, Some(detail)), + RateWindow::informational(detail), ); } @@ -509,3 +501,39 @@ mod tests { assert!(next_reset_at_from_groups(&groups, now).is_none()); } } + +#[cfg(test)] +mod section_b_tests { + use super::*; + + #[test] + fn counters_without_quota_are_informational() { + for body in [ + r#"{"providers":{}}"#, + r#"{"providers":{"openai":{"total_requests":12,"tokens":{"input_cached":2},"approx_cost":3}}}"#, + ] { + let snapshot = snapshot_from_summary(&parse_summary(body.as_bytes()).unwrap()); + assert!(snapshot.primary.is_informational); + assert!(snapshot.primary.resets_at.is_none()); + assert!(snapshot.secondary.unwrap().is_informational); + assert!(snapshot.tertiary.unwrap().is_informational); + assert!( + snapshot + .extra_rate_windows + .iter() + .all(|row| row.window.is_informational) + ); + } + } + + #[test] + fn actual_full_remaining_stays_zero_quota() { + let summary = parse_summary( + br#"{"providers":{"openai":{"quota_groups":[{"remaining_percent":100}]}}}"#, + ) + .unwrap(); + let snapshot = snapshot_from_summary(&summary); + assert!(!snapshot.primary.is_informational); + assert_eq!(snapshot.primary.used_percent, 0.0); + } +} From 876deb718ad7e450367eb51f061726471352c069 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:15 +0100 Subject: [PATCH 22/75] Keep Mistral monthly billing informational --- rust/src/providers/mistral/mod.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/rust/src/providers/mistral/mod.rs b/rust/src/providers/mistral/mod.rs index 5fb8bdf2a3d..f9858bc80a2 100644 --- a/rust/src/providers/mistral/mod.rs +++ b/rust/src/providers/mistral/mod.rs @@ -316,7 +316,7 @@ impl MistralProvider { "No usage this month".to_string() }; - let primary = RateWindow::with_details(0.0, None, reset_date, Some(cost_description)); + let primary = RateWindow::informational(cost_description); let mut usage = UsageSnapshot::new(primary); if summary.model_count > 0 { usage = usage.with_login_method(format!("{} model(s)", summary.model_count)); @@ -589,3 +589,27 @@ mod tests { )); } } + +#[cfg(test)] +mod section_b_tests { + use super::*; + + #[test] + fn monthly_cost_and_tokens_are_informational() { + let billing = serde_json::from_str(r#"{"end_date":"2026-10-31T00:00:00Z","prices":[{"billing_metric":"m","billing_group":"input","price":"0.5"}],"completion":{"models":{"m":{"input":[{"billing_metric":"m","billing_group":"input","value":4}]}}}}"#).unwrap(); + let result = + MistralProvider::build_result(MistralProvider::summarize_billing(billing).unwrap()); + assert!(result.usage.primary.is_informational); + assert!(result.usage.primary.resets_at.is_none()); + assert_eq!(result.cost.as_ref().unwrap().used, 2.0); + assert!(result.cost.unwrap().resets_at.is_some()); + } + + #[test] + fn absent_billing_does_not_create_zero_quota() { + let result = MistralProvider::build_result( + MistralProvider::summarize_billing(serde_json::from_str("{}").unwrap()).unwrap(), + ); + assert!(result.usage.primary.is_informational); + } +} From 64a58b63eb831afbbee2c8a92cc37775fbaea6af Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 23/75] Fix missing Abacus usage being reported as a quota --- rust/src/providers/abacus/mod.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/rust/src/providers/abacus/mod.rs b/rust/src/providers/abacus/mod.rs index 0363d87bd52..cff3d940d95 100644 --- a/rust/src/providers/abacus/mod.rs +++ b/rust/src/providers/abacus/mod.rs @@ -28,9 +28,7 @@ struct ApiEnvelope { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ComputePoints { - #[serde(default)] total_compute_points: f64, - #[serde(default)] compute_points_left: f64, } @@ -334,3 +332,31 @@ mod tests { assert_eq!(format_credit_detail(42.5, 100.0), "42.5 / 100 credits"); } } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + use serde_json::json; + #[test] + fn cx289a_missing_compute_fields_are_rejected() { + for payload in [ + json!({}), + json!({"totalComputePoints":100}), + json!({"computePointsLeft":100}), + ] { + let result = serde_json::from_value::(payload) + .map_err(|e| ProviderError::Parse(e.to_string())) + .and_then(|compute| AbacusProvider::build_snapshot(compute, None)); + assert!(result.is_err()); + } + } + #[test] + fn cx289a_real_zero_is_preserved() { + let compute = + serde_json::from_value(json!({"totalComputePoints":100,"computePointsLeft":100})) + .unwrap(); + let snapshot = AbacusProvider::build_snapshot(compute, None).unwrap(); + assert_eq!(snapshot.primary.used_percent, 0.0); + assert!(!snapshot.primary.is_informational); + } +} From 623f869495e29069f8b93a8175934d9be2b43431 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 24/75] Fix missing Alibaba usage being reported as a quota --- rust/src/providers/alibaba/parser.rs | 119 ++++++++++++++++++++------- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/rust/src/providers/alibaba/parser.rs b/rust/src/providers/alibaba/parser.rs index 6c11ccadbc8..eff81399c3f 100644 --- a/rust/src/providers/alibaba/parser.rs +++ b/rust/src/providers/alibaba/parser.rs @@ -58,14 +58,10 @@ pub(crate) fn parse_response(json: &serde_json::Value) -> Result f64 { - let used = quota.get(used_key).and_then(|v| v.as_f64()).unwrap_or(0.0); - let total = quota.get(total_key).and_then(|v| v.as_f64()).unwrap_or(1.0); - if total > 0.0 { - (used / total * 100.0).clamp(0.0, 100.0) - } else { - 0.0 - } + let pct = |used_key: &str, total_key: &str| -> Option { + let used = quota.get(used_key).and_then(|v| v.as_f64())?; + let total = quota.get(total_key).and_then(|v| v.as_f64())?; + (total > 0.0).then(|| (used / total * 100.0).clamp(0.0, 100.0)) }; let detail = |used_key: &str, total_key: &str| -> Option { @@ -90,30 +86,48 @@ pub(crate) fn parse_response(json: &serde_json::Value) -> Result String { @@ -248,3 +262,44 @@ mod tests { assert_eq!(fmt_tokens(i64::MIN), "-9,223,372,036,854,775,808"); } } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + use serde_json::json; + fn payload(quota: serde_json::Value) -> serde_json::Value { + json!({"data":{"DataV2":{"data":{"data":{"codingPlanInstanceInfos":[{"codingPlanQuotaInfo":quota}]}}}}}) + } + #[test] + fn cx289a_incomplete_quotas_are_rejected() { + for quota in [ + json!({}), + json!({"per5HourTotalQuota":100}), + json!({"per5HourUsedQuota":0}), + ] { + assert!(parse_response(&payload(quota)).is_err()); + } + } + #[test] + fn cx289a_weekly_only_keeps_primary_unknown() { + let snapshot = parse_response(&payload( + json!({"perWeekUsedQuota":0,"perWeekTotalQuota":100}), + )) + .unwrap(); + assert!(snapshot.primary.is_informational); + let weekly = snapshot.secondary.unwrap(); + assert_eq!(weekly.used_percent, 0.0); + assert!(!weekly.is_informational); + assert!(snapshot.tertiary.is_none()); + } + #[test] + fn cx289a_real_zero_is_preserved() { + let snapshot = parse_response(&payload( + json!({"per5HourUsedQuota":0,"per5HourTotalQuota":100}), + )) + .unwrap(); + assert_eq!(snapshot.primary.used_percent, 0.0); + assert!(!snapshot.primary.is_informational); + assert!(snapshot.secondary.is_none()); + } +} From eae0cef06de569e2bce43631adf8f457f968fcaa Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 25/75] Fix missing Amp usage being reported as a quota --- rust/src/providers/amp/mod.rs | 48 +++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/rust/src/providers/amp/mod.rs b/rust/src/providers/amp/mod.rs index 21a9c5ec2a8..560a0153d71 100755 --- a/rust/src/providers/amp/mod.rs +++ b/rust/src/providers/amp/mod.rs @@ -121,19 +121,16 @@ impl AmpProvider { .get("completionsUsed") .or_else(|| json.get("used")) .and_then(|v| v.as_f64()) - .unwrap_or(0.0); + .ok_or_else(|| ProviderError::Parse("Amp usage counter missing".into()))?; let limit = json .get("completionsLimit") .or_else(|| json.get("limit")) .and_then(|v| v.as_f64()) - .unwrap_or(500.0); + .filter(|limit| *limit > 0.0) + .ok_or_else(|| ProviderError::Parse("Amp usage limit missing or invalid".into()))?; - let used_percent = if limit > 0.0 { - (used / limit) * 100.0 - } else { - 0.0 - }; + let used_percent = (used / limit) * 100.0; let plan = json .get("plan") @@ -153,6 +150,12 @@ impl AmpProvider { Ok(usage) } + fn configured_usage() -> Result { + Err(ProviderError::Parse( + "Amp is configured, but usage is unavailable".into(), + )) + } + /// Probe for Amp installation async fn probe_cli(&self, ctx: &FetchContext) -> Result { // Check ctx.api_key first @@ -170,9 +173,7 @@ impl AmpProvider { .unwrap_or(false); if has_api_key || has_env || has_amp_config || has_cody_config { - let usage = - UsageSnapshot::new(RateWindow::new(0.0)).with_login_method("Amp (configured)"); - Ok(usage) + Self::configured_usage() } else { Err(ProviderError::NotInstalled( "Amp not configured. Set SRC_ACCESS_TOKEN environment variable or configure Amp." @@ -643,3 +644,30 @@ mod current_subscription_tests { assert_eq!(sub.reset_description, "renews in 1 month"); } } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + use serde_json::json; + + #[test] + fn cx289a_configuration_is_not_usage() { + assert!(super::AmpProvider::configured_usage().is_err()); + } + + #[test] + fn cx289a_missing_usage_is_rejected() { + let provider = AmpProvider::new(); + for payload in [json!({}), json!({"limit":100}), json!({"used":0})] { + assert!(provider.parse_usage_response(&payload).is_err()); + } + } + #[test] + fn cx289a_real_zero_is_preserved() { + let snapshot = AmpProvider::new() + .parse_usage_response(&json!({"used":0,"limit":100})) + .unwrap(); + assert_eq!(snapshot.primary.used_percent, 0.0); + assert!(!snapshot.primary.is_informational); + } +} From 65a7673ab4506a5931ffcf2fb630315e09583bdf Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 26/75] Fix missing Augment usage being reported as a quota --- rust/src/providers/augment/mod.rs | 36 +++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/rust/src/providers/augment/mod.rs b/rust/src/providers/augment/mod.rs index 388746ef75a..740129a5242 100755 --- a/rust/src/providers/augment/mod.rs +++ b/rust/src/providers/augment/mod.rs @@ -172,19 +172,16 @@ impl AugmentProvider { .get("used_credits") .or_else(|| json.get("usage")) .and_then(|v| v.as_f64()) - .unwrap_or(0.0); + .ok_or_else(|| ProviderError::Parse("Augment usage counter missing".into()))?; let limit = json .get("credit_limit") .or_else(|| json.get("limit")) .and_then(|v| v.as_f64()) - .unwrap_or(100.0); + .filter(|limit| *limit > 0.0) + .ok_or_else(|| ProviderError::Parse("Augment usage limit missing or invalid".into()))?; - let used_percent = if limit > 0.0 { - (used / limit) * 100.0 - } else { - 0.0 - }; + let used_percent = (used / limit) * 100.0; let email = json .get("email") @@ -497,3 +494,28 @@ mod tests { ); } } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + use serde_json::json; + #[test] + fn cx289a_missing_usage_is_rejected() { + let provider = AugmentProvider::new(); + for payload in [ + json!({}), + json!({"credit_limit":100}), + json!({"used_credits":0}), + ] { + assert!(provider.parse_usage_response(&payload).is_err()); + } + } + #[test] + fn cx289a_real_zero_is_preserved() { + let snapshot = AugmentProvider::new() + .parse_usage_response(&json!({"used_credits":0,"credit_limit":100})) + .unwrap(); + assert_eq!(snapshot.primary.used_percent, 0.0); + assert!(!snapshot.primary.is_informational); + } +} From 769e2238285d16d840c583be52fb30aad727050f Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 27/75] Fix missing Antigravity legacy usage being reported as a quota --- .../providers/antigravity/legacy_status.rs | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/rust/src/providers/antigravity/legacy_status.rs b/rust/src/providers/antigravity/legacy_status.rs index 852561a3995..8f4e8f13506 100644 --- a/rust/src/providers/antigravity/legacy_status.rs +++ b/rust/src/providers/antigravity/legacy_status.rs @@ -241,7 +241,9 @@ fn model_window_id(config: &ModelConfig) -> String { } fn rate_window_from_quota(quota: &QuotaInfo) -> RateWindow { - let remaining = quota.remaining_fraction.unwrap_or(1.0); + let Some(remaining) = quota.remaining_fraction else { + return RateWindow::informational("Antigravity model usage unavailable"); + }; let used_percent = (1.0 - remaining) * 100.0; RateWindow::with_details(used_percent, None, None, quota.reset_time.clone()) } @@ -311,6 +313,17 @@ pub(super) fn parse_user_status( .filter(|config| !model_label(config).is_empty()) .collect::>(); quota_configs.sort_by(|a, b| compare_model_configs(a, b)); + if !quota_configs.iter().any(|config| { + config + .quota_info + .as_ref() + .and_then(|quota| quota.remaining_fraction) + .is_some() + }) { + return Err(ProviderError::Parse( + "Antigravity status contains no model quota".into(), + )); + } let summary_candidates = quota_configs .iter() @@ -334,7 +347,8 @@ pub(super) fn parse_user_status( .and_then(|config| config.quota_info.as_ref()) .map(rate_window_from_quota); - let primary = primary.unwrap_or_else(|| RateWindow::new(0.0)); + let primary = primary + .ok_or_else(|| ProviderError::Parse("Antigravity status contains no model quota".into()))?; let mut snapshot = UsageSnapshot::new(primary); if let Some(sec) = secondary { @@ -385,3 +399,35 @@ pub(super) fn parse_user_status( Ok(snapshot) } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + use serde_json::json; + fn parse(models: serde_json::Value) -> Result { + parse_user_status( + serde_json::from_value( + json!({"userStatus":{"cascadeModelConfigData":{"clientModelConfigs":models}}}), + ) + .unwrap(), + ) + } + #[test] + fn cx289a_missing_model_usage_is_rejected() { + assert!(parse(json!([])).is_err()); + assert!(parse(json!([{"label":"Claude","quotaInfo":{}}])).is_err()); + } + #[test] + fn cx289a_real_zero_is_preserved_with_informational_extra() { + let snapshot = parse(json!([ + {"label":"Claude","quotaInfo":{"remainingFraction":1.0}}, + {"label":"Other","quotaInfo":{}} + ])) + .unwrap(); + assert_eq!(snapshot.primary.used_percent, 0.0); + assert!(!snapshot.primary.is_informational); + assert_eq!(snapshot.extra_rate_windows.len(), 1); + assert!(snapshot.extra_rate_windows[0].window.is_informational); + assert!(!snapshot.extra_rate_windows[0].usage_known); + } +} From 68a2995a9e61212f568c03baf633632ccc00cfe8 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 28/75] Fix missing CodeBuddy usage being reported as a quota --- rust/src/providers/codebuddy/mod.rs | 102 ++++++++++++++++++---------- 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/rust/src/providers/codebuddy/mod.rs b/rust/src/providers/codebuddy/mod.rs index eae0fd2faa5..9c05c9f4178 100644 --- a/rust/src/providers/codebuddy/mod.rs +++ b/rust/src/providers/codebuddy/mod.rs @@ -500,44 +500,44 @@ fn totals_from_api_payload(value: &Value) -> Result let mut reset: Option> = None; for account in accounts { - total += precise_or_field(account, "CapacitySizePrecise", "CapacitySize").unwrap_or(0.0); - used += precise_or_field(account, "CapacityUsedPrecise", "CapacityUsed").unwrap_or(0.0); - remaining += - precise_or_field(account, "CapacityRemainPrecise", "CapacityRemain").unwrap_or(0.0); + let account_total = precise_or_field(account, "CapacitySizePrecise", "CapacitySize") + .ok_or_else(|| ProviderError::Parse("CodeBuddy package total missing".into()))?; + let totals = CreditTotals::normalized( + account_total, + precise_or_field(account, "CapacityUsedPrecise", "CapacityUsed"), + precise_or_field(account, "CapacityRemainPrecise", "CapacityRemain"), + None, + )?; + total += totals.total; + used += totals.used; + remaining += totals.remaining; if let Some(reset_at) = expire_time(account) { reset = Some(reset.map_or(reset_at, |prev| prev.min(reset_at))); } } - CreditTotals::normalized(total, used, remaining, reset) + CreditTotals::normalized(total, Some(used), Some(remaining), reset) } impl CreditTotals { - /// Reconcile partial sums: prefer summed remaining, recompute used when it - /// is missing, and clamp every field to non-negative. + /// Derive a missing counter only from a measured total and its counterpart. fn normalized( - mut total: f64, - mut used: f64, - mut remaining: f64, + total: f64, + used: Option, + remaining: Option, reset: Option>, ) -> Result { - if !(total.is_finite() && used.is_finite() && remaining.is_finite()) { + let used = used + .or_else(|| remaining.map(|remaining| total - remaining)) + .ok_or_else(|| ProviderError::Parse("CodeBuddy usage counter missing".into()))?; + let remaining = remaining.unwrap_or(total - used); + if !(total.is_finite() && total > 0.0 && used.is_finite() && remaining.is_finite()) { return Err(ProviderError::Parse( - "CodeBuddy payload contains non-finite credit values".into(), + "CodeBuddy payload contains invalid credit values".into(), )); } - if remaining > 0.0 && total > 0.0 && used <= 0.0 { - used = (total - remaining).max(0.0); - } - if used < 0.0 { - used = 0.0; - } - if total < 0.0 { - total = 0.0; - } - if remaining < 0.0 { - remaining = (total - used).max(0.0); - } + let used = used.max(0.0); + let remaining = remaining.max(0.0); Ok(CreditTotals { total, used, @@ -596,22 +596,15 @@ fn totals_from_cache_json(value: &Value) -> Result "cb_credits.json invalid total: {total}" ))); } - let used = number_field(value, &["used"]).unwrap_or(0.0).max(0.0); - let remaining = number_field(value, &["remaining"]) - .map(|r| r.max(0.0)) - .unwrap_or_else(|| (total - used).max(0.0)); + let used = number_field(value, &["used"]); + let remaining = number_field(value, &["remaining"]); // Prefer real package expiry from cache if present; do not treat updatedAt // as a quota reset (that previously looked like a bogus reset countdown). let reset = value .get("resetsAt") .and_then(|v| v.as_str()) .and_then(parse_datetime); - Ok(CreditTotals { - total, - used, - remaining, - reset, - }) + CreditTotals::normalized(total, used, remaining, reset) } fn write_credits_cache( @@ -1288,3 +1281,44 @@ mod tests { } } } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + use serde_json::json; + fn payload(accounts: Value) -> Value { + json!({"code":0,"data":{"Response":{"Data":{"Accounts":accounts}}}}) + } + #[test] + fn cx289a_missing_api_counter_is_rejected() { + assert!(totals_from_api_payload(&payload(json!([{"CapacitySize":100}]))).is_err()); + assert!(totals_from_api_payload(&payload(json!([{"CapacityUsed":0}]))).is_err()); + assert!( + totals_from_api_payload(&payload(json!([ + {"CapacitySize":100,"CapacityUsed":0},{"CapacitySize":100} + ]))) + .is_err() + ); + } + #[test] + fn cx289a_cache_requires_counter_and_derives_used() { + assert!(totals_from_cache_json(&json!({"total":100})).is_err()); + let totals = totals_from_cache_json(&json!({"total":100,"remaining":75})).unwrap(); + assert_eq!(snapshot_from_totals(&totals).primary.used_percent, 25.0); + let exhausted = + totals_from_api_payload(&payload(json!([{"CapacitySize":100,"CapacityRemain":0}]))) + .unwrap(); + assert_eq!(snapshot_from_totals(&exhausted).primary.used_percent, 100.0); + } + #[test] + fn cx289a_real_zero_is_preserved() { + let totals = + totals_from_api_payload(&payload(json!([{"CapacitySize":100,"CapacityUsed":0}]))) + .unwrap(); + let snapshot = snapshot_from_totals(&totals); + assert_eq!(snapshot.primary.used_percent, 0.0); + assert!(!snapshot.primary.is_informational); + let totals = totals_from_cache_json(&json!({"total":100,"used":0})).unwrap(); + assert_eq!(snapshot_from_totals(&totals).primary.used_percent, 0.0); + } +} From 0ebb4c5ff8bce25d7f009efd879a52e38c43f931 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 29/75] Fix missing Codebuff usage being reported as a quota --- rust/src/providers/codebuff/mod.rs | 126 +++++++++++++++++++++-------- 1 file changed, 93 insertions(+), 33 deletions(-) diff --git a/rust/src/providers/codebuff/mod.rs b/rust/src/providers/codebuff/mod.rs index d0e00fa2575..19d1aa4cc52 100644 --- a/rust/src/providers/codebuff/mod.rs +++ b/rust/src/providers/codebuff/mod.rs @@ -128,7 +128,7 @@ impl CodebuffProvider { })?; let subscription = self.fetch_subscription(&base, &api_key).await; - Ok(Self::snapshot_from_values(&usage, subscription.as_ref())) + Self::snapshot_from_values(&usage, subscription.as_ref()) } async fn fetch_subscription(&self, base: &Url, api_key: &str) -> Option { @@ -150,41 +150,53 @@ impl CodebuffProvider { resp.json::().await.ok() } - fn snapshot_from_values(usage: &Value, subscription: Option<&Value>) -> UsageSnapshot { + fn snapshot_from_values( + usage: &Value, + subscription: Option<&Value>, + ) -> Result { let usage = data_payload(usage); let subscription = subscription.map(data_payload); - let used = number_at(usage, &["usage", "used"]).unwrap_or(0.0); + let used = number_at(usage, &["usage", "used"]); let remaining = number_at(usage, &["remainingBalance", "remaining"]); - let total = number_at(usage, &["creditsTotal", "quota", "limit"]) - .or_else(|| remaining.map(|r| used + r)); - - let percent = match (total, remaining) { - (Some(total), _) if total > 0.0 => (used / total * 100.0).clamp(0.0, 100.0), - (None, Some(_)) => 100.0, - _ => 0.0, - }; - - let mut primary = RateWindow::new(percent); - if let Some(reset) = - string_at(usage, &["next_quota_reset", "nextQuotaReset"]).and_then(parse_datetime) + let total = number_at(usage, &["creditsTotal", "quota", "limit"]).or_else(|| { + used.zip(remaining) + .map(|(used, remaining)| used + remaining) + }); + let used = used.or_else(|| { + total + .zip(remaining) + .map(|(total, remaining)| total - remaining) + }); + let weekly = subscription + .and_then(|v| v.get("rateLimit").or_else(|| v.get("subscription"))) + .and_then(weekly_window); + let primary = match used + .zip(total) + .filter(|(used, total)| used.is_finite() && total.is_finite() && *total > 0.0) { - primary.resets_at = Some(reset); - } - primary.reset_description = match (used, total, remaining) { - (_, Some(total), Some(remaining)) => Some(format!( - "{used:.0}/{total:.0} credits ({remaining:.0} remaining)" - )), - (_, Some(total), None) => Some(format!("{used:.0}/{total:.0} credits")), - (_, None, Some(remaining)) => Some(format!("{remaining:.0} credits remaining")), - _ => None, + Some((used, total)) => { + let mut window = RateWindow::new((used / total * 100.0).clamp(0.0, 100.0)); + window.resets_at = string_at(usage, &["next_quota_reset", "nextQuotaReset"]) + .and_then(parse_datetime); + window.reset_description = Some(match remaining { + Some(remaining) => { + format!("{used:.0}/{total:.0} credits ({remaining:.0} remaining)") + } + None => format!("{used:.0}/{total:.0} credits"), + }); + window + } + None if weekly.is_some() => { + RateWindow::informational("Codebuff credit usage unavailable") + } + None => { + return Err(ProviderError::Parse( + "Codebuff response contains no usable quota".into(), + )); + } }; - let mut snapshot = UsageSnapshot::new(primary); - - if let Some(rate_limit) = - subscription.and_then(|v| v.get("rateLimit").or_else(|| v.get("subscription"))) - && let Some(weekly) = weekly_window(rate_limit) - { + if let Some(weekly) = weekly { snapshot = snapshot.with_secondary(weekly); } @@ -221,7 +233,7 @@ impl CodebuffProvider { snapshot = snapshot.with_login_method(method_parts.join(" · ")); } - snapshot + Ok(snapshot) } } @@ -330,7 +342,7 @@ mod tests { } }); - let snapshot = CodebuffProvider::snapshot_from_values(&usage, Some(&subscription)); + let snapshot = CodebuffProvider::snapshot_from_values(&usage, Some(&subscription)).unwrap(); assert_eq!(snapshot.primary.used_percent, 25.0); assert_eq!( @@ -361,7 +373,7 @@ mod tests { } }); - let snapshot = CodebuffProvider::snapshot_from_values(&usage, Some(&subscription)); + let snapshot = CodebuffProvider::snapshot_from_values(&usage, Some(&subscription)).unwrap(); assert_eq!(snapshot.primary.used_percent, 40.0); assert_eq!( @@ -370,3 +382,51 @@ mod tests { ); } } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + use serde_json::json; + + #[test] + fn cx289a_incomplete_quotas_are_rejected() { + for payload in [ + json!({}), + json!({"creditsTotal":100}), + json!({"remaining":100}), + json!({"used":0}), + ] { + assert!(CodebuffProvider::snapshot_from_values(&payload, None).is_err()); + } + } + #[test] + fn cx289a_remaining_and_total_determine_used() { + let snapshot = CodebuffProvider::snapshot_from_values( + &json!({"creditsTotal":100,"remaining":75}), + None, + ) + .unwrap(); + assert_eq!(snapshot.primary.used_percent, 25.0); + } + + #[test] + fn cx289a_weekly_only_keeps_primary_unknown() { + let snapshot = CodebuffProvider::snapshot_from_values( + &json!({"creditsTotal":100}), + Some(&json!({"rateLimit":{"weeklyUsed":0,"weeklyLimit":100}})), + ) + .unwrap(); + assert!(snapshot.primary.is_informational); + let weekly = snapshot.secondary.unwrap(); + assert_eq!(weekly.used_percent, 0.0); + assert!(!weekly.is_informational); + } + #[test] + fn cx289a_real_zero_is_preserved() { + let snapshot = + CodebuffProvider::snapshot_from_values(&json!({"creditsTotal":100,"used":0}), None) + .unwrap(); + assert_eq!(snapshot.primary.used_percent, 0.0); + assert!(!snapshot.primary.is_informational); + } +} From 1e3c6f0a41af9bcdda640ada4af77cb0751fce0a Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 30/75] Fix missing CommandCode usage being reported as a quota --- rust/src/providers/commandcode/mod.rs | 45 ++++++++++++++++++--------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/rust/src/providers/commandcode/mod.rs b/rust/src/providers/commandcode/mod.rs index 1f8f5cfd061..ee1b2a4fb58 100644 --- a/rust/src/providers/commandcode/mod.rs +++ b/rust/src/providers/commandcode/mod.rs @@ -412,12 +412,9 @@ fn result_from_payloads( } else if let Some(monthly) = monthly_window { UsageSnapshot::new(monthly) } else { - UsageSnapshot::new(RateWindow::with_details( - 0.0, - None, - period_end, - Some(format!("{monthly_credits:.2} monthly credits remaining")), - )) + UsageSnapshot::new(RateWindow::informational(format!( + "{monthly_credits:.2} monthly credits remaining" + ))) }; if let Some(method) = login_method(monthly_credits, purchased, plan) { @@ -455,7 +452,7 @@ fn limit_window(value: Option<&Value>, window_minutes: u32) -> Option) -> Option> { /// Monthly grant window from the plan catalog (upstream `makeMonthlyWindow`): /// catalog total − remaining, clamped to [0, total]. Without a recognized plan -/// the fallback keeps a visible-but-empty bar when credits remain, and no -/// window at all when the account holds nothing. +/// the remaining balance is informational, not a measured usage percentage. fn monthly_window( monthly_remaining: f64, purchased: f64, @@ -511,12 +507,9 @@ fn monthly_window( )); } if monthly_remaining > 0.0 || purchased > 0.0 { - return Some(RateWindow::with_details( - 0.0, - RateWindow::monthly_window_minutes(period_end), - period_end, - None, - )); + return Some(RateWindow::informational(format!( + "{monthly_remaining:.2} monthly credits remaining · {purchased:.2} purchased credits" + ))); } None } @@ -849,3 +842,25 @@ mod tests { assert!(result.usage.login_method.is_none()); } } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + use serde_json::json; + #[test] + fn cx289a_cap_without_counter_is_unknown() { + assert!(limit_window(Some(&json!({"cap":100})), 300).is_none()); + } + #[test] + fn cx289a_unknown_plan_balance_is_informational() { + let window = monthly_window(25.0, 0.0, None, None).unwrap(); + assert!(window.is_informational); + assert!(window.resets_at.is_none()); + } + #[test] + fn cx289a_real_zero_is_preserved() { + let window = limit_window(Some(&json!({"cap":100,"used":0})), 300).unwrap(); + assert_eq!(window.used_percent, 0.0); + assert!(!window.is_informational); + } +} From 962f6a2380ad5996674d058b335a4263c0772a22 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 31/75] Fix missing Devin usage being reported as a quota --- rust/src/providers/devin/mod.rs | 67 ++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/rust/src/providers/devin/mod.rs b/rust/src/providers/devin/mod.rs index 24c0ce4def4..46a9aa4202c 100644 --- a/rust/src/providers/devin/mod.rs +++ b/rust/src/providers/devin/mod.rs @@ -97,7 +97,7 @@ impl Provider for DevinProvider { let value: Value = response.json().await.map_err(|e| { ProviderError::Parse(format!("Failed to parse Devin quota: {e}")) })?; - Ok(fetch_result_from_quota(&value, &org)) + fetch_result_from_quota(&value, &org) } SourceMode::Web | SourceMode::Cli => { Err(ProviderError::UnsupportedSource(ctx.source_mode)) @@ -145,23 +145,31 @@ fn normalized_org(raw: &str) -> String { } } -fn snapshot_from_quota(value: &Value, org: &str) -> UsageSnapshot { +fn snapshot_from_quota(value: &Value, org: &str) -> Result { let daily = percent(value, &["daily_percentage", "dailyPercentage"]) - .unwrap_or_else(|| percent(value, &["used_percent", "usedPercent"]).unwrap_or(0.0)); - let mut snapshot = - UsageSnapshot::new(RateWindow::new(daily)).with_organization(org.to_string()); - if let Some(weekly) = percent(value, &["weekly_percentage", "weeklyPercentage"]) { + .or_else(|| percent(value, &["used_percent", "usedPercent"])); + let weekly = percent(value, &["weekly_percentage", "weeklyPercentage"]); + if daily.is_none() && weekly.is_none() { + return Err(ProviderError::Parse( + "Devin response contains no usable quota".into(), + )); + } + let primary = daily + .map(RateWindow::new) + .unwrap_or_else(|| RateWindow::informational("Daily usage unavailable")); + let mut snapshot = UsageSnapshot::new(primary).with_organization(org.to_string()); + if let Some(weekly) = weekly { snapshot = snapshot.with_secondary(RateWindow::new(weekly)); } - snapshot + Ok(snapshot) } -fn fetch_result_from_quota(value: &Value, org: &str) -> ProviderFetchResult { - let mut result = ProviderFetchResult::new(snapshot_from_quota(value, org), "api"); +fn fetch_result_from_quota(value: &Value, org: &str) -> Result { + let mut result = ProviderFetchResult::new(snapshot_from_quota(value, org)?, "api"); if let Some(balance) = extra_usage_balance(value) { result = result.with_cost(CostSnapshot::new(balance, "USD", "Extra usage balance")); } - result + Ok(result) } fn percent(value: &Value, keys: &[&str]) -> Option { @@ -208,14 +216,14 @@ mod tests { #[test] fn parses_fraction_percent() { let snapshot = - snapshot_from_quota(&serde_json::json!({"daily_percentage":0.25}), "org/demo"); + snapshot_from_quota(&serde_json::json!({"daily_percentage":0.25}), "org/demo").unwrap(); assert_eq!(snapshot.primary.used_percent, 25.0); } #[test] fn parses_exact_one_as_one_percent() { let snapshot = - snapshot_from_quota(&serde_json::json!({"daily_percentage":1.0}), "org/demo"); + snapshot_from_quota(&serde_json::json!({"daily_percentage":1.0}), "org/demo").unwrap(); assert_eq!(snapshot.primary.used_percent, 1.0); } @@ -224,7 +232,8 @@ mod tests { let result = fetch_result_from_quota( &serde_json::json!({"daily_percentage": 0.2, "overage_balance": 12.34}), "org/demo", - ); + ) + .unwrap(); let cost = result.cost.unwrap(); assert_eq!(cost.used, 12.34); @@ -236,7 +245,8 @@ mod tests { let result = fetch_result_from_quota( &serde_json::json!({"daily_percentage": 0.2, "overage_balance_cents": 7087}), "org/demo", - ); + ) + .unwrap(); assert_eq!(result.cost.unwrap().used, 70.87); } @@ -278,3 +288,32 @@ mod tests { assert!(auth_response_error(reqwest::StatusCode::NOT_FOUND, body).is_none()); } } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + use serde_json::json; + fn build(value: Value) -> Result { + snapshot_from_quota(&value, "org/synthetic") + } + #[test] + fn cx289a_missing_quota_is_error() { + for value in [json!({}), json!({"limit":100}), json!({"used":0})] { + assert!(build(value.clone()).is_err(), "{value}"); + } + } + #[test] + fn cx289a_weekly_only_keeps_unknown_primary() { + let snap = build(json!({"weekly_percentage":25})).unwrap(); + assert!(snap.primary.is_informational); + assert_eq!(snap.secondary.unwrap().used_percent, 25.0); + } + #[test] + fn cx289a_real_zero_is_quota() { + for value in [json!({"used":0,"limit":100}), json!({"daily_percentage":0})] { + let snap = build(value).unwrap(); + assert!(!snap.primary.is_informational); + assert_eq!(snap.primary.used_percent, 0.0); + } + } +} From 25baa9714125c3a99d4762e2419070e63feb95b5 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 32/75] Fix missing Doubao usage being reported as a quota --- rust/src/providers/doubao/mod.rs | 168 ++++++++++++++++--------------- 1 file changed, 86 insertions(+), 82 deletions(-) diff --git a/rust/src/providers/doubao/mod.rs b/rust/src/providers/doubao/mod.rs index f448fd7f9f3..bce5eaaf7df 100644 --- a/rust/src/providers/doubao/mod.rs +++ b/rust/src/providers/doubao/mod.rs @@ -75,9 +75,9 @@ impl DoubaoProvider { for model in PROBE_MODELS { match self.probe(api_key, model).await { Ok(result) => { - return Ok(self + return self .confirm_ambiguous_zero_remaining(api_key, model, result) - .await); + .await; } Err(error @ ProviderError::AuthRequired) => return Err(error), Err(error) => { @@ -120,7 +120,7 @@ impl DoubaoProvider { ))); } - Ok(coding_plan_snapshot(decode_coding_plan_usage(&bytes)?)) + coding_plan_snapshot(decode_coding_plan_usage(&bytes)?) } async fn confirm_ambiguous_zero_remaining( @@ -128,7 +128,7 @@ impl DoubaoProvider { api_key: &str, model: &str, initial: DoubaoProbeResult, - ) -> UsageSnapshot { + ) -> Result { if !initial.has_ambiguous_zero_remaining() { return initial.snapshot; } @@ -141,7 +141,6 @@ impl DoubaoProvider { confirmation.remaining, confirmation.limit, confirmation.resets_at, - confirmation.total_tokens, false, ), Ok(confirmation) => confirmation.snapshot, @@ -188,12 +187,11 @@ impl DoubaoProvider { #[derive(Debug)] struct DoubaoProbeResult { - snapshot: UsageSnapshot, + snapshot: Result, status: reqwest::StatusCode, remaining: Option, limit: Option, resets_at: Option>, - total_tokens: Option, request_limits_reliable: bool, } @@ -209,28 +207,18 @@ impl DoubaoProbeResult { fn probe_result_from_response( status: reqwest::StatusCode, headers: &reqwest::header::HeaderMap, - body: &serde_json::Value, + _body: &serde_json::Value, ) -> DoubaoProbeResult { let remaining = int_header(headers, "x-ratelimit-remaining-requests"); let limit = int_header(headers, "x-ratelimit-limit-requests"); let resets_at = string_header(headers, "x-ratelimit-reset-requests").and_then(parse_reset_time); - let total_tokens = body - .get("usage") - .and_then(|usage| usage.get("total_tokens")) - .and_then(|value| value.as_i64()); let request_limits_reliable = if status == reqwest::StatusCode::TOO_MANY_REQUESTS { limit.is_some() } else { limit.is_some() && remaining.is_some() }; - let snapshot = snapshot_from_parts( - remaining, - limit, - resets_at, - total_tokens, - request_limits_reliable, - ); + let snapshot = snapshot_from_parts(remaining, limit, resets_at, request_limits_reliable); DoubaoProbeResult { snapshot, @@ -238,7 +226,6 @@ fn probe_result_from_response( remaining, limit, resets_at, - total_tokens, request_limits_reliable, } } @@ -247,38 +234,25 @@ fn snapshot_from_parts( remaining: Option, limit: Option, resets_at: Option>, - total_tokens: Option, request_limits_reliable: bool, -) -> UsageSnapshot { - let effective_remaining = remaining.unwrap_or(0); - - let (used_percent, detail) = if let (Some(remaining), Some(limit)) = (remaining, limit) { - if request_limits_reliable && limit > 0 { - let used = (limit - remaining).max(0); - let percent = used as f64 / limit as f64 * 100.0; - (percent, format!("{used}/{limit} requests")) - } else { - (0.0, "Active - check dashboard for details".to_string()) - } - } else if let Some(limit) = limit.filter(|limit| request_limits_reliable && *limit > 0) { - let used = (limit - effective_remaining).max(0); - let percent = if limit > 0 { - used as f64 / limit as f64 * 100.0 - } else { - 0.0 - }; - (percent, format!("{used}/{limit} requests")) - } else if let Some(total_tokens) = total_tokens { - (0.0, format!("Active - {total_tokens} tokens observed")) - } else { - (0.0, "Active - check dashboard for details".to_string()) +) -> Result { + let (Some(remaining), Some(limit)) = (remaining, limit.filter(|limit| *limit > 0)) else { + return Err(ProviderError::Parse( + "Doubao response contains no usable request quota".into(), + )); }; - - let mut window = RateWindow::with_details(used_percent, None, resets_at, Some(detail)); - if window.used_percent.is_nan() { - window.used_percent = 0.0; + if !request_limits_reliable { + return Err(ProviderError::Parse( + "Doubao request quota is not reliable".into(), + )); } - UsageSnapshot::new(window) + let used = (limit - remaining).max(0); + Ok(UsageSnapshot::new(RateWindow::with_details( + used as f64 / limit as f64 * 100.0, + None, + resets_at, + Some(format!("{used}/{limit} requests")), + ))) } fn int_header(headers: &reqwest::header::HeaderMap, name: &str) -> Option { @@ -422,13 +396,13 @@ fn decode_coding_plan_usage(bytes: &[u8]) -> Result UsageSnapshot { +fn coding_plan_snapshot(usage: CodingPlanResult) -> Result { let primary = coding_plan_window( &usage, &["session", "5-hour", "five_hour", "5h"], Some(5 * 60), ) - .unwrap_or_else(|| RateWindow::new(0.0)); + .unwrap_or_else(|| RateWindow::informational("Session usage unavailable")); let mut snapshot = UsageSnapshot::new(primary); if let Some(weekly) = coding_plan_window(&usage, &["weekly", "week"], Some(7 * 24 * 60)) { snapshot = snapshot.with_secondary(weekly); @@ -481,13 +455,22 @@ fn coding_plan_snapshot(usage: CodingPlanResult) -> UsageSnapshot { )); } } + if snapshot.primary.is_informational + && snapshot.secondary.is_none() + && snapshot.tertiary.is_none() + && snapshot.extra_rate_windows.is_empty() + { + return Err(ProviderError::Parse( + "Doubao Coding Plan contains no usable quota".into(), + )); + } if let Some(status) = usage.status.filter(|s| !s.trim().is_empty()) { snapshot = snapshot.with_login_method(status); } if let Some(update) = usage.update_timestamp.and_then(datetime_from_epoch) { snapshot.updated_at = update; } - snapshot + Ok(snapshot) } fn coding_plan_window( @@ -745,7 +728,7 @@ fn decode_arkcli_usage(bytes: &[u8]) -> Result fn fetch_arkcli_usage() -> Result { let stdout = run_arkcli_usage_plan()?; let usage = decode_arkcli_usage(&stdout)?; - Ok(coding_plan_snapshot(usage)) + coding_plan_snapshot(usage) } fn datetime_from_epoch(timestamp: f64) -> Option> { @@ -989,13 +972,14 @@ mod tests { "x-ratelimit-limit-requests", HeaderValue::from_static("100"), ); - let snapshot = - probe_result_from_response(reqwest::StatusCode::OK, &headers, &json!({})).snapshot; + let snapshot = probe_result_from_response(reqwest::StatusCode::OK, &headers, &json!({})) + .snapshot + .unwrap(); assert_eq!(snapshot.primary.used_percent, 75.0); } #[test] - fn doubao_repeated_successful_zero_remaining_falls_back_to_active() { + fn doubao_repeated_successful_zero_remaining_is_unavailable() { let mut headers = HeaderMap::new(); headers.insert( "x-ratelimit-remaining-requests", @@ -1009,22 +993,12 @@ mod tests { let result = probe_result_from_response(reqwest::StatusCode::OK, &headers, &json!({})); assert!(result.has_ambiguous_zero_remaining()); - let snapshot = snapshot_from_parts( - result.remaining, - result.limit, - result.resets_at, - result.total_tokens, - false, - ); - assert_eq!(snapshot.primary.used_percent, 0.0); - assert_eq!( - snapshot.primary.reset_description.as_deref(), - Some("Active - check dashboard for details") - ); + let snapshot = snapshot_from_parts(result.remaining, result.limit, result.resets_at, false); + assert!(snapshot.is_err()); } #[test] - fn doubao_rate_limit_with_limit_header_reports_exhausted() { + fn doubao_rate_limit_without_remaining_is_unavailable() { let mut headers = HeaderMap::new(); headers.insert( "x-ratelimit-limit-requests", @@ -1037,15 +1011,11 @@ mod tests { ) .snapshot; - assert_eq!(snapshot.primary.used_percent, 100.0); - assert_eq!( - snapshot.primary.reset_description.as_deref(), - Some("1000/1000 requests") - ); + assert!(snapshot.is_err()); } #[test] - fn doubao_bare_rate_limit_uses_active_fallback() { + fn doubao_bare_rate_limit_is_unavailable() { let snapshot = probe_result_from_response( reqwest::StatusCode::TOO_MANY_REQUESTS, &HeaderMap::new(), @@ -1053,11 +1023,7 @@ mod tests { ) .snapshot; - assert_eq!(snapshot.primary.used_percent, 0.0); - assert_eq!( - snapshot.primary.reset_description.as_deref(), - Some("Active - check dashboard for details") - ); + assert!(snapshot.is_err()); } #[test] @@ -1073,7 +1039,7 @@ mod tests { ] } }"#; - let snapshot = coding_plan_snapshot(decode_coding_plan_usage(body).unwrap()); + let snapshot = coding_plan_snapshot(decode_coding_plan_usage(body).unwrap()).unwrap(); assert_eq!(snapshot.primary.used_percent, 12.5); assert_eq!(snapshot.secondary.unwrap().used_percent, 50.0); let monthly = snapshot.tertiary.expect("monthly"); @@ -1145,7 +1111,7 @@ mod tests { ] }"#; let usage = decode_arkcli_usage(raw).expect("arkcli json"); - let snap = coding_plan_snapshot(usage); + let snap = coding_plan_snapshot(usage).unwrap(); assert!((snap.primary.used_percent - 12.5).abs() < 0.01); assert!((snap.secondary.as_ref().unwrap().used_percent - 40.0).abs() < 0.01); assert!( @@ -1186,3 +1152,41 @@ mod tests { ); } } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + fn parts(remaining: Option, limit: Option) -> Result { + snapshot_from_parts(remaining, limit, None, true) + } + fn coding(body: &[u8]) -> Result { + coding_plan_snapshot(decode_coding_plan_usage(body)?) + } + #[test] + fn cx289a_missing_request_fields_are_error() { + for (remaining, limit) in [(None, None), (None, Some(100)), (Some(100), None)] { + assert!(parts(remaining, limit).is_err()); + } + } + #[test] + fn cx289a_empty_coding_plan_is_error() { + assert!(coding(br#"{"Result":{}}"#).is_err()); + } + #[test] + fn cx289a_weekly_only_keeps_unknown_primary() { + let snap = + coding(br#"{"Result":{"QuotaUsage":[{"Level":"weekly","Percent":0}]}}"#).unwrap(); + assert!(snap.primary.is_informational); + assert_eq!(snap.secondary.unwrap().used_percent, 0.0); + } + #[test] + fn cx289a_real_zero_is_quota() { + let snap = parts(Some(100), Some(100)).unwrap(); + assert!(!snap.primary.is_informational); + assert_eq!(snap.primary.used_percent, 0.0); + let snap = + coding(br#"{"Result":{"QuotaUsage":[{"Level":"session","Percent":0}]}}"#).unwrap(); + assert!(!snap.primary.is_informational); + assert_eq!(snap.primary.used_percent, 0.0); + } +} From eb75eecd4dc905dbaa0d801d976e5f53d73fbbab Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 33/75] Fix missing Factory usage being reported as a quota --- rust/src/providers/factory/mod.rs | 220 ++++++++++++++++++------------ 1 file changed, 134 insertions(+), 86 deletions(-) diff --git a/rust/src/providers/factory/mod.rs b/rust/src/providers/factory/mod.rs index b95b0d7db1a..54257499cda 100755 --- a/rust/src/providers/factory/mod.rs +++ b/rust/src/providers/factory/mod.rs @@ -57,14 +57,10 @@ struct FactoryUsageWindow { } impl FactoryUsageWindow { - fn percent_used(&self) -> f64 { - let used = self.used.unwrap_or(0.0); - let allowance = self.allowance.unwrap_or(1.0); - if allowance > 0.0 { - ((used / allowance) * 100.0).clamp(0.0, 100.0) - } else { - 0.0 - } + fn percent_used(&self) -> Option { + let used = self.used?; + let allowance = self.allowance.filter(|allowance| *allowance > 0.0)?; + Some(((used / allowance) * 100.0).clamp(0.0, 100.0)) } } @@ -78,22 +74,18 @@ struct FactoryTokenUsage { } impl FactoryTokenUsage { - fn percent_used(&self) -> f64 { + fn percent_used(&self) -> Option { if let Some(ratio) = self.used_ratio.filter(|r| r.is_finite()) { if (-0.001..=1.001).contains(&ratio) { - return (ratio * 100.0).clamp(0.0, 100.0); + return Some((ratio * 100.0).clamp(0.0, 100.0)); } if (-0.1..=100.1).contains(&ratio) { - return ratio.clamp(0.0, 100.0); + return Some(ratio.clamp(0.0, 100.0)); } } - let used = self.user_tokens.unwrap_or(0) as f64; - let allowance = self.total_allowance.unwrap_or(0) as f64; - if allowance > 0.0 { - ((used / allowance) * 100.0).clamp(0.0, 100.0) - } else { - 0.0 - } + let used = self.user_tokens? as f64; + let allowance = self.total_allowance.filter(|allowance| *allowance > 0)? as f64; + Some(((used / allowance) * 100.0).clamp(0.0, 100.0)) } } @@ -124,21 +116,11 @@ struct FactoryLimitPool { monthly: FactoryBillingWindow, } -impl FactoryLimitPool { - fn has_usage_data(&self) -> bool { - [&self.five_hour, &self.weekly, &self.monthly] - .iter() - .any(|w| { - w.used_percent > 0.0 || w.window_end.is_some() || w.seconds_remaining.is_some() - }) - } -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct FactoryBillingWindow { #[serde(default)] - used_percent: f64, + used_percent: Option, #[serde(default)] window_end: Option, #[serde(default)] @@ -146,7 +128,8 @@ struct FactoryBillingWindow { } impl FactoryBillingWindow { - fn rate_window(&self, window_minutes: Option) -> RateWindow { + fn rate_window(&self, window_minutes: Option) -> Option { + let used_percent = self.used_percent?; let resets_at = self .seconds_remaining .filter(|s| *s > 0.0 && s.is_finite()) @@ -161,7 +144,7 @@ impl FactoryBillingWindow { Utc::now() + Duration::milliseconds(millis) }); let mut window = RateWindow::with_details( - self.used_percent.clamp(0.0, 100.0), + used_percent.clamp(0.0, 100.0), window_minutes, resets_at, None, @@ -169,7 +152,7 @@ impl FactoryBillingWindow { if let Some(at) = resets_at { window.reset_description = Some(format!("Resets {}", at.format("%b %d at %H:%M UTC"))); } - window + Some(window) } } @@ -521,7 +504,7 @@ impl FactoryProvider { .await) .ok(); return Ok(Self::apply_auth_info( - snapshot_from_billing_limits(&limits, billing.overage_preference.as_deref()), + snapshot_from_billing_limits(&limits, billing.overage_preference.as_deref())?, auth, )); } @@ -566,7 +549,7 @@ impl FactoryProvider { .fetch_usage_api(client, base, None, Some(api_key)) .await?; Ok(Self::apply_auth_info( - Self::usage_snapshot_from_response(&usage_data), + Self::usage_snapshot_from_response(&usage_data)?, auth_info, )) } @@ -584,41 +567,49 @@ impl FactoryProvider { .await?; Ok(Self::apply_auth_info( - Self::usage_snapshot_from_response(&usage_data), + Self::usage_snapshot_from_response(&usage_data)?, auth_info, )) } - fn usage_snapshot_from_response(usage_data: &FactoryUsageResponse) -> UsageSnapshot { - // Prefer nested upstream `usage` only when it carries usable data. - // An empty `usage: {}` must not suppress populated top-level windows. - if let Some(nested) = &usage_data.usage - && nested_usage_has_data(nested) - { - let standard_percent = nested - .standard - .as_ref() - .map(FactoryTokenUsage::percent_used) - .unwrap_or(0.0); - let mut usage = UsageSnapshot::new(RateWindow::new(standard_percent)); - if let Some(premium) = &nested.premium { - usage = usage.with_secondary(RateWindow::new(premium.percent_used())); - } - return usage; - } - - let standard_percent = usage_data - .standard + fn usage_snapshot_from_response( + usage_data: &FactoryUsageResponse, + ) -> Result { + let standard = usage_data + .usage .as_ref() - .map(FactoryUsageWindow::percent_used) - .unwrap_or(0.0); - - let mut usage = UsageSnapshot::new(RateWindow::new(standard_percent)); - if let Some(premium) = &usage_data.premium { - usage = usage.with_secondary(RateWindow::new(premium.percent_used())); + .and_then(|nested| nested.standard.as_ref()) + .and_then(FactoryTokenUsage::percent_used) + .or_else(|| { + usage_data + .standard + .as_ref() + .and_then(FactoryUsageWindow::percent_used) + }); + let premium = usage_data + .usage + .as_ref() + .and_then(|nested| nested.premium.as_ref()) + .and_then(FactoryTokenUsage::percent_used) + .or_else(|| { + usage_data + .premium + .as_ref() + .and_then(FactoryUsageWindow::percent_used) + }); + if standard.is_none() && premium.is_none() { + return Err(ProviderError::Parse( + "Factory response contains no usable quota".into(), + )); } - - usage + let primary = standard + .map(RateWindow::new) + .unwrap_or_else(|| RateWindow::informational("Standard usage unavailable")); + let mut usage = UsageSnapshot::new(primary); + if let Some(premium) = premium { + usage = usage.with_secondary(RateWindow::new(premium)); + } + Ok(usage) } fn apply_auth_info( @@ -689,31 +680,21 @@ impl FactoryProvider { } } -fn nested_usage_has_data(nested: &FactoryUsageNested) -> bool { - let window_has = |w: &FactoryTokenUsage| { - w.user_tokens.is_some() - || w.total_allowance.is_some() - || w.used_ratio.is_some_and(|r| r.is_finite()) - }; - nested.standard.as_ref().is_some_and(window_has) - || nested.premium.as_ref().is_some_and(window_has) -} - fn snapshot_from_billing_limits( limits: &FactoryTokenRateLimits, overage_preference: Option<&str>, -) -> UsageSnapshot { +) -> Result { let primary = limits.standard.five_hour.rate_window(Some(5 * 60)); let secondary = limits.standard.weekly.rate_window(Some(7 * 24 * 60)); let tertiary = limits.standard.monthly.rate_window(None); - let mut usage = UsageSnapshot::new(primary) - .with_secondary(secondary) - .with_tertiary(tertiary); + let mut usage = UsageSnapshot::new( + primary.unwrap_or_else(|| RateWindow::informational("5-hour usage unavailable")), + ); + usage.secondary = secondary; + usage.tertiary = tertiary; - if let Some(core) = &limits.core - && core.has_usage_data() - { + if let Some(core) = &limits.core { for (id, title, window) in [ ( "factory-core-5h", @@ -731,15 +712,26 @@ fn snapshot_from_billing_limits( core.monthly.rate_window(None), ), ] { - usage = usage.with_extra_rate_window(id, title, window); + if let Some(window) = window { + usage = usage.with_extra_rate_window(id, title, window); + } } } + if usage.primary.is_informational + && usage.secondary.is_none() + && usage.tertiary.is_none() + && usage.extra_rate_windows.is_empty() + { + return Err(ProviderError::Parse( + "Factory billing limits contain no usable quota".into(), + )); + } let mut login = "Droid".to_string(); if let Some(pref) = overage_preference.filter(|p| !p.is_empty()) { login = format!("Droid - Fallback: {pref}"); } - usage.with_login_method(login) + Ok(usage.with_login_method(login)) } impl Default for FactoryProvider { @@ -907,7 +899,7 @@ mod tests { let parsed: FactoryBillingLimitsResponse = serde_json::from_str(body).unwrap(); assert!(parsed.uses_token_rate_limits_billing); let limits = parsed.limits.unwrap(); - let snap = snapshot_from_billing_limits(&limits, None); + let snap = snapshot_from_billing_limits(&limits, None).unwrap(); assert!((snap.primary.used_percent - 12.0).abs() < f64::EPSILON); assert!((snap.secondary.unwrap().used_percent - 34.0).abs() < f64::EPSILON); assert!((snap.tertiary.unwrap().used_percent - 56.0).abs() < f64::EPSILON); @@ -920,7 +912,7 @@ mod tests { "premium": { "used": 10.0, "allowance": 50.0 } }"#; let parsed: FactoryUsageResponse = serde_json::from_str(body).unwrap(); - let snap = FactoryProvider::usage_snapshot_from_response(&parsed); + let snap = FactoryProvider::usage_snapshot_from_response(&parsed).unwrap(); assert!((snap.primary.used_percent - 25.0).abs() < f64::EPSILON); assert!((snap.secondary.unwrap().used_percent - 20.0).abs() < f64::EPSILON); } @@ -934,7 +926,7 @@ mod tests { } }"#; let parsed: FactoryUsageResponse = serde_json::from_str(body).unwrap(); - let snap = FactoryProvider::usage_snapshot_from_response(&parsed); + let snap = FactoryProvider::usage_snapshot_from_response(&parsed).unwrap(); assert!((snap.primary.used_percent - 30.0).abs() < f64::EPSILON); assert!((snap.secondary.unwrap().used_percent - 10.0).abs() < f64::EPSILON); } @@ -947,7 +939,7 @@ mod tests { "premium": { "used": 5.0, "allowance": 50.0 } }"#; let parsed: FactoryUsageResponse = serde_json::from_str(body).unwrap(); - let snap = FactoryProvider::usage_snapshot_from_response(&parsed); + let snap = FactoryProvider::usage_snapshot_from_response(&parsed).unwrap(); assert!((snap.primary.used_percent - 40.0).abs() < f64::EPSILON); assert!((snap.secondary.unwrap().used_percent - 10.0).abs() < f64::EPSILON); } @@ -1026,3 +1018,59 @@ mod tests { ); } } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + use serde_json::{Value, json}; + fn legacy(value: Value) -> Result { + FactoryProvider::usage_snapshot_from_response(&serde_json::from_value(value).unwrap()) + } + fn billing(value: Value) -> Result { + snapshot_from_billing_limits(&serde_json::from_value(value).unwrap(), None) + } + #[test] + fn cx289a_legacy_missing_quota_is_error() { + for value in [ + json!({}), + json!({"standard":{"allowance":100}}), + json!({"standard":{"used":0}}), + json!({"usage":{"standard":{"totalAllowance":100}}}), + json!({"usage":{"standard":{"userTokens":0}}}), + ] { + assert!(legacy(value.clone()).is_err(), "{value}"); + } + } + #[test] + fn cx289a_billing_missing_quota_is_error() { + assert!(billing(json!({"standard":{"fiveHour":{},"weekly":{},"monthly":{}}})).is_err()); + } + #[test] + fn cx289a_secondary_only_keeps_unknown_primary() { + let snap = legacy(json!({"premium":{"used":0,"allowance":100}})).unwrap(); + assert!(snap.primary.is_informational); + assert_eq!(snap.secondary.unwrap().used_percent, 0.0); + let snap = + billing(json!({"standard":{"fiveHour":{},"weekly":{"usedPercent":0},"monthly":{}}})) + .unwrap(); + assert!(snap.primary.is_informational); + assert_eq!(snap.secondary.unwrap().used_percent, 0.0); + } + #[test] + fn cx289a_real_zero_is_quota() { + for value in [ + json!({"standard":{"used":0,"allowance":100}}), + json!({"usage":{"standard":{"userTokens":0,"totalAllowance":100}}}), + json!({"usage":{"standard":{"usedRatio":0}}}), + ] { + let snap = legacy(value).unwrap(); + assert!(!snap.primary.is_informational); + assert_eq!(snap.primary.used_percent, 0.0); + } + let snap = + billing(json!({"standard":{"fiveHour":{"usedPercent":0},"weekly":{},"monthly":{}}})) + .unwrap(); + assert!(!snap.primary.is_informational); + assert_eq!(snap.primary.used_percent, 0.0); + } +} From ebf5f9eb97aa1a3e625ea46df40ee11fc6edd0ae Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:50:49 +0100 Subject: [PATCH 34/75] Fix missing JetBrains usage being reported as a quota --- rust/src/providers/jetbrains/mod.rs | 61 ++++++++++++++++++----------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/rust/src/providers/jetbrains/mod.rs b/rust/src/providers/jetbrains/mod.rs index 3e5d29060a7..bb1ccebf83e 100755 --- a/rust/src/providers/jetbrains/mod.rs +++ b/rust/src/providers/jetbrains/mod.rs @@ -131,7 +131,7 @@ impl JetBrainsProvider { // let quota = Self::parse_quota_values(content); - Ok(Self::usage_from_quota(quota)) + Self::usage_from_quota(quota) } fn parse_quota_values(content: &str) -> JetBrainsQuota { @@ -145,8 +145,11 @@ impl JetBrainsProvider { quota } - fn usage_from_quota(quota: JetBrainsQuota) -> UsageSnapshot { - UsageSnapshot::new(RateWindow::new(quota.used_percent())).with_login_method("JetBrains AI") + fn usage_from_quota(quota: JetBrainsQuota) -> Result { + let percent = quota.used_percent().ok_or_else(|| { + ProviderError::Parse("JetBrains config contains no usable credit quota".into()) + })?; + Ok(UsageSnapshot::new(RateWindow::new(percent)).with_login_method("JetBrains AI")) } /// Extract numeric value from XML attribute @@ -168,18 +171,10 @@ impl JetBrainsProvider { } } +#[derive(Default)] struct JetBrainsQuota { - used_credits: f64, - credit_limit: f64, -} - -impl Default for JetBrainsQuota { - fn default() -> Self { - Self { - used_credits: 0.0, - credit_limit: 1000.0, - } - } + used_credits: Option, + credit_limit: Option, } impl JetBrainsQuota { @@ -187,13 +182,13 @@ impl JetBrainsQuota { if Self::is_used_credits_line(line) && let Some(value) = JetBrainsProvider::extract_xml_value(line) { - self.used_credits = value; + self.used_credits = Some(value); } if Self::is_credit_limit_line(line) && let Some(value) = JetBrainsProvider::extract_xml_value(line) { - self.credit_limit = value; + self.credit_limit = Some(value); } } @@ -210,12 +205,10 @@ impl JetBrainsQuota { || line.contains("monthlyLimit") } - fn used_percent(&self) -> f64 { - if self.credit_limit > 0.0 { - (self.used_credits / self.credit_limit) * 100.0 - } else { - 0.0 - } + fn used_percent(&self) -> Option { + let used = self.used_credits?; + let limit = self.credit_limit.filter(|limit| *limit > 0.0)?; + Some(used / limit * 100.0) } } @@ -290,3 +283,27 @@ mod tests { ); } } + +#[cfg(test)] +mod cx289a_tests { + use super::*; + #[test] + fn cx289a_missing_credit_fields_are_error() { + for xml in [ + "", + r#"