mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-23 04:09:49 +00:00
feat(usage): claude quota tracker
This commit is contained in:
+74
-10
@@ -27,8 +27,10 @@ const CODEX_CONFIG = {
|
|||||||
|
|
||||||
// Claude API config
|
// Claude API config
|
||||||
const CLAUDE_CONFIG = {
|
const CLAUDE_CONFIG = {
|
||||||
|
oauthUsageUrl: "https://api.anthropic.com/api/oauth/usage",
|
||||||
usageUrl: "https://api.anthropic.com/v1/organizations/{org_id}/usage",
|
usageUrl: "https://api.anthropic.com/v1/organizations/{org_id}/usage",
|
||||||
settingsUrl: "https://api.anthropic.com/v1/settings",
|
settingsUrl: "https://api.anthropic.com/v1/settings",
|
||||||
|
apiVersion: "2023-06-01",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -358,33 +360,96 @@ async function getAntigravitySubscriptionInfo(accessToken) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claude Usage - Try to fetch from Anthropic API
|
* Claude Usage - Primary: OAuth endpoint, Fallback: legacy settings/org endpoint
|
||||||
*/
|
*/
|
||||||
async function getClaudeUsage(accessToken) {
|
async function getClaudeUsage(accessToken) {
|
||||||
try {
|
try {
|
||||||
// Try to get organization/account settings first
|
// Primary: OAuth usage endpoint (Claude Code consumer OAuth tokens)
|
||||||
const settingsResponse = await fetch("https://api.anthropic.com/v1/settings", {
|
const oauthResponse = await fetch(CLAUDE_CONFIG.oauthUsageUrl, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${accessToken}`,
|
"Authorization": `Bearer ${accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"anthropic-beta": "oauth-2025-04-20",
|
||||||
"anthropic-version": "2023-06-01",
|
"anthropic-version": CLAUDE_CONFIG.apiVersion,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: legacy settings + org usage endpoint
|
||||||
|
console.warn(`[Claude Usage] OAuth endpoint returned ${oauthResponse.status}, falling back to legacy`);
|
||||||
|
return await getClaudeUsageLegacy(accessToken);
|
||||||
|
} 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) {
|
||||||
|
try {
|
||||||
|
const settingsResponse = await fetch(CLAUDE_CONFIG.settingsUrl, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${accessToken}`,
|
||||||
|
"anthropic-version": CLAUDE_CONFIG.apiVersion,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (settingsResponse.ok) {
|
if (settingsResponse.ok) {
|
||||||
const settings = await settingsResponse.json();
|
const settings = await settingsResponse.json();
|
||||||
|
|
||||||
// Try usage endpoint if we have org info
|
|
||||||
if (settings.organization_id) {
|
if (settings.organization_id) {
|
||||||
const usageResponse = await fetch(
|
const usageResponse = await fetch(
|
||||||
`https://api.anthropic.com/v1/organizations/${settings.organization_id}/usage`,
|
CLAUDE_CONFIG.usageUrl.replace("{org_id}", settings.organization_id),
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${accessToken}`,
|
"Authorization": `Bearer ${accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"anthropic-version": CLAUDE_CONFIG.apiVersion,
|
||||||
"anthropic-version": "2023-06-01",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -406,7 +471,6 @@ async function getClaudeUsage(accessToken) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If settings API fails, OAuth token may not have required scope
|
|
||||||
return { message: "Claude connected. Usage API requires admin permissions." };
|
return { message: "Claude connected. Usage API requires admin permissions." };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { message: `Claude connected. Unable to fetch usage: ${error.message}` };
|
return { message: `Claude connected. Unable to fetch usage: ${error.message}` };
|
||||||
|
|||||||
@@ -247,22 +247,11 @@ export default function ProviderLimits() {
|
|||||||
USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) && conn.authType === "oauth"
|
USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) && conn.authType === "oauth"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sort providers: antigravity first, then kiro, then others alphabetically
|
// Sort providers by USAGE_SUPPORTED_PROVIDERS order, then alphabetically
|
||||||
const sortedConnections = [...filteredConnections].sort((a, b) => {
|
const sortedConnections = [...filteredConnections].sort((a, b) => {
|
||||||
const getProviderPriority = (provider) => {
|
const orderA = USAGE_SUPPORTED_PROVIDERS.indexOf(a.provider);
|
||||||
if (provider === "antigravity") return 1;
|
const orderB = USAGE_SUPPORTED_PROVIDERS.indexOf(b.provider);
|
||||||
if (provider === "kiro") return 2;
|
if (orderA !== orderB) return orderA - orderB;
|
||||||
return 3;
|
|
||||||
};
|
|
||||||
|
|
||||||
const priorityA = getProviderPriority(a.provider);
|
|
||||||
const priorityB = getProviderPriority(b.provider);
|
|
||||||
|
|
||||||
if (priorityA !== priorityB) {
|
|
||||||
return priorityA - priorityB;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same priority: sort alphabetically
|
|
||||||
return a.provider.localeCompare(b.provider);
|
return a.provider.localeCompare(b.provider);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -120,8 +120,7 @@ export async function GET(request, { params }) {
|
|||||||
const usage = await getUsageForProvider(connection);
|
const usage = await getUsageForProvider(connection);
|
||||||
return Response.json(usage);
|
return Response.json(usage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Usage API] Error fetching usage:", error);
|
console.warn(`[Usage] ${connection?.provider}: ${error.message}`);
|
||||||
console.error("[Usage API] Error stack:", error.stack);
|
|
||||||
return Response.json({ error: error.message }, { status: 500 });
|
return Response.json({ error: error.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,4 +105,4 @@ export const ID_TO_ALIAS = Object.values(AI_PROVIDERS).reduce((acc, p) => {
|
|||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// Providers that support usage/quota API
|
// Providers that support usage/quota API
|
||||||
export const USAGE_SUPPORTED_PROVIDERS = ["antigravity", "kiro", "github", "codex"];
|
export const USAGE_SUPPORTED_PROVIDERS = [ "claude", "antigravity", "kiro", "github", "codex"];
|
||||||
|
|||||||
Reference in New Issue
Block a user