From b4ff1bc8e33b97f62f0e529da0c7c9909fede0de Mon Sep 17 00:00:00 2001 From: yozu Date: Mon, 10 Aug 2026 10:15:08 +0900 Subject: [PATCH] fix: stop rescaling Claude utilization below 1% to 100% `normalize_utilization` treated any `utilization` value in (0, 1] as a fraction and multiplied it by 100. The Claude usage API already reports these values in percent units, so a session at 1% was rendered as a fully consumed quota with a red "used up" state, while the Claude settings page showed 1%. The sibling `limits[].percent` field has always been consumed as-is by `scoped_weekly::weekly_all_window`, and issue payloads carry the same value in both fields (`seven_day.utilization: 1.0` alongside `weekly_all.percent: 1.0`), confirming the unit. Drop the rescaling in both the OAuth and web API fetchers so five-hour, seven-day, model-scoped and routines windows all report percent directly. --- rust/src/providers/claude/oauth/mod.rs | 42 +++++++++++++++++--------- rust/src/providers/claude/web_api.rs | 17 ++++------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/rust/src/providers/claude/oauth/mod.rs b/rust/src/providers/claude/oauth/mod.rs index 3242f515f1..df659be68d 100644 --- a/rust/src/providers/claude/oauth/mod.rs +++ b/rust/src/providers/claude/oauth/mod.rs @@ -511,8 +511,8 @@ impl ClaudeOAuthFetcher { let mut usage = UsageSnapshot::new(primary); - // Secondary: prefer limits[] weekly_all over legacy seven_day (avoids - // phantom 100% when Anthropic leaves seven_day.utilization stale). + // Secondary: prefer limits[] weekly_all over legacy seven_day, which + // Anthropic can leave stale. if let Some(weekly) = super::scoped_weekly::weekly_all_window(&response.limits).or_else(|| { response @@ -571,7 +571,11 @@ impl ClaudeOAuthFetcher { /// Convert OAuth usage window to RateWindow fn to_rate_window(window: &UsageWindow, window_minutes: Option) -> Option { - let utilization = normalize_utilization(window.utilization?); + // `utilization` is already expressed in percent units: `1.0` means 1%, + // not 100%. Treating values <= 1 as fractions reported a 1% session as a + // fully consumed quota. `scoped_weekly::weekly_all_window` has always + // read the sibling `limits[].percent` field this way. + let utilization = window.utilization?; let resets_at = window .resets_at @@ -595,14 +599,6 @@ impl Default for ClaudeOAuthFetcher { } } -fn normalize_utilization(utilization: f64) -> f64 { - if utilization > 0.0 && utilization <= 1.0 { - utilization * 100.0 - } else { - utilization - } -} - /// Parse an ISO8601 date string fn parse_iso8601_date(s: &str) -> Option> { // Try parsing with various formats @@ -629,7 +625,7 @@ mod tests { use std::time::Duration; #[test] - fn converts_fractional_utilization_to_percent() { + fn keeps_sub_one_utilization_in_percent_units() { let window = UsageWindow { utilization: Some(0.23), resets_at: None, @@ -637,7 +633,23 @@ mod tests { let rate = ClaudeOAuthFetcher::to_rate_window(&window, Some(300)).expect("rate window"); - assert!((rate.used_percent - 23.0).abs() < f64::EPSILON); + assert!((rate.used_percent - 0.23).abs() < f64::EPSILON); + } + + #[test] + fn one_percent_session_is_not_reported_as_full_quota() { + let window = UsageWindow { + utilization: Some(1.0), + resets_at: None, + }; + + let rate = ClaudeOAuthFetcher::to_rate_window(&window, Some(300)).expect("rate window"); + + assert!( + (rate.used_percent - 1.0).abs() < f64::EPSILON, + "session was {}, expected 1% (not 100%)", + rate.used_percent + ); } #[test] @@ -681,8 +693,8 @@ mod tests { }; let usage = ClaudeOAuthFetcher::new().build_usage_snapshot(&response, &credentials); - assert_eq!(usage.primary.used_percent, 100.0); - assert!((usage.secondary.expect("weekly").used_percent - 14.0).abs() < 0.001); + assert_eq!(usage.primary.used_percent, 1.0); + assert!((usage.secondary.expect("weekly").used_percent - 0.14).abs() < 0.001); let scoped = usage .extra_rate_windows .iter() diff --git a/rust/src/providers/claude/web_api.rs b/rust/src/providers/claude/web_api.rs index 9998862b7e..72e647ef0e 100755 --- a/rust/src/providers/claude/web_api.rs +++ b/rust/src/providers/claude/web_api.rs @@ -611,7 +611,10 @@ impl ClaudeWebApiFetcher { /// Convert a usage window to a RateWindow fn to_rate_window(&self, window: &UsageWindow, window_minutes: Option) -> RateWindow { - let used_percent = normalize_utilization(window.utilization.unwrap_or(0.0)); + // `utilization` is already expressed in percent units: `1.0` means 1%, + // not 100%. Treating values <= 1 as fractions reported a 1% session as a + // fully consumed quota. + let used_percent = window.utilization.unwrap_or(0.0); let resets_at = window .resets_at @@ -652,14 +655,6 @@ impl Default for ClaudeWebApiFetcher { } } -fn normalize_utilization(utilization: f64) -> f64 { - if utilization > 0.0 && utilization <= 1.0 { - utilization * 100.0 - } else { - utilization - } -} - fn cookie_value(cookie_header: &str, name: &str) -> Option { cookie_header.split(';').find_map(|part| { let (key, value) = part.trim().split_once('=')?; @@ -756,7 +751,7 @@ mod tests { } #[test] - fn converts_fractional_utilization_to_percent() { + fn keeps_sub_one_utilization_in_percent_units() { let window = UsageWindow { utilization: Some(0.23), resets_at: None, @@ -764,7 +759,7 @@ mod tests { let rate = ClaudeWebApiFetcher::new().to_rate_window(&window, Some(300)); - assert!((rate.used_percent - 23.0).abs() < f64::EPSILON); + assert!((rate.used_percent - 0.23).abs() < f64::EPSILON); } #[test]