Files
decolua--9router/open-sse/services/usage/claude.js
T
wehub-resource-sync 05fcd08057
Deploy GitBook to 9router.github.io / build-deploy (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:21:01 +08:00

148 lines
4.8 KiB
JavaScript

/**
* Claude usage handler
*/
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { ANTHROPIC_API_VERSION } from "../../providers/shared.js";
import { U, parseResetTime } from "./shared.js";
// Claude API config (urls from registry, apiVersion is header logic kept here)
const CLAUDE_CONFIG = {
oauthUsageUrl: U("claude").oauthUrl,
usageUrl: U("claude").orgUrl,
settingsUrl: U("claude").settingsUrl,
apiVersion: ANTHROPIC_API_VERSION,
};
// 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",
headers: {
"Authorization": `Bearer ${accessToken}`,
"anthropic-beta": "oauth-2025-04-20",
"anthropic-version": CLAUDE_CONFIG.apiVersion,
},
}, proxyOptions);
if (oauthResponse.ok) {
const data = await oauthResponse.json();
const quotas = {};
// utilization = % USED (e.g. 87 means 87% used, 13% remaining)
const hasUtilization = (window) =>
window && typeof window === "object" && typeof window.utilization === "number";
const createQuotaObject = (window) => {
const used = window.utilization;
const remaining = Math.max(0, 100 - used);
return {
used,
total: 100,
remaining,
remainingPercentage: remaining,
resetAt: parseResetTime(window.resets_at),
unlimited: false,
};
};
if (hasUtilization(data.five_hour)) {
quotas["session (5h)"] = createQuotaObject(data.five_hour);
}
if (hasUtilization(data.seven_day)) {
quotas["weekly (7d)"] = createQuotaObject(data.seven_day);
}
// Parse model-specific weekly windows (e.g. seven_day_sonnet, seven_day_opus)
for (const [key, value] of Object.entries(data)) {
if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(value)) {
const modelName = key.replace("seven_day_", "");
quotas[`weekly ${modelName} (7d)`] = createQuotaObject(value);
}
}
return {
plan: "Claude Code",
extraUsage: data.extra_usage ?? null,
quotas,
};
}
// 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);
} catch (error) {
return { message: `Claude connected. Unable to fetch usage: ${error.message}` };
}
}
/**
* Legacy Claude usage for API key / org admin users
*/
async function getClaudeUsageLegacy(accessToken, proxyOptions = null) {
try {
const settingsResponse = await proxyAwareFetch(CLAUDE_CONFIG.settingsUrl, {
method: "GET",
headers: {
"Authorization": `Bearer ${accessToken}`,
"anthropic-version": CLAUDE_CONFIG.apiVersion,
},
}, proxyOptions);
if (settingsResponse.ok) {
const settings = await settingsResponse.json();
if (settings.organization_id) {
const usageResponse = await proxyAwareFetch(
CLAUDE_CONFIG.usageUrl.replace("{org_id}", settings.organization_id),
{
method: "GET",
headers: {
"Authorization": `Bearer ${accessToken}`,
"anthropic-version": CLAUDE_CONFIG.apiVersion,
},
},
proxyOptions
);
if (usageResponse.ok) {
const usage = await usageResponse.json();
return {
plan: settings.plan || "Unknown",
organization: settings.organization_name,
quotas: usage,
};
}
}
return {
plan: settings.plan || "Unknown",
organization: settings.organization_name,
message: "Claude connected. Usage details require admin access.",
};
}
return { message: "Claude connected. Usage API requires admin permissions." };
} catch (error) {
return { message: `Claude connected. Unable to fetch usage: ${error.message}` };
}
}