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,
}
}
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/abacus/mod.rs b/rust/src/providers/abacus/mod.rs
index 0363d87bd52..ae82a9ad97a 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,
}
@@ -43,6 +41,18 @@ struct BillingInfo {
current_tier: Option,
}
+fn parse_compute_response(body: &str) -> Result {
+ let env: ApiEnvelope = serde_json::from_str(body)
+ .map_err(|_| ProviderError::Parse("Abacus compute response is invalid".into()))?;
+
+ if !env.success {
+ return Err(ProviderError::AuthRequired);
+ }
+ env.result.ok_or_else(|| {
+ ProviderError::Parse("Abacus response contains no compute points result".into())
+ })
+}
+
pub struct AbacusProvider {
metadata: ProviderMetadata,
client: Client,
@@ -88,12 +98,17 @@ impl AbacusProvider {
.and_then(|b| b.next_billing_date.as_deref())
.and_then(|ts| DateTime::parse_from_rfc3339(ts).ok())
.map(|dt| dt.with_timezone(&Utc));
- let primary = RateWindow::with_details(
- percent,
- RateWindow::monthly_window_minutes(resets_at).or(Some(FALLBACK_MONTHLY_WINDOW_MINUTES)),
- resets_at,
- Some(format_credit_detail(used, total)),
- );
+ let primary = if total <= 0.0 {
+ RateWindow::informational(format_credit_detail(used, total))
+ } else {
+ RateWindow::with_details(
+ percent,
+ RateWindow::monthly_window_minutes(resets_at)
+ .or(Some(FALLBACK_MONTHLY_WINDOW_MINUTES)),
+ resets_at,
+ Some(format_credit_detail(used, total)),
+ )
+ };
let mut snapshot = UsageSnapshot::new(primary).with_primary_label(CREDITS_LABEL);
if let Some(b) = billing
@@ -126,14 +141,7 @@ impl AbacusProvider {
}
let body = resp.text().await?;
- let env: ApiEnvelope = serde_json::from_str(&body)
- .map_err(|e| ProviderError::Parse(format!("Failed to parse compute points: {}", e)))?;
-
- if !env.success {
- return Err(ProviderError::AuthRequired);
- }
- env.result
- .ok_or_else(|| ProviderError::Parse("Missing compute points result".to_string()))
+ parse_compute_response(&body)
}
async fn fetch_billing(&self, cookie_header: &str) -> Option {
@@ -334,3 +342,74 @@ 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);
+ }
+}
+
+#[cfg(test)]
+mod p1_11_zero_denominator_is_information_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_11_zero_denominator_is_information() {
+ let compute = serde_json::from_value(
+ serde_json::json!({"totalComputePoints":0,"computePointsLeft":0}),
+ )
+ .unwrap();
+ let snapshot = AbacusProvider::build_snapshot(compute, None).unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert!(snapshot.primary.window_minutes.is_none());
+ assert!(snapshot.primary.resets_at.is_none());
+ }
+}
+
+#[cfg(test)]
+mod p2_02_incomplete_compute_is_a_data_error_tests {
+ use super::*;
+ #[test]
+ fn review16_p2_02_incomplete_compute_is_a_data_error() {
+ for body in [
+ r#"{"success":true,"result":{"totalComputePoints":100}}"#,
+ r#"{"success":true}"#,
+ ] {
+ let error = parse_compute_response(body).unwrap_err();
+ assert!(matches!(error, ProviderError::Parse(_)));
+ let text = error.to_string().to_lowercase();
+ for marker in [
+ "missing",
+ "not found",
+ "required",
+ "auth",
+ "token",
+ "cookie",
+ "login",
+ ] {
+ assert!(!text.contains(marker), "{text}");
+ }
+ }
+ }
+}
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());
+ }
+}
diff --git a/rust/src/providers/amp/mod.rs b/rust/src/providers/amp/mod.rs
index 21a9c5ec2a8..d61258623fd 100755
--- a/rust/src/providers/amp/mod.rs
+++ b/rust/src/providers/amp/mod.rs
@@ -121,19 +121,18 @@ 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 response contains no usage counter".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 response contains no valid usage limit".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 +152,12 @@ impl AmpProvider {
Ok(usage)
}
+ fn configured_usage() -> Result {
+ Err(ProviderError::Parse(
+ "Amp response contains no usage data".into(),
+ ))
+ }
+
/// Probe for Amp installation
async fn probe_cli(&self, ctx: &FetchContext) -> Result {
// Check ctx.api_key first
@@ -170,9 +175,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 +646,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);
+ }
+}
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);
+ }
+}
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/augment/mod.rs b/rust/src/providers/augment/mod.rs
index 388746ef75a..c79417896cd 100755
--- a/rust/src/providers/augment/mod.rs
+++ b/rust/src/providers/augment/mod.rs
@@ -172,19 +172,20 @@ 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 response contains no usage counter".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 response contains no valid usage limit".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 +498,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);
+ }
+}
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());
+ }
+ }
+}
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);
+ }
+}
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);
+ }
+}
diff --git a/rust/src/providers/codebuddy/mod.rs b/rust/src/providers/codebuddy/mod.rs
index eae0fd2faa5..3da6079a6c2 100644
--- a/rust/src/providers/codebuddy/mod.rs
+++ b/rust/src/providers/codebuddy/mod.rs
@@ -500,44 +500,51 @@ 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 response contains no package total".into())
+ })?;
+ let account_used = precise_or_field(account, "CapacityUsedPrecise", "CapacityUsed");
+ let account_remaining =
+ precise_or_field(account, "CapacityRemainPrecise", "CapacityRemain");
+ // A fully known empty package contributes nothing to the aggregate.
+ if account_total == 0.0 && account_used == Some(0.0) && account_remaining == Some(0.0) {
+ continue;
+ }
+ let totals =
+ CreditTotals::normalized(account_total, account_used, account_remaining, 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 response contains no usage counter".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 +603,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 +1288,74 @@ 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 review16_empty_package_preserves_known_aggregate() {
+ let totals = totals_from_api_payload(&payload(json!([
+ {"CapacitySize":100,"CapacityUsed":25,"CapacityRemain":75},
+ {"CapacitySize":0,"CapacityUsed":0,"CapacityRemain":0}
+ ])))
+ .unwrap();
+ let snapshot = snapshot_from_totals(&totals);
+ assert_eq!(snapshot.primary.used_percent, 25.0);
+ assert!(!snapshot.primary.is_informational);
+ assert_eq!(totals.remaining, 75.0);
+ }
+
+ #[test]
+ fn review16_empty_package_alone_does_not_invent_quota() {
+ assert!(
+ totals_from_api_payload(&payload(json!([
+ {"CapacitySize":0,"CapacityUsed":0,"CapacityRemain":0}
+ ])))
+ .is_err()
+ );
+ assert!(
+ totals_from_api_payload(&payload(json!([
+ {"CapacitySize":100,"CapacityUsed":25,"CapacityRemain":75},
+ {"CapacitySize":0}
+ ])))
+ .is_err()
+ );
+ }
+
+ #[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);
+ }
+}
diff --git a/rust/src/providers/codebuff/mod.rs b/rust/src/providers/codebuff/mod.rs
index d0e00fa2575..ea84150fb72 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,57 @@ 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((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 remaining.is_some() => RateWindow::informational(format!(
+ "{} credits remaining; quota unavailable",
+ remaining.unwrap()
)),
- (_, Some(total), None) => Some(format!("{used:.0}/{total:.0} credits")),
- (_, None, Some(remaining)) => Some(format!("{remaining:.0} credits remaining")),
- _ => None,
+ 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 +237,7 @@ impl CodebuffProvider {
snapshot = snapshot.with_login_method(method_parts.join(" · "));
}
- snapshot
+ Ok(snapshot)
}
}
@@ -330,7 +346,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 +377,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 +386,60 @@ 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!({"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);
+ }
+}
+
+#[cfg(test)]
+mod review16_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_01_remaining_without_quota_is_information() {
+ let snapshot =
+ CodebuffProvider::snapshot_from_values(&json!({"remaining":100}), None).unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert!(snapshot.primary.reset_description.unwrap().contains("100"));
+ assert!(snapshot.primary.resets_at.is_none());
+ assert!(snapshot.secondary.is_none());
+ }
+}
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);
+ }
+}
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);
+ }
+}
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);
+ }
+}
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);
+ }
+ }
+}
diff --git a/rust/src/providers/deepinfra/mod.rs b/rust/src/providers/deepinfra/mod.rs
index b8ab5e2600b..218d1592319 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,82 @@ 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 depleted_or_suspended_account_preserves_measured_quota() {
+ for checklist in [
+ r#"{"stripe_balance":2,"recent":1,"limit":10}"#,
+ r#"{"stripe_balance":-20,"recent":1,"limit":10,"suspended":true}"#,
+ ] {
+ let snapshot = parse_snapshot_for_testing(checklist, r#"{"months":[]}"#)
+ .unwrap()
+ .to_usage_snapshot();
+ assert!(!snapshot.primary.is_informational, "{checklist}");
+ assert_eq!(snapshot.primary.used_percent, 10.0, "{checklist}");
+ }
+ }
+
+ #[test]
+ fn missing_billing_fields_are_rejected() {
+ assert!(parse_snapshot_for_testing(r#"{"limit":10}"#, r#"{"months":[]}"#).is_err());
+ }
+}
+
+#[cfg(test)]
+mod p1_15_account_status_does_not_override_measured_usage_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_15_account_status_does_not_override_measured_usage() {
+ for recent in [0, 1] {
+ for (balance, suspended) in [(-20, true), (0, false)] {
+ let value = serde_json::json!({"stripe_balance":balance,"recent":recent,"limit":10,"suspended":suspended});
+ let parsed =
+ parse_snapshot_for_testing(&value.to_string(), r#"{"months":[]}"#).unwrap();
+ let snapshot = parsed.to_usage_snapshot();
+ assert!(!snapshot.primary.is_informational);
+ assert_eq!(snapshot.primary.used_percent, f64::from(recent) * 10.0);
+ assert_eq!(parsed.to_cost_snapshot().unwrap().used, f64::from(recent));
+ if suspended {
+ assert!(
+ snapshot
+ .primary
+ .reset_description
+ .unwrap()
+ .contains("Suspended")
+ );
+ }
+ }
+ }
+ }
+}
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());
+ }
+ }
+}
diff --git a/rust/src/providers/devin/mod.rs b/rust/src/providers/devin/mod.rs
index 24c0ce4def4..67308b058bc 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,32 @@ 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"]))
+ .or_else(|| generic_percent(value));
+ let weekly = percent(value, &["weekly_percentage", "weeklyPercentage"]);
+ if daily.is_none() && weekly.is_none() && extra_usage_balance(value).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 {
@@ -170,6 +179,10 @@ fn percent(value: &Value, keys: &[&str]) -> Option {
return Some(if v < 1.0 { v * 100.0 } else { v });
}
}
+ None
+}
+
+fn generic_percent(value: &Value) -> Option {
let used = ["used", "usage", "used_count", "usedCount", "consumed"]
.iter()
.find_map(|k| value.get(*k).and_then(Value::as_f64));
@@ -208,14 +221,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 +237,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 +250,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 +293,65 @@ 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);
+ }
+ }
+}
+
+#[cfg(test)]
+mod p1_02_balance_without_quota_survives_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_02_balance_without_quota_survives() {
+ for (value, amount) in [
+ (serde_json::json!({"overage_balance":12.34}), 12.34),
+ (serde_json::json!({"overage_balance_cents":7087}), 70.87),
+ ] {
+ let result = fetch_result_from_quota(&value, "org/demo").unwrap();
+ assert!(result.usage.primary.is_informational);
+ assert!(result.usage.secondary.is_none());
+ assert_eq!(result.cost.unwrap().used, amount);
+ }
+ }
+}
+
+#[cfg(test)]
+mod p1_12_generic_counter_has_no_weekly_duplicate_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_12_generic_counter_has_no_weekly_duplicate() {
+ for used in [0, 25] {
+ let snapshot =
+ snapshot_from_quota(&serde_json::json!({"used":used,"limit":100}), "org/demo")
+ .unwrap();
+ assert_eq!(snapshot.primary.used_percent, f64::from(used));
+ assert!(!snapshot.primary.is_informational);
+ assert!(snapshot.secondary.is_none());
+ }
+ }
+}
diff --git a/rust/src/providers/doubao/mod.rs b/rust/src/providers/doubao/mod.rs
index f448fd7f9f3..ef383a608d2 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;
}
@@ -137,13 +137,9 @@ impl DoubaoProvider {
Ok(confirmation) if confirmation.status == reqwest::StatusCode::TOO_MANY_REQUESTS => {
initial.snapshot
}
- Ok(confirmation) if confirmation.has_ambiguous_zero_remaining() => snapshot_from_parts(
- confirmation.remaining,
- confirmation.limit,
- confirmation.resets_at,
- confirmation.total_tokens,
- false,
- ),
+ Ok(confirmation) if confirmation.has_ambiguous_zero_remaining() => {
+ confirmation.without_request_quota()
+ }
Ok(confirmation) => confirmation.snapshot,
Err(error) => {
tracing::warn!(
@@ -188,16 +184,24 @@ 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,
+ tokens: Option,
}
impl DoubaoProbeResult {
+ /// The request quota is ambiguous: keep only what the probe observed.
+ fn without_request_quota(&self) -> Result {
+ match self.tokens {
+ Some(tokens) => Ok(UsageSnapshot::new(tokens_information(tokens))),
+ None => snapshot_from_parts(self.remaining, self.limit, self.resets_at, false),
+ }
+ }
+
fn has_ambiguous_zero_remaining(&self) -> bool {
self.status == reqwest::StatusCode::OK
&& self.request_limits_reliable
@@ -214,23 +218,27 @@ fn probe_result_from_response(
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 mut snapshot = snapshot_from_parts(remaining, limit, resets_at, request_limits_reliable);
+ let tokens = body
+ .pointer("/usage/total_tokens")
+ .and_then(serde_json::Value::as_u64)
+ .filter(|_| status.is_success());
+ if let Some(tokens) = tokens {
+ snapshot = Ok(match snapshot {
+ Ok(snapshot) => snapshot.with_extra_rate_window(
+ "probe",
+ "Probe",
+ RateWindow::informational(format!("{tokens} tokens observed in probe")),
+ ),
+ Err(_) => UsageSnapshot::new(tokens_information(tokens)),
+ });
+ }
DoubaoProbeResult {
snapshot,
@@ -238,47 +246,40 @@ fn probe_result_from_response(
remaining,
limit,
resets_at,
- total_tokens,
request_limits_reliable,
+ tokens,
}
}
+fn tokens_information(tokens: u64) -> RateWindow {
+ RateWindow::informational(format!(
+ "{tokens} tokens observed in probe; request quota unavailable"
+ ))
+}
+
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 +423,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 +482,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 +755,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 +999,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 +1020,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 +1038,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 +1050,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 +1066,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 +1138,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 +1179,99 @@ 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);
+ }
+}
+
+#[cfg(test)]
+mod p1_03_probe_tokens_are_information_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_03_probe_tokens_are_information() {
+ let result = probe_result_from_response(
+ reqwest::StatusCode::OK,
+ &reqwest::header::HeaderMap::new(),
+ &serde_json::json!({"usage":{"total_tokens":7}}),
+ );
+ let snapshot = result.snapshot.unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert!(
+ snapshot
+ .primary
+ .reset_description
+ .unwrap()
+ .contains("7 tokens")
+ );
+ assert!(snapshot.primary.resets_at.is_none());
+ assert!(snapshot.primary.window_minutes.is_none());
+ assert!(!result.request_limits_reliable);
+ }
+
+ #[test]
+ fn review16_p2_ambiguous_zero_remaining_keeps_observed_tokens() {
+ let mut headers = reqwest::header::HeaderMap::new();
+ headers.insert("x-ratelimit-limit-requests", "100".parse().unwrap());
+ headers.insert("x-ratelimit-remaining-requests", "0".parse().unwrap());
+ let result = probe_result_from_response(
+ reqwest::StatusCode::OK,
+ &headers,
+ &serde_json::json!({"usage":{"total_tokens":7}}),
+ );
+ assert!(result.has_ambiguous_zero_remaining());
+ let snapshot = result.without_request_quota().unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert!(
+ snapshot
+ .primary
+ .reset_description
+ .unwrap()
+ .contains("7 tokens")
+ );
+ assert!(snapshot.extra_rate_windows.is_empty());
+ }
+
+ #[test]
+ fn review16_p2_ambiguous_zero_remaining_without_tokens_stays_error() {
+ let mut headers = reqwest::header::HeaderMap::new();
+ headers.insert("x-ratelimit-limit-requests", "100".parse().unwrap());
+ headers.insert("x-ratelimit-remaining-requests", "0".parse().unwrap());
+ let result =
+ probe_result_from_response(reqwest::StatusCode::OK, &headers, &serde_json::json!({}));
+ assert!(result.without_request_quota().is_err());
+ }
+}
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);
+ }
+}
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());
+ }
+}
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
+ );
+ }
+}
diff --git a/rust/src/providers/jetbrains/mod.rs b/rust/src/providers/jetbrains/mod.rs
index 3e5d29060a7..c89ae5a73ed 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 quota data 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#""#,
+ r#""#,
+ ] {
+ assert!(
+ JetBrainsProvider::new().parse_xml_config(xml).is_err(),
+ "{xml}"
+ );
+ }
+ }
+ #[test]
+ fn cx289a_real_zero_is_quota() {
+ let snap = JetBrainsProvider::new().parse_xml_config("\n").unwrap();
+ assert!(!snap.primary.is_informational);
+ assert_eq!(snap.primary.used_percent, 0.0);
+ }
+}
diff --git a/rust/src/providers/kilo/mod.rs b/rust/src/providers/kilo/mod.rs
index db391585696..8850f0da72c 100644
--- a/rust/src/providers/kilo/mod.rs
+++ b/rust/src/providers/kilo/mod.rs
@@ -91,70 +91,63 @@ impl KiloProvider {
credit_blocks_data: Option<&Value>,
kilo_pass_data: Option<&Value>,
) -> Result {
- // --- Credit blocks (primary window) ---
- // `user.getCreditBlocks` returns an object wrapping a `creditBlocks`
- // array plus a `totalBalance_mUsd` scalar (micro-USD). Accounts without
- // a Kilo Pass subscription get an empty array; fall back to the general
- // balance instead of rendering an empty $0.00 / $0.00 state (#355).
- let mut total = 0.0_f64;
- let mut remaining = 0.0_f64;
- let mut has_blocks = false;
-
- if let Some(payload) = credit_blocks_data {
- let blocks = payload
+ // A balance alone does not establish the original credit allowance.
+ let blocks = credit_blocks_data.and_then(|payload| {
+ payload
.get("creditBlocks")
- .and_then(|v| v.as_array())
- .or_else(|| payload.as_array());
-
- if let Some(arr) = blocks {
- has_blocks = !arr.is_empty();
- for block in arr {
- if let Ok(b) = serde_json::from_value::(block.clone()) {
- total += b.amount_m_usd.unwrap_or(0.0);
- remaining += b.balance_m_usd.unwrap_or(0.0);
- }
- }
- }
-
- if !has_blocks
- && let Some(balance_m_usd) =
- payload.get("totalBalance_mUsd").and_then(|v| v.as_f64())
- {
- total = balance_m_usd;
- remaining = balance_m_usd;
- }
- }
-
- let total_usd = total / 1_000_000.0;
- let remaining_usd = remaining / 1_000_000.0;
- let used_usd = (total_usd - remaining_usd).max(0.0);
- let percent = if total_usd > 0.0 {
- ((used_usd / total_usd) * 100.0).clamp(0.0, 100.0)
+ .and_then(Value::as_array)
+ .or_else(|| payload.as_array())
+ });
+ let totals = blocks
+ .filter(|blocks| !blocks.is_empty())
+ .and_then(|blocks| {
+ blocks
+ .iter()
+ .try_fold((0.0, 0.0), |(total, remaining), block| {
+ let block = serde_json::from_value::(block.clone()).ok()?;
+ Some((
+ total + block.amount_m_usd?,
+ remaining + block.balance_m_usd?,
+ ))
+ })
+ });
+ let balance = credit_blocks_data
+ .and_then(|payload| payload.get("totalBalance_mUsd"))
+ .and_then(Value::as_f64)
+ .filter(|balance| balance.is_finite());
+ let primary = if let Some((total, remaining)) = totals.filter(|(total, _)| *total > 0.0) {
+ let used = (total - remaining).max(0.0);
+ RateWindow::with_details(
+ (used / total * 100.0).clamp(0.0, 100.0),
+ None,
+ None,
+ Some(format!(
+ "${:.2}/${:.2}",
+ used / 1_000_000.0,
+ total / 1_000_000.0
+ )),
+ )
+ } else if let Some(balance) = balance {
+ RateWindow::informational(format!("${:.2} balance", balance / 1_000_000.0))
} else {
- 0.0
+ RateWindow::informational("Credit usage unavailable")
};
- let mut primary = RateWindow::new(percent);
- primary.reset_description = Some(format!("${:.2}/${:.2}", used_usd, total_usd));
-
let mut snap = UsageSnapshot::new(primary);
// --- Kilo Pass (secondary window) ---
if let Some(pass) = kilo_pass_data {
- let usage = pass
- .get("currentPeriodUsageUsd")
- .and_then(|v| v.as_f64())
- .unwrap_or(0.0);
+ let usage = pass.get("currentPeriodUsageUsd").and_then(Value::as_f64);
let base = pass
.get("currentPeriodBaseCreditsUsd")
- .and_then(|v| v.as_f64())
- .unwrap_or(0.0);
+ .and_then(Value::as_f64);
let bonus = pass
.get("currentPeriodBonusCreditsUsd")
- .and_then(|v| v.as_f64())
- .unwrap_or(0.0);
- let pass_total = base + bonus;
- if pass_total > 0.0 {
+ .and_then(Value::as_f64);
+ if let (Some(usage), Some(base), Some(bonus)) = (usage, base, bonus)
+ && base + bonus > 0.0
+ {
+ let pass_total = base + bonus;
let pass_pct = ((usage / pass_total) * 100.0).clamp(0.0, 100.0);
let mut secondary = RateWindow::new(pass_pct);
secondary.reset_description = Some(format!("${:.2}/${:.2}", usage, pass_total));
@@ -172,6 +165,11 @@ impl KiloProvider {
}
}
+ if snap.primary.is_informational && snap.secondary.is_none() && balance.is_none() {
+ return Err(ProviderError::Parse(
+ "Kilo response contains no usable credit quota".into(),
+ ));
+ }
Ok(snap)
}
@@ -348,13 +346,11 @@ mod tests {
#[test]
fn handles_missing_credit_blocks() {
- let snap = KiloProvider::build_snapshot(None, None).unwrap();
- assert!((snap.primary.used_percent - 0.0).abs() < f64::EPSILON);
- assert!(snap.secondary.is_none());
+ assert!(KiloProvider::build_snapshot(None, None).is_err());
}
#[test]
- fn falls_back_to_balance_for_payg_without_pass() {
+ fn review16_payg_balance_without_allowance_is_information() {
// Pay-as-you-go account: empty creditBlocks, no Kilo Pass subscription.
// Backs the "Remaining Credits" field on app.kilo.ai/profile (#355).
let credit_blocks = serde_json::json!({
@@ -363,13 +359,59 @@ mod tests {
"autoTopUpEnabled": false
});
let kilo_pass = serde_json::json!({ "subscription": null });
- let snap = KiloProvider::build_snapshot(Some(&credit_blocks), Some(&kilo_pass)).unwrap();
- // $1.06 balance, nothing used → 0% used, $0.00/$1.06 (used/total)
- assert!((snap.primary.used_percent - 0.0).abs() < f64::EPSILON);
+ let snapshot =
+ KiloProvider::build_snapshot(Some(&credit_blocks), Some(&kilo_pass)).unwrap();
+ assert!(snapshot.primary.is_informational);
assert_eq!(
- snap.primary.reset_description.as_deref(),
- Some("$0.00/$1.06")
+ snapshot.primary.reset_description.as_deref(),
+ Some("$1.06 balance")
+ );
+ assert_eq!(snapshot.primary.resets_at, None);
+ assert!(snapshot.secondary.is_none());
+ }
+}
+
+#[cfg(test)]
+mod cx289a_tests {
+ use super::*;
+ use serde_json::json;
+ #[test]
+ fn cx289a_missing_quota_is_error() {
+ assert!(KiloProvider::build_snapshot(None, None).is_err());
+ for value in [
+ json!({"creditBlocks":[{"amount_mUsd":100}]}),
+ json!({"creditBlocks":[{"balance_mUsd":100}]}),
+ ] {
+ assert!(
+ KiloProvider::build_snapshot(Some(&value), None).is_err(),
+ "{value}"
+ );
+ }
+ }
+ #[test]
+ fn cx289a_pass_missing_counter_is_error() {
+ assert!(
+ KiloProvider::build_snapshot(
+ None,
+ Some(&json!({"currentPeriodBaseCreditsUsd":100,"currentPeriodBonusCreditsUsd":0}))
+ )
+ .is_err()
);
- assert!(snap.secondary.is_none());
+ }
+ #[test]
+ fn cx289a_pass_only_keeps_unknown_primary() {
+ let snap = KiloProvider::build_snapshot(None, Some(&json!({"currentPeriodUsageUsd":0,"currentPeriodBaseCreditsUsd":100,"currentPeriodBonusCreditsUsd":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 = KiloProvider::build_snapshot(
+ Some(&json!({"creditBlocks":[{"amount_mUsd":100,"balance_mUsd":100}]})),
+ None,
+ )
+ .unwrap();
+ assert!(!snap.primary.is_informational);
+ assert_eq!(snap.primary.used_percent, 0.0);
}
}
diff --git a/rust/src/providers/kimik2/mod.rs b/rust/src/providers/kimik2/mod.rs
index cc9e2b085b3..7dcce32b68b 100755
--- a/rust/src/providers/kimik2/mod.rs
+++ b/rust/src/providers/kimik2/mod.rs
@@ -306,42 +306,54 @@ 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()
+ && voucher_balance.is_none()
+ && cash_balance.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 +376,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 +675,93 @@ 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 cash_or_voucher_without_main_balance_keep_their_amounts() {
+ for body in [
+ json!({"code":0,"status":true,"data":{"cash_balance":12.5,"voucher_balance":3.25}}),
+ json!({"data":{"cash_balance":0}}),
+ json!({"data":{"voucher_balance":3.25}}),
+ ] {
+ let snapshot = KimiK2Provider::new()
+ .parse_usage_response(&body, MoonshotRegion::International)
+ .unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert_eq!(
+ snapshot.primary.reset_description.as_deref(),
+ Some("Balance unavailable")
+ );
+ assert!(!snapshot.extra_rate_windows.is_empty());
+ }
+ }
+
+ #[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);
+ }
+ }
+}
diff --git a/rust/src/providers/litellm/mod.rs b/rust/src/providers/litellm/mod.rs
index c30e039b182..bb5a6eba67f 100644
--- a/rust/src/providers/litellm/mod.rs
+++ b/rust/src/providers/litellm/mod.rs
@@ -78,7 +78,7 @@ impl Provider for LiteLLMProvider {
let value: Value = response.json().await.map_err(|e| {
ProviderError::Parse(format!("Failed to parse LiteLLM key/info: {e}"))
})?;
- Ok(result_from_key_info(&value))
+ result_from_key_info(&value)
}
SourceMode::Web | SourceMode::Cli => {
Err(ProviderError::UnsupportedSource(ctx.source_mode))
@@ -131,47 +131,59 @@ fn management_url(base: &str, path: &str) -> Result {
.map_err(|e| ProviderError::Other(format!("Invalid LiteLLM URL: {e}")))
}
-fn result_from_key_info(value: &Value) -> ProviderFetchResult {
+fn result_from_key_info(value: &Value) -> Result {
let root = value
.get("info")
.or_else(|| value.get("key"))
.unwrap_or(value);
- let spend = number(root, &["spend", "spend_usd", "spendUSD"]).unwrap_or(0.0);
+ let spend = number(root, &["spend", "spend_usd", "spendUSD"]);
let limit = number(root, &["max_budget", "maxBudget", "budget", "limit"]);
- let percent = limit
- .filter(|v| *v > 0.0)
- .map_or(0.0, |limit| spend / limit * 100.0);
- let mut primary = RateWindow::new(percent);
- if let Some(limit) = limit.filter(|value| *value > 0.0) {
- primary.reset_description = Some(budget_detail(spend, limit));
- }
- let mut snapshot = UsageSnapshot::new(primary).with_login_method(format!("Spend ${spend:.2}"));
+ let primary = match (spend, limit.filter(|v| *v > 0.0)) {
+ (Some(spend), Some(limit)) => RateWindow::with_details(
+ spend / limit * 100.0,
+ None,
+ None,
+ Some(budget_detail(spend, limit)),
+ ),
+ _ => RateWindow::informational("Key budget usage unavailable"),
+ };
+ let mut snapshot = UsageSnapshot::new(primary);
+ if let Some(spend) = spend {
+ snapshot = snapshot.with_login_method(format!("Spend ${spend:.2}"));
+ }
if let Some(team) = root.get("team_info").or_else(|| root.get("teamInfo"))
&& let Some(team_spend) = number(team, &["spend", "team_spend", "teamSpend"])
{
let team_limit = number(team, &["max_budget", "budget", "limit"]);
- let team_percent = team_limit
- .filter(|v| *v > 0.0)
- .map_or(0.0, |limit| team_spend / limit * 100.0);
- let mut team_window = RateWindow::new(team_percent);
- if let Some(team_limit) = team_limit.filter(|value| *value > 0.0) {
+ let team_window = if let Some(team_limit) = team_limit.filter(|value| *value > 0.0) {
let alias = string(team, &["team_alias", "teamAlias", "alias"])
.map(|value| format!("Team {value}: "))
.unwrap_or_default();
- team_window.reset_description =
- Some(format!("{alias}{}", budget_detail(team_spend, team_limit)));
- }
+ RateWindow::with_details(
+ team_spend / team_limit * 100.0,
+ None,
+ None,
+ Some(format!("{alias}{}", budget_detail(team_spend, team_limit))),
+ )
+ } else {
+ RateWindow::informational(format!("Team spend ${team_spend:.2}"))
+ };
snapshot = snapshot.with_extra_rate_window("team", "Team budget", team_window);
}
+ if spend.is_none() && snapshot.extra_rate_windows.is_empty() {
+ return Err(ProviderError::Parse(
+ "LiteLLM response contains no usage data".into(),
+ ));
+ }
let mut result = ProviderFetchResult::new(snapshot, "api");
- if spend > 0.0 {
+ if let Some(spend) = spend {
let mut cost = CostSnapshot::new(spend, "USD", "Spend");
if let Some(limit) = limit {
cost = cost.with_limit(limit);
}
result = result.with_cost(cost);
}
- result
+ Ok(result)
}
fn number(value: &Value, keys: &[&str]) -> Option {
@@ -198,7 +210,8 @@ mod tests {
#[test]
fn parses_spend_budget() {
let result =
- result_from_key_info(&serde_json::json!({"info":{"spend":25.0,"max_budget":100.0}}));
+ result_from_key_info(&serde_json::json!({"info":{"spend":25.0,"max_budget":100.0}}))
+ .unwrap();
assert_eq!(result.usage.primary.used_percent, 25.0);
assert_eq!(
result.usage.primary.reset_description.as_deref(),
@@ -216,7 +229,8 @@ mod tests {
"max_budget": 1000.0
}
}
- }));
+ }))
+ .unwrap();
assert_eq!(result.usage.extra_rate_windows.len(), 1);
assert_eq!(
result.usage.extra_rate_windows[0]
@@ -244,3 +258,36 @@ mod tests {
assert_eq!(key, "sk-app");
}
}
+
+#[cfg(test)]
+mod cx289a_tests {
+ use super::*;
+ use serde_json::json;
+ fn build(value: Value) -> Result {
+ result_from_key_info(&value)
+ }
+ #[test]
+ fn cx289a_missing_spend_is_error() {
+ for value in [json!({}), json!({"max_budget":100})] {
+ assert!(build(value.clone()).is_err(), "{value}");
+ }
+ }
+ #[test]
+ fn cx289a_spend_only_is_informational() {
+ let result = build(json!({"spend":12})).unwrap();
+ assert!(result.usage.primary.is_informational);
+ assert_eq!(result.cost.unwrap().used, 12.0);
+ }
+ #[test]
+ fn cx289a_team_only_keeps_unknown_primary() {
+ let result = build(json!({"team_info":{"spend":0,"max_budget":100}})).unwrap();
+ assert!(result.usage.primary.is_informational);
+ assert!(!result.usage.extra_rate_windows[0].window.is_informational);
+ }
+ #[test]
+ fn cx289a_real_zero_is_quota() {
+ let result = build(json!({"spend":0,"max_budget":100})).unwrap();
+ assert!(!result.usage.primary.is_informational);
+ assert_eq!(result.usage.primary.used_percent, 0.0);
+ }
+}
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);
+ }
+}
diff --git a/rust/src/providers/longcat/mod.rs b/rust/src/providers/longcat/mod.rs
index 449c37c887e..fc0d7ffada8 100644
--- a/rust/src/providers/longcat/mod.rs
+++ b/rust/src/providers/longcat/mod.rs
@@ -233,32 +233,23 @@ fn build_snapshot(
) -> Result {
let account_data = envelope_data(account);
- let (total, used) = if let Some(lot) = token_packs.and_then(active_token_pack_lot) {
- // Active token-pack lot: consumed/total drive the quota directly.
- let total = json_f64(&lot, "totalToken")
- .ok_or_else(|| ProviderError::Parse("token-packs lot missing totalToken".into()))?;
- let used = json_f64(&lot, "consumedToken").unwrap_or(0.0);
- (total, used)
- } else if let Some(usage_raw) = usage_raw {
- // Legacy token quota: data.usage is the canonical aggregate.
- let usage_outer = envelope_data(usage_raw);
- let usage = usage_outer
- .get("usage")
- .filter(|u| u.is_object())
- .unwrap_or(usage_outer);
- let total = json_f64(usage, "totalToken")
- .ok_or_else(|| ProviderError::Parse("tokenUsage data was missing totalToken".into()))?;
- let remaining = json_f64(usage, "availableToken");
- let used = remaining.map(|r| (total - r).max(0.0)).unwrap_or(0.0);
- (total, used)
+ let quota = if let Some(lot) = token_packs.and_then(active_token_pack_lot) {
+ // Do not substitute another source when the active lot lacks its counter.
+ json_f64(&lot, "totalToken").zip(json_f64(&lot, "consumedToken"))
} else {
- return Err(ProviderError::Parse(
- "LongCat usage data was missing (no active token-pack lot and no tokenUsage payload)"
- .into(),
- ));
+ usage_raw.and_then(|usage_raw| {
+ let usage_outer = envelope_data(usage_raw);
+ let usage = usage_outer
+ .get("usage")
+ .filter(|u| u.is_object())
+ .unwrap_or(usage_outer);
+ let total = json_f64(usage, "totalToken")?;
+ let remaining = json_f64(usage, "availableToken")?;
+ Some((total, (total - remaining).max(0.0)))
+ })
};
- let primary = if total > 0.0 {
+ let primary = if let Some((total, used)) = quota.filter(|(total, _)| *total > 0.0) {
let mut w = RateWindow::new(((used / total) * 100.0).clamp(0.0, 100.0));
// Display-only rendering of token counts; values beyond i64 are
// unrealistic quota sizes and would only affect this label.
@@ -270,7 +261,7 @@ fn build_snapshot(
w.reset_description = Some(desc);
w
} else {
- RateWindow::informational("No token quota")
+ RateWindow::informational("Token usage unavailable")
};
let account_name = json_str(account_data, "name")
@@ -302,6 +293,11 @@ fn build_snapshot(
}
}
+ if snap.primary.is_informational && snap.secondary.is_none() {
+ return Err(ProviderError::Parse(
+ "LongCat response contains no usable quota".into(),
+ ));
+ }
Ok(snap)
}
@@ -318,7 +314,7 @@ fn parse_fuel(fuel: &Value) -> Option<(f64, f64, Option>)> {
.unwrap_or(0.0);
let mut package_total = 0.0;
let mut remaining = 0.0;
- let mut saw_remaining = false;
+ let mut complete_totals = true;
let mut nearest: Option> = None;
for pkg in packages {
@@ -327,13 +323,16 @@ fn parse_fuel(fuel: &Value) -> Option<(f64, f64, Option>)> {
.or_else(|| json_f64(pkg, "amount"))
{
package_total += t.max(0.0);
+ } else {
+ complete_totals = false;
}
if let Some(r) = json_f64(pkg, "availableToken")
.or_else(|| json_f64(pkg, "remainingToken"))
.or_else(|| json_f64(pkg, "remaining"))
{
remaining += r.max(0.0);
- saw_remaining = true;
+ } else {
+ return None;
}
if let Some(raw) = pkg
.get("expireTime")
@@ -348,18 +347,16 @@ fn parse_fuel(fuel: &Value) -> Option<(f64, f64, Option>)> {
}
}
- let mut total = if declared_total > 0.0 {
+ let total = if declared_total > 0.0 {
declared_total
- } else {
+ } else if complete_totals {
package_total
+ } else {
+ return None;
};
- if total <= 0.0 && !saw_remaining {
+ if total <= 0.0 || packages.is_empty() {
return None;
}
- if total <= 0.0 {
- total = remaining;
- }
- let remaining = if saw_remaining { remaining } else { total };
Some((total, remaining, nearest))
}
@@ -494,3 +491,57 @@ mod tests {
);
}
}
+
+#[cfg(test)]
+mod cx289a_tests {
+ use super::*;
+ use serde_json::json;
+ #[test]
+ fn cx289a_missing_counters_are_error() {
+ assert!(build_snapshot(&json!({}), None, Some(&json!({"totalToken":100})), None).is_err());
+ assert!(
+ build_snapshot(
+ &json!({}),
+ Some(&json!({"currentLot":{"status":"ACTIVE","totalToken":100}})),
+ None,
+ None
+ )
+ .is_err()
+ );
+ }
+ #[test]
+ fn cx289a_incomplete_fuel_is_not_quota() {
+ for fuel in [
+ json!({"packages":[{"totalToken":100}]}),
+ json!({"packages":[{"availableToken":100}]}),
+ json!({"packages":[{"totalToken":100,"availableToken":50},{"totalToken":100}]}),
+ ] {
+ assert!(parse_fuel(&fuel).is_none(), "{fuel}");
+ }
+ }
+ #[test]
+ fn cx289a_fuel_only_keeps_unknown_primary() {
+ let fuel = json!({"packages":[{"totalToken":100,"availableToken":100}]});
+ for usage in [json!({"totalToken":100}), json!({})] {
+ let snap = build_snapshot(&json!({}), None, Some(&usage), Some(&fuel)).unwrap();
+ assert!(snap.primary.is_informational);
+ let secondary = snap.secondary.unwrap();
+ assert!(!secondary.is_informational);
+ assert_eq!(secondary.used_percent, 0.0);
+ }
+ }
+ #[test]
+ fn cx289a_real_zero_is_quota() {
+ for (packs, usage) in [
+ (None, Some(json!({"totalToken":100,"availableToken":100}))),
+ (
+ Some(json!({"currentLot":{"status":"ACTIVE","totalToken":100,"consumedToken":0}})),
+ None,
+ ),
+ ] {
+ let snap = build_snapshot(&json!({}), packs.as_ref(), usage.as_ref(), None).unwrap();
+ assert!(!snap.primary.is_informational);
+ assert_eq!(snap.primary.used_percent, 0.0);
+ }
+ }
+}
diff --git a/rust/src/providers/manus/mod.rs b/rust/src/providers/manus/mod.rs
index 73bc90ba350..420a9c582c9 100644
--- a/rust/src/providers/manus/mod.rs
+++ b/rust/src/providers/manus/mod.rs
@@ -19,21 +19,21 @@ const MANUS_CREDITS_URL: &str = "https://api.manus.im/user.v1.UserService/GetAva
#[serde(rename_all = "camelCase")]
struct ManusCreditsResponse {
#[serde(default)]
- total_credits: f64,
+ total_credits: Option,
#[serde(default)]
free_credits: f64,
#[serde(default)]
- periodic_credits: f64,
+ periodic_credits: Option,
#[serde(default)]
- addon_credits: f64,
+ addon_credits: Option,
#[serde(default)]
- refresh_credits: f64,
+ refresh_credits: Option,
#[serde(default)]
- max_refresh_credits: f64,
+ max_refresh_credits: Option,
#[serde(default)]
- pro_monthly_credits: f64,
+ pro_monthly_credits: Option,
#[serde(default)]
- event_credits: f64,
+ event_credits: Option,
#[serde(default)]
next_refresh_time: Option>,
#[serde(default)]
@@ -99,7 +99,7 @@ impl ManusProvider {
.await
.map_err(|e| ProviderError::Parse(format!("Failed to parse Manus response: {e}")))?;
let credits = parse_credits(body)?;
- Ok(snapshot_from_credits(&credits))
+ snapshot_from_credits(&credits)
}
}
@@ -138,43 +138,51 @@ fn parse_credits(body: Value) -> Result {
.map_err(|e| ProviderError::Parse(format!("Failed to parse Manus credits: {e}")))
}
-fn snapshot_from_credits(credits: &ManusCreditsResponse) -> UsageSnapshot {
- let primary = if credits.pro_monthly_credits > 0.0 {
- let used = (credits.pro_monthly_credits - credits.periodic_credits).max(0.0);
- let percent = used / credits.pro_monthly_credits * 100.0;
+fn snapshot_from_credits(credits: &ManusCreditsResponse) -> Result {
+ let primary = if let (Some(total), Some(remaining)) = (
+ credits.pro_monthly_credits.filter(|total| *total > 0.0),
+ credits.periodic_credits,
+ ) {
+ let used = (total - remaining).max(0.0);
+ let percent = used / total * 100.0;
RateWindow::with_details(
percent,
None,
None,
- Some(format!(
- "{:.0} total credits ({:.0} free)",
- credits.total_credits, credits.free_credits
+ Some(credits.total_credits.map_or_else(
+ || format!("{remaining:.0}/{total:.0} monthly credits"),
+ |balance| {
+ format!(
+ "{balance:.0} total credits ({:.0} free)",
+ credits.free_credits
+ )
+ },
)),
)
+ } else if let Some(balance) = credits.total_credits {
+ RateWindow::informational(format!("{balance:.0} total credits"))
} else {
- RateWindow::with_details(
- 0.0,
- None,
- None,
- Some(format!("{:.0} credits available", credits.total_credits)),
- )
+ RateWindow::informational("Monthly credit usage unavailable")
};
- let mut snapshot = UsageSnapshot::new(primary).with_login_method(format!(
- "Balance: {:.0} credits",
- credits.total_credits.round()
- ));
+ let mut snapshot = UsageSnapshot::new(primary);
+ if let Some(balance) = credits.total_credits {
+ snapshot = snapshot.with_login_method(format!("Balance: {:.0} credits", balance.round()));
+ }
- if credits.max_refresh_credits > 0.0 {
- let used = (credits.max_refresh_credits - credits.refresh_credits).max(0.0);
- let mut secondary = RateWindow::with_details(
- used / credits.max_refresh_credits * 100.0,
+ if let (Some(total), Some(remaining)) = (
+ credits.max_refresh_credits.filter(|total| *total > 0.0),
+ credits.refresh_credits,
+ ) {
+ let used = (total - remaining).max(0.0);
+ let secondary = RateWindow::with_details(
+ used / total * 100.0,
None,
credits.next_refresh_time,
Some(format!(
"{:.0}/{:.0} refresh credits{}",
- credits.refresh_credits,
- credits.max_refresh_credits,
+ remaining,
+ total,
credits
.refresh_interval
.as_deref()
@@ -182,23 +190,27 @@ fn snapshot_from_credits(credits: &ManusCreditsResponse) -> UsageSnapshot {
.unwrap_or_default()
)),
);
- if !secondary.used_percent.is_finite() {
- secondary.used_percent = 0.0;
- }
snapshot = snapshot.with_secondary(secondary);
}
- if credits.addon_credits > 0.0 {
- let mut addon = RateWindow::new(0.0);
- addon.reset_description = Some(format!("{:.0} add-on credits", credits.addon_credits));
+ if let Some(balance) = credits.addon_credits {
+ let addon = RateWindow::informational(format!("{balance:.0} add-on credits"));
snapshot = snapshot.with_extra_rate_window("addon", "Add-on credits", addon);
}
- if credits.event_credits > 0.0 {
- let mut event = RateWindow::new(0.0);
- event.reset_description = Some(format!("{:.0} event credits", credits.event_credits));
+ if let Some(balance) = credits.event_credits {
+ let event = RateWindow::informational(format!("{balance:.0} event credits"));
snapshot = snapshot.with_extra_rate_window("event", "Event credits", event);
}
- snapshot
+ if snapshot.primary.is_informational
+ && snapshot.secondary.is_none()
+ && credits.total_credits.is_none()
+ && snapshot.extra_rate_windows.is_empty()
+ {
+ return Err(ProviderError::Parse(
+ "Manus response contains no usable credit quota or balance".into(),
+ ));
+ }
+ Ok(snapshot)
}
impl Default for ManusProvider {
@@ -256,3 +268,106 @@ mod tests {
);
}
}
+
+#[cfg(test)]
+mod cx289a_tests {
+ use super::*;
+ use serde_json::json;
+ fn build(value: Value) -> Result {
+ snapshot_from_credits(&parse_credits(value)?)
+ }
+ #[test]
+ fn review16_quota_less_response_preserves_all_balances() {
+ let snapshot =
+ build(json!({"totalCredits":47,"addonCredits":40,"eventCredits":7})).unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert_eq!(
+ snapshot.primary.reset_description.as_deref(),
+ Some("47 total credits")
+ );
+ assert_eq!(
+ snapshot.login_method.as_deref(),
+ Some("Balance: 47 credits")
+ );
+ for (id, description) in [("addon", "40 add-on credits"), ("event", "7 event credits")] {
+ let window = &snapshot
+ .extra_rate_windows
+ .iter()
+ .find(|w| w.id == id)
+ .unwrap()
+ .window;
+ assert!(window.is_informational);
+ assert_eq!(window.reset_description.as_deref(), Some(description));
+ }
+ }
+
+ #[test]
+ fn review16_explicit_zero_balance_is_not_missing() {
+ let snapshot = build(json!({"totalCredits":0})).unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert_eq!(
+ snapshot.primary.reset_description.as_deref(),
+ Some("0 total credits")
+ );
+ assert!(build(json!({})).is_err());
+ }
+
+ #[test]
+ fn review16_addon_only_does_not_invent_total_balance() {
+ let snapshot = build(json!({"addonCredits":40})).unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert_eq!(
+ snapshot.primary.reset_description.as_deref(),
+ Some("Monthly credit usage unavailable")
+ );
+ assert!(snapshot.login_method.is_none());
+ assert_eq!(
+ snapshot.extra_rate_windows[0]
+ .window
+ .reset_description
+ .as_deref(),
+ Some("40 add-on credits")
+ );
+ }
+
+ #[test]
+ fn cx289a_missing_quota_is_error() {
+ for value in [
+ json!({}),
+ json!({"proMonthlyCredits":100}),
+ json!({"periodicCredits":100}),
+ json!({"maxRefreshCredits":100}),
+ ] {
+ assert!(build(value.clone()).is_err(), "{value}");
+ }
+ }
+ #[test]
+ fn cx289a_refresh_only_keeps_unknown_primary() {
+ let snap = build(json!({"maxRefreshCredits":100,"refreshCredits":100})).unwrap();
+ assert!(snap.primary.is_informational);
+ assert_eq!(snap.secondary.unwrap().used_percent, 0.0);
+ }
+ #[test]
+ fn addon_and_event_balances_are_information_not_zero_percent() {
+ let snap = build(json!({
+ "proMonthlyCredits":100,"periodicCredits":100,
+ "addonCredits":40,"eventCredits":7
+ }))
+ .unwrap();
+ for id in ["addon", "event"] {
+ let window = &snap
+ .extra_rate_windows
+ .iter()
+ .find(|w| w.id == id)
+ .unwrap_or_else(|| panic!("{id} window"))
+ .window;
+ assert!(window.is_informational, "{id}");
+ }
+ }
+ #[test]
+ fn cx289a_real_zero_is_quota() {
+ let snap = build(json!({"proMonthlyCredits":100,"periodicCredits":100})).unwrap();
+ assert!(!snap.primary.is_informational);
+ assert_eq!(snap.primary.used_percent, 0.0);
+ }
+}
diff --git a/rust/src/providers/mimo/mod.rs b/rust/src/providers/mimo/mod.rs
index e2b163a1394..f6188f31278 100644
--- a/rust/src/providers/mimo/mod.rs
+++ b/rust/src/providers/mimo/mod.rs
@@ -123,14 +123,14 @@ impl MiMoProvider {
self.get_json("tokenPlan/detail", &cookie).await.ok();
let usage: Option =
self.get_json("tokenPlan/usage", &cookie).await.ok();
- Ok(snapshot_from_parts(
+ snapshot_from_parts(
balance_value,
data.currency,
data.cash_balance,
data.gift_balance,
detail,
usage,
- ))
+ )
}
async fn get_json Deserialize<'de>>(
@@ -213,7 +213,7 @@ fn snapshot_from_parts(
gift_balance: Option,
detail: Option,
usage: Option,
-) -> UsageSnapshot {
+) -> Result {
let detail_data =
detail.and_then(|response| (response.code == 0).then_some(response.data).flatten());
let usage_item = usage
@@ -238,15 +238,9 @@ fn snapshot_from_parts(
Some(format!("{}/{} tokens", item.used, item.limit)),
)
} else {
- RateWindow::with_details(
- 0.0,
- RateWindow::monthly_window_minutes(period_end),
- period_end,
- Some("No token-plan usage".into()),
- )
+ RateWindow::informational("MiMo plan usage unavailable")
};
- let mut secondary = RateWindow::new(0.0);
- secondary.reset_description = Some(balance_description(
+ let secondary = RateWindow::informational(balance_description(
balance,
¤cy,
cash_balance.as_deref(),
@@ -259,7 +253,7 @@ fn snapshot_from_parts(
} else {
snapshot = snapshot.with_login_method(format!("{balance:.2} {currency}"));
}
- snapshot
+ Ok(snapshot)
}
fn parse_mimo_date(value: &str) -> Option> {
@@ -368,3 +362,41 @@ mod tests {
assert_eq!(primary.resets_at, Some(period_end));
}
}
+
+#[cfg(test)]
+mod cx289a_parent_tests {
+ use super::*;
+ use serde_json::json;
+
+ fn parse(usage: Option) -> Result {
+ snapshot_from_parts(5.0, "CNY".into(), None, None, None, usage)
+ }
+ #[test]
+ fn review16_missing_optional_plan_preserves_balance() {
+ for usage in [
+ None,
+ Some(json!({"code":0,"data":{"monthUsage":{"items":[]}}})),
+ Some(json!({"code":503})),
+ ] {
+ let snapshot =
+ parse(usage.map(|value| serde_json::from_value(value).unwrap())).unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert_eq!(
+ snapshot.primary.reset_description.as_deref(),
+ Some("MiMo plan usage unavailable")
+ );
+ let balance = snapshot.secondary.unwrap();
+ assert!(balance.is_informational);
+ assert_eq!(
+ balance.reset_description.as_deref(),
+ Some("5.00 CNY balance")
+ );
+ }
+ }
+ #[test]
+ fn real_zero_usage_stays_a_quota() {
+ let snapshot = parse(Some(serde_json::from_value(json!({"code":0,"data":{"monthUsage":{"items":[{"used":0,"limit":100,"percent":0}]}}})).unwrap())).unwrap();
+ assert_eq!(snapshot.primary.used_percent, 0.0);
+ assert!(!snapshot.primary.is_informational);
+ }
+}
diff --git a/rust/src/providers/minimax/coding_plan.rs b/rust/src/providers/minimax/coding_plan.rs
index 5169101e716..05b707e94ce 100644
--- a/rust/src/providers/minimax/coding_plan.rs
+++ b/rust/src/providers/minimax/coding_plan.rs
@@ -9,7 +9,7 @@ use chrono::{DateTime, Duration, FixedOffset, TimeZone, Utc};
use regex_lite::Regex;
use serde_json::Value;
-use crate::core::ProviderError;
+use crate::core::{ProviderError, RateWindow};
use super::{scalar_string, value_f64, value_i64};
@@ -58,6 +58,21 @@ pub(super) struct RemainsRow {
pub is_weekly: bool,
}
+impl RemainsRow {
+ pub(super) fn to_rate_window(&self) -> RateWindow {
+ if self.is_unlimited {
+ RateWindow::informational("Unlimited")
+ } else {
+ RateWindow::with_details(
+ self.percent,
+ self.window_minutes,
+ self.resets_at,
+ self.reset_description.clone(),
+ )
+ }
+ }
+}
+
// ponytail: skipped from upstream — optional `Authorization: Bearer` on cookie
// requests (our cookie import has no token concept), `GroupId` remains query
// param (no group id in cookie mode), `pointsBalance`, `formatMiniMaxDateTimeRange`
@@ -365,7 +380,7 @@ fn make_remains_row(
used_percent_from_remaining(rp)
} else {
let total = total?;
- let remaining = remaining.unwrap_or(0);
+ let remaining = remaining?;
used_percent_from_counts(total, remaining)?
};
@@ -453,11 +468,11 @@ fn parse_resets_at_from_time_range(
re.replace_all(end_part, "").trim().to_string()
};
let time = chrono::NaiveTime::parse_from_str(&end_clean, "%H:%M").ok()?;
- let today = now.date_naive();
+ let today = now.with_timezone(&tz).date_naive();
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 +712,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!({
@@ -955,3 +986,17 @@ mod tests {
)));
}
}
+
+#[cfg(test)]
+mod p2_01_reset_uses_timezone_calendar_date_tests {
+ use super::*;
+ #[test]
+ fn review16_p2_01_reset_uses_timezone_calendar_date() {
+ let now = Utc.with_ymd_and_hms(2026, 10, 28, 18, 0, 0).unwrap();
+ let expected = Utc.with_ymd_and_hms(2026, 10, 29, 17, 0, 0).unwrap();
+ assert_eq!(
+ parse_resets_at_from_time_range("20:00-01:00(UTC+8)", "5 hours", now),
+ Some(expected)
+ );
+ }
+}
diff --git a/rust/src/providers/minimax/coding_plan_html.rs b/rust/src/providers/minimax/coding_plan_html.rs
index 35bc47f205b..cd09b608577 100644
--- a/rust/src/providers/minimax/coding_plan_html.rs
+++ b/rust/src/providers/minimax/coding_plan_html.rs
@@ -265,11 +265,11 @@ fn date_for_time(time: &str, tz_hint: Option<&str>, now: DateTime) -> Optio
None => FixedOffset::east_opt(0).unwrap(),
};
let time_only = chrono::NaiveTime::parse_from_str(time.trim(), "%H:%M").ok()?;
- let today = now.date_naive();
+ let today = now.with_timezone(&tz).date_naive();
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)
}
@@ -396,7 +396,9 @@ pub(super) fn to_usage_snapshot(
}
MiniMaxCodingPlanSnapshot::Remains { plan_name, rows } => {
if rows.is_empty() {
- return Err(ProviderError::Parse("Missing coding plan data.".into()));
+ return Err(ProviderError::Parse(
+ "Coding plan response contains no usable usage rows".into(),
+ ));
}
// Primary = first row with a computable percent (skip placeholders)
@@ -406,22 +408,12 @@ pub(super) fn to_usage_snapshot(
.or_else(|| rows.first())
.ok_or_else(|| ProviderError::Parse("Missing coding plan data.".into()))?;
- let primary = RateWindow::with_details(
- primary_row.percent,
- primary_row.window_minutes,
- primary_row.resets_at,
- primary_row.reset_description.clone(),
- );
+ let primary = primary_row.to_rate_window();
let mut usage = UsageSnapshot::new(primary);
// Secondary = first qualifying weekly row
if let Some(weekly) = rows.iter().find(|r| r.is_weekly) {
- usage = usage.with_secondary(RateWindow::with_details(
- weekly.percent,
- weekly.window_minutes,
- weekly.resets_at,
- weekly.reset_description.clone(),
- ));
+ usage = usage.with_secondary(weekly.to_rate_window());
}
// Every row as an extra window
@@ -432,16 +424,7 @@ pub(super) fn to_usage_snapshot(
row.service_type.clone()
};
let title = format!("{} · {}", row.service_type, row.window_type);
- usage = usage.with_extra_rate_window(
- id,
- title,
- RateWindow::with_details(
- row.percent,
- row.window_minutes,
- row.resets_at,
- row.reset_description.clone(),
- ),
- );
+ usage = usage.with_extra_rate_window(id, title, row.to_rate_window());
}
if let Some(pn) = plan_name {
@@ -479,6 +462,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!({
@@ -609,3 +605,63 @@ mod tests {
assert_eq!(usage.login_method.as_deref(), Some("Text Generation Pro"));
}
}
+
+#[cfg(test)]
+mod p1_13_missing_remaining_never_becomes_exhausted_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_13_missing_remaining_never_becomes_exhausted() {
+ let now = Utc::now();
+ let value = serde_json::json!({"model_remains":[{"model_name":"General","current_interval_total_count":100}]});
+ let parsed = parse_coding_plan_value(&value, now).unwrap();
+ assert!(to_usage_snapshot(&parsed, now).is_err());
+ let value = serde_json::json!({"model_remains":[
+ {"model_name":"General","current_interval_total_count":100},
+ {"model_name":"General","current_interval_total_count":100,"current_interval_usage_count":75}
+ ]});
+ let parsed = parse_coding_plan_value(&value, now).unwrap();
+ let snapshot = to_usage_snapshot(&parsed, now).unwrap();
+ assert_eq!(snapshot.primary.used_percent, 25.0);
+ assert_eq!(snapshot.extra_rate_windows.len(), 1);
+ }
+}
+
+#[cfg(test)]
+mod p1_14_unlimited_is_information_in_every_slot_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_14_unlimited_is_information_in_every_slot() {
+ let now = Utc::now();
+ let value = serde_json::json!({"model_remains":[{"model_name":"General","current_weekly_status":3,"current_weekly_remaining_percent":100}]});
+ let parsed = parse_coding_plan_value(&value, now).unwrap();
+ let snapshot = to_usage_snapshot(&parsed, now).unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert!(snapshot.secondary.as_ref().unwrap().is_informational);
+ assert!(!snapshot.extra_rate_windows.is_empty());
+ for window in std::iter::once(&snapshot.primary)
+ .chain(snapshot.secondary.iter())
+ .chain(
+ snapshot
+ .extra_rate_windows
+ .iter()
+ .map(|extra| &extra.window),
+ )
+ {
+ assert!(window.is_informational);
+ assert!(window.resets_at.is_none());
+ assert!(window.window_minutes.is_none());
+ }
+ }
+}
+
+#[cfg(test)]
+mod p2_01_html_reset_uses_timezone_calendar_date_tests {
+ use super::*;
+ #[test]
+ fn review16_p2_01_html_reset_uses_timezone_calendar_date() {
+ use chrono::TimeZone;
+ let now = Utc.with_ymd_and_hms(2026, 10, 28, 18, 0, 0).unwrap();
+ let expected = Utc.with_ymd_and_hms(2026, 10, 29, 17, 0, 0).unwrap();
+ assert_eq!(date_for_time("01:00", Some("UTC+8"), now), Some(expected));
+ }
+}
diff --git a/rust/src/providers/minimax/mod.rs b/rust/src/providers/minimax/mod.rs
index b47992664e5..16bd78991a5 100755
--- a/rust/src/providers/minimax/mod.rs
+++ b/rust/src/providers/minimax/mod.rs
@@ -377,19 +377,20 @@ impl MiniMaxProvider {
.get("used_amount")
.or_else(|| json.get("total_amount"))
.and_then(|v| v.as_f64())
- .unwrap_or(0.0);
+ .ok_or_else(|| {
+ ProviderError::Parse("MiniMax response contains no usage counter".into())
+ })?;
let credit_limit = json
.get("total_quota")
.or_else(|| json.get("quota"))
.and_then(|v| v.as_f64())
- .unwrap_or(100.0);
+ .filter(|limit| *limit > 0.0)
+ .ok_or_else(|| {
+ ProviderError::Parse("MiniMax response contains no quota total".into())
+ })?;
- let used_percent = if credit_limit > 0.0 {
- (used_credits / credit_limit) * 100.0
- } else {
- 0.0
- };
+ let used_percent = (used_credits / credit_limit) * 100.0;
let plan = json
.get("plan_name")
@@ -708,6 +709,12 @@ impl MiniMaxProvider {
attach_billing_summary(ProviderFetchResult::new(usage, source_label), summary)
}
+ fn configured_usage() -> Result {
+ Err(ProviderError::Parse(
+ "MiniMax response contains no usage data".into(),
+ ))
+ }
+
/// Probe for MiniMax installation (credentials check)
async fn probe_cli(&self) -> Result {
// Check if API key is configured
@@ -717,9 +724,7 @@ impl MiniMaxProvider {
.unwrap_or(false);
if has_env_vars || has_config {
- let usage =
- UsageSnapshot::new(RateWindow::new(0.0)).with_login_method("MiniMax (configured)");
- Ok(usage)
+ Self::configured_usage()
} else {
Err(ProviderError::NotInstalled(
"MiniMax API not configured. Set MINIMAX_API_KEY and MINIMAX_GROUP_ID environment variables".to_string()
@@ -818,24 +823,14 @@ fn result_from_billing_summary(
summary: MiniMaxBillingSummary,
source_label: &str,
) -> ProviderFetchResult {
- let usage = UsageSnapshot::new(RateWindow::with_details(
- 0.0,
- None,
- None,
- Some(format!(
- "{} tokens today",
- format_count(summary.today_tokens)
- )),
- ))
- .with_secondary(RateWindow::with_details(
- 0.0,
- None,
- None,
- Some(format!(
- "{} tokens over last 30 days",
- format_count(summary.last_30_days_tokens)
- )),
- ))
+ let usage = UsageSnapshot::new(RateWindow::informational(format!(
+ "{} tokens today",
+ format_count(summary.today_tokens)
+ )))
+ .with_secondary(RateWindow::informational(format!(
+ "{} tokens over last 30 days",
+ format_count(summary.last_30_days_tokens)
+ )))
.with_login_method("MiniMax billing");
attach_billing_summary(ProviderFetchResult::new(usage, source_label), summary)
}
@@ -847,30 +842,25 @@ fn attach_billing_summary(
result.usage = result.usage.with_extra_rate_window(
"billing-tokens-today",
"Tokens today",
- RateWindow::with_details(0.0, None, None, Some(format_count(summary.today_tokens))),
+ RateWindow::informational(format_count(summary.today_tokens)),
);
result.usage = result.usage.with_extra_rate_window(
"billing-tokens-30d",
"Tokens (30 days)",
- RateWindow::with_details(
- 0.0,
- None,
- None,
- Some(format_count(summary.last_30_days_tokens)),
- ),
+ RateWindow::informational(format_count(summary.last_30_days_tokens)),
);
if let Some(cash) = summary.today_cash {
result.usage = result.usage.with_extra_rate_window(
"billing-cash-today",
"Spend today",
- RateWindow::with_details(0.0, None, None, Some(format!("${cash:.2}"))),
+ RateWindow::informational(format!("${cash:.2}")),
);
}
if let Some(cash) = summary.last_30_days_cash {
result.usage = result.usage.with_extra_rate_window(
"billing-cash-30d",
"Spend (30 days)",
- RateWindow::with_details(0.0, None, None, Some(format!("${cash:.2}"))),
+ RateWindow::informational(format!("${cash:.2}")),
);
result.cost = Some(CostSnapshot::new(cash, "USD", "Last 30 days"));
}
@@ -878,14 +868,14 @@ fn attach_billing_summary(
result.usage = result.usage.with_extra_rate_window(
format!("billing-method-{idx}"),
format!("Method: {}", item.name),
- RateWindow::with_details(0.0, None, None, Some(breakdown_description(item))),
+ RateWindow::informational(breakdown_description(item)),
);
}
for (idx, item) in summary.top_models.iter().enumerate() {
result.usage = result.usage.with_extra_rate_window(
format!("billing-model-{idx}"),
format!("Model: {}", item.name),
- RateWindow::with_details(0.0, None, None, Some(breakdown_description(item))),
+ RateWindow::informational(breakdown_description(item)),
);
}
result
@@ -1205,6 +1195,28 @@ mod tests {
);
}
+ #[test]
+ fn billing_counters_without_a_denominator_are_informational() {
+ let json = serde_json::json!({
+ "base_resp": { "status_code": 0 },
+ "charge_records": [{
+ "consume_token": "2500",
+ "consume_cash_after_voucher": "1.25",
+ "ymd": Utc::now().format("%Y-%m-%d").to_string(),
+ "method": "chat",
+ "model": "abab6.5"
+ }]
+ });
+ let result =
+ result_from_billing_summary(parse_billing_summary(&json).unwrap(), "web-billing");
+ assert!(result.usage.primary.is_informational);
+ assert!(result.usage.secondary.as_ref().unwrap().is_informational);
+ assert!(!result.usage.extra_rate_windows.is_empty());
+ for extra in &result.usage.extra_rate_windows {
+ assert!(extra.window.is_informational, "{}", extra.id);
+ }
+ }
+
#[test]
fn parses_plan_title_from_coding_plan_fields() {
let provider = MiniMaxProvider::new();
@@ -1307,3 +1319,37 @@ mod tests {
assert_eq!(summary.top_methods[1].tokens, 1000);
}
}
+
+#[cfg(test)]
+mod cx289a_parent_tests {
+ use super::*;
+ use serde_json::json;
+
+ #[test]
+ fn configuration_is_not_usage() {
+ assert!(MiniMaxProvider::configured_usage().is_err());
+ }
+
+ #[test]
+ fn missing_usage_is_an_error() {
+ for value in [
+ json!({}),
+ json!({"total_quota":100}),
+ json!({"used_amount":0}),
+ json!({"used_amount":0,"total_quota":0}),
+ ] {
+ assert!(
+ MiniMaxProvider::new().parse_usage_response(&value).is_err(),
+ "{value}"
+ );
+ }
+ }
+ #[test]
+ fn real_zero_usage_stays_a_quota() {
+ let snapshot = MiniMaxProvider::new()
+ .parse_usage_response(&json!({"used_amount":0,"total_quota":100}))
+ .unwrap();
+ assert_eq!(snapshot.primary.used_percent, 0.0);
+ assert!(!snapshot.primary.is_informational);
+ }
+}
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);
+ }
+}
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()
+ );
+ }
+}
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);
+ }
+}
diff --git a/rust/src/providers/opencode/mod.rs b/rust/src/providers/opencode/mod.rs
index 5e714f024ba..dcb474d6a6b 100755
--- a/rust/src/providers/opencode/mod.rs
+++ b/rust/src/providers/opencode/mod.rs
@@ -225,7 +225,7 @@ impl OpenCodeProvider {
has_usage_hint,
"OpenCode subscription body missing rolling/weekly usage windows"
);
- ProviderError::Parse("Missing usage percent (rolling or weekly)".into())
+ ProviderError::Parse("OpenCode response contains no usable usage windows".into())
})
}
@@ -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 });
@@ -832,3 +872,27 @@ mod tests {
);
}
}
+
+#[cfg(test)]
+mod p2_02_empty_usage_is_a_data_error_tests {
+ use super::*;
+ #[test]
+ fn review16_p2_02_empty_usage_is_a_data_error() {
+ let error = OpenCodeProvider::new()
+ .parse_subscription("{}")
+ .unwrap_err();
+ assert!(matches!(error, ProviderError::Parse(_)));
+ let text = error.to_string().to_lowercase();
+ for marker in [
+ "missing",
+ "not found",
+ "required",
+ "auth",
+ "token",
+ "cookie",
+ "login",
+ ] {
+ assert!(!text.contains(marker), "{text}");
+ }
+ }
+}
diff --git a/rust/src/providers/opencodego/local.rs b/rust/src/providers/opencodego/local.rs
index 57777068642..535ac8d6ea9 100644
--- a/rust/src/providers/opencodego/local.rs
+++ b/rust/src/providers/opencodego/local.rs
@@ -2,7 +2,7 @@
//!
//! Mirrors upstream `OpenCodeGoLocalUsageReader`: sums `opencode-go` assistant
//! message / step-finish costs from the local OpenCode database and maps them
-//! onto session ($12 / 5h), weekly ($30), and monthly ($60) windows.
+//! as observed local costs. Local history cannot establish server quotas.
use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, TimeZone, Timelike, Utc};
use rusqlite::Connection;
@@ -12,9 +12,6 @@ use crate::core::{ProviderError, ProviderFetchResult, RateWindow, UsageSnapshot}
const FIVE_HOURS_MS: i64 = 5 * 60 * 60 * 1000;
const WEEK_MS: i64 = 7 * 24 * 60 * 60 * 1000;
-const SESSION_LIMIT_USD: f64 = 12.0;
-const WEEKLY_LIMIT_USD: f64 = 30.0;
-const MONTHLY_LIMIT_USD: f64 = 60.0;
const PROVIDER_ID: &str = "opencode-go";
const MESSAGE_USAGE_SQL: &str = r#"
@@ -80,41 +77,23 @@ pub(crate) struct UsageRow {
#[derive(Debug, Clone)]
pub struct LocalUsageSnapshot {
- pub rolling_usage_percent: f64,
- pub weekly_usage_percent: f64,
- pub monthly_usage_percent: f64,
- pub rolling_reset_in_sec: i64,
- pub weekly_reset_in_sec: i64,
- pub monthly_reset_in_sec: i64,
+ pub rolling_cost_usd: f64,
+ pub weekly_cost_usd: f64,
+ pub monthly_cost_usd: f64,
+ pub observed_cost_usd: f64,
}
impl LocalUsageSnapshot {
pub fn to_fetch_result(&self) -> ProviderFetchResult {
- let now = Utc::now();
- let primary = RateWindow::with_details(
- self.rolling_usage_percent,
- Some(300),
- Some(now + Duration::seconds(self.rolling_reset_in_sec)),
- None,
- );
- let mut snap = UsageSnapshot::new(primary).with_login_method("OpenCode Go");
- snap = snap.with_secondary(RateWindow::with_details(
- self.weekly_usage_percent,
- Some(10080),
- Some(now + Duration::seconds(self.weekly_reset_in_sec)),
- None,
- ));
- let monthly_reset = now + Duration::seconds(self.monthly_reset_in_sec);
- snap = snap.with_tertiary(RateWindow::with_details(
- self.monthly_usage_percent,
- RateWindow::monthly_window_minutes(Some(monthly_reset)).or(Some(43200)),
- Some(monthly_reset),
- None,
+ let primary = RateWindow::informational(format!(
+ "Local cost: ${:.2} in last 5h, ${:.2} this week, ${:.2} in the local monthly period, ${:.2} in history; quota unavailable",
+ self.rolling_cost_usd,
+ self.weekly_cost_usd,
+ self.monthly_cost_usd,
+ self.observed_cost_usd,
));
- // Upstream 0.51 (#2982): local SQLite quota reconstruction is useful
- // but it is not server-confirmed authority. Keep that distinction in
- // the data contract so CLI/React can present it without guessing.
- ProviderFetchResult::new(snap, super::LOCAL_ESTIMATE_SOURCE_LABEL)
+ let snapshot = UsageSnapshot::new(primary).with_login_method("OpenCode Go");
+ ProviderFetchResult::new(snapshot, super::LOCAL_ESTIMATE_SOURCE_LABEL)
.with_non_authoritative_pace()
}
}
@@ -282,15 +261,10 @@ fn snapshot_from_rows(rows: &[UsageRow], now: DateTime) -> LocalUsageSnapsh
let mut session_cost = 0.0;
let mut weekly_cost = 0.0;
let mut monthly_cost = 0.0;
- let mut oldest_session_ms: Option = None;
for row in rows {
if row.created_ms >= session_start && row.created_ms < now_ms {
session_cost += row.cost;
- oldest_session_ms = Some(match oldest_session_ms {
- Some(prev) => prev.min(row.created_ms),
- None => row.created_ms,
- });
}
if row.created_ms >= week_start_ms && row.created_ms < week_end_ms {
weekly_cost += row.cost;
@@ -300,27 +274,14 @@ 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);
-
LocalUsageSnapshot {
- rolling_usage_percent: percent(session_cost, SESSION_LIMIT_USD),
- weekly_usage_percent: percent(weekly_cost, WEEKLY_LIMIT_USD),
- monthly_usage_percent: percent(monthly_cost, MONTHLY_LIMIT_USD),
- rolling_reset_in_sec,
- weekly_reset_in_sec: ((week_end_ms - now_ms) / 1000).max(0),
- monthly_reset_in_sec: ((month_end_ms - now_ms) / 1000).max(0),
+ rolling_cost_usd: session_cost,
+ weekly_cost_usd: weekly_cost,
+ monthly_cost_usd: monthly_cost,
+ observed_cost_usd: rows.iter().map(|row| row.cost).sum(),
}
}
-fn percent(used: f64, limit: f64) -> f64 {
- if !used.is_finite() || limit <= 0.0 {
- return 0.0;
- }
- let value = (used / limit * 100.0).clamp(0.0, 100.0);
- (value * 10.0).round() / 10.0
-}
-
/// Bucket label for rows whose local `modelID` is missing or blank (upstream #2649).
const UNKNOWN_MODEL_NAME: &str = "unknown";
@@ -607,27 +568,26 @@ mod tests {
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
- fn local_fetch_result_keeps_estimate_source_and_reset_windows() {
+ fn local_fetch_result_preserves_observed_costs_without_quota_windows() {
let result = LocalUsageSnapshot {
- rolling_usage_percent: 12.0,
- weekly_usage_percent: 23.0,
- monthly_usage_percent: 34.0,
- rolling_reset_in_sec: 300,
- weekly_reset_in_sec: 1_000,
- monthly_reset_in_sec: 2_000,
+ rolling_cost_usd: 1.234,
+ weekly_cost_usd: 23.0,
+ monthly_cost_usd: 34.0,
+ observed_cost_usd: 50.0,
}
.to_fetch_result();
-
assert_eq!(
result.source_label,
super::super::LOCAL_ESTIMATE_SOURCE_LABEL
);
- assert_eq!(result.usage.primary.used_percent, 12.0);
- assert_eq!(result.usage.secondary.as_ref().unwrap().used_percent, 23.0);
- assert_eq!(result.usage.tertiary.as_ref().unwrap().used_percent, 34.0);
- assert!(result.usage.primary.resets_at.is_some());
- assert!(result.usage.secondary.as_ref().unwrap().resets_at.is_some());
- assert!(result.usage.tertiary.as_ref().unwrap().resets_at.is_some());
+ assert!(result.usage.primary.is_informational);
+ let detail = result.usage.primary.reset_description.unwrap();
+ for amount in ["1.23", "23.00", "34.00", "50.00"] {
+ assert!(detail.contains(amount));
+ }
+ assert!(result.usage.primary.resets_at.is_none());
+ assert!(result.usage.secondary.is_none());
+ assert!(result.usage.tertiary.is_none());
}
fn temp_db_path(label: &str) -> PathBuf {
@@ -760,9 +720,9 @@ mod tests {
let db = temp_db_path("sums");
let now = Utc.with_ymd_and_hms(2026, 3, 18, 12, 0, 0).unwrap(); // Wednesday
let now_ms = now.timestamp_millis();
- // $6 in the rolling 5h window → 50% of $12
- // $15 in ISO week → 50% of $30
- // $30 in anchored month → 50% of $60
+ // $6 in the rolling 5h window
+ // $15 in ISO week
+ // $30 in anchored month
let session_ms = now_ms - 60_000;
let week_ms = start_of_utc_iso_week_ms(now) + 3_600_000;
let month_anchor_ms = now_ms - 10 * 24 * 60 * 60 * 1000;
@@ -781,11 +741,11 @@ mod tests {
let _written = std::fs::write(&auth, r#"{"opencode-go":{"key":"test-key"}}"#);
let snap = fetch_from_paths(&auth, &db, now).unwrap();
- assert!((snap.rolling_usage_percent - 50.0).abs() < 0.05, "{snap:?}");
- // session 6 + week-only 9 = 15 → 50%
- assert!((snap.weekly_usage_percent - 50.0).abs() < 0.05, "{snap:?}");
- // session 6 + week 9 + month 15 = 30 → 50%
- assert!((snap.monthly_usage_percent - 50.0).abs() < 0.05, "{snap:?}");
+ assert!((snap.rolling_cost_usd - 6.0).abs() < 0.05, "{snap:?}");
+ // session 6 + week-only 9 = 15
+ assert!((snap.weekly_cost_usd - 15.0).abs() < 0.05, "{snap:?}");
+ // session 6 + week 9 + month 15 = 30
+ assert!((snap.monthly_cost_usd - 30.0).abs() < 0.05, "{snap:?}");
// Best-effort teardown; leftover temp files are harmless.
let _removed_db = std::fs::remove_file(&db);
@@ -832,7 +792,7 @@ mod tests {
let _written_auth = std::fs::write(&auth, r#"{"opencode-go":{"key":"k"}}"#);
let snap = fetch_from_paths(&auth, &db, now).unwrap();
assert!(
- (snap.rolling_usage_percent - 25.0).abs() < 0.05,
+ (snap.rolling_cost_usd - 3.0).abs() < 0.05,
"expected step-finish cost only, got {snap:?}"
);
// Best-effort teardown; leftover temp files are harmless.
@@ -840,13 +800,6 @@ mod tests {
let _removed_auth = std::fs::remove_file(&auth);
}
- #[test]
- fn percent_rounds_to_one_decimal() {
- assert!((percent(1.0, 12.0) - 8.3).abs() < 0.05);
- assert_eq!(percent(0.0, 12.0), 0.0);
- assert_eq!(percent(f64::NAN, 12.0), 0.0);
- }
-
#[test]
fn iso_week_starts_monday_utc() {
// 2026-03-18 is a Wednesday; week start should be 2026-03-16 00:00 UTC.
@@ -911,7 +864,7 @@ mod tests {
assert!(!shm.exists(), "precondition: no -shm");
let snap = fetch_from_paths(&auth, &db, Utc::now()).expect("read idle WAL db");
- assert!(snap.rolling_usage_percent > 0.0, "{snap:?}");
+ assert!(snap.rolling_cost_usd > 0.0, "{snap:?}");
assert!(
!wal.exists() && !shm.exists(),
@@ -1227,3 +1180,35 @@ mod tests {
assert_eq!(b1.len(), 1);
}
}
+
+#[cfg(test)]
+mod p1_16_local_history_never_exports_a_quota_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_16_local_history_never_exports_a_quota() {
+ let now = Utc::now();
+ for age_days in [90, 0] {
+ let rows = [UsageRow {
+ created_ms: now.timestamp_millis() - age_days * 24 * 60 * 60 * 1000 - 60000,
+ cost: 3.0,
+ request_count: 1,
+ model: String::new(),
+ }];
+ let result = snapshot_from_rows(&rows, now).to_fetch_result();
+ let json = serde_json::to_value(&result.usage).unwrap();
+ assert_eq!(json["primary"]["is_informational"], true);
+ assert!(result.usage.primary.resets_at.is_none());
+ assert!(result.usage.primary.window_minutes.is_none());
+ assert!(result.usage.secondary.is_none());
+ assert!(result.usage.tertiary.is_none());
+ assert!(
+ result
+ .usage
+ .primary
+ .reset_description
+ .unwrap()
+ .contains("3.00")
+ );
+ }
+ }
+}
diff --git a/rust/src/providers/opencodego/mod.rs b/rust/src/providers/opencodego/mod.rs
index 62d0ca09d89..eb863b653a3 100644
--- a/rust/src/providers/opencodego/mod.rs
+++ b/rust/src/providers/opencodego/mod.rs
@@ -160,35 +160,40 @@ 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 rolling = Self::extract_window(text, &["rollingUsage", "rolling_usage", "rolling"]);
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,
- );
+ if rolling.is_none() && weekly.is_none() && monthly.is_none() {
+ return Err(ProviderError::Parse(
+ "OpenCode Go response contains no usable usage windows".into(),
+ ));
+ }
+ let primary = rolling
+ .map(|(percent, reset)| {
+ RateWindow::with_details(percent, Some(300), reset_at(reset), None)
+ })
+ .unwrap_or_else(|| RateWindow::informational("Rolling usage unavailable"));
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 +209,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 +227,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 +250,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 +794,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" }"#;
@@ -979,3 +999,45 @@ mod tests {
assert_eq!(parse_billing_server_balance(r#"customerID: "cus_9""#), None);
}
}
+
+#[cfg(test)]
+mod p1_17_weekly_only_preserves_zero_and_nonzero_usage_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_17_weekly_only_preserves_zero_and_nonzero_usage() {
+ for percent in [0, 40] {
+ let snapshot = OpenCodeGoProvider::parse_usage_text(&format!(
+ "weeklyUsage: {{ usagePercent: {percent}, resetInSec: 3600 }}"
+ ))
+ .unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert!(snapshot.primary.resets_at.is_none());
+ let weekly = snapshot.secondary.unwrap();
+ assert!(!weekly.is_informational);
+ assert_eq!(weekly.used_percent, f64::from(percent));
+ assert!(weekly.resets_at.is_some());
+ }
+ }
+}
+
+#[cfg(test)]
+mod p2_02_empty_usage_is_a_data_error_tests {
+ use super::*;
+ #[test]
+ fn review16_p2_02_empty_usage_is_a_data_error() {
+ let error = OpenCodeGoProvider::parse_usage_text("{}").unwrap_err();
+ assert!(matches!(error, ProviderError::Parse(_)));
+ let text = error.to_string().to_lowercase();
+ for marker in [
+ "missing",
+ "not found",
+ "required",
+ "auth",
+ "token",
+ "cookie",
+ "login",
+ ] {
+ assert!(!text.contains(marker), "{text}");
+ }
+ }
+}
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);
+ }
+ }
+}
diff --git a/rust/src/providers/perplexity/mod.rs b/rust/src/providers/perplexity/mod.rs
index 99ddc3ee2b9..a446ed60d4a 100644
--- a/rust/src/providers/perplexity/mod.rs
+++ b/rust/src/providers/perplexity/mod.rs
@@ -30,7 +30,7 @@ struct CreditsResponse {
#[serde(default)]
credit_grants: Vec,
#[serde(default)]
- total_usage_cents: f64,
+ total_usage_cents: Option,
}
#[derive(Debug, Deserialize)]
@@ -38,7 +38,7 @@ struct CreditGrant {
#[serde(default, rename = "type")]
grant_type: String,
#[serde(default)]
- amount_cents: f64,
+ amount_cents: Option,
#[serde(default)]
expires_at_ts: Option,
}
@@ -88,17 +88,20 @@ impl PerplexityProvider {
let mut purchased_total = 0.0_f64;
for grant in &resp.credit_grants {
+ let amount = grant.amount_cents.ok_or_else(|| {
+ ProviderError::Parse("Perplexity response contains no credit grant amount".into())
+ })?;
match grant.grant_type.as_str() {
- "recurring" => recurring_total += grant.amount_cents,
+ "recurring" => recurring_total += amount,
"promotional" | "bonus" => {
- bonus_total += grant.amount_cents;
+ bonus_total += amount;
if let Some(ts) = grant.expires_at_ts.and_then(Self::ts_to_datetime)
&& bonus_expiry.is_none_or(|cur| ts < cur)
{
bonus_expiry = Some(ts);
}
}
- "purchased" => purchased_total += grant.amount_cents,
+ "purchased" => purchased_total += amount,
_ => {}
}
}
@@ -108,7 +111,17 @@ impl PerplexityProvider {
}
// Waterfall attribution: usage burns recurring first, then bonus, then purchased.
- let mut remaining_usage = resp.total_usage_cents.max(0.0);
+ let mut remaining_usage = resp
+ .total_usage_cents
+ .ok_or_else(|| {
+ ProviderError::Parse("Perplexity response contains no usage counter".into())
+ })?
+ .max(0.0);
+ if recurring_total <= 0.0 && bonus_total <= 0.0 && purchased_total <= 0.0 {
+ return Err(ProviderError::Parse(
+ "Perplexity response contains no credit quota".into(),
+ ));
+ }
let recurring_used = remaining_usage.min(recurring_total);
remaining_usage -= recurring_used;
let bonus_used = remaining_usage.min(bonus_total);
@@ -125,13 +138,20 @@ impl PerplexityProvider {
let renewal = resp.renewal_date_ts.and_then(Self::ts_to_datetime);
- let mut primary = RateWindow::new(pct(recurring_used, recurring_total));
- primary.resets_at = renewal;
- primary.reset_description = Some(format!(
- "${:.2}/${:.2}",
- recurring_used / 100.0,
- recurring_total / 100.0
- ));
+ let primary = if recurring_total > 0.0 {
+ RateWindow::with_details(
+ pct(recurring_used, recurring_total),
+ None,
+ renewal,
+ Some(format!(
+ "${:.2}/${:.2}",
+ recurring_used / 100.0,
+ recurring_total / 100.0
+ )),
+ )
+ } else {
+ RateWindow::informational("No recurring credit quota")
+ };
let mut snapshot = UsageSnapshot::new(primary);
@@ -317,3 +337,39 @@ mod tests {
assert_eq!(snap.login_method.as_deref(), Some("Max"));
}
}
+
+#[cfg(test)]
+mod cx289a_parent_tests {
+ use super::*;
+ use serde_json::json;
+
+ fn parse(value: serde_json::Value) -> Result {
+ PerplexityProvider::parse_response(serde_json::from_value(value).unwrap())
+ }
+ #[test]
+ fn missing_usage_is_an_error() {
+ for value in [
+ json!({}),
+ json!({"credit_grants":[{"type":"recurring","amount_cents":100}]}),
+ json!({"total_usage_cents":0}),
+ json!({"total_usage_cents":0,"credit_grants":[{"type":"recurring"}]}),
+ ] {
+ assert!(parse(value.clone()).is_err(), "{value}");
+ }
+ }
+ #[test]
+ fn real_zero_usage_stays_a_quota() {
+ let snapshot = parse(json!({"total_usage_cents":0,"credit_grants":[{"type":"recurring","amount_cents":100}]})).unwrap();
+ assert_eq!(snapshot.primary.used_percent, 0.0);
+ assert!(!snapshot.primary.is_informational);
+ }
+ #[test]
+ fn bonus_only_keeps_recurring_unavailable() {
+ let snapshot = parse(
+ json!({"total_usage_cents":0,"credit_grants":[{"type":"bonus","amount_cents":100}]}),
+ )
+ .unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert_eq!(snapshot.secondary.unwrap().used_percent, 0.0);
+ }
+}
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());
+ }
+}
diff --git a/rust/src/providers/sub2api/mod.rs b/rust/src/providers/sub2api/mod.rs
index e738e8700bc..6bd76527437 100644
--- a/rust/src/providers/sub2api/mod.rs
+++ b/rust/src/providers/sub2api/mod.rs
@@ -58,9 +58,9 @@ struct ParsedRateLimit {
#[derive(Debug, Clone, PartialEq)]
struct ParsedSubscription {
- daily_usage_usd: f64,
- weekly_usage_usd: f64,
- monthly_usage_usd: f64,
+ daily_usage_usd: Option,
+ weekly_usage_usd: Option,
+ monthly_usage_usd: Option,
daily_limit_usd: Option,
weekly_limit_usd: Option,
monthly_limit_usd: Option,
@@ -198,7 +198,7 @@ impl Sub2ApiProvider {
if !parsed.is_valid {
return Err(ProviderError::AuthRequired);
}
- Ok(snapshot_from_parsed(parsed))
+ snapshot_from_parsed(parsed)
}
}
@@ -434,9 +434,9 @@ fn parse_usage_response(response: UsageResponse) -> ParsedUsage {
})
.collect(),
subscription: response.subscription.map(|s| ParsedSubscription {
- daily_usage_usd: s.daily_usage_usd.unwrap_or(0.0),
- weekly_usage_usd: s.weekly_usage_usd.unwrap_or(0.0),
- monthly_usage_usd: s.monthly_usage_usd.unwrap_or(0.0),
+ daily_usage_usd: s.daily_usage_usd,
+ weekly_usage_usd: s.weekly_usage_usd,
+ monthly_usage_usd: s.monthly_usage_usd,
daily_limit_usd: s.daily_limit_usd,
weekly_limit_usd: s.weekly_limit_usd,
monthly_limit_usd: s.monthly_limit_usd,
@@ -472,7 +472,7 @@ fn parse_date(raw: Option<&str>) -> Option> {
})
}
-fn snapshot_from_parsed(parsed: ParsedUsage) -> ProviderFetchResult {
+fn snapshot_from_parsed(parsed: ParsedUsage) -> Result {
let kind = classify_usage_kind(&parsed);
let mut cost: Option = None;
@@ -504,9 +504,22 @@ fn snapshot_from_parsed(parsed: ParsedUsage) -> ProviderFetchResult {
)
.or_else(|| informational_usage_window(sub.monthly_usage_usd, "month", &parsed.unit));
- let mut snap = UsageSnapshot::new(
- primary.unwrap_or_else(|| RateWindow::informational("Subscription active")),
- );
+ if primary.is_none()
+ && secondary.is_none()
+ && tertiary.is_none()
+ && parsed.balance.is_none()
+ && parsed.today.is_none()
+ && parsed.total.is_none()
+ && !parsed.rate_limits.iter().any(|window| window.limit > 0.0)
+ {
+ return Err(ProviderError::Parse(
+ "Sub2API subscription contains no usage counters".into(),
+ ));
+ }
+ let mut snap =
+ UsageSnapshot::new(primary.unwrap_or_else(|| {
+ RateWindow::informational("Subscription usage unavailable")
+ }));
if let Some(secondary) = secondary {
snap = snap.with_secondary(secondary);
}
@@ -524,26 +537,34 @@ fn snapshot_from_parsed(parsed: ParsedUsage) -> ProviderFetchResult {
}
Kind::KeyQuota => {
let mut snap = if let Some(quota) = &parsed.quota {
- UsageSnapshot::new(RateWindow::with_details(
- used_percent(quota.used, quota.limit),
- None,
- None,
- Some(amount_description(quota.used, quota.limit, "a.unit)),
- ))
+ if quota.limit <= 0.0 {
+ UsageSnapshot::new(RateWindow::informational("Key quota total unavailable"))
+ } else {
+ UsageSnapshot::new(RateWindow::with_details(
+ used_percent(quota.used, quota.limit),
+ None,
+ None,
+ Some(amount_description(quota.used, quota.limit, "a.unit)),
+ ))
+ }
} else if let Some(first) = parsed.rate_limits.first() {
// Rate-limit-only payload: promote the first window to primary.
- UsageSnapshot::new(RateWindow::with_details(
- used_percent(first.used, first.limit),
- window_minutes(&first.window),
- first.reset_at,
- Some(amount_description(first.used, first.limit, &parsed.unit)),
- ))
+ UsageSnapshot::new(if first.limit <= 0.0 {
+ RateWindow::informational("Rate limit total unavailable")
+ } else {
+ RateWindow::with_details(
+ used_percent(first.used, first.limit),
+ window_minutes(&first.window),
+ first.reset_at,
+ Some(amount_description(first.used, first.limit, &parsed.unit)),
+ )
+ })
} else {
UsageSnapshot::new(RateWindow::informational("Key quota"))
};
let skip_first_rate_limit = parsed.quota.is_none() && !parsed.rate_limits.is_empty();
for (idx, rate_limit) in parsed.rate_limits.iter().enumerate() {
- if skip_first_rate_limit && idx == 0 {
+ if (skip_first_rate_limit && idx == 0) || rate_limit.limit <= 0.0 {
continue;
}
snap = snap.with_extra_rate_window(
@@ -596,6 +617,9 @@ fn snapshot_from_parsed(parsed: ParsedUsage) -> ProviderFetchResult {
// Rate-limit extras for subscription/wallet kinds that also ship them.
if kind != Kind::KeyQuota {
for rate_limit in &parsed.rate_limits {
+ if rate_limit.limit <= 0.0 {
+ continue;
+ }
snapshot = snapshot.with_extra_rate_window(
rate_limit.window.clone(),
rate_limit_title(&rate_limit.window),
@@ -660,12 +684,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()),
);
}
@@ -673,7 +692,7 @@ fn snapshot_from_parsed(parsed: ParsedUsage) -> ProviderFetchResult {
if let Some(cost) = cost {
result = result.with_cost(cost);
}
- result
+ Ok(result)
}
fn classify_usage_kind(parsed: &ParsedUsage) -> Kind {
@@ -696,7 +715,8 @@ fn wallet_remaining(parsed: &ParsedUsage) -> Option {
parsed.remaining.filter(|r| r.is_finite())
}
-fn informational_usage_window(usage: f64, period: &str, unit: &str) -> Option {
+fn informational_usage_window(usage: Option, period: &str, unit: &str) -> Option {
+ let usage = usage?;
if !usage.is_finite() {
return None;
}
@@ -715,12 +735,13 @@ enum Kind {
}
fn rate_window_from_usage(
- usage: f64,
+ usage: Option,
limit: Option,
window_minutes: Option,
unit: &str,
) -> Option {
- let limit = limit.filter(|l| *l > 0.0)?;
+ let usage = usage.filter(|usage| usage.is_finite())?;
+ let limit = limit.filter(|l| l.is_finite() && *l > 0.0)?;
Some(RateWindow::with_details(
used_percent(usage, limit),
window_minutes,
@@ -845,7 +866,7 @@ mod tests {
assert_eq!(parsed.rate_limits.len(), 2);
assert_eq!(parsed.today.as_ref().unwrap().total_tokens, 1200);
- let result = snapshot_from_parsed(parsed);
+ let result = snapshot_from_parsed(parsed).unwrap();
assert!((result.usage.primary.used_percent - 25.0).abs() < f64::EPSILON);
assert!(result.cost.is_none());
assert!(
@@ -892,7 +913,7 @@ mod tests {
}
"#;
- let result = snapshot_from_parsed(parse_usage_body(json).unwrap());
+ let result = snapshot_from_parsed(parse_usage_body(json).unwrap()).unwrap();
assert!((result.usage.primary.used_percent - 20.0).abs() < f64::EPSILON);
assert!(
(result.usage.secondary.as_ref().unwrap().used_percent - 25.0).abs() < f64::EPSILON
@@ -912,6 +933,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()).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#"
@@ -927,7 +979,7 @@ mod tests {
"balance": 12.0
}
"#;
- let result = snapshot_from_parsed(parse_usage_body(json).unwrap());
+ let result = snapshot_from_parsed(parse_usage_body(json).unwrap()).unwrap();
assert!(result.usage.primary.is_informational);
assert!(
result
@@ -965,7 +1017,7 @@ mod tests {
}
"#;
- let result = snapshot_from_parsed(parse_usage_body(json).unwrap());
+ let result = snapshot_from_parsed(parse_usage_body(json).unwrap()).unwrap();
assert!((result.usage.primary.used_percent - 100.0).abs() < f64::EPSILON);
assert!(
(result.usage.secondary.as_ref().unwrap().used_percent - (229.20 / 700.0 * 100.0))
@@ -999,7 +1051,7 @@ mod tests {
}
"#;
- let result = snapshot_from_parsed(parse_usage_body(json).unwrap());
+ let result = snapshot_from_parsed(parse_usage_body(json).unwrap()).unwrap();
assert!(result.usage.primary.is_informational);
assert_eq!(
result.usage.primary.reset_description.as_deref(),
@@ -1071,3 +1123,56 @@ mod tests {
assert!(!query.get("timezone").unwrap().is_empty());
}
}
+
+#[cfg(test)]
+mod cx289a_parent_tests {
+ use super::*;
+ use serde_json::json;
+
+ fn parse(value: serde_json::Value) -> ProviderFetchResult {
+ snapshot_from_parsed(parse_usage_body(&value.to_string()).unwrap()).unwrap()
+ }
+ #[test]
+ fn missing_all_subscription_counters_is_an_error() {
+ let result = parse_usage_body(r#"{"subscription":{"daily_limit_usd":100}}"#)
+ .and_then(snapshot_from_parsed);
+ assert!(result.is_err());
+ }
+ #[test]
+ fn missing_subscription_usage_is_unavailable() {
+ let result = parse(
+ json!({"subscription":{"daily_limit_usd":100,"weekly_limit_usd":200,"weekly_usage_usd":25}}),
+ );
+ assert!(result.usage.primary.is_informational);
+ assert_eq!(result.usage.secondary.unwrap().used_percent, 12.5);
+ assert!(result.usage.tertiary.is_none());
+ }
+ #[test]
+ fn real_zero_usage_stays_a_quota() {
+ let result = parse(json!({"subscription":{"daily_limit_usd":100,"daily_usage_usd":0}}));
+ assert_eq!(result.usage.primary.used_percent, 0.0);
+ assert!(!result.usage.primary.is_informational);
+ }
+ #[test]
+ fn zero_denominator_is_not_a_quota() {
+ let result = parse(json!({"quota":{"limit":0,"used":0,"remaining":0}}));
+ assert!(result.usage.primary.is_informational);
+ }
+}
+
+#[cfg(test)]
+mod p1_08_empty_subscription_preserves_balance_and_usage_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_08_empty_subscription_preserves_balance_and_usage() {
+ let parsed = parse_usage_body(r#"{"subscription":{},"balance":12,"usage":{"today":{"requests":3,"total_tokens":1200,"actual_cost":1.25}}}"#).unwrap();
+ let result = snapshot_from_parsed(parsed).unwrap();
+ assert!(result.usage.primary.is_informational);
+ assert!(result.usage.secondary.is_none());
+ assert_eq!(result.cost.unwrap().limit, Some(12.0));
+ let text = serde_json::to_string(&result.usage.extra_rate_windows).unwrap();
+ for value in ["3", "1200", "1.25"] {
+ assert!(text.contains(value), "{text}");
+ }
+ }
+}
diff --git a/rust/src/providers/t3chat.rs b/rust/src/providers/t3chat.rs
index 29e5ac0a456..2407f7e3e89 100644
--- a/rust/src/providers/t3chat.rs
+++ b/rust/src/providers/t3chat.rs
@@ -127,7 +127,7 @@ impl T3ChatProvider {
}
let customer = Self::parse_json_lines(&body)?;
- Ok(Self::usage_from_customer_data(customer))
+ Self::usage_from_customer_data(customer)
}
fn resolve_request_context(
@@ -237,7 +237,7 @@ impl T3ChatProvider {
))
}
- fn usage_from_customer_data(customer: T3CustomerData) -> UsageSnapshot {
+ fn usage_from_customer_data(customer: T3CustomerData) -> Result {
let base_reset = date_from_epoch(customer.usage_four_hour_next_reset_at)
.or_else(|| date_from_epoch(customer.usage_window_next_reset_at));
let overage_reset = customer
@@ -249,28 +249,40 @@ impl T3ChatProvider {
_ => "Base".to_string(),
};
- let primary = RateWindow::with_details(
- percent(customer.usage_four_hour_percentage),
- Some(4 * 60),
- base_reset,
- Some(base_description),
- );
- let secondary = RateWindow::with_details(
- percent(
- customer
- .usage_month_percentage
- .or(customer.usage_period_percentage),
- ),
- None,
- overage_reset,
- Some("Overage".to_string()),
+ let primary = customer.usage_four_hour_percentage.map(|percent| {
+ RateWindow::with_details(
+ percent.clamp(0.0, 100.0),
+ Some(4 * 60),
+ base_reset,
+ Some(base_description),
+ )
+ });
+ let secondary = customer
+ .usage_month_percentage
+ .or(customer.usage_period_percentage)
+ .map(|percent| {
+ RateWindow::with_details(
+ percent.clamp(0.0, 100.0),
+ None,
+ overage_reset,
+ Some("Overage".into()),
+ )
+ });
+ if primary.is_none() && secondary.is_none() {
+ return Err(ProviderError::Parse(
+ "T3 Chat response contains no usage percentages".into(),
+ ));
+ }
+ let mut usage = UsageSnapshot::new(
+ primary.unwrap_or_else(|| RateWindow::informational("Base usage unavailable")),
);
-
- let mut usage = UsageSnapshot::new(primary).with_secondary(secondary);
+ if let Some(secondary) = secondary {
+ usage = usage.with_secondary(secondary);
+ }
if let Some(plan) = plan_name(&customer) {
usage = usage.with_login_method(plan);
}
- usage
+ Ok(usage)
}
}
@@ -321,10 +333,6 @@ fn find_customer_data(value: &serde_json::Value) -> Option<&serde_json::Value> {
}
}
-fn percent(value: Option) -> f64 {
- value.unwrap_or(0.0).clamp(0.0, 100.0)
-}
-
fn date_from_epoch(value: Option) -> Option> {
let raw = value?;
if raw <= 0.0 {
@@ -426,7 +434,7 @@ mod tests {
#[test]
fn maps_customer_data_to_windows() {
let customer = T3ChatProvider::parse_json_lines(SAMPLE_RESPONSE.as_bytes()).unwrap();
- let usage = T3ChatProvider::usage_from_customer_data(customer);
+ let usage = T3ChatProvider::usage_from_customer_data(customer).expect("usage");
assert_eq!(usage.primary.used_percent, 12.5);
assert_eq!(usage.primary.window_minutes, Some(240));
assert_eq!(
@@ -458,3 +466,30 @@ mod tests {
);
}
}
+
+#[cfg(test)]
+mod cx289a_parent_tests {
+ use super::*;
+ use serde_json::json;
+
+ fn parse(value: serde_json::Value) -> Result {
+ T3ChatProvider::usage_from_customer_data(serde_json::from_value(value).unwrap())
+ }
+ #[test]
+ fn missing_usage_is_an_error() {
+ assert!(parse(json!({"subscription":{},"usageBand":"base"})).is_err());
+ }
+ #[test]
+ fn real_zero_usage_stays_a_quota() {
+ let snapshot = parse(json!({"usageFourHourPercentage":0})).unwrap();
+ assert_eq!(snapshot.primary.used_percent, 0.0);
+ assert!(!snapshot.primary.is_informational);
+ assert!(snapshot.secondary.is_none());
+ }
+ #[test]
+ fn overage_only_keeps_primary_unavailable() {
+ let snapshot = parse(json!({"usageMonthPercentage":25})).unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert_eq!(snapshot.secondary.unwrap().used_percent, 25.0);
+ }
+}
diff --git a/rust/src/providers/venice/mod.rs b/rust/src/providers/venice/mod.rs
index 9ff20acf441..6cb97a4d608 100644
--- a/rust/src/providers/venice/mod.rs
+++ b/rust/src/providers/venice/mod.rs
@@ -85,52 +85,51 @@ 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 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 mut detail = window.reset_description.clone().unwrap_or_default();
+ if !balance.can_consume {
+ detail.push_str(" · Balance unavailable for API calls");
+ }
+ Ok(UsageSnapshot::new(window).with_login_method(detail))
}
impl Default for VeniceProvider {
@@ -212,7 +211,56 @@ 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);
+ }
+}
+
+#[cfg(test)]
+mod p1_10_disabled_consumption_preserves_diem_quota_tests {
+ use super::*;
+ #[test]
+ fn review16_p1_10_disabled_consumption_preserves_diem_quota() {
+ for (remaining, expected) in [(0, 100.0), (100, 0.0)] {
+ let balance: VeniceBalanceResponse = serde_json::from_value(serde_json::json!({"canConsume":false,"consumptionCurrency":"DIEM","balances":{"diem":remaining},"diemEpochAllocation":100})).unwrap();
+ let snapshot = snapshot_from_balance(&balance).unwrap();
+ assert!(!snapshot.primary.is_informational);
+ assert_eq!(snapshot.primary.used_percent, expected);
+ assert!(
+ snapshot
+ .login_method
+ .unwrap()
+ .contains("unavailable for API calls")
+ );
+ }
+ }
+}
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()
+ );
+ }
+}
diff --git a/rust/src/providers/warp/mod.rs b/rust/src/providers/warp/mod.rs
index e8e502b77cb..8462452ff50 100755
--- a/rust/src/providers/warp/mod.rs
+++ b/rust/src/providers/warp/mod.rs
@@ -265,45 +265,41 @@ impl WarpProvider {
workspaces,
} = user;
- let limit_info = request_limit_info.ok_or_else(|| {
- ProviderError::Parse("Missing requestLimitInfo in response".to_string())
- })?;
- let primary = Self::primary_window(&limit_info);
-
- let mut usage = UsageSnapshot::new(primary).with_login_method("Warp API");
- if let Some(secondary) = Self::bonus_window(Self::all_bonus_grants(
+ let primary = request_limit_info.as_ref().and_then(Self::primary_window);
+ let secondary = Self::bonus_window(Self::all_bonus_grants(
bonus_grants.as_ref(),
workspaces.as_ref(),
- )) {
+ ));
+ if primary.is_none() && secondary.is_none() {
+ return Err(ProviderError::Parse(
+ "Warp response contains no usable quota".into(),
+ ));
+ }
+ let mut usage = UsageSnapshot::new(
+ primary.unwrap_or_else(|| RateWindow::informational("Warp request usage unavailable")),
+ )
+ .with_login_method("Warp API");
+ if let Some(secondary) = secondary {
usage = usage.with_secondary(secondary);
}
Ok(usage)
}
- fn primary_window(limit_info: &RequestLimitInfo) -> RateWindow {
- let is_unlimited = limit_info.is_unlimited.unwrap_or(false);
- let request_limit = limit_info.request_limit.unwrap_or(0);
- let requests_used = limit_info.requests_used.unwrap_or(0);
- let used_percent = Self::primary_used_percent(is_unlimited, requests_used, request_limit);
- let reset_description = if is_unlimited {
- "Unlimited".to_string()
- } else {
- format!("{requests_used}/{request_limit} credits")
- };
-
- let resets_at = (!is_unlimited)
- .then(|| Self::parse_timestamp(limit_info.next_refresh_time.as_deref()))
- .flatten();
- RateWindow::with_details(used_percent, None, resets_at, Some(reset_description))
- }
-
- fn primary_used_percent(is_unlimited: bool, requests_used: i64, request_limit: i64) -> f64 {
- if is_unlimited || request_limit <= 0 {
- return 0.0;
+ fn primary_window(limit_info: &RequestLimitInfo) -> Option {
+ if limit_info.is_unlimited == Some(true) {
+ return Some(RateWindow::informational("Unlimited"));
}
-
- ((requests_used as f64) / (request_limit as f64) * 100.0).clamp(0.0, 100.0)
+ let request_limit = limit_info.request_limit.filter(|limit| *limit > 0)?;
+ let requests_used = limit_info.requests_used?;
+ let used_percent =
+ ((requests_used as f64) / (request_limit as f64) * 100.0).clamp(0.0, 100.0);
+ Some(RateWindow::with_details(
+ used_percent,
+ None,
+ Self::parse_timestamp(limit_info.next_refresh_time.as_deref()),
+ Some(format!("{requests_used}/{request_limit} credits")),
+ ))
}
fn all_bonus_grants<'a>(
@@ -335,9 +331,10 @@ impl WarpProvider {
let mut next_expiration: Option<(DateTime, i64)> = None;
for grant in all_grants {
- let remaining = grant.request_credits_remaining.unwrap_or(0);
+ let remaining = grant.request_credits_remaining?;
+ let granted = grant.request_credits_granted?;
bonus_remaining = bonus_remaining.saturating_add(remaining);
- bonus_total = bonus_total.saturating_add(grant.request_credits_granted.unwrap_or(0));
+ bonus_total = bonus_total.saturating_add(granted);
if remaining > 0
&& let Some(expiration) = Self::parse_timestamp(grant.expiration.as_deref())
@@ -356,7 +353,7 @@ impl WarpProvider {
}
}
- if bonus_total <= 0 && bonus_remaining <= 0 {
+ if bonus_total <= 0 {
return None;
}
@@ -440,16 +437,18 @@ impl Provider for WarpProvider {
mod tests {
use super::*;
- #[test]
- fn primary_usage_is_zero_for_unlimited_accounts() {
- assert_eq!(WarpProvider::primary_used_percent(true, 75, 100), 0.0);
- }
-
#[test]
fn primary_usage_clamps_to_percent_range() {
- assert_eq!(WarpProvider::primary_used_percent(false, 25, 100), 25.0);
- assert_eq!(WarpProvider::primary_used_percent(false, 150, 100), 100.0);
- assert_eq!(WarpProvider::primary_used_percent(false, 25, 0), 0.0);
+ for (used, expected) in [(25, 25.0), (150, 100.0), (-25, 0.0)] {
+ let window = WarpProvider::primary_window(&RequestLimitInfo {
+ is_unlimited: Some(false),
+ next_refresh_time: None,
+ request_limit: Some(100),
+ requests_used: Some(used),
+ })
+ .unwrap();
+ assert_eq!(window.used_percent, expected);
+ }
}
#[test]
@@ -488,7 +487,8 @@ mod tests {
next_refresh_time: Some("2026-09-20T12:34:56Z".to_string()),
request_limit: Some(100),
requests_used: Some(25),
- });
+ })
+ .expect("quota");
assert_eq!(window.used_percent, 25.0);
assert_eq!(
@@ -525,3 +525,84 @@ mod tests {
);
}
}
+
+#[cfg(test)]
+mod cx289a_parent_tests {
+ use super::*;
+ use serde_json::json;
+
+ fn parse(value: serde_json::Value) -> Result {
+ WarpProvider::build_usage_snapshot(serde_json::from_value(value).unwrap())
+ }
+ #[test]
+ fn review16_unlimited_with_counters_is_information() {
+ let snapshot = parse(json!({"requestLimitInfo":{
+ "isUnlimited":true,"requestLimit":100,"requestsUsedSinceLastRefresh":25
+ }}))
+ .unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert_eq!(
+ snapshot.primary.reset_description.as_deref(),
+ Some("Unlimited")
+ );
+ assert_eq!(snapshot.primary.resets_at, None);
+ }
+
+ #[test]
+ fn review16_unlimited_without_counters_keeps_bonus_quota() {
+ let snapshot = parse(json!({"requestLimitInfo":{"isUnlimited":true},
+ "bonusGrants":[{"requestCreditsGranted":100,"requestCreditsRemaining":75}]
+ }))
+ .unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert_eq!(
+ snapshot.primary.reset_description.as_deref(),
+ Some("Unlimited")
+ );
+ let bonus = snapshot.secondary.unwrap();
+ assert!(!bonus.is_informational);
+ assert_eq!(bonus.used_percent, 25.0);
+ assert!(
+ parse(json!({"requestLimitInfo":{"isUnlimited":true}}))
+ .unwrap()
+ .primary
+ .is_informational
+ );
+ }
+
+ #[test]
+ fn missing_usage_is_an_error() {
+ for value in [
+ json!({"requestLimitInfo":{}}),
+ json!({"requestLimitInfo":{"requestLimit":100}}),
+ json!({"requestLimitInfo":{"requestsUsedSinceLastRefresh":0}}),
+ ] {
+ assert!(parse(value.clone()).is_err(), "{value}");
+ }
+ }
+ #[test]
+ fn real_zero_usage_stays_a_quota() {
+ let snapshot = parse(
+ json!({"requestLimitInfo":{"requestLimit":100,"requestsUsedSinceLastRefresh":0}}),
+ )
+ .unwrap();
+ assert_eq!(snapshot.primary.used_percent, 0.0);
+ assert!(!snapshot.primary.is_informational);
+ }
+ #[test]
+ fn incomplete_bonus_is_not_a_quota() {
+ for bonus in [
+ json!({"requestCreditsGranted":100}),
+ json!({"requestCreditsRemaining":100}),
+ ] {
+ let snapshot = parse(json!({"requestLimitInfo":{"requestLimit":100,"requestsUsedSinceLastRefresh":0},"bonusGrants":[bonus]})).unwrap();
+ assert!(snapshot.secondary.is_none());
+ }
+ }
+ #[test]
+ fn bonus_only_keeps_primary_unavailable() {
+ let snapshot = parse(json!({"requestLimitInfo":{},"bonusGrants":[{"requestCreditsGranted":100,"requestCreditsRemaining":100}]})).unwrap();
+ assert!(snapshot.primary.is_informational);
+ assert_eq!(snapshot.secondary.unwrap().used_percent, 0.0);
+ }
+}
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());
+ }
+}
diff --git a/rust/src/providers/zai/mod.rs b/rust/src/providers/zai/mod.rs
index aca7e297028..92324c00a6a 100755
--- a/rust/src/providers/zai/mod.rs
+++ b/rust/src/providers/zai/mod.rs
@@ -360,39 +360,26 @@ impl ZaiProvider {
// used signal (`usage - remaining`, or `currentValue`) wins over the
// API's own `percentage`; otherwise `percentage` is trusted, and
// legacy `limit`/`used` responses fall back to the old math.
- fn compute_percent(l: &ZaiLimit) -> f64 {
+ fn compute_percent(l: &ZaiLimit) -> Option {
if let Some(usage) = l.usage.filter(|&usage| usage > 0.0) {
- let used = if let Some(remaining) = l.remaining {
- let from_remaining = usage - remaining;
- let baseline = l.current_value.unwrap_or(from_remaining);
- from_remaining.max(baseline)
- } else {
- l.current_value.unwrap_or(0.0)
+ let used = match (l.remaining, l.current_value) {
+ (Some(remaining), current) => {
+ (usage - remaining).max(current.unwrap_or(usage - remaining))
+ }
+ (None, Some(current)) => current,
+ (None, None) => return l.percentage.map(|v| v.clamp(0.0, 100.0)),
};
- let clamped = used.clamp(0.0, usage);
- return (clamped / usage * 100.0).clamp(0.0, 100.0);
+ return Some((used / usage * 100.0).clamp(0.0, 100.0));
}
if let Some(percentage) = l.percentage {
- return percentage.clamp(0.0, 100.0);
+ return Some(percentage.clamp(0.0, 100.0));
}
-
- let limit = l.limit.unwrap_or(0.0);
- if limit <= 0.0 {
- return if l.used.unwrap_or(0.0) > 0.0 || l.current_value.unwrap_or(0.0) > 0.0 {
- 100.0
- } else {
- 0.0
- };
- }
- let used = {
- let from_remaining = l.remaining.map(|r| limit - r);
- let from_current = l.current_value;
- let from_used = l.used;
- // Use max of available signals
- let candidates = [from_remaining, from_current, from_used];
- candidates.iter().filter_map(|&v| v).fold(0.0_f64, f64::max)
- };
- ((used / limit) * 100.0).clamp(0.0, 100.0)
+ let limit = l.limit.filter(|limit| *limit > 0.0)?;
+ let used = [l.remaining.map(|r| limit - r), l.current_value, l.used]
+ .into_iter()
+ .flatten()
+ .reduce(f64::max)?;
+ Some((used / limit * 100.0).clamp(0.0, 100.0))
}
// Upstream 0.48.0 `rateWindow`/`resetDescription`: only token-type
@@ -400,7 +387,7 @@ impl ZaiProvider {
// label and no window duration; 5-hour token windows are labeled
// "5-hour"; otherwise the explicit window label is used.
let now = Utc::now();
- let make_window = |l: &ZaiLimit| -> RateWindow {
+ let make_window = |l: &ZaiLimit| -> Option {
let is_tokens = matches!(
l.limit_type.as_deref(),
Some("TOKENS_LIMIT") | Some("CREDIT_LIMIT") | Some("tokens")
@@ -421,12 +408,12 @@ impl ZaiProvider {
});
let resets_at =
resets_at.filter(|reset| is_plausible_five_hour_reset(window_mins, *reset, now));
- RateWindow::with_details(
- compute_percent(l),
+ Some(RateWindow::with_details(
+ compute_percent(l)?,
window_mins,
resets_at,
rate_window_reset_description(l, window_mins),
- )
+ ))
};
// Upstream 0.48.0 bucket split: with 2+ token limits, the shortest
@@ -445,19 +432,25 @@ impl ZaiProvider {
None
};
- let primary = primary_limit
- .map(make_window)
- .unwrap_or_else(|| RateWindow::new(0.0));
- let mut usage = UsageSnapshot::new(primary).with_login_method(plan_name);
- if let Some(secondary) = secondary_limit {
- usage = usage.with_secondary(make_window(secondary));
+ let primary = primary_limit.and_then(make_window);
+ let secondary = secondary_limit.and_then(make_window);
+ let mcp = (token_limit.is_some() || session_token_limit.is_some())
+ .then(|| time_limit.and_then(make_window))
+ .flatten();
+ if primary.is_none() && secondary.is_none() && mcp.is_none() {
+ return Err(ProviderError::Parse(
+ "z.ai response contains no usable quota".into(),
+ ));
}
- // MCP usage is a separate named window whenever a coding-limit
- // primary exists; with no token limits it already owns the primary.
- if (token_limit.is_some() || session_token_limit.is_some())
- && let Some(mcp) = time_limit
- {
- usage = usage.with_extra_rate_window("zai-mcp", "MCP", make_window(mcp));
+ let mut usage = UsageSnapshot::new(
+ primary.unwrap_or_else(|| RateWindow::informational("z.ai primary usage unavailable")),
+ )
+ .with_login_method(plan_name);
+ if let Some(secondary) = secondary {
+ usage = usage.with_secondary(secondary);
+ }
+ if let Some(mcp) = mcp {
+ usage = usage.with_extra_rate_window("zai-mcp", "MCP", mcp);
}
Ok(usage)
@@ -618,3 +611,36 @@ impl Provider for ZaiProvider {
#[cfg(test)]
mod tests;
+
+#[cfg(test)]
+mod cx289a_parent_tests {
+ use super::*;
+ use serde_json::json;
+
+ fn parse(value: serde_json::Value) -> Result {
+ ZaiProvider::new().parse_quota_response(&serde_json::from_value(value).unwrap())
+ }
+ #[test]
+ fn missing_usage_is_an_error() {
+ for value in [
+ json!({}),
+ json!({"data":{"planName":"Pro","limits":[]}}),
+ json!({"limits":[{"type":"TOKENS_LIMIT","usage":100}]}),
+ json!({"limits":[{"type":"TOKENS_LIMIT","limit":100}]}),
+ ] {
+ assert!(parse(value.clone()).is_err(), "{value}");
+ }
+ }
+ #[test]
+ fn real_zero_usage_stays_a_quota() {
+ for limit in [
+ json!({"type":"TOKENS_LIMIT","usage":100,"currentValue":0}),
+ json!({"type":"tokens","limit":100,"used":0}),
+ json!({"type":"TOKENS_LIMIT","percentage":0}),
+ ] {
+ let snapshot = parse(json!({"limits":[limit]})).unwrap();
+ assert_eq!(snapshot.primary.used_percent, 0.0);
+ assert!(!snapshot.primary.is_informational);
+ }
+ }
+}
diff --git a/rust/src/providers/zai/tests.rs b/rust/src/providers/zai/tests.rs
index 12689811a38..d5cccd679bd 100644
--- a/rust/src/providers/zai/tests.rs
+++ b/rust/src/providers/zai/tests.rs
@@ -390,7 +390,7 @@ fn plan_name_falls_back_to_level_key() {
"code": 200,
"data": {
"level": "GLM Coding Plan",
- "limits": []
+ "limits": [{"type":"TOKENS_LIMIT","percentage":0}]
}
}))
.unwrap();
@@ -400,7 +400,7 @@ fn plan_name_falls_back_to_level_key() {
for key in ["plan", "plan_type", "packageName"] {
let quota: ZaiQuotaResponse = serde_json::from_value(serde_json::json!({
"code": 200,
- "data": { key: "Coding Plan", "limits": [] }
+ "data": { key: "Coding Plan", "limits": [{"type":"TOKENS_LIMIT","percentage":0}] }
}))
.unwrap();
let usage = provider.parse_quota_response("a).unwrap();
@@ -413,7 +413,7 @@ fn empty_plan_fields_fall_back_to_default() {
let provider = ZaiProvider::new();
let quota: ZaiQuotaResponse = serde_json::from_value(serde_json::json!({
"code": 200,
- "data": { "planName": " ", "level": "", "limits": [] }
+ "data": { "planName": " ", "level": "", "limits": [{"type":"TOKENS_LIMIT","percentage":0}] }
}))
.unwrap();
let usage = provider.parse_quota_response("a).unwrap();
diff --git a/rust/src/providers/zed/mod.rs b/rust/src/providers/zed/mod.rs
index 58fd5ab9396..0bf5234f8c4 100644
--- a/rust/src/providers/zed/mod.rs
+++ b/rust/src/providers/zed/mod.rs
@@ -85,7 +85,7 @@ impl Provider for ZedProvider {
.json()
.await
.map_err(|e| ProviderError::Parse(format!("Failed to parse Zed usage: {e}")))?;
- Ok(ProviderFetchResult::new(snapshot_from_user(&value), "api"))
+ Ok(ProviderFetchResult::new(snapshot_from_user(&value)?, "api"))
}
SourceMode::Web | SourceMode::Cli => {
Err(ProviderError::UnsupportedSource(ctx.source_mode))
@@ -98,19 +98,17 @@ impl Provider for ZedProvider {
}
}
-fn snapshot_from_user(value: &Value) -> UsageSnapshot {
+fn snapshot_from_user(value: &Value) -> Result {
let plan = value.get("plan").unwrap_or(value);
let usage = plan.get("usage").unwrap_or(plan);
let edits = usage
.pointer("/edit_predictions")
.or_else(|| usage.pointer("/editPredictions"))
.unwrap_or(usage);
- let used = number(edits, &["used"]).unwrap_or(0.0);
- let limit = number(edits, &["limit"]);
- let percent = limit
- .filter(|v| *v > 0.0)
- .map_or(0.0, |limit| used / limit * 100.0);
- UsageSnapshot::new(RateWindow::new(percent)).with_login_method("Zed")
+ let (used, limit) = number(edits, &["used"])
+ .zip(number(edits, &["limit"]).filter(|limit| *limit > 0.0))
+ .ok_or_else(|| ProviderError::Parse("Zed response contains no usable quota".into()))?;
+ Ok(UsageSnapshot::new(RateWindow::new(used / limit * 100.0)).with_login_method("Zed"))
}
fn number(value: &Value, keys: &[&str]) -> Option {
@@ -126,7 +124,35 @@ mod tests {
fn parses_edit_predictions() {
let snapshot = snapshot_from_user(
&serde_json::json!({"plan":{"usage":{"editPredictions":{"used":50,"limit":200}}}}),
- );
+ )
+ .expect("quota");
assert_eq!(snapshot.primary.used_percent, 25.0);
}
}
+
+#[cfg(test)]
+mod cx289a_parent_tests {
+ use super::*;
+ use serde_json::json;
+
+ fn parse(value: serde_json::Value) -> Result {
+ snapshot_from_user(&value)
+ }
+ #[test]
+ fn missing_usage_is_an_error() {
+ for value in [
+ json!({}),
+ json!({"used":0}),
+ json!({"limit":100}),
+ json!({"used":0,"limit":0}),
+ ] {
+ assert!(parse(value.clone()).is_err(), "{value}");
+ }
+ }
+ #[test]
+ fn real_zero_usage_stays_a_quota() {
+ let snapshot = parse(json!({"used":0,"limit":100})).unwrap();
+ assert_eq!(snapshot.primary.used_percent, 0.0);
+ assert!(!snapshot.primary.is_informational);
+ }
+}
diff --git a/rust/src/providers/zoommate/mod.rs b/rust/src/providers/zoommate/mod.rs
index 8cc31f2e37d..8f4ce7ea40d 100644
--- a/rust/src/providers/zoommate/mod.rs
+++ b/rust/src/providers/zoommate/mod.rs
@@ -445,11 +445,7 @@ impl ZoomMateProvider {
.and_then(|d| d.credit_status)
.ok_or_else(|| ProviderError::Parse("Missing credit_status object.".into()))?;
- Ok(snapshot_from_credit_status(
- &credit_status,
- account_email,
- Utc::now(),
- ))
+ snapshot_from_credit_status(&credit_status, account_email, Utc::now())
}
}
@@ -730,50 +726,45 @@ pub(crate) fn snapshot_from_credit_status(
status: &CreditStatus,
account_email: Option<&str>,
now: DateTime,
-) -> UsageSnapshot {
- let budget_cap = status.budget_cap.unwrap_or(0.0);
- let used_credit = status.used_credit.unwrap_or(0.0);
- let is_unlimited = status.is_unlimited.unwrap_or(false);
-
- let used_percent =
- if is_unlimited || budget_cap <= 0.0 || !budget_cap.is_finite() || !used_credit.is_finite()
- {
- 0.0
- } else {
- ((used_credit / budget_cap) * 100.0).clamp(0.0, 100.0)
- };
-
- let cycle_start = date_from_millis(status.cycle_start_date);
- let cycle_end = date_from_millis(status.cycle_end_date);
-
- let resets_at = if is_unlimited || budget_cap <= 0.0 {
- None
+) -> Result {
+ let primary = if status.is_unlimited.unwrap_or(false) {
+ RateWindow::informational("Unlimited credits")
} else {
- cycle_end
- };
-
- let window_minutes = match (cycle_start, cycle_end) {
- (Some(start), Some(end)) if end > start => {
- let mins = (end - start).num_minutes();
- if mins > 0 {
- // Window length is clamped to u32::MAX, so the cast cannot truncate.
- #[expect(clippy::cast_possible_truncation, reason = "clamped to u32 range")]
- let window = mins.min(u32::MAX as i64) as u32;
- Some(window)
- } else {
- None
+ let (used_credit, budget_cap) = status
+ .used_credit
+ .filter(|used| used.is_finite())
+ .zip(
+ status
+ .budget_cap
+ .filter(|limit| limit.is_finite() && *limit > 0.0),
+ )
+ .ok_or_else(|| {
+ ProviderError::Parse("ZoomMate response contains no usable credit quota".into())
+ })?;
+ let cycle_start = date_from_millis(status.cycle_start_date);
+ let cycle_end = date_from_millis(status.cycle_end_date);
+ let window_minutes = match (cycle_start, cycle_end) {
+ (Some(start), Some(end)) if end > start => {
+ let mins = (end - start).num_minutes();
+ if mins > 0 {
+ // Window length is clamped to u32::MAX, so the cast cannot truncate.
+ #[expect(clippy::cast_possible_truncation, reason = "clamped to u32 range")]
+ let window = mins.min(u32::MAX as i64) as u32;
+ Some(window)
+ } else {
+ None
+ }
}
- }
- _ => None,
+ _ => None,
+ };
+ RateWindow::with_details(
+ ((used_credit / budget_cap) * 100.0).clamp(0.0, 100.0),
+ window_minutes,
+ cycle_end,
+ Some("Credits".into()),
+ )
};
- let primary = RateWindow::with_details(
- used_percent,
- window_minutes,
- resets_at,
- Some("Credits".into()),
- );
-
let mut snap = UsageSnapshot::new(primary);
snap.updated_at = now;
if let Some(email) = account_email.map(str::trim).filter(|s| !s.is_empty()) {
@@ -786,7 +777,7 @@ pub(crate) fn snapshot_from_credit_status(
status.allow_overage,
status.is_quota_available,
);
- snap
+ Ok(snap)
}
// Classify failover errors for pure unit tests.
@@ -823,7 +814,8 @@ mod tests {
let envelope: CreditsStatusEnvelope =
serde_json::from_str(sample_status_json()).expect("fixture parses");
let status = envelope.data.unwrap().credit_status.unwrap();
- let snap = snapshot_from_credit_status(&status, Some("user@zoom.us"), Utc::now());
+ let snap =
+ snapshot_from_credit_status(&status, Some("user@zoom.us"), Utc::now()).expect("quota");
assert!((snap.primary.used_percent - 25.0).abs() < 0.01);
assert_eq!(snap.primary.reset_description.as_deref(), Some("Credits"));
assert!(snap.primary.resets_at.is_some());
@@ -834,25 +826,32 @@ mod tests {
}
#[test]
- fn unlimited_or_zero_cap_yields_zero_percent() {
+ fn unlimited_is_information_and_zero_cap_is_unavailable() {
let unlimited = CreditStatus {
budget_cap: Some(100.0),
used_credit: Some(50.0),
is_unlimited: Some(true),
..Default::default()
};
- let snap = snapshot_from_credit_status(&unlimited, None, Utc::now());
- assert_eq!(snap.primary.used_percent, 0.0);
+ let snap = snapshot_from_credit_status(&unlimited, None, Utc::now()).expect("quota");
+ assert!(snap.primary.is_informational);
assert!(snap.primary.resets_at.is_none());
+ let unlimited_without_counters = CreditStatus {
+ is_unlimited: Some(true),
+ ..Default::default()
+ };
+ let snap = snapshot_from_credit_status(&unlimited_without_counters, None, Utc::now())
+ .expect("unlimited is a state, not missing data");
+ assert!(snap.primary.is_informational);
+
let zero_cap = CreditStatus {
budget_cap: Some(0.0),
used_credit: Some(10.0),
is_unlimited: Some(false),
..Default::default()
};
- let snap = snapshot_from_credit_status(&zero_cap, None, Utc::now());
- assert_eq!(snap.primary.used_percent, 0.0);
+ assert!(snapshot_from_credit_status(&zero_cap, None, Utc::now()).is_err());
}
#[test]
@@ -864,7 +863,7 @@ mod tests {
cycle_end_date: Some(1_900_000_000_000),
..Default::default()
};
- let snap = snapshot_from_credit_status(&status, None, Utc::now());
+ let snap = snapshot_from_credit_status(&status, None, Utc::now()).expect("quota");
assert_eq!(snap.primary.used_percent, 100.0);
}
@@ -1057,3 +1056,30 @@ mod tests {
assert!(!cookie_is_sendable_to_host("", "ai.zoom.us"));
}
}
+
+#[cfg(test)]
+mod cx289a_parent_tests {
+ use super::*;
+ use serde_json::json;
+
+ fn parse(value: serde_json::Value) -> Result {
+ snapshot_from_credit_status(&serde_json::from_value(value).unwrap(), None, Utc::now())
+ }
+ #[test]
+ fn missing_usage_is_an_error() {
+ for value in [
+ json!({}),
+ json!({"budget_cap":100}),
+ json!({"used_credit":0}),
+ json!({"used_credit":0,"budget_cap":0}),
+ ] {
+ assert!(parse(value.clone()).is_err(), "{value}");
+ }
+ }
+ #[test]
+ fn real_zero_usage_stays_a_quota() {
+ let snapshot = parse(json!({"used_credit":0,"budget_cap":100})).unwrap();
+ assert_eq!(snapshot.primary.used_percent, 0.0);
+ assert!(!snapshot.primary.is_informational);
+ }
+}