Skip to content

Commit 8874c74

Browse files
[cherry-pick] Handle showing weekly and session rate limit data (#310857)
Co-authored-by: vs-code-engineering[bot] <vs-code-engineering[bot]@users.noreply.github.com>
1 parent f89829d commit 8874c74

4 files changed

Lines changed: 116 additions & 22 deletions

File tree

extensions/copilot/src/extension/conversation/vscode-node/chatParticipants.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,29 @@ Learn more about [GitHub Copilot](https://docs.github.com/copilot/using-github-c
268268

269269
return result;
270270
} finally {
271+
const rateLimitWarning = this._chatQuotaService.consumeRateLimitWarning();
272+
if (rateLimitWarning) {
273+
const resetDate = rateLimitWarning.resetDate;
274+
const now = new Date();
275+
const includeYear = resetDate.getFullYear() !== now.getFullYear();
276+
const dateStr = new Intl.DateTimeFormat(undefined, includeYear
277+
? { month: 'long', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' }
278+
: { month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit' }
279+
).format(resetDate);
280+
stream.warning(new vscode.MarkdownString(
281+
rateLimitWarning.type === 'session'
282+
? vscode.l10n.t({
283+
message: "You've used {0}% of your session rate limit. Your session rate limit will reset on {1}. [Learn More]({2})",
284+
args: [rateLimitWarning.percentUsed, dateStr, 'https://aka.ms/github-copilot-rate-limit-error'],
285+
comment: [`{Locked=']({'}`]
286+
})
287+
: vscode.l10n.t({
288+
message: "You've used {0}% of your weekly rate limit. Your weekly rate limit will reset on {1}. [Learn More]({2})",
289+
args: [rateLimitWarning.percentUsed, dateStr, 'https://aka.ms/github-copilot-rate-limit-error'],
290+
comment: [`{Locked=']({'}`]
291+
})
292+
));
293+
}
271294
markChatExt(request.sessionId, ChatExtPerfMark.DidHandleParticipant);
272295
clearChatExtMarks(request.sessionId);
273296
}

extensions/copilot/src/platform/chat/common/chatQuotaService.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export interface CopilotUserQuotaInfo {
4545

4646
export interface IChatQuota {
4747
quota: number;
48-
used: number;
48+
percentRemaining: number;
4949
unlimited: boolean;
5050
overageUsed: number;
5151
overageEnabled: boolean;
@@ -67,13 +67,20 @@ export interface QuotaSnapshot {
6767

6868
export type QuotaSnapshots = Record<string, QuotaSnapshot>;
6969

70+
export interface IRateLimitWarning {
71+
percentUsed: number;
72+
type: 'session' | 'weekly';
73+
resetDate: Date;
74+
}
75+
7076
export interface IChatQuotaService {
7177
readonly _serviceBrand: undefined;
7278
quotaExhausted: boolean;
7379
overagesEnabled: boolean;
7480
processQuotaHeaders(headers: IHeaders): void;
7581
processQuotaSnapshots(snapshots: QuotaSnapshots): void;
7682
clearQuota(): void;
83+
consumeRateLimitWarning(): IRateLimitWarning | undefined;
7784
}
7885

7986
export const IChatQuotaService = createServiceIdentifier<IChatQuotaService>('IChatQuotaService');

extensions/copilot/src/platform/chat/common/chatQuotaServiceImpl.ts

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@
66
import { Disposable } from '../../../util/vs/base/common/lifecycle';
77
import { IAuthenticationService } from '../../authentication/common/authentication';
88
import { IHeaders } from '../../networking/common/fetcherService';
9-
import { CopilotUserQuotaInfo, IChatQuota, IChatQuotaService, QuotaSnapshots } from './chatQuotaService';
9+
import { CopilotUserQuotaInfo, IChatQuota, IChatQuotaService, IRateLimitWarning, QuotaSnapshots } from './chatQuotaService';
1010

1111
export 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+
}

extensions/copilot/src/platform/chat/common/commonTypes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,14 @@ function getRateLimitMessage(fetchResult: ChatFetchError, copilotPlan: string |
227227
if (fetchResult.capiError?.code?.startsWith('user_global_rate_limited')) {
228228
if (copilotPlan === 'free' || copilotPlan === 'individual' || copilotPlan === 'individual_pro') {
229229
return l10n.t({
230-
message: 'You\'ve hit your global rate limit. Please upgrade your plan or wait {0} for your limit to reset. [Learn More]({1})',
230+
message: 'You\'ve hit your session rate limit. Please upgrade your plan or wait {0} for your limit to reset. [Learn More]({1})',
231231
args: [retryAfterString, 'https://aka.ms/github-copilot-rate-limit-error'],
232232
comment: [`{Locked=']({'}`]
233233
});
234234
}
235235

236236
return l10n.t({
237-
message: 'You\'ve hit your global rate limit. Please wait {0} for your limit to reset. [Learn More]({1})',
237+
message: 'You\'ve hit your session rate limit. Please wait {0} for your limit to reset. [Learn More]({1})',
238238
args: [retryAfterString, 'https://aka.ms/github-copilot-rate-limit-error'],
239239
comment: [`{Locked=']({'}`]
240240
});

0 commit comments

Comments
 (0)