@@ -31,90 +31,97 @@ const PLAN_MAP = {
3131 */
3232async function fetchUsage ( token ) {
3333 const controller = new AbortController ( ) ;
34+ // Keep the timer alive across both fetch() (headers) and res.json() (body)
35+ // so a stalled body can also be aborted. Cleared in finally below.
3436 const timeout = setTimeout ( ( ) => controller . abort ( ) , 15_000 ) ;
35- let res ;
3637 try {
37- res = await fetch ( "https://api.github.com/copilot_internal/user" , {
38- signal : controller . signal ,
39- headers : {
40- Authorization : `Bearer ${ token } ` ,
41- Accept : "application/json" ,
42- "User-Agent" : `vscode-github-copilot-usage/ ${ version } ` ,
43- } ,
44- } ) ;
45- } catch ( e ) {
46- clearTimeout ( timeout ) ;
47- const isTimeout = e ?. name === "AbortError" ;
48- throw makeError (
49- isTimeout ? "TIMEOUT" : "NETWORK_ERROR" ,
50- isTimeout ? "Request timed out " : "Network error " ,
51- ) ;
52- }
53- clearTimeout ( timeout ) ;
38+ let res ;
39+ try {
40+ res = await fetch ( "https://api.github.com/copilot_internal/user" , {
41+ signal : controller . signal ,
42+ headers : {
43+ Authorization : `Bearer ${ token } ` ,
44+ Accept : "application/json" ,
45+ "User-Agent" : `vscode-github-copilot-usage/ ${ version } ` ,
46+ } ,
47+ } ) ;
48+ } catch ( e ) {
49+ const isTimeout = e ?. name === "AbortError" ;
50+ throw makeError (
51+ isTimeout ? "TIMEOUT " : "NETWORK_ERROR " ,
52+ isTimeout ? "Request timed out" : "Network error" ,
53+ ) ;
54+ }
5455
55- if ( res . status === 401 ) {
56- throw makeError ( "AUTH" , "Not signed in (401)" ) ;
57- }
58- if ( res . status === 403 ) {
59- throw makeError ( "FORBIDDEN" , `Forbidden (403)` ) ;
60- }
56+ if ( res . status === 401 ) {
57+ throw makeError ( "AUTH" , "Not signed in (401)" ) ;
58+ }
59+ if ( res . status === 403 ) {
60+ throw makeError ( "FORBIDDEN" , `Forbidden (403)` ) ;
61+ }
6162
62- if ( res . status === 429 ) {
63- throw makeError ( "RATE_LIMIT" , "Rate limited" ) ;
64- }
63+ if ( res . status === 429 ) {
64+ throw makeError ( "RATE_LIMIT" , "Rate limited" ) ;
65+ }
6566
66- if ( ! res . ok ) {
67- throw makeError ( res . status >= 500 ? "SERVER_ERROR" : "API_ERROR" , `API error: ${ res . status } ` ) ;
68- }
67+ if ( ! res . ok ) {
68+ throw makeError ( res . status >= 500 ? "SERVER_ERROR" : "API_ERROR" , `API error: ${ res . status } ` ) ;
69+ }
6970
70- let data ;
71- try {
72- data = await res . json ( ) ;
73- } catch {
74- throw makeError ( "API_ERROR" , "Invalid JSON from GitHub API" ) ;
75- }
76- const plan = PLAN_MAP [ data . copilot_plan ] ?? data . copilot_plan ?? "Unknown" ;
71+ let data ;
72+ try {
73+ data = await res . json ( ) ;
74+ } catch ( e ) {
75+ if ( e ?. name === "AbortError" ) {
76+ throw makeError ( "TIMEOUT" , "Request timed out" ) ;
77+ }
78+ throw makeError ( "API_ERROR" , "Invalid JSON from GitHub API" ) ;
79+ }
80+ const plan = PLAN_MAP [ data . copilot_plan ] ?? data . copilot_plan ?? "Unknown" ;
81+
82+ const pi = data ?. quota_snapshots ?. premium_interactions ;
83+ if ( ! pi || pi . percent_remaining == null ) {
84+ const unlimited = ! ! pi ?. unlimited ;
85+ return {
86+ used : 0 ,
87+ quota : pi ?. entitlement ?? 0 ,
88+ usedPct : 0 ,
89+ unlimited,
90+ noData : ! unlimited ,
91+ overageEnabled : ! ! pi ?. overage_permitted ,
92+ overageUsed : pi ?. overage_count ?? 0 ,
93+ plan,
94+ resetDate : getNextMonthReset ( ) ,
95+ } ;
96+ }
97+
98+ const entitlement = pi . entitlement ?? 0 ;
99+ const percentRemaining = Number ( pi . percent_remaining ) ;
100+ if ( ! Number . isFinite ( percentRemaining ) ) {
101+ throw makeError ( "API_ERROR" , "Invalid percent_remaining from GitHub API" ) ;
102+ }
103+ const usedPct = Math . max ( 0 , Math . round ( ( 100 - percentRemaining ) * 10 ) / 10 ) ;
104+ const used =
105+ entitlement > 0 ? Math . max ( 0 , Math . round ( ( entitlement * ( 100 - percentRemaining ) ) / 100 ) ) : 0 ;
106+
107+ const rawResetDate = data . quota_reset_date ? new Date ( data . quota_reset_date ) : null ;
108+ const resetDate =
109+ rawResetDate && ! isNaN ( rawResetDate . getTime ( ) ) ? rawResetDate : getNextMonthReset ( ) ;
77110
78- const pi = data ?. quota_snapshots ?. premium_interactions ;
79- if ( ! pi || pi . percent_remaining == null ) {
80- const unlimited = ! ! pi ?. unlimited ;
81111 return {
82- used : 0 ,
83- quota : pi ?. entitlement ?? 0 ,
84- usedPct : 0 ,
85- unlimited,
86- noData : ! unlimited ,
87- overageEnabled : ! ! pi ? .overage_permitted ,
88- overageUsed : pi ? .overage_count ?? 0 ,
112+ used,
113+ quota : entitlement ,
114+ usedPct,
115+ unlimited : ! ! pi . unlimited ,
116+ noData : false ,
117+ overageEnabled : ! ! pi . overage_permitted ,
118+ overageUsed : pi . overage_count ?? 0 ,
89119 plan,
90- resetDate : getNextMonthReset ( ) ,
120+ resetDate,
91121 } ;
122+ } finally {
123+ clearTimeout ( timeout ) ;
92124 }
93-
94- const entitlement = pi . entitlement ?? 0 ;
95- const percentRemaining = Number ( pi . percent_remaining ) ;
96- if ( ! Number . isFinite ( percentRemaining ) ) {
97- throw makeError ( "API_ERROR" , "Invalid percent_remaining from GitHub API" ) ;
98- }
99- const usedPct = Math . max ( 0 , Math . round ( ( 100 - percentRemaining ) * 10 ) / 10 ) ;
100- const used =
101- entitlement > 0 ? Math . max ( 0 , Math . round ( ( entitlement * ( 100 - percentRemaining ) ) / 100 ) ) : 0 ;
102-
103- const rawResetDate = data . quota_reset_date ? new Date ( data . quota_reset_date ) : null ;
104- const resetDate =
105- rawResetDate && ! isNaN ( rawResetDate . getTime ( ) ) ? rawResetDate : getNextMonthReset ( ) ;
106-
107- return {
108- used,
109- quota : entitlement ,
110- usedPct,
111- unlimited : ! ! pi . unlimited ,
112- noData : false ,
113- overageEnabled : ! ! pi . overage_permitted ,
114- overageUsed : pi . overage_count ?? 0 ,
115- plan,
116- resetDate,
117- } ;
118125}
119126
120127function getNextMonthReset ( ) {
0 commit comments