Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 27 additions & 15 deletions rust/src/providers/claude/oauth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -571,7 +571,11 @@ impl ClaudeOAuthFetcher {

/// Convert OAuth usage window to RateWindow
fn to_rate_window(window: &UsageWindow, window_minutes: Option<u32>) -> Option<RateWindow> {
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
Expand All @@ -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<DateTime<Utc>> {
// Try parsing with various formats
Expand All @@ -629,15 +625,31 @@ 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,
};

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]
Expand Down Expand Up @@ -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()
Expand Down
17 changes: 6 additions & 11 deletions rust/src/providers/claude/web_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,10 @@ impl ClaudeWebApiFetcher {

/// Convert a usage window to a RateWindow
fn to_rate_window(&self, window: &UsageWindow, window_minutes: Option<u32>) -> 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
Expand Down Expand Up @@ -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<String> {
cookie_header.split(';').find_map(|part| {
let (key, value) = part.trim().split_once('=')?;
Expand Down Expand Up @@ -756,15 +751,15 @@ 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,
};

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]
Expand Down
Loading