Files
2026-07-13 13:39:12 +08:00

52 lines
1.8 KiB
TypeScript

/**
* Per-token cooldown for the Claude OAuth usage endpoint
* (`https://api.anthropic.com/api/oauth/usage`).
*
* Anthropic rate-limits this quota endpoint independently of `/v1/messages`.
* When polled from multiple connections at once (dashboard auto-refresh + the
* combo health-scheduler), it spams `429` and that surfaces as noisy provider
* errors plus increased upstream load. Chat with the same token still works.
*
* We track a per-access-token "skip until" timestamp. When we see a `429`,
* we suppress further OAuth-usage polls for that token until the cooldown
* expires and the caller falls back to the legacy settings/org endpoint
* (which is what `getClaudeUsage` does on every non-OK response).
*
* Pure helpers (no fetch, no timers) keep this TDD-friendly.
*
* Inspired-by upstream 9router commit `79df34ca`.
*/
export const OAUTH_USAGE_429_COOLDOWN_MS = 180_000; // 3 minutes
const oauthCooldown = new Map<string, number>();
/** Returns true while `accessToken` is still inside its 429 cooldown window. */
export function isClaudeOauthUsageCoolingDown(
accessToken: string | undefined,
now: number = Date.now()
): boolean {
if (!accessToken) return false;
const until = oauthCooldown.get(accessToken);
if (until === undefined) return false;
if (until > now) return true;
// Lazy GC of expired entries — keeps the Map bounded by active tokens.
oauthCooldown.delete(accessToken);
return false;
}
/** Record a 429 from the OAuth usage endpoint for `accessToken`. */
export function markClaudeOauthUsage429(
accessToken: string | undefined,
now: number = Date.now(),
cooldownMs: number = OAUTH_USAGE_429_COOLDOWN_MS
): void {
if (!accessToken) return;
oauthCooldown.set(accessToken, now + cooldownMs);
}
/** Test-only: clear all entries. */
export function _resetClaudeOauthUsageCooldown(): void {
oauthCooldown.clear();
}