fix(quota): coalesce provider usage refreshes
This commit is contained in:
@@ -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:
|
||||
|
||||
Binary file not shown.
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user