diff --git a/rust/src/providers/claude/mod.rs b/rust/src/providers/claude/mod.rs index d67770f4..d16360fa 100755 --- a/rust/src/providers/claude/mod.rs +++ b/rust/src/providers/claude/mod.rs @@ -4,6 +4,7 @@ mod admin_api; mod cli_reset; mod oauth; mod scoped_weekly; +mod usage_api; mod web_api; use async_trait::async_trait; diff --git a/rust/src/providers/claude/oauth/mod.rs b/rust/src/providers/claude/oauth/mod.rs index 35800b1c..33fac0fa 100644 --- a/rust/src/providers/claude/oauth/mod.rs +++ b/rust/src/providers/claude/oauth/mod.rs @@ -5,13 +5,13 @@ use chrono::{DateTime, Utc}; use reqwest::Client; use reqwest::header::{HeaderValue, RETRY_AFTER}; -use serde::Deserialize; use std::path::PathBuf; use std::sync::{Mutex, OnceLock}; use std::time::{Duration, Instant}; use super::UtilizationScale; -use crate::core::{NamedRateWindow, ProviderError, ProviderFetchResult, RateWindow, UsageSnapshot}; +use super::usage_api::{ClaudeUsageResponse, ClaudeUsageWindow}; +use crate::core::{ProviderError, ProviderFetchResult, RateWindow, UsageSnapshot}; mod credentials_store; mod refresh; @@ -53,65 +53,9 @@ impl ClaudeOAuthCredentials { } } -/// OAuth usage response from Claude API -#[derive(Debug, Deserialize)] -pub struct OAuthUsageResponse { - #[serde(rename = "fiveHour", alias = "five_hour")] - pub five_hour: Option, - - #[serde(rename = "sevenDay", alias = "seven_day")] - pub seven_day: Option, - - #[serde(rename = "sevenDaySonnet", alias = "seven_day_sonnet")] - pub seven_day_sonnet: Option, - - #[serde(rename = "sevenDayOpus", alias = "seven_day_opus")] - pub seven_day_opus: Option, - - #[serde( - rename = "sevenDayDesign", - alias = "seven_day_design", - alias = "seven_day_oauth_apps" - )] - pub seven_day_design: Option, - - #[serde( - rename = "sevenDayRoutines", - alias = "seven_day_routines", - alias = "seven_day_omelette" - )] - pub seven_day_routines: Option, - - #[serde(rename = "extraUsage", alias = "extra_usage")] - pub extra_usage: Option, - - #[serde(default)] - limits: Vec, -} - -/// A usage window from the OAuth API -#[derive(Debug, Deserialize)] -pub struct UsageWindow { - pub utilization: Option, - - #[serde(rename = "resetsAt", alias = "resets_at")] - pub resets_at: Option, -} - -/// Extra usage (credits) info -#[derive(Debug, Deserialize)] -pub struct ExtraUsage { - #[serde(rename = "isEnabled", alias = "is_enabled")] - pub is_enabled: Option, - - #[serde(rename = "usedCredits", alias = "used_credits")] - pub used_credits: Option, - - #[serde(rename = "monthlyLimit", alias = "monthly_limit")] - pub monthly_limit: Option, - - pub currency: Option, -} +/// OAuth and web usage now share one normalized response shape. +pub type OAuthUsageResponse = ClaudeUsageResponse; +pub type UsageWindow = ClaudeUsageWindow; /// Claude OAuth fetcher pub struct ClaudeOAuthFetcher { @@ -208,7 +152,11 @@ impl ClaudeOAuthFetcher { } } - Ok(ProviderFetchResult::new(usage, "oauth")) + let mut result = ProviderFetchResult::new(usage, "oauth"); + if let Some(cost) = usage_response.extra_usage_cost() { + result = result.with_cost(cost); + } + Ok(result) } /// Identity of the account this fetcher reads, for labeling and scoping. @@ -416,63 +364,7 @@ impl ClaudeOAuthFetcher { response: &OAuthUsageResponse, credentials: &ClaudeOAuthCredentials, ) -> UsageSnapshot { - // Anthropic mixes fractions and percentages between payloads, so settle - // the unit once from the whole response before reading any window. - let scale = response.utilization_scale(); - - // Primary: 5-hour session window - let primary = response - .five_hour - .as_ref() - .and_then(|w| Self::to_rate_window(w, Some(300), scale)) - .unwrap_or_else(|| RateWindow::new(0.0)); - - let mut usage = UsageSnapshot::new(primary); - - // Secondary: 7-day window - if let Some(weekly) = response - .seven_day - .as_ref() - .and_then(|w| Self::to_rate_window(w, Some(10080), scale)) - { - usage = usage.with_secondary(weekly); - } - - // Model-specific: Opus or Sonnet - if let Some(opus) = response - .seven_day_opus - .as_ref() - .and_then(|w| Self::to_rate_window(w, Some(10080), scale)) - { - usage = usage.with_model_specific(opus); - } else if let Some(sonnet) = response - .seven_day_sonnet - .as_ref() - .and_then(|w| Self::to_rate_window(w, Some(10080), scale)) - { - usage = usage.with_model_specific(sonnet); - } - - let extra_windows = [( - "claude-routines", - "Daily Routines", - response - .seven_day_routines - .as_ref() - .and_then(|w| Self::to_rate_window(w, Some(10080), scale)), - )]; - for (id, title, window) in extra_windows { - if let Some(window) = window { - usage - .extra_rate_windows - .push(NamedRateWindow::new(id, title, window)); - } - usage - .extra_rate_windows - .extend(super::scoped_weekly::scoped_weekly_windows( - &response.limits, - )); - } + let mut usage = response.build_snapshot(Self::to_rate_window); // Plan name from the rate limit tier, falling back to the subscription // type when the tier is shared across plans (Pro and Free both report @@ -518,25 +410,6 @@ impl Default for ClaudeOAuthFetcher { } } -impl OAuthUsageResponse { - /// Decide the utilization unit from every window this response carries. - fn utilization_scale(&self) -> UtilizationScale { - UtilizationScale::detect( - [ - self.five_hour.as_ref(), - self.seven_day.as_ref(), - self.seven_day_sonnet.as_ref(), - self.seven_day_opus.as_ref(), - self.seven_day_design.as_ref(), - self.seven_day_routines.as_ref(), - ] - .into_iter() - .flatten() - .filter_map(|window| window.utilization), - ) - } -} - /// Parse an ISO8601 date string fn parse_iso8601_date(s: &str) -> Option> { // Try parsing with various formats diff --git a/rust/src/providers/claude/usage_api.rs b/rust/src/providers/claude/usage_api.rs new file mode 100644 index 00000000..467a0446 --- /dev/null +++ b/rust/src/providers/claude/usage_api.rs @@ -0,0 +1,310 @@ +use serde::Deserialize; + +use super::UtilizationScale; +use crate::core::{CostSnapshot, NamedRateWindow, RateWindow, UsageSnapshot}; + +/// Usage payload shared by Claude's OAuth and web endpoints. +/// +/// The two endpoints expose the same windows with different casing and have +/// drifted independently in the past. Normalize the wire shape once so every +/// source renders the same set of windows and extra-usage dollars. +#[derive(Debug)] +pub struct ClaudeUsageResponse { + pub five_hour: Option, + pub seven_day: Option, + pub seven_day_opus: Option, + pub seven_day_sonnet: Option, + pub seven_day_oauth_apps: Option, + pub seven_day_design: Option, + pub seven_day_promotional: Option, + pub seven_day_routines: Option, + pub extra_usage: Option, + pub(super) limits: Vec, +} + +impl<'de> Deserialize<'de> for ClaudeUsageResponse { + fn deserialize>(deserializer: D) -> Result { + let mut map: std::collections::HashMap = + std::collections::HashMap::deserialize(deserializer)?; + + let take_window = |map: &mut std::collections::HashMap, + keys: &[&str]| + -> Result, D::Error> { + for key in keys { + if let Some(value) = map.remove(*key) { + if value.is_null() { + continue; + } + return serde_json::from_value(value) + .map(Some) + .map_err(serde::de::Error::custom); + } + } + Ok(None) + }; + + let take_value = |map: &mut std::collections::HashMap, + keys: &[&str]| { + keys.iter() + .find_map(|key| map.remove(*key).filter(|value| !value.is_null())) + }; + + Ok(Self { + five_hour: take_window(&mut map, &["five_hour", "fiveHour"])?, + seven_day: take_window(&mut map, &["seven_day", "sevenDay"])?, + seven_day_opus: take_window(&mut map, &["seven_day_opus", "sevenDayOpus"])?, + seven_day_sonnet: take_window(&mut map, &["seven_day_sonnet", "sevenDaySonnet"])?, + seven_day_oauth_apps: take_window( + &mut map, + &[ + "seven_day_oauth_apps", + "sevenDayOAuthApps", + "seven_day_claude_oauth_apps", + "oauth_apps", + "oauth", + ], + )?, + seven_day_design: take_window( + &mut map, + &[ + "seven_day_design", + "sevenDayDesign", + "seven_day_claude_design", + "claude_design", + "design", + ], + )?, + seven_day_promotional: take_window( + &mut map, + &[ + "omelette_promotional", + "omelettePromotional", + "omelette", + "seven_day_omelette", + "sevenDayOmelette", + ], + )?, + seven_day_routines: take_window( + &mut map, + &[ + "seven_day_routines", + "sevenDayRoutines", + "seven_day_claude_routines", + "claude_routines", + "routines", + "routine", + "seven_day_cowork", + "sevenDayCowork", + "cowork", + ], + )?, + limits: take_value(&mut map, &["limits"]) + .map(serde_json::from_value) + .transpose() + .map_err(serde::de::Error::custom)? + .unwrap_or_default(), + extra_usage: take_value(&mut map, &["extra_usage", "extraUsage"]) + .map(serde_json::from_value) + .transpose() + .map_err(serde::de::Error::custom)?, + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct ClaudeUsageWindow { + pub utilization: Option, + #[serde(rename = "resets_at", alias = "resetsAt")] + pub resets_at: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ClaudeExtraUsage { + #[serde( + rename = "monthly_credit_limit", + alias = "monthlyCreditLimit", + alias = "monthly_limit", + alias = "monthlyLimit" + )] + pub monthly_limit: Option, + #[serde(rename = "used_credits", alias = "usedCredits")] + pub used_credits: Option, + pub currency: Option, + #[serde(rename = "is_enabled", alias = "isEnabled")] + pub is_enabled: Option, +} + +impl ClaudeUsageResponse { + pub(super) fn utilization_scale(&self) -> UtilizationScale { + UtilizationScale::detect( + [ + self.five_hour.as_ref(), + self.seven_day.as_ref(), + self.seven_day_opus.as_ref(), + self.seven_day_sonnet.as_ref(), + self.seven_day_oauth_apps.as_ref(), + self.seven_day_design.as_ref(), + self.seven_day_promotional.as_ref(), + self.seven_day_routines.as_ref(), + ] + .into_iter() + .flatten() + .filter_map(|window| window.utilization), + ) + } + + pub(super) fn build_snapshot(&self, mut convert: F) -> UsageSnapshot + where + F: FnMut(&ClaudeUsageWindow, Option, UtilizationScale) -> Option, + { + let scale = self.utilization_scale(); + let primary = self + .five_hour + .as_ref() + .and_then(|window| convert(window, Some(300), scale)) + .unwrap_or_else(|| RateWindow::new(0.0)); + let mut snapshot = UsageSnapshot::new(primary); + + if let Some(window) = self + .seven_day + .as_ref() + .and_then(|window| convert(window, Some(10080), scale)) + { + snapshot = snapshot.with_secondary(window); + } + + let model_specific = self + .seven_day_opus + .as_ref() + .and_then(|window| convert(window, Some(10080), scale)) + .or_else(|| { + self.seven_day_sonnet + .as_ref() + .and_then(|window| convert(window, Some(10080), scale)) + }); + if let Some(window) = model_specific { + snapshot = snapshot.with_model_specific(window); + } + + for (id, title, window) in [ + ( + "claude-oauth-apps", + "OAuth apps", + self.seven_day_oauth_apps.as_ref(), + ), + ( + "claude-routines", + "Daily Routines", + self.seven_day_routines.as_ref(), + ), + ("claude-design", "Design", self.seven_day_design.as_ref()), + ( + "claude-weekly-promo", + "Weekly promo", + self.seven_day_promotional.as_ref(), + ), + ] { + if let Some(window) = window.and_then(|window| convert(window, Some(10080), scale)) { + snapshot + .extra_rate_windows + .push(NamedRateWindow::new(id, title, window)); + } + } + + snapshot + .extra_rate_windows + .extend(super::scoped_weekly::scoped_weekly_windows(&self.limits)); + snapshot + } + + pub(super) fn extra_usage_cost(&self) -> Option { + self.extra_usage.as_ref()?.cost_snapshot() + } +} + +impl ClaudeExtraUsage { + pub(super) fn cost_snapshot(&self) -> Option { + let extra = self; + if !extra.is_enabled.unwrap_or(false) { + return None; + } + + let mut cost = CostSnapshot::new( + extra.used_credits.unwrap_or(0.0) / 100.0, + extra.currency.clone().unwrap_or_else(|| "USD".to_string()), + "Monthly", + ); + if let Some(limit) = extra.monthly_limit { + cost = cost.with_limit(limit / 100.0); + } + Some(cost) + } +} + +#[cfg(test)] +mod tests { + use super::ClaudeUsageResponse; + use crate::core::RateWindow; + + fn snapshot(response: &ClaudeUsageResponse) -> crate::core::UsageSnapshot { + response.build_snapshot(|window, minutes, scale| { + Some(RateWindow::with_details( + scale.to_percent(window.utilization?), + minutes, + None, + None, + )) + }) + } + + #[test] + fn oauth_shape_keeps_design_and_extra_usage_dollars() { + let response: ClaudeUsageResponse = serde_json::from_str( + r#"{ + "fiveHour": {"utilization": 12}, + "sevenDayDesign": {"utilization": 34}, + "extraUsage": { + "isEnabled": true, + "usedCredits": 1234, + "monthlyLimit": 5000, + "currency": "USD" + } + }"#, + ) + .expect("OAuth response parses"); + + let usage = snapshot(&response); + let design = usage + .extra_rate_windows + .iter() + .find(|window| window.id == "claude-design") + .expect("Design window survives OAuth mapping"); + assert_eq!(design.window.used_percent, 34.0); + + let cost = response.extra_usage_cost().expect("extra usage cost"); + assert_eq!(cost.used, 12.34); + assert_eq!(cost.limit, Some(50.0)); + assert_eq!(cost.currency_code, "USD"); + } + + #[test] + fn web_shape_keeps_sonnet_as_the_model_specific_window() { + let response: ClaudeUsageResponse = serde_json::from_str( + r#"{ + "five_hour": {"utilization": 12}, + "seven_day": {"utilization": 23}, + "seven_day_sonnet": {"utilization": 45} + }"#, + ) + .expect("web response parses"); + + let usage = snapshot(&response); + assert_eq!( + usage + .model_specific + .expect("Sonnet model-specific window") + .used_percent, + 45.0 + ); + } +} diff --git a/rust/src/providers/claude/web_api.rs b/rust/src/providers/claude/web_api.rs index 66f26b4f..504f4736 100755 --- a/rust/src/providers/claude/web_api.rs +++ b/rust/src/providers/claude/web_api.rs @@ -6,12 +6,10 @@ use serde::Deserialize; use std::path::PathBuf; use super::UtilizationScale; +use super::usage_api::{ClaudeExtraUsage, ClaudeUsageResponse, ClaudeUsageWindow}; use crate::browser::cookies::{get_cookie_header, get_cookie_header_from_browser}; use crate::browser::detection::{BrowserProfile, BrowserType, DetectedBrowser}; -use crate::core::{ - CostSnapshot, NamedRateWindow, PromoSignal, ProviderError, ProviderFetchResult, RateWindow, - UsageSnapshot, -}; +use crate::core::{PromoSignal, ProviderError, ProviderFetchResult, RateWindow}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClaudeDesktopSessionStatus { @@ -185,130 +183,6 @@ struct Organization { name: Option, } -/// Usage response from Claude API. -/// -/// Anthropic ships overlapping field names for the design and routines -/// windows (e.g. both `seven_day_design` and `seven_day_omelette` may appear -/// in the same payload). Serde aliases can't accept that — it errors with -/// "duplicate field" if more than one alias is present. We deserialize into -/// a generic map and pick the first alias that yields a non-null value. -#[derive(Debug)] -struct UsageResponse { - five_hour: Option, - seven_day: Option, - seven_day_opus: Option, - seven_day_sonnet: Option, - seven_day_oauth_apps: Option, - seven_day_design: Option, - /// Temporary promotional weekly pool when Anthropic reports omelette fields. - seven_day_promotional: Option, - seven_day_routines: Option, - extra_usage: Option, - limits: Vec, -} - -impl<'de> Deserialize<'de> for UsageResponse { - fn deserialize>(deserializer: D) -> Result { - let mut map: std::collections::HashMap = - std::collections::HashMap::deserialize(deserializer)?; - - let take = |map: &mut std::collections::HashMap, - keys: &[&str]| - -> Result, D::Error> { - for key in keys { - if let Some(value) = map.remove(*key) { - if value.is_null() { - continue; - } - let window: UsageWindow = - serde_json::from_value(value).map_err(serde::de::Error::custom)?; - return Ok(Some(window)); - } - } - Ok(None) - }; - - Ok(UsageResponse { - five_hour: take(&mut map, &["five_hour"])?, - seven_day: take(&mut map, &["seven_day"])?, - seven_day_opus: take(&mut map, &["seven_day_opus"])?, - seven_day_sonnet: take(&mut map, &["seven_day_sonnet"])?, - seven_day_oauth_apps: take( - &mut map, - &[ - "seven_day_oauth_apps", - "seven_day_claude_oauth_apps", - "oauth_apps", - "oauth", - ], - )?, - seven_day_design: take( - &mut map, - &[ - "seven_day_design", - "seven_day_claude_design", - "claude_design", - "design", - ], - )?, - seven_day_promotional: take( - &mut map, - &["omelette_promotional", "omelette", "seven_day_omelette"], - )?, - seven_day_routines: take( - &mut map, - &[ - "seven_day_routines", - "seven_day_claude_routines", - "claude_routines", - "routines", - "routine", - "seven_day_cowork", - "cowork", - ], - )?, - limits: map - .get("limits") - .filter(|value| !value.is_null()) - .cloned() - .map(serde_json::from_value) - .transpose() - .map_err(serde::de::Error::custom)? - .unwrap_or_default(), - extra_usage: map - .remove("extra_usage") - .filter(|value| !value.is_null()) - .map(serde_json::from_value) - .transpose() - .map_err(serde::de::Error::custom)?, - }) - } -} - -/// A usage window from the API -#[derive(Debug, Deserialize)] -struct UsageWindow { - utilization: Option, - - #[serde(rename = "resets_at")] - resets_at: Option, -} - -/// Extra usage (credits) response -#[derive(Debug, Clone, Deserialize)] -struct ExtraUsageResponse { - #[serde(rename = "monthly_credit_limit")] - monthly_credit_limit: Option, - - #[serde(rename = "used_credits")] - used_credits: Option, - - currency: Option, - - #[serde(rename = "is_enabled")] - is_enabled: Option, -} - /// Account info response #[derive(Debug, Deserialize)] struct AccountResponse { @@ -435,79 +309,11 @@ impl ClaudeWebApiFetcher { // Step 4: Fetch account info - optional let account = self.get_account_info(&headers).await.ok(); - // Build the result. Anthropic mixes fractions and percentages between - // payloads, so settle the unit once from the whole response first. - let scale = usage.utilization_scale(); - - let primary = usage - .five_hour - .as_ref() - .map(|w| self.to_rate_window(w, Some(300), scale)) // 5 hours = 300 minutes - .unwrap_or_else(|| RateWindow::new(0.0)); - - let secondary = usage - .seven_day - .as_ref() - .map(|w| self.to_rate_window(w, Some(10080), scale)); // 7 days = 10080 minutes - - let model_specific = usage - .seven_day_opus - .as_ref() - .map(|w| self.to_rate_window(w, Some(10080), scale)); - - let mut snapshot = UsageSnapshot::new(primary); - - if let Some(s) = secondary { - snapshot = snapshot.with_secondary(s); - } - - if let Some(m) = model_specific { - snapshot = snapshot.with_model_specific(m); - } - - for (id, title, window) in [ - ( - "claude-oauth-apps", - "OAuth apps", - usage - .seven_day_oauth_apps - .as_ref() - .map(|w| self.to_rate_window(w, Some(10080), scale)), - ), - ( - "claude-routines", - "Daily Routines", - usage - .seven_day_routines - .as_ref() - .map(|w| self.to_rate_window(w, Some(10080), scale)), - ), - ( - "claude-design", - "Design", - usage - .seven_day_design - .as_ref() - .map(|w| self.to_rate_window(w, Some(10080), scale)), - ), - ( - "claude-weekly-promo", - "Weekly promo", - usage - .seven_day_promotional - .as_ref() - .map(|w| self.to_rate_window(w, Some(10080), scale)), - ), - ] { - if let Some(window) = window { - snapshot - .extra_rate_windows - .push(NamedRateWindow::new(id, title, window)); - } - } - snapshot - .extra_rate_windows - .extend(super::scoped_weekly::scoped_weekly_windows(&usage.limits)); + // Build every common Claude usage lane through one normalized path so + // OAuth and web cannot silently drop fields the other source renders. + let mut snapshot = usage.build_snapshot(|window, minutes, scale| { + Some(self.to_rate_window(window, minutes, scale)) + }); if let Some(promo) = usage.seven_day_promotional.as_ref() { let ends_at = promo @@ -534,24 +340,13 @@ impl ClaudeWebApiFetcher { let mut result = ProviderFetchResult::new(snapshot, "web"); - // Add cost info if available - if let Some(extra) = extra_usage - && extra.is_enabled.unwrap_or(false) + // `extra_usage` already fell back to the embedded payload when the + // dedicated endpoint failed, so a `None` here means the endpoint was + // read and reported overage as disabled. Do not claim a cost for it. + if let Some(cost) = extra_usage + .as_ref() + .and_then(ClaudeExtraUsage::cost_snapshot) { - let used_cents = extra.used_credits.unwrap_or(0.0); - let limit_cents = extra.monthly_credit_limit; - let currency = extra.currency.unwrap_or_else(|| "USD".to_string()); - - let mut cost = CostSnapshot::new( - used_cents / 100.0, // Convert cents to dollars - currency, - "Monthly", - ); - - if let Some(limit) = limit_cents { - cost = cost.with_limit(limit / 100.0); - } - result = result.with_cost(cost); } @@ -658,7 +453,7 @@ impl ClaudeWebApiFetcher { &self, org_id: &str, headers: &reqwest::header::HeaderMap, - ) -> Result { + ) -> Result { let url = format!("{}/organizations/{}/usage", Self::BASE_URL, org_id); let response = self @@ -683,7 +478,7 @@ impl ClaudeWebApiFetcher { &self, org_id: &str, headers: &reqwest::header::HeaderMap, - ) -> Result { + ) -> Result { let url = format!( "{}/organizations/{}/overage_spend_limit", Self::BASE_URL, @@ -734,7 +529,7 @@ impl ClaudeWebApiFetcher { /// Convert a usage window to a RateWindow fn to_rate_window( &self, - window: &UsageWindow, + window: &ClaudeUsageWindow, window_minutes: Option, scale: UtilizationScale, ) -> RateWindow { @@ -779,27 +574,6 @@ impl Default for ClaudeWebApiFetcher { } } -impl UsageResponse { - /// Decide the utilization unit from every window this response carries. - fn utilization_scale(&self) -> UtilizationScale { - UtilizationScale::detect( - [ - self.five_hour.as_ref(), - self.seven_day.as_ref(), - self.seven_day_opus.as_ref(), - self.seven_day_sonnet.as_ref(), - self.seven_day_oauth_apps.as_ref(), - self.seven_day_design.as_ref(), - self.seven_day_promotional.as_ref(), - self.seven_day_routines.as_ref(), - ] - .into_iter() - .flatten() - .filter_map(|window| window.utilization), - ) - } -} - fn cookie_value(cookie_header: &str, name: &str) -> Option { cookie_header.split(';').find_map(|part| { let (key, value) = part.trim().split_once('=')?; @@ -818,8 +592,8 @@ fn cookie_value(cookie_header: &str, name: &str) -> Option { #[cfg(test)] mod tests { use super::{ - AccountResponse, ClaudeWebApiFetcher, UsageWindow, UtilizationScale, - claude_desktop_data_dirs_from, cookie_value, + AccountResponse, ClaudeUsageResponse, ClaudeUsageWindow, ClaudeWebApiFetcher, + UtilizationScale, claude_desktop_data_dirs_from, cookie_value, }; use reqwest::header; use std::path::PathBuf; @@ -832,7 +606,7 @@ mod tests { #[test] fn converts_fractional_utilization_to_percent() { - let window = UsageWindow { + let window = ClaudeUsageWindow { utilization: Some(0.23), resets_at: None, }; @@ -848,7 +622,7 @@ mod tests { #[test] fn preserves_existing_percentage_utilization() { - let window = UsageWindow { + let window = ClaudeUsageWindow { utilization: Some(23.0), resets_at: None, }; @@ -998,7 +772,7 @@ mod tests { #[test] fn parses_extra_design_and_routines_aliases() { - let usage: super::UsageResponse = serde_json::from_str( + let usage: ClaudeUsageResponse = serde_json::from_str( r#"{ "five_hour": { "utilization": 0.1 }, "seven_day_design": { "utilization": 31 }, @@ -1032,7 +806,7 @@ mod tests { #[test] fn maps_scoped_weekly_limits_even_when_inactive() { - let usage: super::UsageResponse = serde_json::from_str( + let usage: ClaudeUsageResponse = serde_json::from_str( r#"{ "limits": [{ "kind": "weekly_scoped", @@ -1054,7 +828,7 @@ mod tests { #[test] fn parses_duplicate_design_and_routines_aliases_with_preferred_key() { - let usage: super::UsageResponse = serde_json::from_str( + let usage: ClaudeUsageResponse = serde_json::from_str( r#"{ "seven_day_design": { "utilization": 31 }, "seven_day_omelette": { "utilization": 26 }, @@ -1082,7 +856,7 @@ mod tests { #[test] fn parses_oauth_apps_window_and_embedded_extra_usage() { - let usage: super::UsageResponse = serde_json::from_str( + let usage: ClaudeUsageResponse = serde_json::from_str( r#"{ "five_hour": { "utilization": 0.1 }, "seven_day_oauth_apps": { "utilization": 42 }, @@ -1106,7 +880,7 @@ mod tests { assert!((oauth_apps.used_percent - 42.0).abs() < f64::EPSILON); assert_eq!(extra.is_enabled, Some(true)); - assert_eq!(extra.monthly_credit_limit, Some(2000.0)); + assert_eq!(extra.monthly_limit, Some(2000.0)); assert_eq!(extra.used_credits, Some(550.0)); } }