From 3613127e2d70aae617c8a665044b3b276adc3f49 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Thu, 20 Aug 2026 19:09:13 +0300 Subject: [PATCH] fix(quota): support z.ai credit limits --- .../components/usage/UsageProviderCards.tsx | 11 +++- .../ui/src/components/usage/usageGroups.ts | 2 + packages/ui/src/lib/i18n/messages/de.ts | 4 +- packages/ui/src/lib/i18n/messages/en.ts | 4 +- packages/ui/src/lib/i18n/messages/es.ts | 4 +- packages/ui/src/lib/i18n/messages/fr.ts | 4 +- packages/ui/src/lib/i18n/messages/ja.ts | 4 +- packages/ui/src/lib/i18n/messages/ko.ts | 4 +- packages/ui/src/lib/i18n/messages/pl.ts | 4 +- packages/ui/src/lib/i18n/messages/pt-BR.ts | 4 +- packages/ui/src/lib/i18n/messages/uk.ts | 4 +- packages/ui/src/lib/i18n/messages/zh-CN.ts | 4 +- packages/ui/src/lib/i18n/messages/zh-TW.ts | 4 +- packages/vscode/src/quotaProviders.test.ts | 27 +++++++++ packages/vscode/src/quotaProviders.ts | 55 ++++++++++++++----- .../web/server/lib/quota/DOCUMENTATION.md | 2 +- .../lib/quota/providers/claude/index.js | 4 +- .../web/server/lib/quota/providers/zai.js | 32 +++++++++-- .../server/lib/quota/providers/zai.test.js | 33 +++++++++++ 19 files changed, 162 insertions(+), 48 deletions(-) diff --git a/packages/ui/src/components/usage/UsageProviderCards.tsx b/packages/ui/src/components/usage/UsageProviderCards.tsx index 68e2a518..56f2ada0 100644 --- a/packages/ui/src/components/usage/UsageProviderCards.tsx +++ b/packages/ui/src/components/usage/UsageProviderCards.tsx @@ -35,6 +35,11 @@ export const UsageProviderCards: React.FC<{ {group.providerName} + {group.planLabel ? ( + + {group.planLabel} + + ) : null} {group.status && group.rows.length === 0 ? ( {group.status} ) : null} @@ -54,12 +59,12 @@ export const UsageProviderCards: React.FC<{ ); return (
- - + + {row.subtitle ? `${row.subtitle} · ${row.label}` : row.label} {resetLabel ? ( - + {resetLabel} ) : null} diff --git a/packages/ui/src/components/usage/usageGroups.ts b/packages/ui/src/components/usage/usageGroups.ts index 3b1bdb0e..68a85bce 100644 --- a/packages/ui/src/components/usage/usageGroups.ts +++ b/packages/ui/src/components/usage/usageGroups.ts @@ -15,6 +15,7 @@ export type UsageLimitRow = { export type UsageProviderGroup = { providerId: QuotaProviderId; providerName: string; + planLabel?: string | null; rows: UsageLimitRow[]; /** Provider-level message: a fetch error, or "nothing reported". */ status: string | null; @@ -76,6 +77,7 @@ export const useUsageProviderGroups = (): UsageProviderGroup[] => { return { providerId: providerMeta.id, providerName: providerMeta.name, + planLabel: result.planLabel, rows, status, }; diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index 35d1cab5..fec60411 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -2853,9 +2853,9 @@ export const dict = { 'quota.window.5h': '5-Stunden-Limit', 'quota.window.7d': '7-Tage-Limit', 'quota.window.extraUsage': 'Zusätzliche Nutzung', - 'quota.window.weekly': 'Wöchentliches Limit', + 'quota.window.weekly': 'Wöchentlich', 'quota.window.daily': 'Täglich', - 'quota.window.monthly': 'Monatliches Limit', + 'quota.window.monthly': 'Monatlich', 'quota.window.credits': 'Credits', 'quota.window.creditsBalance': 'Kreditguthaben', 'quota.window.monthlyCredits': 'Monatliche Credits', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index a2af1dcc..4abb4e1d 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -3031,9 +3031,9 @@ export const dict = { 'quota.window.5h': '5-Hour', 'quota.window.7d': '7-Day Limit', 'quota.window.extraUsage': 'Extra Usage', - 'quota.window.weekly': 'Weekly Limit', + 'quota.window.weekly': 'Weekly', 'quota.window.daily': 'Daily', - 'quota.window.monthly': 'Monthly Limit', + 'quota.window.monthly': 'Monthly', 'quota.window.credits': 'Credits', 'quota.window.creditsBalance': 'Credits Balance', 'quota.window.monthlyCredits': 'Monthly Credits', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 34649bcd..458a6adc 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -3032,9 +3032,9 @@ export const dict: Record = { "quota.window.5h": "5-Hour", "quota.window.7d": "7-Day Limit", "quota.window.extraUsage": "Uso adicional", - "quota.window.weekly": "Weekly Limit", + "quota.window.weekly": "Semanal", "quota.window.daily": "Daily", - "quota.window.monthly": "Monthly Limit", + "quota.window.monthly": "Mensual", "quota.window.credits": "Credits", "quota.window.creditsBalance": "Credits Balance", "quota.window.monthlyCredits": "Créditos mensuales", diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index bde799e2..bba07ce2 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -2724,9 +2724,9 @@ export const dict = { 'quota.window.5h': '5 heures', 'quota.window.7d': 'Limite sur 7 jours', 'quota.window.extraUsage': 'Utilisation supplémentaire', - 'quota.window.weekly': 'Limite hebdomadaire', + 'quota.window.weekly': 'Hebdomadaire', 'quota.window.daily': 'Quotidien', - 'quota.window.monthly': 'Limite mensuelle', + 'quota.window.monthly': 'Mensuel', 'quota.window.credits': 'Crédits', 'quota.window.creditsBalance': 'Solde de crédits', 'quota.window.monthlyCredits': 'Crédits mensuels', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index a18ca8dc..7d26028d 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -3027,9 +3027,9 @@ export const dict: Record = { 'quota.window.5h': '5時間', 'quota.window.7d': '7日間制限', 'quota.window.extraUsage': '追加利用', - 'quota.window.weekly': '週間制限', + 'quota.window.weekly': '毎週', 'quota.window.daily': '日次', - 'quota.window.monthly': '月間制限', + 'quota.window.monthly': '毎月', 'quota.window.credits': 'クレジット', 'quota.window.creditsBalance': 'クレジット残高', 'quota.window.monthlyCredits': '月間クレジット', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 170ac4b2..b7a0accd 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -3031,9 +3031,9 @@ export const dict: Record = { 'quota.window.5h': '5-Hour', 'quota.window.7d': '7-Day Limit', 'quota.window.extraUsage': '추가 사용량', - 'quota.window.weekly': 'Weekly Limit', + 'quota.window.weekly': '매주', 'quota.window.daily': 'Daily', - 'quota.window.monthly': 'Monthly Limit', + 'quota.window.monthly': '매월', 'quota.window.credits': 'Credits', 'quota.window.creditsBalance': 'Credits Balance', 'quota.window.monthlyCredits': '월간 크레딧', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index fd0ebd78..d9247510 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -3048,9 +3048,9 @@ export const dict: Record = { 'quota.window.5h': '5-Hour', 'quota.window.7d': '7-Day Limit', 'quota.window.extraUsage': 'Dodatkowe zużycie', - 'quota.window.weekly': 'Weekly Limit', + 'quota.window.weekly': 'Tygodniowo', 'quota.window.daily': 'Daily', - 'quota.window.monthly': 'Monthly Limit', + 'quota.window.monthly': 'Miesięcznie', 'quota.window.credits': 'Credits', 'quota.window.creditsBalance': 'Credits Balance', 'quota.window.monthlyCredits': 'Kredyty miesięczne', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 93a53b2c..b5dd9ae7 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -3032,9 +3032,9 @@ export const dict: Record = { "quota.window.5h": "5-Hour", "quota.window.7d": "7-Day Limit", "quota.window.extraUsage": "Uso adicional", - "quota.window.weekly": "Weekly Limit", + "quota.window.weekly": "Semanal", "quota.window.daily": "Daily", - "quota.window.monthly": "Monthly Limit", + "quota.window.monthly": "Mensal", "quota.window.credits": "Credits", "quota.window.creditsBalance": "Credits Balance", "quota.window.monthlyCredits": "Créditos mensais", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index ce9a0963..e30dea88 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -3032,9 +3032,9 @@ export const dict: Record = { "quota.window.5h": "5-Hour", "quota.window.7d": "7-Day Limit", "quota.window.extraUsage": "Додаткове використання", - "quota.window.weekly": "Weekly Limit", + "quota.window.weekly": "Щотижня", "quota.window.daily": "Daily", - "quota.window.monthly": "Monthly Limit", + "quota.window.monthly": "Щомісяця", "quota.window.credits": "Credits", "quota.window.creditsBalance": "Credits Balance", "quota.window.monthlyCredits": "Місячні кредити", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index eb6b7241..0b1034e3 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -3032,9 +3032,9 @@ export const dict: Record = { 'quota.window.5h': '5-Hour', 'quota.window.7d': '7-Day Limit', 'quota.window.extraUsage': '额外用量', - 'quota.window.weekly': 'Weekly Limit', + 'quota.window.weekly': '每周', 'quota.window.daily': 'Daily', - 'quota.window.monthly': 'Monthly Limit', + 'quota.window.monthly': '每月', 'quota.window.credits': 'Credits', 'quota.window.creditsBalance': 'Credits Balance', 'quota.window.monthlyCredits': '每月积分', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index 89ab710d..4b2caab8 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -3031,9 +3031,9 @@ export const dict: Record = { 'quota.window.5h': '5-Hour', 'quota.window.7d': '7-Day Limit', 'quota.window.extraUsage': '額外用量', - 'quota.window.weekly': 'Weekly Limit', + 'quota.window.weekly': '每週', 'quota.window.daily': 'Daily', - 'quota.window.monthly': 'Monthly Limit', + 'quota.window.monthly': '每月', 'quota.window.credits': 'Credits', 'quota.window.creditsBalance': 'Credits Balance', 'quota.window.monthlyCredits': '每月點數', diff --git a/packages/vscode/src/quotaProviders.test.ts b/packages/vscode/src/quotaProviders.test.ts index 2309ea6d..ee88916c 100644 --- a/packages/vscode/src/quotaProviders.test.ts +++ b/packages/vscode/src/quotaProviders.test.ts @@ -327,6 +327,33 @@ describe('Z.ai quota provider (VS Code parity)', () => { assert.equal(windows['MCP Tools']!.windowSeconds, 30 * 24 * 60 * 60); assert.equal(windows['MCP Tools']!.resetAt, 1787128459979); }); + + test('maps CREDIT_LIMIT entries to windows with credit value labels and plan level', async () => { + stubFetchReturning(() => Promise.resolve(mockResponse({ + code: 200, + data: { + limits: [ + { type: 'CREDIT_LIMIT', unit: 3, number: 5, usage: 12000, currentValue: 65, remaining: 11934, percentage: 1, nextResetTime: 1787257978907 }, + { type: 'CREDIT_LIMIT', unit: 6, number: 1, usage: 60000, currentValue: 65, remaining: 59934, percentage: 1, nextResetTime: 1787844668997 }, + ], + level: 'pro', + }, + }))); + + const result = await fetchQuotaForProvider('zai-coding-plan'); + const windows = result.usage!.windows; + + assert.equal(result.ok, true); + assert.equal(result.planLabel, 'pro'); + assert.equal(windows['5h']!.usedPercent, 1); + assert.equal(windows['5h']!.windowSeconds, 5 * 60 * 60); + assert.equal(windows['5h']!.resetAt, 1787257978907); + assert.equal(windows['5h']!.valueLabel, '65 / 12k credits'); + assert.equal(windows.weekly!.usedPercent, 1); + assert.equal(windows.weekly!.windowSeconds, 7 * 24 * 60 * 60); + assert.equal(windows.weekly!.resetAt, 1787844668997); + assert.equal(windows.weekly!.valueLabel, '65 / 60k credits'); + }); }); describe('NeuralWatt quota provider (VS Code parity)', () => { diff --git a/packages/vscode/src/quotaProviders.ts b/packages/vscode/src/quotaProviders.ts index a33bf6b6..042fc5d4 100644 --- a/packages/vscode/src/quotaProviders.ts +++ b/packages/vscode/src/quotaProviders.ts @@ -72,13 +72,31 @@ type ZaiLimit = { type?: string; number?: number; unit?: number; + usage?: number; + currentValue?: number; + remaining?: number; nextResetTime?: number; percentage?: number; }; +// CREDIT_LIMIT entries carry `usage` (total credits) and `currentValue` (consumed); +// TOKENS_LIMIT entries only carry a percentage. +const formatZaiCreditAmount = (value: number): string => { + if (value < 1000) return value.toLocaleString('en-US'); + return `${Math.round(value / 100) / 10}k`; +}; + +const formatZaiCreditValueLabel = (limit: ZaiLimit): string | null => { + const used = toNumber(limit.currentValue); + const total = toNumber(limit.usage); + if (used === null || total === null) return null; + return `${formatZaiCreditAmount(used)} / ${formatZaiCreditAmount(total)} credits`; +}; + type ZaiPayload = { data?: { limits?: ZaiLimit[]; + level?: string; }; }; @@ -411,15 +429,20 @@ const buildResult = (data: { configured: boolean; usage?: ProviderUsage | null; error?: string; -}): ProviderResult => ({ - providerId: data.providerId, - providerName: data.providerName, - ok: data.ok, - configured: data.configured, - usage: data.usage ?? null, - ...(data.error ? { error: data.error } : {}), - fetchedAt: Date.now(), -}); + planLabel?: string | null; +}): ProviderResult => { + const result: ProviderResult = { + providerId: data.providerId, + providerName: data.providerName, + ok: data.ok, + configured: data.configured, + usage: data.usage ?? null, + ...(data.error ? { error: data.error } : {}), + fetchedAt: Date.now(), + }; + if (data.planLabel) result.planLabel = data.planLabel; + return result; +}; const resolveXaiAuth = (): XaiAuthEntry | null => { const entry = getProviderAuth('xai'); @@ -1291,7 +1314,7 @@ const buildClaudeRateLimitResult = (): ProviderResult => ( providerName: 'Claude', ok: false, configured: true, - error: 'Rate limited by Anthropic. Retrying shortly.', + error: 'Rate limited. Retrying soon.', }) ); @@ -2059,16 +2082,19 @@ const fetchZaiQuota = async (): Promise => { const payload = await response.json() as ZaiPayload; const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : []; const windows: Record = {}; - for (const tokensLimit of limits.filter((limit) => limit?.type === 'TOKENS_LIMIT')) { - const windowSeconds = resolveWindowSeconds(tokensLimit as Record); + // The API renamed TOKENS_LIMIT to CREDIT_LIMIT; field semantics stayed the same, + // so both limit types map to the same windows. + for (const limit of limits.filter((entry) => entry?.type === 'TOKENS_LIMIT' || entry?.type === 'CREDIT_LIMIT')) { + const windowSeconds = resolveWindowSeconds(limit as Record); const windowLabel = resolveWindowLabel(windowSeconds); - const resetAt = tokensLimit.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null; - const usedPercent = typeof tokensLimit.percentage === 'number' ? tokensLimit.percentage : null; + const resetAt = limit.nextResetTime ? normalizeTimestamp(limit.nextResetTime) : null; + const usedPercent = typeof limit.percentage === 'number' ? limit.percentage : null; windows[windowLabel] = toUsageWindow({ usedPercent, windowSeconds, resetAt, + valueLabel: formatZaiCreditValueLabel(limit), }); } @@ -2087,6 +2113,7 @@ const fetchZaiQuota = async (): Promise => { ok: true, configured: true, usage: { windows }, + planLabel: payload?.data?.level || null, }); } catch (error) { return buildResult({ diff --git a/packages/web/server/lib/quota/DOCUMENTATION.md b/packages/web/server/lib/quota/DOCUMENTATION.md index 78d86706..83e905a4 100644 --- a/packages/web/server/lib/quota/DOCUMENTATION.md +++ b/packages/web/server/lib/quota/DOCUMENTATION.md @@ -101,4 +101,4 @@ The provider computes `usedPercent` from whichever of `used`/`remaining` is pres - Keep provider IDs stable; clients use them directly. - Avoid adding alias-based dispatch in `fetchQuotaForProvider`; dispatch currently expects exact provider IDs. - Keep Google behavior changes isolated and review `providers/google/*` together. -- Z.ai Coding Plan exposes separate 5-hour and weekly `TOKENS_LIMIT` entries plus a monthly `TIME_LIMIT` for MCP tools; web and VS Code must preserve all three windows. +- Z.ai Coding Plan exposes separate 5-hour and weekly token/credit limit entries plus a monthly `TIME_LIMIT` for MCP tools. The API renamed the limit type from `TOKENS_LIMIT` to `CREDIT_LIMIT` (same `unit`/`number` window semantics); `CREDIT_LIMIT` entries additionally carry `usage` (total), `currentValue` (consumed), and `remaining`, surfaced as a credit `valueLabel`, and the payload's `data.level` becomes `planLabel`. Web and VS Code must preserve these windows and stay in sync. diff --git a/packages/web/server/lib/quota/providers/claude/index.js b/packages/web/server/lib/quota/providers/claude/index.js index 8ee0d677..71fa3e43 100644 --- a/packages/web/server/lib/quota/providers/claude/index.js +++ b/packages/web/server/lib/quota/providers/claude/index.js @@ -81,7 +81,7 @@ const fetchQuotaUncoalesced = async () => { if (Date.now() < cooldownUntil) { return cachedResultFor(fingerprint, credential.planLabel) - ?? failure('Rate limited by Anthropic. Retrying shortly.'); + ?? failure('Rate limited. Retrying soon.'); } let response; @@ -100,7 +100,7 @@ const fetchQuotaUncoalesced = async () => { if (response.status === 429) { cooldownUntil = Date.now() + cooldownFromHeader(response); return cachedResultFor(fingerprint, credential.planLabel) - ?? failure('Rate limited by Anthropic. Retrying shortly.'); + ?? failure('Rate limited. Retrying soon.'); } if (response.status === 401 || response.status === 403) { diff --git a/packages/web/server/lib/quota/providers/zai.js b/packages/web/server/lib/quota/providers/zai.js index 0c526f99..b3d9efac 100644 --- a/packages/web/server/lib/quota/providers/zai.js +++ b/packages/web/server/lib/quota/providers/zai.js @@ -4,6 +4,7 @@ import { normalizeAuthEntry, buildResult, toUsageWindow, + toNumber, resolveWindowSeconds, resolveWindowLabel, normalizeTimestamp @@ -13,6 +14,20 @@ export const providerId = 'zai-coding-plan'; export const providerName = 'z.ai'; const aliases = ['zai-coding-plan', 'zai', 'z.ai']; +// CREDIT_LIMIT entries carry `usage` (total credits), `currentValue` (consumed), +// and `remaining`; TOKENS_LIMIT entries only carry a percentage. +const formatCreditAmount = (value) => { + if (value < 1000) return value.toLocaleString('en-US'); + return `${Math.round(value / 100) / 10}k`; +}; + +const formatCreditValueLabel = (limit) => { + const used = toNumber(limit?.currentValue); + const total = toNumber(limit?.usage); + if (used === null || total === null) return null; + return `${formatCreditAmount(used)} / ${formatCreditAmount(total)} credits`; +}; + export const isConfigured = () => { const auth = readAuthFile(); const entry = normalizeAuthEntry(getAuthEntry(auth, aliases)); @@ -56,16 +71,20 @@ export const fetchQuota = async () => { const payload = await response.json(); const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : []; const windows = {}; - for (const tokensLimit of limits.filter((limit) => limit?.type === 'TOKENS_LIMIT')) { - const windowSeconds = resolveWindowSeconds(tokensLimit); + // The API renamed TOKENS_LIMIT to CREDIT_LIMIT; field semantics stayed the same, + // so both limit types map to the same windows. + for (const limit of limits.filter((entry) => entry?.type === 'TOKENS_LIMIT' || entry?.type === 'CREDIT_LIMIT')) { + const windowSeconds = resolveWindowSeconds(limit); const windowLabel = resolveWindowLabel(windowSeconds); - const resetAt = tokensLimit?.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null; - const usedPercent = typeof tokensLimit?.percentage === 'number' ? tokensLimit.percentage : null; + const resetAt = limit?.nextResetTime ? normalizeTimestamp(limit.nextResetTime) : null; + const usedPercent = typeof limit?.percentage === 'number' ? limit.percentage : null; + const creditValueLabel = formatCreditValueLabel(limit); windows[windowLabel] = toUsageWindow({ usedPercent, windowSeconds, - resetAt + resetAt, + valueLabel: creditValueLabel }); } @@ -83,7 +102,8 @@ export const fetchQuota = async () => { providerName, ok: true, configured: true, - usage: { windows } + usage: { windows }, + planLabel: typeof payload?.data?.level === 'string' && payload.data.level ? payload.data.level : null }); } catch (error) { return buildResult({ diff --git a/packages/web/server/lib/quota/providers/zai.test.js b/packages/web/server/lib/quota/providers/zai.test.js index 39d5dcf5..d721cd9e 100644 --- a/packages/web/server/lib/quota/providers/zai.test.js +++ b/packages/web/server/lib/quota/providers/zai.test.js @@ -51,4 +51,37 @@ describe('Z.ai quota provider', () => { resetAt: 1787128459979, }); }); + + it('maps CREDIT_LIMIT entries to windows with credit value labels and plan level', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({ + code: 200, + data: { + limits: [ + { type: 'CREDIT_LIMIT', unit: 3, number: 5, usage: 12000, currentValue: 65, remaining: 11934, percentage: 1, nextResetTime: 1787257978907 }, + { type: 'CREDIT_LIMIT', unit: 6, number: 1, usage: 60000, currentValue: 65, remaining: 59934, percentage: 1, nextResetTime: 1787844668997 }, + ], + level: 'pro', + }, + }))); + + const result = await fetchQuota(); + const windows = result.usage.windows; + + expect(result.ok).toBe(true); + expect(result.planLabel).toBe('pro'); + expect(windows['5h']).toMatchObject({ + usedPercent: 1, + remainingPercent: 99, + windowSeconds: 5 * 60 * 60, + resetAt: 1787257978907, + valueLabel: '65 / 12k credits', + }); + expect(windows.weekly).toMatchObject({ + usedPercent: 1, + remainingPercent: 99, + windowSeconds: 7 * 24 * 60 * 60, + resetAt: 1787844668997, + valueLabel: '65 / 60k credits', + }); + }); });