66import { Disposable } from '../../../util/vs/base/common/lifecycle' ;
77import { IAuthenticationService } from '../../authentication/common/authentication' ;
88import { IHeaders } from '../../networking/common/fetcherService' ;
9- import { CopilotUserQuotaInfo , IChatQuota , IChatQuotaService , QuotaSnapshots } from './chatQuotaService' ;
9+ import { CopilotUserQuotaInfo , IChatQuota , IChatQuotaService , IRateLimitWarning , QuotaSnapshots } from './chatQuotaService' ;
1010
1111export class ChatQuotaService extends Disposable implements IChatQuotaService {
1212 declare readonly _serviceBrand : undefined ;
13+ private static readonly _RATE_LIMIT_THRESHOLDS = [ 50 , 75 , 90 , 95 ] ;
1314 private _quotaInfo : IChatQuota | undefined ;
15+ private _rateLimitInfo : { session : IChatQuota | undefined ; weekly : IChatQuota | undefined } ;
16+ private readonly _shownSessionThresholds = new Set < number > ( ) ;
17+ private readonly _shownWeeklyThresholds = new Set < number > ( ) ;
18+ private _pendingRateLimitWarning : IRateLimitWarning | undefined ;
1419
1520 constructor ( @IAuthenticationService private readonly _authService : IAuthenticationService ) {
1621 super ( ) ;
22+ this . _rateLimitInfo = { session : undefined , weekly : undefined } ;
1723 this . _register ( this . _authService . onDidAuthenticationChange ( ( ) => {
1824 this . processUserInfoQuotaSnapshot ( this . _authService . copilotToken ?. quotaInfo ) ;
1925 } ) ) ;
@@ -23,7 +29,7 @@ export class ChatQuotaService extends Disposable implements IChatQuotaService {
2329 if ( ! this . _quotaInfo ) {
2430 return false ;
2531 }
26- return this . _quotaInfo . used >= this . _quotaInfo . quota && ! this . _quotaInfo . overageEnabled && ! this . _quotaInfo . unlimited ;
32+ return this . _quotaInfo . percentRemaining <= 0 && ! this . _quotaInfo . overageEnabled && ! this . _quotaInfo . unlimited ;
2733 }
2834
2935 get overagesEnabled ( ) : boolean {
@@ -37,15 +43,10 @@ export class ChatQuotaService extends Disposable implements IChatQuotaService {
3743 this . _quotaInfo = undefined ;
3844 }
3945
40- processQuotaHeaders ( headers : IHeaders ) : void {
41- const quotaHeader = this . _authService . copilotToken ?. isFreeUser ? headers . get ( 'x-quota-snapshot-chat' ) : headers . get ( 'x-quota-snapshot-premium_models' ) || headers . get ( 'x-quota-snapshot-premium_interactions' ) ;
42- if ( ! quotaHeader ) {
43- return ;
44- }
45-
46+ private _processHeaderValue ( header : string ) : IChatQuota | undefined {
4647 try {
4748 // Parse URL encoded string into key-value pairs
48- const params = new URLSearchParams ( quotaHeader ) ;
49+ const params = new URLSearchParams ( header ) ;
4950
5051 // Extract values with fallbacks to ensure type safety
5152 const entitlement = parseInt ( params . get ( 'ent' ) || '0' , 10 ) ;
@@ -63,21 +64,38 @@ export class ChatQuotaService extends Disposable implements IChatQuotaService {
6364 resetDate . setMonth ( resetDate . getMonth ( ) + 1 ) ;
6465 }
6566
66- // Calculate used based on entitlement and remaining
67- const used = Math . max ( 0 , entitlement * ( 1 - percentRemaining / 100 ) ) ;
68-
69- // Update quota info
70- this . _quotaInfo = {
67+ return {
7168 quota : entitlement ,
7269 unlimited : entitlement === - 1 ,
73- used ,
70+ percentRemaining ,
7471 overageUsed,
7572 overageEnabled,
7673 resetDate
7774 } ;
7875 } catch ( error ) {
7976 console . error ( 'Failed to parse quota header' , error ) ;
77+ return undefined ;
78+ }
79+ }
80+
81+
82+ processQuotaHeaders ( headers : IHeaders ) : void {
83+ const quotaHeader = this . _authService . copilotToken ?. isFreeUser ? headers . get ( 'x-quota-snapshot-chat' ) : headers . get ( 'x-quota-snapshot-premium_models' ) || headers . get ( 'x-quota-snapshot-premium_interactions' ) ;
84+ if ( ! quotaHeader ) {
85+ return ;
86+ }
87+ const quotaInfo = this . _processHeaderValue ( quotaHeader ) ;
88+ if ( ! quotaInfo ) {
89+ return ;
8090 }
91+ this . _quotaInfo = quotaInfo ;
92+ const sessionRateLimitHeader = headers . get ( 'x-usage-ratelimit-session' ) ;
93+ const weeklyRateLimitHeader = headers . get ( 'x-usage-ratelimit-weekly' ) ;
94+ this . _rateLimitInfo . session = sessionRateLimitHeader ? this . _processHeaderValue ( sessionRateLimitHeader ) : undefined ;
95+ this . _rateLimitInfo . weekly = weeklyRateLimitHeader ? this . _processHeaderValue ( weeklyRateLimitHeader ) : undefined ;
96+ this . _clearStaleThresholds ( this . _rateLimitInfo . session , this . _shownSessionThresholds ) ;
97+ this . _clearStaleThresholds ( this . _rateLimitInfo . weekly , this . _shownWeeklyThresholds ) ;
98+ this . _pendingRateLimitWarning = this . _computeRateLimitWarning ( ) ?? this . _pendingRateLimitWarning ;
8199 }
82100
83101 processQuotaSnapshots ( snapshots : QuotaSnapshots ) : void {
@@ -91,12 +109,11 @@ export class ChatQuotaService extends Disposable implements IChatQuotaService {
91109 try {
92110 const entitlement = parseInt ( snapshot . entitlement , 10 ) ;
93111 const resetDate = snapshot . reset_date ? new Date ( snapshot . reset_date ) : ( ( ) => { const d = new Date ( ) ; d . setMonth ( d . getMonth ( ) + 1 ) ; return d ; } ) ( ) ;
94- const used = Math . max ( 0 , entitlement * ( 1 - snapshot . percent_remaining / 100 ) ) ;
95112
96113 this . _quotaInfo = {
97114 quota : entitlement ,
98115 unlimited : entitlement === - 1 ,
99- used ,
116+ percentRemaining : snapshot . percent_remaining ,
100117 overageUsed : snapshot . overage_count ,
101118 overageEnabled : snapshot . overage_permitted ,
102119 resetDate
@@ -106,6 +123,53 @@ export class ChatQuotaService extends Disposable implements IChatQuotaService {
106123 }
107124 }
108125
126+ consumeRateLimitWarning ( ) : IRateLimitWarning | undefined {
127+ const warning = this . _pendingRateLimitWarning ;
128+ this . _pendingRateLimitWarning = undefined ;
129+ return warning ;
130+ }
131+
132+ private _computeRateLimitWarning ( ) : IRateLimitWarning | undefined {
133+ // Session rate limit takes priority over weekly
134+ const sessionWarning = this . _checkThreshold ( this . _rateLimitInfo . session , this . _shownSessionThresholds , 'session' ) ;
135+ if ( sessionWarning ) {
136+ return sessionWarning ;
137+ }
138+ return this . _checkThreshold ( this . _rateLimitInfo . weekly , this . _shownWeeklyThresholds , 'weekly' ) ;
139+ }
140+
141+ private _clearStaleThresholds ( info : IChatQuota | undefined , shownThresholds : Set < number > ) : void {
142+ if ( ! info ) {
143+ shownThresholds . clear ( ) ;
144+ return ;
145+ }
146+ const percentUsed = 100 - info . percentRemaining ;
147+ for ( const threshold of shownThresholds ) {
148+ if ( percentUsed < threshold ) {
149+ shownThresholds . delete ( threshold ) ;
150+ }
151+ }
152+ }
153+
154+ private _checkThreshold ( info : IChatQuota | undefined , shownThresholds : Set < number > , type : 'session' | 'weekly' ) : IRateLimitWarning | undefined {
155+ if ( ! info || info . unlimited ) {
156+ return undefined ;
157+ }
158+ const percentUsed = 100 - info . percentRemaining ;
159+ // Walk thresholds highest-first so we report the most severe crossed threshold
160+ for ( let i = ChatQuotaService . _RATE_LIMIT_THRESHOLDS . length - 1 ; i >= 0 ; i -- ) {
161+ const threshold = ChatQuotaService . _RATE_LIMIT_THRESHOLDS [ i ] ;
162+ if ( percentUsed >= threshold && ! shownThresholds . has ( threshold ) ) {
163+ // Mark this and all lower thresholds as shown
164+ for ( let j = 0 ; j <= i ; j ++ ) {
165+ shownThresholds . add ( ChatQuotaService . _RATE_LIMIT_THRESHOLDS [ j ] ) ;
166+ }
167+ return { percentUsed : Math . round ( percentUsed ) , type, resetDate : info . resetDate } ;
168+ }
169+ }
170+ return undefined ;
171+ }
172+
109173 private processUserInfoQuotaSnapshot ( quotaInfo : CopilotUserQuotaInfo | undefined ) {
110174 if ( ! quotaInfo || ! quotaInfo . quota_snapshots || ! quotaInfo . quota_reset_date ) {
111175 return ;
@@ -116,7 +180,7 @@ export class ChatQuotaService extends Disposable implements IChatQuotaService {
116180 overageUsed : quotaInfo . quota_snapshots . premium_interactions . overage_count ,
117181 quota : quotaInfo . quota_snapshots . premium_interactions . entitlement ,
118182 resetDate : new Date ( quotaInfo . quota_reset_date ) ,
119- used : Math . max ( 0 , quotaInfo . quota_snapshots . premium_interactions . entitlement * ( 1 - quotaInfo . quota_snapshots . premium_interactions . percent_remaining / 100 ) ) ,
183+ percentRemaining : quotaInfo . quota_snapshots . premium_interactions . percent_remaining ,
120184 } ;
121185 }
122- }
186+ }
0 commit comments