From 72cd861eb878ee94f21e564b5ba3464d7a9e9db5 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Fri, 14 Aug 2026 21:07:55 +0300 Subject: [PATCH] fix(quota): match the Claude Code integration provider to Claude quota The opencode-claude integration registers its provider as `claude-code`, which never matched the `claude` quota provider, so the collapsed Usage section in the work status panel showed no limit for a model from that integration. Claude windows also reported no duration, leaving the headline to fall back to whichever row came first instead of the limit that runs out soonest. The session and weekly windows now carry their length; extra usage stays without one because it is a monthly spend cap. --- .../chat/work-status/usageHeadline.test.ts | 4 ++++ .../chat/work-status/usageHeadline.ts | 4 ++++ .../lib/quota/providers/claude/transforms.js | 21 ++++++++++++------- .../quota/providers/claude/transforms.test.js | 9 ++++++++ 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/components/chat/work-status/usageHeadline.test.ts b/packages/ui/src/components/chat/work-status/usageHeadline.test.ts index 998808a0..64b66292 100644 --- a/packages/ui/src/components/chat/work-status/usageHeadline.test.ts +++ b/packages/ui/src/components/chat/work-status/usageHeadline.test.ts @@ -36,6 +36,10 @@ describe('resolveQuotaProviderId', () => { expect(resolveQuotaProviderId('anthropic')).toBe('claude'); }); + test('maps the opencode-claude integration provider onto Claude quota', () => { + expect(resolveQuotaProviderId('claude-code')).toBe('claude'); + }); + test('is case and whitespace tolerant, and rejects empties', () => { expect(resolveQuotaProviderId(' OpenAI ')).toBe('codex'); expect(resolveQuotaProviderId('')).toBeNull(); diff --git a/packages/ui/src/components/chat/work-status/usageHeadline.ts b/packages/ui/src/components/chat/work-status/usageHeadline.ts index ad80e553..502a1bc3 100644 --- a/packages/ui/src/components/chat/work-status/usageHeadline.ts +++ b/packages/ui/src/components/chat/work-status/usageHeadline.ts @@ -13,11 +13,15 @@ import type { UsageProviderGroup, UsageLimitRow } from '@/components/usage/usage /** * Quota provider ids mostly match OpenCode provider ids; these are the ones * that do not. Unmatched providers simply produce no headline. + * + * `claude-code` is the provider the opencode-claude integration registers, and + * it bills against the same Claude subscription the `claude` quota reports. */ const QUOTA_PROVIDER_ALIASES = new Map([ ['openai', 'codex'], ['chatgpt', 'codex'], ['anthropic', 'claude'], + ['claude-code', 'claude'], ['gemini', 'google'], ]); diff --git a/packages/web/server/lib/quota/providers/claude/transforms.js b/packages/web/server/lib/quota/providers/claude/transforms.js index 6ca6c0e4..c61a552a 100644 --- a/packages/web/server/lib/quota/providers/claude/transforms.js +++ b/packages/web/server/lib/quota/providers/claude/transforms.js @@ -16,6 +16,11 @@ const SESSION_WINDOW = '5h'; const WEEKLY_WINDOW = '7d'; const EXTRA_USAGE_WINDOW = 'extra_usage'; +// Consumers rank limits by how soon they run out, so each window carries its +// duration. Extra usage is a monthly spend cap, not a rolling window. +const SESSION_WINDOW_SECONDS = 5 * 60 * 60; +const WEEKLY_WINDOW_SECONDS = 7 * 24 * 60 * 60; + const asArray = (value) => (Array.isArray(value) ? value : []); /** Money in Anthropic's minor-unit form, e.g. `{ amount_minor: 10000, exponent: 2 }`. */ @@ -35,9 +40,9 @@ const formatSpendLabel = (used, limit, currency) => { return limitLabel === null ? `${prefix}${usedLabel}` : `${prefix}${usedLabel} / ${prefix}${limitLabel}`; }; -const addWindow = (target, key, { percent, resetAt, valueLabel }) => { +const addWindow = (target, key, { percent, resetAt, valueLabel, windowSeconds = null }) => { if (percent === null && !valueLabel) return; - target[key] = toUsageWindow({ usedPercent: percent, windowSeconds: null, resetAt, valueLabel }); + target[key] = toUsageWindow({ usedPercent: percent, windowSeconds, resetAt, valueLabel }); }; const applyLimitsArray = (limits, windows, models) => { @@ -49,16 +54,16 @@ const applyLimitsArray = (limits, windows, models) => { const modelName = asNonEmptyString(asObject(asObject(limit.scope)?.model)?.display_name); if (limit.kind === 'session') { - addWindow(windows, SESSION_WINDOW, { percent, resetAt }); + addWindow(windows, SESSION_WINDOW, { percent, resetAt, windowSeconds: SESSION_WINDOW_SECONDS }); continue; } if (limit.kind === 'weekly_all') { - addWindow(windows, WEEKLY_WINDOW, { percent, resetAt }); + addWindow(windows, WEEKLY_WINDOW, { percent, resetAt, windowSeconds: WEEKLY_WINDOW_SECONDS }); continue; } if (limit.kind === 'weekly_scoped' && modelName) { const modelWindows = {}; - addWindow(modelWindows, WEEKLY_WINDOW, { percent, resetAt }); + addWindow(modelWindows, WEEKLY_WINDOW, { percent, resetAt, windowSeconds: WEEKLY_WINDOW_SECONDS }); if (Object.keys(modelWindows).length > 0) models[modelName] = { windows: modelWindows }; } } @@ -70,13 +75,15 @@ const applyLegacyFields = (payload, windows) => { if (fiveHour) { addWindow(windows, SESSION_WINDOW, { percent: toNumber(fiveHour.utilization), - resetAt: toTimestamp(fiveHour.resets_at) + resetAt: toTimestamp(fiveHour.resets_at), + windowSeconds: SESSION_WINDOW_SECONDS }); } if (sevenDay) { addWindow(windows, WEEKLY_WINDOW, { percent: toNumber(sevenDay.utilization), - resetAt: toTimestamp(sevenDay.resets_at) + resetAt: toTimestamp(sevenDay.resets_at), + windowSeconds: WEEKLY_WINDOW_SECONDS }); } }; diff --git a/packages/web/server/lib/quota/providers/claude/transforms.test.js b/packages/web/server/lib/quota/providers/claude/transforms.test.js index 679b8ff9..77d20b3b 100644 --- a/packages/web/server/lib/quota/providers/claude/transforms.test.js +++ b/packages/web/server/lib/quota/providers/claude/transforms.test.js @@ -39,6 +39,15 @@ describe('Claude usage transforms', () => { expect(models.Fable.windows['7d'].usedPercent).toBe(12); }); + it('reports each window duration so callers can rank limits by urgency', () => { + const { windows, models } = toClaudeUsage(LIVE_PAYLOAD); + + expect(windows['5h'].windowSeconds).toBe(5 * 60 * 60); + expect(windows['7d'].windowSeconds).toBe(7 * 24 * 60 * 60); + expect(models.Fable.windows['7d'].windowSeconds).toBe(7 * 24 * 60 * 60); + expect(windows.extra_usage.windowSeconds).toBeNull(); + }); + it('reports extra usage as a spend window with a money label', () => { const { windows } = toClaudeUsage(LIVE_PAYLOAD);