fix(quota): coalesce provider usage refreshes

This commit is contained in:
Bohdan Triapitsyn
2026-08-18 19:13:58 +03:00
parent 84e940a9e4
commit 3326ad0ea8
7 changed files with 267 additions and 39 deletions
@@ -66,6 +66,7 @@ Claude quota reports the subscription limits Claude Code itself is bound by, rea
- **Limits come from the `limits` array**, keyed by `kind`: `session` maps to the `5h` window, `weekly_all` to `7d`, and `weekly_scoped` to a per-model `7d` window named by `scope.model.display_name`. The legacy `five_hour`/`seven_day` fields are only a fallback; `seven_day_sonnet`/`seven_day_opus` are no longer populated by Anthropic. Unrecognized limit kinds and Anthropic's rotating internal code names (`nimbus_quill`, `tangelo`, ...) are ignored rather than guessed at.
- **Extra usage** is reported as the `extra_usage` window from `spend`, only while `spend.enabled` is true, with a money `valueLabel`.
- **Rate limiting**: Anthropic returns 429 aggressively. The last successful usage payload is cached in memory and reserved during a cooldown (`Retry-After`, else five minutes, capped at one hour). The cache is keyed by a hash of the access and refresh tokens, so switching accounts drops it instead of showing the previous account's numbers.
- **Runtime parity**: Web/Electron and VS Code preserve the last successful Claude values during the same bounded 429 cooldown. Quota dispatchers also coalesce concurrent refreshes for the same provider in each runtime, while requests for different providers remain parallel.
## Add a new provider (quick steps)
1. Choose module shape based on complexity:
@@ -67,6 +67,24 @@ describe('Claude quota provider', () => {
expect(result.usage.windows['5h'].usedPercent).toBe(5);
});
it('coalesces concurrent refreshes into one Anthropic request', async () => {
let resolveResponse;
const fetchMock = vi.fn().mockReturnValue(new Promise((resolve) => {
resolveResponse = resolve;
}));
vi.stubGlobal('fetch', fetchMock);
const first = fetchQuota();
const second = fetchQuota();
resolveResponse(jsonResponse(PAYLOAD));
const [firstResult, secondResult] = await Promise.all([first, second]);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(firstResult.ok).toBe(true);
expect(secondResult.ok).toBe(true);
});
it('keeps serving the last good values while Anthropic rate limits', async () => {
const fetchMock = vi.fn()
.mockResolvedValueOnce(jsonResponse(PAYLOAD))
@@ -158,6 +158,8 @@ const registry = {
}
};
const pendingFetches = new Map();
export const listConfiguredQuotaProviders = () => {
const configured = [];
@@ -174,7 +176,7 @@ export const listConfiguredQuotaProviders = () => {
return configured;
};
export const fetchQuotaForProvider = async (providerId) => {
const fetchQuotaForProviderUncoalesced = async (providerId) => {
const provider = registry[providerId];
if (!provider) {
@@ -200,6 +202,17 @@ export const fetchQuotaForProvider = async (providerId) => {
}
};
export const fetchQuotaForProvider = (providerId) => {
const existing = pendingFetches.get(providerId);
if (existing) return existing;
const pending = fetchQuotaForProviderUncoalesced(providerId).finally(() => {
if (pendingFetches.get(providerId) === pending) pendingFetches.delete(providerId);
});
pendingFetches.set(providerId, pending);
return pending;
};
export const fetchClaudeQuota = claude.fetchQuota;
export const fetchOpenaiQuota = openai.fetchQuota;
export const fetchGoogleQuota = google.fetchGoogleQuota;
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';
import * as google from './google/index.js';
import { listConfiguredQuotaProviders } from './index.js';
import { fetchQuotaForProvider, listConfiguredQuotaProviders } from './index.js';
describe('quota provider registry', () => {
it('exposes google provider configuration helpers through the provider module', () => {
@@ -14,4 +14,13 @@ describe('quota provider registry', () => {
it('can list configured providers without missing provider exports', () => {
expect(() => listConfiguredQuotaProviders()).not.toThrow();
});
it('coalesces concurrent refreshes by provider ID', async () => {
const first = fetchQuotaForProvider('unsupported-test-provider');
const second = fetchQuotaForProvider('unsupported-test-provider');
expect(first).toBe(second);
await first;
expect(fetchQuotaForProvider('unsupported-test-provider')).not.toBe(first);
});
});