diff --git a/open-sse/services/usage/claude.js b/open-sse/services/usage/claude.js index db3448ae..85ab8e6f 100644 --- a/open-sse/services/usage/claude.js +++ b/open-sse/services/usage/claude.js @@ -14,11 +14,19 @@ const CLAUDE_CONFIG = { apiVersion: ANTHROPIC_API_VERSION, }; -/** - * Claude Usage - Primary: OAuth endpoint, Fallback: legacy settings/org endpoint - */ +// OAuth usage endpoint rate-limits (429); cool down per-token to stop hammering it. +// Only the quota endpoint is affected — chat with the same token still works. +const OAUTH_429_COOLDOWN_MS = 180000; +const oauthCooldown = new Map(); + export async function getClaudeUsage(accessToken, proxyOptions = null) { try { + // Skip OAuth usage call while this token is cooling down from a recent 429 + const cooldownUntil = oauthCooldown.get(accessToken); + if (cooldownUntil && Date.now() < cooldownUntil) { + return await getClaudeUsageLegacy(accessToken, proxyOptions); + } + // Primary: OAuth usage endpoint (Claude Code consumer OAuth tokens) const oauthResponse = await proxyAwareFetch(CLAUDE_CONFIG.oauthUsageUrl, { method: "GET", @@ -73,6 +81,11 @@ export async function getClaudeUsage(accessToken, proxyOptions = null) { }; } + // Cool down OAuth usage polling after a 429 (quota endpoint only) + if (oauthResponse.status === 429) { + oauthCooldown.set(accessToken, Date.now() + OAUTH_429_COOLDOWN_MS); + } + // Fallback: legacy settings + org usage endpoint console.warn(`[Claude Usage] OAuth endpoint returned ${oauthResponse.status}, falling back to legacy`); return await getClaudeUsageLegacy(accessToken, proxyOptions); diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js index f0e163ac..dfec68e3 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js @@ -26,6 +26,7 @@ import { setQuotaCache, QUOTA_CACHE_KEY, REFRESH_INTERVAL_MS, + CLAUDE_REFRESH_INTERVAL_MS, DEPLETED_QUOTA_THRESHOLD, AUTO_REFRESH_STORAGE_KEY, CONNECTIONS_PAGE_SIZE, @@ -131,6 +132,7 @@ export default function ProviderLimits() { const intervalRef = useRef(null); const countdownRef = useRef(null); + const tickCountRef = useRef(0); const fetchConnections = useCallback( async (targetPage = page) => { @@ -401,12 +403,18 @@ export default function ProviderLimits() { }; }, []); - const refreshAll = useCallback(async () => { + const refreshAll = useCallback(async (force = false) => { if (refreshingAll) return; setRefreshingAll(true); setCountdown(60); + // Throttle Claude: poll its quota every Nth auto-tick (manual force bypasses) + const tick = (tickCountRef.current += 1); + const claudeEvery = Math.round(CLAUDE_REFRESH_INTERVAL_MS / REFRESH_INTERVAL_MS); + const shouldFetch = (conn) => + force || conn.provider !== "claude" || tick % claudeEvery === 0; + try { const visibleConnections = await fetchConnections(page); @@ -419,7 +427,9 @@ export default function ProviderLimits() { ); await Promise.all( - visibleConnections.map((conn) => fetchQuota(conn.id, conn.provider)), + visibleConnections + .filter(shouldFetch) + .map((conn) => fetchQuota(conn.id, conn.provider)), ); setLastUpdated(new Date()); @@ -539,7 +549,7 @@ export default function ProviderLimits() { } } else if (autoRefresh && hasHydratedAutoRefresh) { // Resume auto-refresh when tab becomes visible - intervalRef.current = setInterval(refreshAll, REFRESH_INTERVAL_MS); + intervalRef.current = setInterval(() => refreshAll(), REFRESH_INTERVAL_MS); countdownRef.current = setInterval(() => { setCountdown((prev) => (prev <= 1 ? 60 : prev - 1)); }, 1000); @@ -866,7 +876,7 @@ export default function ProviderLimits() { {/* Refresh all button */}