@@ -7,6 +7,12 @@ use serde::Deserialize;
77use crate :: models:: { UsageData , UsageSection } ;
88
99const USAGE_URL : & str = "https://api.anthropic.com/api/oauth/usage" ;
10+ const MESSAGES_URL : & str = "https://api.anthropic.com/v1/messages" ;
11+
12+ const MODEL_FALLBACK_CHAIN : & [ & str ] = & [
13+ "claude-3-haiku-20240307" ,
14+ "claude-haiku-4-5-20251001" ,
15+ ] ;
1016
1117#[ derive( Debug ) ]
1218pub enum PollError {
@@ -46,7 +52,7 @@ pub fn poll() -> Result<UsageData, PollError> {
4652 }
4753 }
4854
49- fetch_usage ( & creds. access_token )
55+ fetch_usage_with_fallback ( & creds. access_token )
5056}
5157
5258/// Invoke the Claude CLI with a minimal prompt to force its internal
@@ -105,22 +111,51 @@ fn resolve_claude_path() -> String {
105111 "claude.cmd" . to_string ( )
106112}
107113
108- fn fetch_usage ( token : & str ) -> Result < UsageData , PollError > {
114+ fn build_agent ( ) -> Result < ureq :: Agent , PollError > {
109115 let tls = native_tls:: TlsConnector :: new ( ) . map_err ( |_| PollError :: RequestFailed ) ?;
110- let agent = ureq:: AgentBuilder :: new ( )
116+ Ok ( ureq:: AgentBuilder :: new ( )
111117 . timeout ( Duration :: from_secs ( 30 ) )
112118 . tls_connector ( std:: sync:: Arc :: new ( tls) )
113- . build ( ) ;
119+ . build ( ) )
120+ }
121+
122+ fn fetch_usage_with_fallback ( token : & str ) -> Result < UsageData , PollError > {
123+ // Try the dedicated usage endpoint first
124+ if let Some ( data) = try_usage_endpoint ( token) {
125+ // If reset timers are missing, fill them in from the Messages API
126+ if data. session . resets_at . is_none ( ) || data. weekly . resets_at . is_none ( ) {
127+ if let Ok ( fallback) = fetch_usage_via_messages ( token) {
128+ let mut merged = data;
129+ if merged. session . resets_at . is_none ( ) {
130+ merged. session . resets_at = fallback. session . resets_at ;
131+ }
132+ if merged. weekly . resets_at . is_none ( ) {
133+ merged. weekly . resets_at = fallback. weekly . resets_at ;
134+ }
135+ return Ok ( merged) ;
136+ }
137+ }
138+ return Ok ( data) ;
139+ }
140+
141+ // Fall back to Messages API with rate limit headers
142+ fetch_usage_via_messages ( token)
143+ }
114144
115- let response: UsageResponse = agent
145+ fn try_usage_endpoint ( token : & str ) -> Option < UsageData > {
146+ let agent = build_agent ( ) . ok ( ) ?;
147+
148+ let resp = match agent
116149 . get ( USAGE_URL )
117150 . set ( "Authorization" , & format ! ( "Bearer {token}" ) )
118151 . set ( "anthropic-beta" , "oauth-2025-04-20" )
119152 . call ( )
120- . map_err ( |_| PollError :: RequestFailed ) ?
121- . into_json ( )
122- . map_err ( |_| PollError :: RequestFailed ) ?;
153+ {
154+ Ok ( resp) => resp,
155+ _ => return None ,
156+ } ;
123157
158+ let response: UsageResponse = resp. into_json ( ) . ok ( ) ?;
124159 let mut data = UsageData :: default ( ) ;
125160
126161 if let Some ( bucket) = & response. five_hour {
@@ -133,7 +168,92 @@ fn fetch_usage(token: &str) -> Result<UsageData, PollError> {
133168 data. weekly . resets_at = parse_iso8601 ( bucket. resets_at . as_deref ( ) ) ;
134169 }
135170
136- Ok ( data)
171+ Some ( data)
172+ }
173+
174+ fn fetch_usage_via_messages ( token : & str ) -> Result < UsageData , PollError > {
175+ let agent = build_agent ( ) ?;
176+
177+ for model in MODEL_FALLBACK_CHAIN {
178+ let body = serde_json:: json!( {
179+ "model" : model,
180+ "max_tokens" : 1 ,
181+ "messages" : [ { "role" : "user" , "content" : "." } ]
182+ } ) ;
183+
184+ let response = match agent
185+ . post ( MESSAGES_URL )
186+ . set ( "Authorization" , & format ! ( "Bearer {token}" ) )
187+ . set ( "anthropic-version" , "2023-06-01" )
188+ . set ( "anthropic-beta" , "oauth-2025-04-20" )
189+ . send_json ( & body)
190+ {
191+ Ok ( resp) => resp,
192+ Err ( ureq:: Error :: Status ( _code, resp) ) => resp,
193+ Err ( _) => continue ,
194+ } ;
195+
196+ let h5 = response. header ( "anthropic-ratelimit-unified-5h-utilization" ) ;
197+ let h7 = response. header ( "anthropic-ratelimit-unified-7d-utilization" ) ;
198+ let hs = response. header ( "anthropic-ratelimit-unified-status" ) ;
199+
200+ if h5. is_some ( ) || h7. is_some ( ) || hs. is_some ( ) {
201+ return Ok ( parse_rate_limit_headers ( & response) ) ;
202+ }
203+ }
204+
205+ Err ( PollError :: RequestFailed )
206+ }
207+
208+ fn parse_rate_limit_headers ( response : & ureq:: Response ) -> UsageData {
209+ let mut data = UsageData :: default ( ) ;
210+
211+ data. session . percentage = get_header_f64 ( response, "anthropic-ratelimit-unified-5h-utilization" ) * 100.0 ;
212+ data. session . resets_at = unix_to_system_time ( get_header_i64 ( response, "anthropic-ratelimit-unified-5h-reset" ) ) ;
213+
214+ data. weekly . percentage = get_header_f64 ( response, "anthropic-ratelimit-unified-7d-utilization" ) * 100.0 ;
215+ data. weekly . resets_at = unix_to_system_time ( get_header_i64 ( response, "anthropic-ratelimit-unified-7d-reset" ) ) ;
216+
217+ let overall_reset = get_header_i64 ( response, "anthropic-ratelimit-unified-reset" ) ;
218+
219+ if data. session . percentage == 0.0 && data. weekly . percentage == 0.0 {
220+ let status = response. header ( "anthropic-ratelimit-unified-status" ) ;
221+ if status == Some ( "rejected" ) {
222+ let claim = response. header ( "anthropic-ratelimit-unified-representative-claim" ) ;
223+ match claim {
224+ Some ( "five_hour" ) => data. session . percentage = 100.0 ,
225+ Some ( "seven_day" ) => data. weekly . percentage = 100.0 ,
226+ _ => { }
227+ }
228+ }
229+
230+ if data. session . resets_at . is_none ( ) && overall_reset. is_some ( ) {
231+ data. session . resets_at = unix_to_system_time ( overall_reset) ;
232+ }
233+ }
234+
235+ data
236+ }
237+
238+ fn get_header_f64 ( response : & ureq:: Response , name : & str ) -> f64 {
239+ response
240+ . header ( name)
241+ . and_then ( |s| s. parse :: < f64 > ( ) . ok ( ) )
242+ . unwrap_or ( 0.0 )
243+ }
244+
245+ fn get_header_i64 ( response : & ureq:: Response , name : & str ) -> Option < i64 > {
246+ response
247+ . header ( name)
248+ . and_then ( |s| s. parse :: < i64 > ( ) . ok ( ) )
249+ }
250+
251+ fn unix_to_system_time ( unix_secs : Option < i64 > ) -> Option < SystemTime > {
252+ let secs = unix_secs?;
253+ if secs < 0 {
254+ return None ;
255+ }
256+ Some ( UNIX_EPOCH + Duration :: from_secs ( secs as u64 ) )
137257}
138258
139259struct Credentials {
0 commit comments