Skip to content

Commit 63127a7

Browse files
authored
feat: [New billing] Add warning notification for copilot usage limit. (#1621)
1 parent 9b23aa0 commit 63127a7

17 files changed

Lines changed: 522 additions & 7 deletions

File tree

com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClientTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
import static org.mockito.Mockito.verify;
88
import static org.mockito.Mockito.when;
99

10+
import java.lang.reflect.Field;
1011
import java.util.HashMap;
1112
import java.util.Map;
1213
import java.util.concurrent.CompletableFuture;
1314
import java.util.concurrent.ExecutionException;
1415

1516
import org.eclipse.core.resources.IFile;
17+
import org.eclipse.e4.core.services.events.IEventBroker;
1618
import org.junit.jupiter.api.BeforeEach;
1719
import org.junit.jupiter.api.Test;
1820
import org.junit.jupiter.api.extension.ExtendWith;
@@ -25,10 +27,12 @@
2527
import com.microsoft.copilot.eclipse.core.FeatureFlags;
2628
import com.microsoft.copilot.eclipse.core.chat.service.IChatServiceManager;
2729
import com.microsoft.copilot.eclipse.core.chat.service.IReferencedFileService;
30+
import com.microsoft.copilot.eclipse.core.events.CopilotEventConstants;
2831
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationCapabilities;
2932
import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationContextParams;
3033
import com.microsoft.copilot.eclipse.core.lsp.protocol.CurrentEditorContext;
3134
import com.microsoft.copilot.eclipse.core.lsp.protocol.DidChangeFeatureFlagsParams;
35+
import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.QuotaWarningNotification;
3236
import com.microsoft.copilot.eclipse.core.utils.FileUtils;
3337

3438
@ExtendWith(MockitoExtension.class)
@@ -45,6 +49,9 @@ class CopilotLanguageClientTests {
4549
@Mock
4650
private IReferencedFileService fileService;
4751

52+
@Mock
53+
private IEventBroker eventBroker;
54+
4855
@BeforeEach
4956
void setUp() {
5057
client = new CopilotLanguageClient();
@@ -128,4 +135,24 @@ void testOnDidChangeFeatureFlagsWithEmptyFeatureFlags() {
128135
verify(mockFeatureFlags).setByokEnabled(true);
129136
}
130137
}
138+
139+
@Test
140+
void testOnQuotaWarning_PostsNotificationToEventBroker() {
141+
QuotaWarningNotification notification = new QuotaWarningNotification("Approaching quota", 90.0);
142+
setEventBroker(eventBroker);
143+
144+
client.onQuotaWarning(notification);
145+
146+
verify(eventBroker).post(CopilotEventConstants.TOPIC_QUOTA_WARNING, notification);
147+
}
148+
149+
private void setEventBroker(IEventBroker broker) {
150+
try {
151+
Field field = CopilotLanguageClient.class.getDeclaredField("eventBroker");
152+
field.setAccessible(true);
153+
field.set(client, broker);
154+
} catch (ReflectiveOperationException e) {
155+
throw new AssertionError("Failed to inject event broker", e);
156+
}
157+
}
131158
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/events/CopilotEventConstants.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ public class CopilotEventConstants {
2727
*/
2828
private static final String TOPIC_MCP = TOPIC_BASE + "MCP/";
2929

30+
/**
31+
* Topic for quota events.
32+
*/
33+
private static final String TOPIC_QUOTA = TOPIC_BASE + "QUOTA/";
34+
3035
/**
3136
* Topic for Next Edit Suggestion (NES) events.
3237
*/
@@ -152,4 +157,9 @@ public class CopilotEventConstants {
152157
* Event when NES suggestion is rejected.
153158
*/
154159
public static final String TOPIC_NES_REJECT_SUGGESTION = TOPIC_NES + "REJECT_SUGGESTION";
160+
161+
/**
162+
* Event when a quota warning notification is received from the language server.
163+
*/
164+
public static final String TOPIC_QUOTA_WARNING = TOPIC_QUOTA + "WARNING";
155165
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import com.microsoft.copilot.eclipse.core.lsp.protocol.codingagent.CodingAgentMessageRequestParams;
5656
import com.microsoft.copilot.eclipse.core.lsp.protocol.codingagent.CodingAgentMessageResult;
5757
import com.microsoft.copilot.eclipse.core.lsp.protocol.policy.DidChangePolicyParams;
58+
import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.QuotaWarningNotification;
5859
import com.microsoft.copilot.eclipse.core.utils.FileUtils;
5960
import com.microsoft.copilot.eclipse.core.utils.PlatformUtils;
6061

@@ -313,6 +314,17 @@ public CompletableFuture<CodingAgentMessageResult> onCodingAgentMessage(CodingAg
313314
return CompletableFuture.completedFuture(result);
314315
}
315316

317+
/**
318+
* Notify when a quota warning is received from the language server.
319+
*/
320+
@JsonNotification("copilot/quotaWarning")
321+
public void onQuotaWarning(QuotaWarningNotification notification) {
322+
CopilotCore.LOGGER.info("Quota warning received: " + notification);
323+
if (eventBroker != null) {
324+
eventBroker.post(CopilotEventConstants.TOPIC_QUOTA_WARNING, notification);
325+
}
326+
}
327+
316328
/**
317329
* Reads the contents and stats of a file given its URI.
318330
*/

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/quota/CheckQuotaResult.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public class CheckQuotaResult {
1111
private Quota chat;
1212
private Quota completions;
1313
private Quota premiumInteractions;
14+
private IntervalQuota immediateUsageInterval;
15+
private IntervalQuota extendedUsageInterval;
1416
private String resetDate;
1517
private CopilotPlan copilotPlan;
1618

@@ -38,6 +40,28 @@ public void setPremiumInteractionsQuota(Quota premiumInteractions) {
3840
this.premiumInteractions = premiumInteractions;
3941
}
4042

43+
/**
44+
* Gets the immediate usage interval quota (for individual plans).
45+
*/
46+
public IntervalQuota getImmediateUsageInterval() {
47+
return immediateUsageInterval;
48+
}
49+
50+
public void setImmediateUsageInterval(IntervalQuota immediateUsageInterval) {
51+
this.immediateUsageInterval = immediateUsageInterval;
52+
}
53+
54+
/**
55+
* Gets the extended usage interval quota (for individual plans).
56+
*/
57+
public IntervalQuota getExtendedUsageInterval() {
58+
return extendedUsageInterval;
59+
}
60+
61+
public void setExtendedUsageInterval(IntervalQuota extendedUsageInterval) {
62+
this.extendedUsageInterval = extendedUsageInterval;
63+
}
64+
4165
public String getResetDate() {
4266
return resetDate;
4367
}
@@ -56,7 +80,8 @@ public void setCopilotPlan(CopilotPlan copilotPlan) {
5680

5781
@Override
5882
public int hashCode() {
59-
return Objects.hash(chat, completions, copilotPlan, premiumInteractions, resetDate);
83+
return Objects.hash(chat, completions, copilotPlan, extendedUsageInterval,
84+
immediateUsageInterval, premiumInteractions, resetDate);
6085
}
6186

6287
@Override
@@ -72,7 +97,10 @@ public boolean equals(Object obj) {
7297
}
7398
CheckQuotaResult other = (CheckQuotaResult) obj;
7499
return Objects.equals(chat, other.chat) && Objects.equals(completions, other.completions)
75-
&& copilotPlan == other.copilotPlan && Objects.equals(premiumInteractions, other.premiumInteractions)
100+
&& copilotPlan == other.copilotPlan
101+
&& Objects.equals(extendedUsageInterval, other.extendedUsageInterval)
102+
&& Objects.equals(immediateUsageInterval, other.immediateUsageInterval)
103+
&& Objects.equals(premiumInteractions, other.premiumInteractions)
76104
&& Objects.equals(resetDate, other.resetDate);
77105
}
78106

@@ -82,6 +110,8 @@ public String toString() {
82110
builder.append("chat", chat);
83111
builder.append("completions", completions);
84112
builder.append("premiumInteractions", premiumInteractions);
113+
builder.append("immediateUsageInterval", immediateUsageInterval);
114+
builder.append("extendedUsageInterval", extendedUsageInterval);
85115
builder.append("resetDate", resetDate);
86116
builder.append("copilotPlan", copilotPlan);
87117
return builder.toString();

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/quota/CopilotPlan.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
* Enum representing the different Copilot plans.
55
*/
66
public enum CopilotPlan {
7-
free, individual, individual_pro, business, enterprise
7+
free, individual, individual_pro, individual_max, business, enterprise
88
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.microsoft.copilot.eclipse.core.lsp.protocol.quota;
2+
3+
/**
4+
* Interval-based quota information, used for immediateUsageInterval and extendedUsageInterval.
5+
*/
6+
public record IntervalQuota(
7+
double percentRemaining,
8+
boolean unlimited,
9+
boolean overagePermitted,
10+
Integer entitlement,
11+
Integer quotaRemaining,
12+
String timeStamp,
13+
String resetAt) {
14+
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/quota/Quota.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ public class Quota {
1111
private double percentRemaining;
1212
private boolean unlimited;
1313
private boolean overagePermitted;
14+
private Integer entitlement;
15+
private Integer quotaRemaining;
16+
private String timeStamp;
1417

1518
/**
1619
* Creates a new CompletionsQuota quota information with default values.
@@ -53,9 +56,42 @@ public void setOveragePermitted(boolean overagePermitted) {
5356
this.overagePermitted = overagePermitted;
5457
}
5558

59+
/**
60+
* Gets the total entitlement (quota limit).
61+
*/
62+
public Integer getEntitlement() {
63+
return entitlement;
64+
}
65+
66+
public void setEntitlement(Integer entitlement) {
67+
this.entitlement = entitlement;
68+
}
69+
70+
/**
71+
* Gets the remaining quota count.
72+
*/
73+
public Integer getQuotaRemaining() {
74+
return quotaRemaining;
75+
}
76+
77+
public void setQuotaRemaining(Integer quotaRemaining) {
78+
this.quotaRemaining = quotaRemaining;
79+
}
80+
81+
/**
82+
* Gets the timestamp of the quota snapshot.
83+
*/
84+
public String getTimeStamp() {
85+
return timeStamp;
86+
}
87+
88+
public void setTimeStamp(String timeStamp) {
89+
this.timeStamp = timeStamp;
90+
}
91+
5692
@Override
5793
public int hashCode() {
58-
return Objects.hash(overagePermitted, percentRemaining, unlimited);
94+
return Objects.hash(entitlement, overagePermitted, percentRemaining, quotaRemaining, timeStamp, unlimited);
5995
}
6096

6197
@Override
@@ -70,8 +106,9 @@ public boolean equals(Object obj) {
70106
return false;
71107
}
72108
Quota other = (Quota) obj;
73-
return overagePermitted == other.overagePermitted
109+
return Objects.equals(entitlement, other.entitlement) && overagePermitted == other.overagePermitted
74110
&& Double.doubleToLongBits(percentRemaining) == Double.doubleToLongBits(other.percentRemaining)
111+
&& Objects.equals(quotaRemaining, other.quotaRemaining) && Objects.equals(timeStamp, other.timeStamp)
75112
&& unlimited == other.unlimited;
76113
}
77114

@@ -81,6 +118,9 @@ public String toString() {
81118
builder.append("percentRemaining", percentRemaining);
82119
builder.append("unlimited", unlimited);
83120
builder.append("overagePermitted", overagePermitted);
121+
builder.append("entitlement", entitlement);
122+
builder.append("quotaRemaining", quotaRemaining);
123+
builder.append("timeStamp", timeStamp);
84124
return builder.toString();
85125
}
86126
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.microsoft.copilot.eclipse.core.lsp.protocol.quota;
2+
3+
/**
4+
* Parameters for the "copilot/quotaWarning" notification. Sent by the language server when the user's AI quota exceeds
5+
* the warning threshold.
6+
*/
7+
public record QuotaWarningNotification(String message, double percentUsed) {
8+
}

com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/i18n/MessagesTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@ void testMessagesInitialization() {
1111
// Ensure that the static fields are initialized
1212
assertNotNull(Messages.menu_signToGitHub);
1313
assertNotNull(Messages.menu_signOutOfGitHub);
14+
assertNotNull(Messages.quotaWarning_title);
15+
assertNotNull(Messages.quotaWarning_closeButton);
16+
assertNotNull(Messages.quotaWarning_increaseBudgetButton);
1417
}
1518
}

0 commit comments

Comments
 (0)