Merge pull request #2907 from jakoss/github-usage-rework

Align GitHub Copilot quota usage with AI credits
This commit is contained in:
Bohdan Triapitsyn
2026-08-28 01:23:40 +03:00
committed by GitHub
17 changed files with 232 additions and 32 deletions
@@ -97,6 +97,25 @@ In 2025/2026 MiniMax rebranded "Coding Plan" to "Token Plan" alongside the M3 mo
The provider computes `usedPercent` from whichever of `used`/`remaining` is present (`used` takes precedence when both exist) rather than assuming one field name. Both `packages/web/server/lib/quota/providers/kimi.js` and `packages/vscode/src/quotaProviders.ts` (`fetchKimiQuota`) must stay in sync — the VS Code extension duplicates this parsing logic rather than importing it.
## GitHub Copilot quota semantics
GitHub Copilot usage exposes only the `premium_interactions` snapshot as the
`premium_interactions` window. Shared UI labels that window **AI Credits** and treats it as
the provider's primary usage marker. Legacy chat-request quota and unlimited
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({
@@ -31,9 +52,7 @@ const buildCopilotWindows = (payload) => {
});
};
addWindow('chat', quota.chat);
addWindow('completions', quota.completions);
addWindow('premium', quota.premium_interactions);
addWindow('premium_interactions', quota.premium_interactions);
return windows;
};
@@ -143,15 +162,12 @@ export const fetchQuotaAddon = async () => {
}
const payload = await response.json();
const windows = buildCopilotWindows(payload);
const premium = windows.premium ? { premium: windows.premium } : windows;
return buildResult({
providerId: providerIdAddon,
providerName: providerNameAddon,
ok: true,
configured: true,
usage: { windows: premium }
usage: { windows: buildCopilotWindows(payload) }
});
} catch (error) {
return buildResult({
@@ -0,0 +1,78 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
vi.mock('../../opencode/auth.js', () => ({
readAuthFile: () => ({ 'github-copilot': { access: 'test-token' } }),
}));
import { fetchQuota, fetchQuotaAddon } from './copilot.js';
afterEach(() => {
vi.unstubAllGlobals();
});
const payload = {
quota_reset_date: '2026-09-01T00:00:00Z',
quota_snapshots: {
chat: { entitlement: 100, remaining: 80 },
completions: { entitlement: 1000, remaining: 900 },
premium_interactions: { entitlement: 300, remaining: 225 },
},
};
const mockResponse = (body = payload) => ({
ok: true,
status: 200,
json: async () => body,
});
describe('GitHub Copilot quota provider', () => {
it.each([
['primary provider', fetchQuota],
['add-on provider', fetchQuotaAddon],
])('exposes only premium interactions for the %s', async (_name, fetchProviderQuota) => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse()));
const result = await fetchProviderQuota();
expect(result.ok).toBe(true);
expect(Object.keys(result.usage.windows)).toEqual(['premium_interactions']);
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();
});
});