Align GitHub Copilot quota usage with AI credits
This commit is contained in:
@@ -2820,7 +2820,7 @@ export const dict = {
|
|||||||
'quota.window.premium': 'Premium-Interaktionen',
|
'quota.window.premium': 'Premium-Interaktionen',
|
||||||
'quota.window.chat': 'Chat-Anfragen',
|
'quota.window.chat': 'Chat-Anfragen',
|
||||||
'quota.window.completions': 'Vervollständigungen',
|
'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.attachSelection': 'Ausgewählte Ausgabe anhängen',
|
||||||
'terminalView.actions.restart': 'Terminal neu starten',
|
'terminalView.actions.restart': 'Terminal neu starten',
|
||||||
'chat.message.terminalContext': '{terminal}, Zeilen {start}-{end}',
|
'chat.message.terminalContext': '{terminal}, Zeilen {start}-{end}',
|
||||||
|
|||||||
@@ -2998,7 +2998,7 @@ export const dict = {
|
|||||||
'quota.window.premium': 'Premium Interactions',
|
'quota.window.premium': 'Premium Interactions',
|
||||||
'quota.window.chat': 'Chat Requests',
|
'quota.window.chat': 'Chat Requests',
|
||||||
'quota.window.completions': 'Completions',
|
'quota.window.completions': 'Completions',
|
||||||
'quota.window.premiumInteractions': 'Premium interactions',
|
'quota.window.premiumInteractions': 'AI Credits',
|
||||||
'chat.workStatus.ariaLabel': 'Work status',
|
'chat.workStatus.ariaLabel': 'Work status',
|
||||||
'chat.workStatus.context.label': 'Context',
|
'chat.workStatus.context.label': 'Context',
|
||||||
'chat.workStatus.git.changedFileSingle': '{count} file changed',
|
'chat.workStatus.git.changedFileSingle': '{count} file changed',
|
||||||
|
|||||||
@@ -2999,7 +2999,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"quota.window.premium": "Premium Interactions",
|
"quota.window.premium": "Premium Interactions",
|
||||||
"quota.window.chat": "Chat Requests",
|
"quota.window.chat": "Chat Requests",
|
||||||
"quota.window.completions": "Completions",
|
"quota.window.completions": "Completions",
|
||||||
"quota.window.premiumInteractions": "Premium interactions",
|
"quota.window.premiumInteractions": "Créditos de IA",
|
||||||
'chat.workStatus.ariaLabel': 'Estado del trabajo',
|
'chat.workStatus.ariaLabel': 'Estado del trabajo',
|
||||||
'chat.workStatus.context.label': 'Contexto',
|
'chat.workStatus.context.label': 'Contexto',
|
||||||
'chat.workStatus.git.changedFileSingle': '{count} archivo modificado',
|
'chat.workStatus.git.changedFileSingle': '{count} archivo modificado',
|
||||||
|
|||||||
@@ -2691,7 +2691,7 @@ export const dict = {
|
|||||||
'quota.window.premium': 'Interactions premium',
|
'quota.window.premium': 'Interactions premium',
|
||||||
'quota.window.chat': 'Requêtes de chat',
|
'quota.window.chat': 'Requêtes de chat',
|
||||||
'quota.window.completions': 'Complétions',
|
'quota.window.completions': 'Complétions',
|
||||||
'quota.window.premiumInteractions': 'Interactions premium',
|
'quota.window.premiumInteractions': 'Crédits IA',
|
||||||
'layout.mainTab.diagram': 'Diagramme',
|
'layout.mainTab.diagram': 'Diagramme',
|
||||||
'mobile.nav.aria': 'Navigation mobile',
|
'mobile.nav.aria': 'Navigation mobile',
|
||||||
'mobile.connect.welcome.title': 'Se connecter à OpenChamber',
|
'mobile.connect.welcome.title': 'Se connecter à OpenChamber',
|
||||||
|
|||||||
@@ -2997,7 +2997,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'onboarding.localSetup.actions.checkAndContinue': 'インストール完了、確認して続行',
|
'onboarding.localSetup.actions.checkAndContinue': 'インストール完了、確認して続行',
|
||||||
'onboarding.localSetup.status.autoContinue': '検出され次第自動的に続行します。',
|
'onboarding.localSetup.status.autoContinue': '検出され次第自動的に続行します。',
|
||||||
'updateDialog.changelog.title': '新機能',
|
'updateDialog.changelog.title': '新機能',
|
||||||
'quota.window.premiumInteractions': 'プレミアムインタラクション',
|
'quota.window.premiumInteractions': 'AIクレジット',
|
||||||
|
|
||||||
'chat.workStatus.ariaLabel': '作業状況',
|
'chat.workStatus.ariaLabel': '作業状況',
|
||||||
'chat.workStatus.context.label': 'コンテキスト',
|
'chat.workStatus.context.label': 'コンテキスト',
|
||||||
|
|||||||
@@ -2998,7 +2998,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'quota.window.premium': 'Premium Interactions',
|
'quota.window.premium': 'Premium Interactions',
|
||||||
'quota.window.chat': 'Chat Requests',
|
'quota.window.chat': 'Chat Requests',
|
||||||
'quota.window.completions': 'Completions',
|
'quota.window.completions': 'Completions',
|
||||||
'quota.window.premiumInteractions': 'Premium interactions',
|
'quota.window.premiumInteractions': 'AI 크레딧',
|
||||||
'chat.workStatus.ariaLabel': '작업 상태',
|
'chat.workStatus.ariaLabel': '작업 상태',
|
||||||
'chat.workStatus.context.label': '컨텍스트',
|
'chat.workStatus.context.label': '컨텍스트',
|
||||||
'chat.workStatus.git.changedFileSingle': '파일 {count}개 변경됨',
|
'chat.workStatus.git.changedFileSingle': '파일 {count}개 변경됨',
|
||||||
|
|||||||
@@ -3015,7 +3015,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'quota.window.premium': 'Premium Interactions',
|
'quota.window.premium': 'Premium Interactions',
|
||||||
'quota.window.chat': 'Chat Requests',
|
'quota.window.chat': 'Chat Requests',
|
||||||
'quota.window.completions': 'Completions',
|
'quota.window.completions': 'Completions',
|
||||||
'quota.window.premiumInteractions': 'Premium interactions',
|
'quota.window.premiumInteractions': 'Kredyty AI',
|
||||||
'chat.workStatus.ariaLabel': 'Stan pracy',
|
'chat.workStatus.ariaLabel': 'Stan pracy',
|
||||||
'chat.workStatus.context.label': 'Kontekst',
|
'chat.workStatus.context.label': 'Kontekst',
|
||||||
'chat.workStatus.git.changedFileSingle': 'Zmieniono {count} plik',
|
'chat.workStatus.git.changedFileSingle': 'Zmieniono {count} plik',
|
||||||
|
|||||||
@@ -2999,7 +2999,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"quota.window.premium": "Premium Interactions",
|
"quota.window.premium": "Premium Interactions",
|
||||||
"quota.window.chat": "Chat Requests",
|
"quota.window.chat": "Chat Requests",
|
||||||
"quota.window.completions": "Completions",
|
"quota.window.completions": "Completions",
|
||||||
"quota.window.premiumInteractions": "Premium interactions",
|
"quota.window.premiumInteractions": "Créditos de IA",
|
||||||
'chat.workStatus.ariaLabel': 'Status do trabalho',
|
'chat.workStatus.ariaLabel': 'Status do trabalho',
|
||||||
'chat.workStatus.context.label': 'Contexto',
|
'chat.workStatus.context.label': 'Contexto',
|
||||||
'chat.workStatus.git.changedFileSingle': '{count} arquivo alterado',
|
'chat.workStatus.git.changedFileSingle': '{count} arquivo alterado',
|
||||||
|
|||||||
@@ -2999,7 +2999,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"quota.window.premium": "Premium Interactions",
|
"quota.window.premium": "Premium Interactions",
|
||||||
"quota.window.chat": "Chat Requests",
|
"quota.window.chat": "Chat Requests",
|
||||||
"quota.window.completions": "Completions",
|
"quota.window.completions": "Completions",
|
||||||
"quota.window.premiumInteractions": "Premium interactions",
|
"quota.window.premiumInteractions": "Кредити ШІ",
|
||||||
'chat.workStatus.ariaLabel': 'Стан роботи',
|
'chat.workStatus.ariaLabel': 'Стан роботи',
|
||||||
'chat.workStatus.context.label': 'Контекст',
|
'chat.workStatus.context.label': 'Контекст',
|
||||||
'chat.workStatus.git.changedFileSingle': 'Змінено {count} файл',
|
'chat.workStatus.git.changedFileSingle': 'Змінено {count} файл',
|
||||||
|
|||||||
@@ -2999,7 +2999,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'quota.window.premium': 'Premium Interactions',
|
'quota.window.premium': 'Premium Interactions',
|
||||||
'quota.window.chat': 'Chat Requests',
|
'quota.window.chat': 'Chat Requests',
|
||||||
'quota.window.completions': 'Completions',
|
'quota.window.completions': 'Completions',
|
||||||
'quota.window.premiumInteractions': 'Premium interactions',
|
'quota.window.premiumInteractions': 'AI 点数',
|
||||||
'chat.workStatus.ariaLabel': '工作状态',
|
'chat.workStatus.ariaLabel': '工作状态',
|
||||||
'chat.workStatus.context.label': '上下文',
|
'chat.workStatus.context.label': '上下文',
|
||||||
'chat.workStatus.git.changedFileSingle': '已更改 {count} 个文件',
|
'chat.workStatus.git.changedFileSingle': '已更改 {count} 个文件',
|
||||||
|
|||||||
@@ -2998,7 +2998,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'quota.window.premium': 'Premium Interactions',
|
'quota.window.premium': 'Premium Interactions',
|
||||||
'quota.window.chat': 'Chat Requests',
|
'quota.window.chat': 'Chat Requests',
|
||||||
'quota.window.completions': 'Completions',
|
'quota.window.completions': 'Completions',
|
||||||
'quota.window.premiumInteractions': 'Premium interactions',
|
'quota.window.premiumInteractions': 'AI 點數',
|
||||||
'chat.workStatus.ariaLabel': '工作狀態',
|
'chat.workStatus.ariaLabel': '工作狀態',
|
||||||
'chat.workStatus.context.label': '上下文',
|
'chat.workStatus.context.label': '上下文',
|
||||||
'chat.workStatus.git.changedFileSingle': '已變更 {count} 個檔案',
|
'chat.workStatus.git.changedFileSingle': '已變更 {count} 個檔案',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from 'bun:test';
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
import { clampPercent, formatPercent } from './utils';
|
import { clampPercent, formatPercent, formatWindowLabel } from './utils';
|
||||||
|
|
||||||
describe('quota utils', () => {
|
describe('quota utils', () => {
|
||||||
test('treats non-finite percentages as missing', () => {
|
test('treats non-finite percentages as missing', () => {
|
||||||
@@ -10,4 +10,9 @@ describe('quota utils', () => {
|
|||||||
expect(formatPercent(Infinity)).toBe('-');
|
expect(formatPercent(Infinity)).toBe('-');
|
||||||
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const AUTH = JSON.stringify({
|
|||||||
'opencode-go': { key: 'test-token' },
|
'opencode-go': { key: 'test-token' },
|
||||||
'zai-coding-plan': { key: 'test-token' },
|
'zai-coding-plan': { key: 'test-token' },
|
||||||
deepseek: { key: 'test-token' },
|
deepseek: { key: 'test-token' },
|
||||||
|
'github-copilot': { access: 'test-token' },
|
||||||
});
|
});
|
||||||
((fs as unknown) as { existsSync: () => boolean }).existsSync = () => true;
|
((fs as unknown) as { existsSync: () => boolean }).existsSync = () => true;
|
||||||
((fs as unknown) as { readFileSync: () => string }).readFileSync = () => AUTH;
|
((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)', () => {
|
describe('Z.ai quota provider (VS Code parity)', () => {
|
||||||
test('surfaces 5-hour, weekly, and MCP quota windows', async () => {
|
test('surfaces 5-hour, weekly, and MCP quota windows', async () => {
|
||||||
stubFetchReturning(() => Promise.resolve(mockResponse({
|
stubFetchReturning(() => Promise.resolve(mockResponse({
|
||||||
|
|||||||
@@ -1362,9 +1362,7 @@ const buildCopilotWindows = (payload: Record<string, unknown>) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
addWindow('chat', quota.chat as Record<string, unknown> | undefined);
|
addWindow('premium_interactions', quota.premium_interactions as Record<string, unknown> | undefined);
|
||||||
addWindow('completions', quota.completions as Record<string, unknown> | undefined);
|
|
||||||
addWindow('premium', quota.premium_interactions as Record<string, unknown> | undefined);
|
|
||||||
|
|
||||||
return windows;
|
return windows;
|
||||||
};
|
};
|
||||||
@@ -1462,7 +1460,9 @@ const fetchCopilotAddonQuota = async (): Promise<ProviderResult> => {
|
|||||||
|
|
||||||
const payload = await response.json() as Record<string, unknown>;
|
const payload = await response.json() as Record<string, unknown>;
|
||||||
const windows = buildCopilotWindows(payload);
|
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({
|
return buildResult({
|
||||||
providerId: 'github-copilot-addon',
|
providerId: 'github-copilot-addon',
|
||||||
|
|||||||
@@ -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.
|
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
|
## Notes for contributors
|
||||||
- Keep provider IDs stable; clients use them directly.
|
- Keep provider IDs stable; clients use them directly.
|
||||||
- Avoid adding alias-based dispatch in `fetchQuotaForProvider`; dispatch currently expects exact provider IDs.
|
- Avoid adding alias-based dispatch in `fetchQuotaForProvider`; dispatch currently expects exact provider IDs.
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ const buildCopilotWindows = (payload) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
addWindow('chat', quota.chat);
|
addWindow('premium_interactions', quota.premium_interactions);
|
||||||
addWindow('completions', quota.completions);
|
|
||||||
addWindow('premium', quota.premium_interactions);
|
|
||||||
|
|
||||||
return windows;
|
return windows;
|
||||||
};
|
};
|
||||||
@@ -144,7 +142,9 @@ export const fetchQuotaAddon = async () => {
|
|||||||
|
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
const windows = buildCopilotWindows(payload);
|
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({
|
return buildResult({
|
||||||
providerId: providerIdAddon,
|
providerId: providerIdAddon,
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user