From dcf02f13a1df3d6b2f19a5d36bbcdacc3135df9f Mon Sep 17 00:00:00 2001 From: Jakub Syty Date: Fri, 14 Aug 2026 15:56:13 +0200 Subject: [PATCH] Align GitHub Copilot quota usage with AI credits --- packages/ui/src/lib/i18n/messages/de.ts | 2 +- packages/ui/src/lib/i18n/messages/en.ts | 2 +- packages/ui/src/lib/i18n/messages/es.ts | 2 +- packages/ui/src/lib/i18n/messages/fr.ts | 2 +- packages/ui/src/lib/i18n/messages/ja.ts | 2 +- packages/ui/src/lib/i18n/messages/ko.ts | 2 +- packages/ui/src/lib/i18n/messages/pl.ts | 2 +- packages/ui/src/lib/i18n/messages/pt-BR.ts | 2 +- packages/ui/src/lib/i18n/messages/uk.ts | 2 +- packages/ui/src/lib/i18n/messages/zh-CN.ts | 2 +- packages/ui/src/lib/i18n/messages/zh-TW.ts | 2 +- packages/ui/src/lib/quota/utils.test.ts | 7 +++- packages/vscode/src/quotaProviders.test.ts | 21 ++++++++++ packages/vscode/src/quotaProviders.ts | 8 ++-- .../web/server/lib/quota/DOCUMENTATION.md | 9 ++++ .../web/server/lib/quota/providers/copilot.js | 8 ++-- .../lib/quota/providers/copilot.test.js | 42 +++++++++++++++++++ 17 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 packages/web/server/lib/quota/providers/copilot.test.js diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index 4018c5ae..1ff00100 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -2820,7 +2820,7 @@ export const dict = { 'quota.window.premium': 'Premium-Interaktionen', 'quota.window.chat': 'Chat-Anfragen', 'quota.window.completions': 'Vervollständigungen', - 'quota.window.premiumInteractions': 'Premium-Interaktionen', + 'quota.window.premiumInteractions': 'KI-Guthaben', 'terminalView.actions.attachSelection': 'Ausgewählte Ausgabe anhängen', 'terminalView.actions.restart': 'Terminal neu starten', 'chat.message.terminalContext': '{terminal}, Zeilen {start}-{end}', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 77546094..9de01595 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -2998,7 +2998,7 @@ export const dict = { 'quota.window.premium': 'Premium Interactions', 'quota.window.chat': 'Chat Requests', 'quota.window.completions': 'Completions', - 'quota.window.premiumInteractions': 'Premium interactions', + 'quota.window.premiumInteractions': 'AI Credits', 'chat.workStatus.ariaLabel': 'Work status', 'chat.workStatus.context.label': 'Context', 'chat.workStatus.git.changedFileSingle': '{count} file changed', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 911387e2..1d034f86 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -2999,7 +2999,7 @@ export const dict: Record = { "quota.window.premium": "Premium Interactions", "quota.window.chat": "Chat Requests", "quota.window.completions": "Completions", - "quota.window.premiumInteractions": "Premium interactions", + "quota.window.premiumInteractions": "Créditos de IA", 'chat.workStatus.ariaLabel': 'Estado del trabajo', 'chat.workStatus.context.label': 'Contexto', 'chat.workStatus.git.changedFileSingle': '{count} archivo modificado', diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 4a9f2ffc..570e5fef 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -2691,7 +2691,7 @@ export const dict = { 'quota.window.premium': 'Interactions premium', 'quota.window.chat': 'Requêtes de chat', 'quota.window.completions': 'Complétions', - 'quota.window.premiumInteractions': 'Interactions premium', + 'quota.window.premiumInteractions': 'Crédits IA', 'layout.mainTab.diagram': 'Diagramme', 'mobile.nav.aria': 'Navigation mobile', 'mobile.connect.welcome.title': 'Se connecter à OpenChamber', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index f58a6b89..9cb86e7a 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -2997,7 +2997,7 @@ export const dict: Record = { 'onboarding.localSetup.actions.checkAndContinue': 'インストール完了、確認して続行', 'onboarding.localSetup.status.autoContinue': '検出され次第自動的に続行します。', 'updateDialog.changelog.title': '新機能', - 'quota.window.premiumInteractions': 'プレミアムインタラクション', + 'quota.window.premiumInteractions': 'AIクレジット', 'chat.workStatus.ariaLabel': '作業状況', 'chat.workStatus.context.label': 'コンテキスト', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 34610de1..32f865b6 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -2998,7 +2998,7 @@ export const dict: Record = { 'quota.window.premium': 'Premium Interactions', 'quota.window.chat': 'Chat Requests', 'quota.window.completions': 'Completions', - 'quota.window.premiumInteractions': 'Premium interactions', + 'quota.window.premiumInteractions': 'AI 크레딧', 'chat.workStatus.ariaLabel': '작업 상태', 'chat.workStatus.context.label': '컨텍스트', 'chat.workStatus.git.changedFileSingle': '파일 {count}개 변경됨', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 8c78bd5d..3207639d 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -3015,7 +3015,7 @@ export const dict: Record = { 'quota.window.premium': 'Premium Interactions', 'quota.window.chat': 'Chat Requests', 'quota.window.completions': 'Completions', - 'quota.window.premiumInteractions': 'Premium interactions', + 'quota.window.premiumInteractions': 'Kredyty AI', 'chat.workStatus.ariaLabel': 'Stan pracy', 'chat.workStatus.context.label': 'Kontekst', 'chat.workStatus.git.changedFileSingle': 'Zmieniono {count} plik', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 7d940f6f..9dbc78ac 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -2999,7 +2999,7 @@ export const dict: Record = { "quota.window.premium": "Premium Interactions", "quota.window.chat": "Chat Requests", "quota.window.completions": "Completions", - "quota.window.premiumInteractions": "Premium interactions", + "quota.window.premiumInteractions": "Créditos de IA", 'chat.workStatus.ariaLabel': 'Status do trabalho', 'chat.workStatus.context.label': 'Contexto', 'chat.workStatus.git.changedFileSingle': '{count} arquivo alterado', diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 058f152f..b20e059a 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -2999,7 +2999,7 @@ export const dict: Record = { "quota.window.premium": "Premium Interactions", "quota.window.chat": "Chat Requests", "quota.window.completions": "Completions", - "quota.window.premiumInteractions": "Premium interactions", + "quota.window.premiumInteractions": "Кредити ШІ", 'chat.workStatus.ariaLabel': 'Стан роботи', 'chat.workStatus.context.label': 'Контекст', 'chat.workStatus.git.changedFileSingle': 'Змінено {count} файл', diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index f1c3322e..480ac2fe 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -2999,7 +2999,7 @@ export const dict: Record = { 'quota.window.premium': 'Premium Interactions', 'quota.window.chat': 'Chat Requests', 'quota.window.completions': 'Completions', - 'quota.window.premiumInteractions': 'Premium interactions', + 'quota.window.premiumInteractions': 'AI 点数', 'chat.workStatus.ariaLabel': '工作状态', 'chat.workStatus.context.label': '上下文', 'chat.workStatus.git.changedFileSingle': '已更改 {count} 个文件', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index 59f99c81..e17c74a1 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -2998,7 +2998,7 @@ export const dict: Record = { 'quota.window.premium': 'Premium Interactions', 'quota.window.chat': 'Chat Requests', 'quota.window.completions': 'Completions', - 'quota.window.premiumInteractions': 'Premium interactions', + 'quota.window.premiumInteractions': 'AI 點數', 'chat.workStatus.ariaLabel': '工作狀態', 'chat.workStatus.context.label': '上下文', 'chat.workStatus.git.changedFileSingle': '已變更 {count} 個檔案', diff --git a/packages/ui/src/lib/quota/utils.test.ts b/packages/ui/src/lib/quota/utils.test.ts index afca3d75..abda6e48 100644 --- a/packages/ui/src/lib/quota/utils.test.ts +++ b/packages/ui/src/lib/quota/utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'bun:test'; -import { clampPercent, formatPercent } from './utils'; +import { clampPercent, formatPercent, formatWindowLabel } from './utils'; describe('quota utils', () => { test('treats non-finite percentages as missing', () => { @@ -10,4 +10,9 @@ describe('quota utils', () => { expect(formatPercent(Infinity)).toBe('-'); expect(formatPercent(-Infinity)).toBe('-'); }); + + test('labels Copilot usage as AI Credits without changing generic premium usage', () => { + expect(formatWindowLabel('premium')).toBe('Premium Interactions'); + expect(formatWindowLabel('premium_interactions')).toBe('AI Credits'); + }); }); diff --git a/packages/vscode/src/quotaProviders.test.ts b/packages/vscode/src/quotaProviders.test.ts index a2e314c0..2525e714 100644 --- a/packages/vscode/src/quotaProviders.test.ts +++ b/packages/vscode/src/quotaProviders.test.ts @@ -19,6 +19,7 @@ const AUTH = JSON.stringify({ 'opencode-go': { key: 'test-token' }, 'zai-coding-plan': { key: 'test-token' }, deepseek: { key: 'test-token' }, + 'github-copilot': { access: 'test-token' }, }); ((fs as unknown) as { existsSync: () => boolean }).existsSync = () => true; ((fs as unknown) as { readFileSync: () => string }).readFileSync = () => AUTH; @@ -173,6 +174,26 @@ describe('Codex quota provider (VS Code parity)', () => { }); }); +describe('GitHub Copilot quota provider (VS Code parity)', () => { + test('exposes only premium interactions as the primary usage window', async () => { + stubFetchReturning(() => Promise.resolve(mockResponse({ + 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 result = await fetchQuotaForProvider('github-copilot'); + + assert.equal(result.ok, true); + assert.deepEqual(Object.keys(result.usage!.windows), ['premium_interactions']); + assert.equal(result.usage!.windows.premium_interactions!.usedPercent, 25); + assert.equal(result.usage!.windows.premium_interactions!.valueLabel, '225 / 300 left'); + }); +}); + describe('Z.ai quota provider (VS Code parity)', () => { test('surfaces 5-hour, weekly, and MCP quota windows', async () => { stubFetchReturning(() => Promise.resolve(mockResponse({ diff --git a/packages/vscode/src/quotaProviders.ts b/packages/vscode/src/quotaProviders.ts index 32c54441..b513ad0b 100644 --- a/packages/vscode/src/quotaProviders.ts +++ b/packages/vscode/src/quotaProviders.ts @@ -1362,9 +1362,7 @@ const buildCopilotWindows = (payload: Record) => { }); }; - addWindow('chat', quota.chat as Record | undefined); - addWindow('completions', quota.completions as Record | undefined); - addWindow('premium', quota.premium_interactions as Record | undefined); + addWindow('premium_interactions', quota.premium_interactions as Record | undefined); return windows; }; @@ -1462,7 +1460,9 @@ const fetchCopilotAddonQuota = async (): Promise => { const payload = await response.json() as Record; const windows = buildCopilotWindows(payload); - const premium = windows.premium ? { premium: windows.premium } : windows; + const premium = windows.premium_interactions + ? { premium_interactions: windows.premium_interactions } + : windows; return buildResult({ providerId: 'github-copilot-addon', diff --git a/packages/web/server/lib/quota/DOCUMENTATION.md b/packages/web/server/lib/quota/DOCUMENTATION.md index fb876284..a8cdceb6 100644 --- a/packages/web/server/lib/quota/DOCUMENTATION.md +++ b/packages/web/server/lib/quota/DOCUMENTATION.md @@ -82,6 +82,15 @@ 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. + ## Notes for contributors - Keep provider IDs stable; clients use them directly. - Avoid adding alias-based dispatch in `fetchQuotaForProvider`; dispatch currently expects exact provider IDs. diff --git a/packages/web/server/lib/quota/providers/copilot.js b/packages/web/server/lib/quota/providers/copilot.js index 964f2f71..71037c93 100644 --- a/packages/web/server/lib/quota/providers/copilot.js +++ b/packages/web/server/lib/quota/providers/copilot.js @@ -31,9 +31,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; }; @@ -144,7 +142,9 @@ export const fetchQuotaAddon = async () => { const payload = await response.json(); const windows = buildCopilotWindows(payload); - const premium = windows.premium ? { premium: windows.premium } : windows; + const premium = windows.premium_interactions + ? { premium_interactions: windows.premium_interactions } + : windows; return buildResult({ providerId: providerIdAddon, diff --git a/packages/web/server/lib/quota/providers/copilot.test.js b/packages/web/server/lib/quota/providers/copilot.test.js new file mode 100644 index 00000000..15e3c963 --- /dev/null +++ b/packages/web/server/lib/quota/providers/copilot.test.js @@ -0,0 +1,42 @@ +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 = () => ({ + ok: true, + status: 200, + json: async () => payload, +}); + +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'); + }); +});