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]