fix: giảm spam 429 từ Claude OAuth usage endpoint
- claudeAutoPing: cache resetAt in-mem, bỏ qua poll usage cho tới gần reset - ProviderLimits: throttle auto-refresh Claude 3 phút, nút bấm tay vẫn refresh ngay - claude.js: 429 ở OAuth usage → cooldown 3 phút, fallback legacy (không ảnh hưởng chat) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={refreshAll}
|
||||
onClick={() => refreshAll(true)}
|
||||
disabled={refreshingAll}
|
||||
className="flex h-8 shrink-0 items-center gap-1 rounded-lg border border-black/10 px-2 text-xs text-text-primary transition-colors hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/5 disabled:opacity-50"
|
||||
title="Refresh all"
|
||||
|
||||
@@ -3,6 +3,8 @@ import { getModelsByProviderId } from "open-sse/config/providerModels.js";
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
export const QUOTA_CACHE_KEY = "quotaCacheData";
|
||||
export const REFRESH_INTERVAL_MS = 60000;
|
||||
// Claude usage/quota endpoint rate-limits; poll it less often than other providers
|
||||
export const CLAUDE_REFRESH_INTERVAL_MS = 180000;
|
||||
export const DEPLETED_QUOTA_THRESHOLD = 5;
|
||||
export const AUTO_REFRESH_STORAGE_KEY = "quotaAutoRefresh";
|
||||
export const CONNECTIONS_PAGE_SIZE = 20;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { CLAUDE_AUTOPING_CONFIG } from "@/shared/constants/config";
|
||||
const C = CLAUDE_AUTOPING_CONFIG;
|
||||
const PING_URL = "https://api.anthropic.com/v1/messages?beta=true";
|
||||
|
||||
const g = (global.__claudeAutoPing ??= { interval: null, running: false });
|
||||
const g = (global.__claudeAutoPing ??= { interval: null, running: false, resetCache: {} });
|
||||
|
||||
function buildProxyOptions(cfg) {
|
||||
return {
|
||||
@@ -43,6 +43,10 @@ async function sendPing(accessToken, proxyOptions) {
|
||||
}
|
||||
|
||||
async function pingConnection(conn) {
|
||||
// Cached resetAt is stable for the whole 5h window; skip usage poll until near reset
|
||||
const cachedReset = g.resetCache[conn.id];
|
||||
if (cachedReset && Date.now() < new Date(cachedReset).getTime() - C.refreshAheadMs) return;
|
||||
|
||||
const proxyCfg = await resolveConnectionProxyConfig(conn.providerSpecificData);
|
||||
const proxyOptions = buildProxyOptions(proxyCfg);
|
||||
|
||||
@@ -60,6 +64,9 @@ async function pingConnection(conn) {
|
||||
const resetAt = usage?.quotas?.[C.fiveHourKey]?.resetAt;
|
||||
if (!resetAt) return;
|
||||
|
||||
// Cache resetAt to gate future ticks
|
||||
g.resetCache[conn.id] = resetAt;
|
||||
|
||||
const resetMs = new Date(resetAt).getTime();
|
||||
const now = Date.now();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user