Adjust for unlimited plan option
This commit is contained in:
@@ -267,6 +267,51 @@ describe('GitHub Copilot quota provider (VS Code parity)', () => {
|
||||
assert.equal(result.usage!.windows.premium_interactions!.usedPercent, 25);
|
||||
assert.equal(result.usage!.windows.premium_interactions!.valueLabel, '225 / 300 left');
|
||||
});
|
||||
|
||||
test('add-on path mirrors the primary window shaping', async () => {
|
||||
stubFetchReturning(() => Promise.resolve(mockResponse({
|
||||
quota_reset_date: '2026-09-01T00:00:00Z',
|
||||
quota_snapshots: {
|
||||
premium_interactions: { entitlement: 300, remaining: 225 },
|
||||
},
|
||||
})));
|
||||
|
||||
const result = await fetchQuotaForProvider('github-copilot-addon');
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(Object.keys(result.usage!.windows), ['premium_interactions']);
|
||||
assert.equal(result.usage!.windows.premium_interactions!.usedPercent, 25);
|
||||
});
|
||||
|
||||
test('reports unlimited plans without a percent', async () => {
|
||||
stubFetchReturning(() => Promise.resolve(mockResponse({
|
||||
quota_reset_date: '2026-09-01T00:00:00Z',
|
||||
quota_snapshots: {
|
||||
premium_interactions: { unlimited: true, entitlement: -1, remaining: -1 },
|
||||
},
|
||||
})));
|
||||
|
||||
const result = await fetchQuotaForProvider('github-copilot');
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.usage!.windows.premium_interactions!.usedPercent, null);
|
||||
assert.equal(result.usage!.windows.premium_interactions!.valueLabel, 'Unlimited');
|
||||
});
|
||||
|
||||
test('falls back to percent_remaining when entitlement is unusable', async () => {
|
||||
stubFetchReturning(() => Promise.resolve(mockResponse({
|
||||
quota_reset_date: '2026-09-01T00:00:00Z',
|
||||
quota_snapshots: {
|
||||
premium_interactions: { entitlement: 0, remaining: 0, percent_remaining: 75.5 },
|
||||
},
|
||||
})));
|
||||
|
||||
const result = await fetchQuotaForProvider('github-copilot');
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.ok(Math.abs(result.usage!.windows.premium_interactions!.usedPercent! - 24.5) < 1e-9);
|
||||
assert.equal(result.usage!.windows.premium_interactions!.valueLabel, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Claude quota provider (VS Code parity)', () => {
|
||||
|
||||
@@ -1469,14 +1469,35 @@ const buildCopilotWindows = (payload: Record<string, unknown>) => {
|
||||
const resetAt = toTimestamp(payload.quota_reset_date);
|
||||
const windows: Record<string, UsageWindow> = {};
|
||||
|
||||
// Mirrors the quota semantics of microsoft/vscode-copilot-chat
|
||||
// (CopilotUserQuotaInfo): each snapshot carries entitlement, remaining,
|
||||
// unlimited, and percent_remaining. Unlimited plans report no usable
|
||||
// entitlement; percent_remaining is a server-computed fallback.
|
||||
const addWindow = (label: string, snapshot?: Record<string, unknown>) => {
|
||||
if (!snapshot) return;
|
||||
|
||||
if (snapshot.unlimited === true) {
|
||||
windows[label] = toUsageWindow({
|
||||
usedPercent: null,
|
||||
windowSeconds: null,
|
||||
resetAt,
|
||||
valueLabel: 'Unlimited',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const entitlement = toNumber(snapshot.entitlement);
|
||||
const remaining = toNumber(snapshot.remaining);
|
||||
const usedPercent = entitlement && remaining !== null
|
||||
? Math.max(0, Math.min(100, 100 - (remaining / entitlement) * 100))
|
||||
let usedPercent = entitlement !== null && entitlement > 0 && remaining !== null
|
||||
? Math.min(100, Math.max(0, 100 - (remaining / entitlement) * 100))
|
||||
: null;
|
||||
const valueLabel = entitlement !== null && remaining !== null
|
||||
if (usedPercent === null) {
|
||||
const percentRemaining = toNumber(snapshot.percent_remaining);
|
||||
if (percentRemaining !== null) {
|
||||
usedPercent = Math.min(100, Math.max(0, 100 - percentRemaining));
|
||||
}
|
||||
}
|
||||
const valueLabel = entitlement !== null && entitlement > 0 && remaining !== null
|
||||
? `${remaining.toFixed(0)} / ${entitlement.toFixed(0)} left`
|
||||
: null;
|
||||
windows[label] = toUsageWindow({
|
||||
@@ -1584,17 +1605,12 @@ const fetchCopilotAddonQuota = async (): Promise<ProviderResult> => {
|
||||
}
|
||||
|
||||
const payload = await response.json() as Record<string, unknown>;
|
||||
const windows = buildCopilotWindows(payload);
|
||||
const premium = windows.premium_interactions
|
||||
? { premium_interactions: windows.premium_interactions }
|
||||
: windows;
|
||||
|
||||
return buildResult({
|
||||
providerId: 'github-copilot-addon',
|
||||
providerName: 'GitHub Copilot Add-on',
|
||||
ok: true,
|
||||
configured: true,
|
||||
usage: { windows: premium },
|
||||
usage: { windows: buildCopilotWindows(payload) },
|
||||
});
|
||||
} catch (error) {
|
||||
return buildResult({
|
||||
|
||||
@@ -106,6 +106,16 @@ completion quota are intentionally omitted. Keep
|
||||
`packages/web/server/lib/quota/providers/copilot.js` and
|
||||
`packages/vscode/src/quotaProviders.ts` in sync.
|
||||
|
||||
The `/copilot_internal/user` endpoint is undocumented; its quota semantics mirror
|
||||
what `microsoft/vscode-copilot-chat` consumes (`CopilotUserQuotaInfo`). Each
|
||||
snapshot carries `entitlement`, `remaining`, `unlimited`, and
|
||||
`percent_remaining`. Providers must honor these rules:
|
||||
|
||||
- `unlimited: true` renders a percent-less window with an "Unlimited" value label.
|
||||
- Percent math requires a positive `entitlement`; entitlements of `0`, `-1`, or null are unusable.
|
||||
- When entitlement/remaining are unusable, fall back to `100 - percent_remaining`.
|
||||
- Snapshots other than `premium_interactions` (legacy annual plans) yield zero windows.
|
||||
|
||||
## Notes for contributors
|
||||
- Keep provider IDs stable; clients use them directly.
|
||||
- Avoid adding alias-based dispatch in `fetchQuotaForProvider`; dispatch currently expects exact provider IDs.
|
||||
|
||||
@@ -13,14 +13,35 @@ const buildCopilotWindows = (payload) => {
|
||||
const resetAt = toTimestamp(payload?.quota_reset_date);
|
||||
const windows = {};
|
||||
|
||||
// Mirrors the quota semantics of microsoft/vscode-copilot-chat
|
||||
// (CopilotUserQuotaInfo): each snapshot carries entitlement, remaining,
|
||||
// unlimited, and percent_remaining. Unlimited plans report no usable
|
||||
// entitlement; percent_remaining is a server-computed fallback.
|
||||
const addWindow = (label, snapshot) => {
|
||||
if (!snapshot) return;
|
||||
|
||||
if (snapshot.unlimited === true) {
|
||||
windows[label] = toUsageWindow({
|
||||
usedPercent: null,
|
||||
windowSeconds: null,
|
||||
resetAt,
|
||||
valueLabel: 'Unlimited'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const entitlement = toNumber(snapshot.entitlement);
|
||||
const remaining = toNumber(snapshot.remaining);
|
||||
const usedPercent = entitlement && remaining !== null
|
||||
? Math.max(0, 100 - (remaining / entitlement) * 100)
|
||||
let usedPercent = entitlement !== null && entitlement > 0 && remaining !== null
|
||||
? Math.min(100, Math.max(0, 100 - (remaining / entitlement) * 100))
|
||||
: null;
|
||||
const valueLabel = entitlement !== null && remaining !== null
|
||||
if (usedPercent === null) {
|
||||
const percentRemaining = toNumber(snapshot.percent_remaining);
|
||||
if (percentRemaining !== null) {
|
||||
usedPercent = Math.min(100, Math.max(0, 100 - percentRemaining));
|
||||
}
|
||||
}
|
||||
const valueLabel = entitlement !== null && entitlement > 0 && remaining !== null
|
||||
? `${remaining.toFixed(0)} / ${entitlement.toFixed(0)} left`
|
||||
: null;
|
||||
windows[label] = toUsageWindow({
|
||||
@@ -141,17 +162,12 @@ export const fetchQuotaAddon = async () => {
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const windows = buildCopilotWindows(payload);
|
||||
const premium = windows.premium_interactions
|
||||
? { premium_interactions: windows.premium_interactions }
|
||||
: windows;
|
||||
|
||||
return buildResult({
|
||||
providerId: providerIdAddon,
|
||||
providerName: providerNameAddon,
|
||||
ok: true,
|
||||
configured: true,
|
||||
usage: { windows: premium }
|
||||
usage: { windows: buildCopilotWindows(payload) }
|
||||
});
|
||||
} catch (error) {
|
||||
return buildResult({
|
||||
|
||||
@@ -19,10 +19,10 @@ const payload = {
|
||||
},
|
||||
};
|
||||
|
||||
const mockResponse = () => ({
|
||||
const mockResponse = (body = payload) => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => payload,
|
||||
json: async () => body,
|
||||
});
|
||||
|
||||
describe('GitHub Copilot quota provider', () => {
|
||||
@@ -39,4 +39,40 @@ describe('GitHub Copilot quota provider', () => {
|
||||
expect(result.usage.windows.premium_interactions.usedPercent).toBe(25);
|
||||
expect(result.usage.windows.premium_interactions.valueLabel).toBe('225 / 300 left');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['primary provider', fetchQuota],
|
||||
['add-on provider', fetchQuotaAddon],
|
||||
])('reports unlimited plans without a percent for the %s', async (_name, fetchProviderQuota) => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
||||
quota_reset_date: '2026-09-01T00:00:00Z',
|
||||
quota_snapshots: {
|
||||
premium_interactions: { unlimited: true, entitlement: -1, remaining: -1 },
|
||||
},
|
||||
})));
|
||||
|
||||
const result = await fetchProviderQuota();
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.usage.windows.premium_interactions.usedPercent).toBeNull();
|
||||
expect(result.usage.windows.premium_interactions.valueLabel).toBe('Unlimited');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['primary provider', fetchQuota],
|
||||
['add-on provider', fetchQuotaAddon],
|
||||
])('falls back to percent_remaining when entitlement is unusable for the %s', async (_name, fetchProviderQuota) => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
||||
quota_reset_date: '2026-09-01T00:00:00Z',
|
||||
quota_snapshots: {
|
||||
premium_interactions: { entitlement: 0, remaining: 0, percent_remaining: 75.5 },
|
||||
},
|
||||
})));
|
||||
|
||||
const result = await fetchProviderQuota();
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.usage.windows.premium_interactions.usedPercent).toBeCloseTo(24.5);
|
||||
expect(result.usage.windows.premium_interactions.valueLabel ?? null).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user