refactor(usage): extract 5 provider usage families into leaves (#5782)

Split open-sse/services/usage.ts (1723 -> 901 LOC) by moving the Cursor, Kimi,
Codex, Claude and Kiro usage-fetcher families into cohesive leaves under
open-sse/services/usage/ (mirroring the existing glm/minimax/antigravity/quota/
scalars leaves):

- usage/cursor.ts   getCursorUsage (+ CURSOR_USAGE_CONFIG, decodeCursorJwtSub)
- usage/kimi.ts     getKimiUsage (+ KIMI_CONFIG, getKimiPlanName)
- usage/codex.ts    getCodexUsage (+ CODEX_CONFIG)
- usage/claude.ts   getClaudeUsage / getClaudePlanLabel (+ CLAUDE_CONFIG, legacy)
- usage/kiro.ts     getKiroUsage / buildKiroUsageResult / discoverKiroProfileArn (+ helpers)

The host keeps the getUsageForProvider dispatcher and imports the fetchers back;
the public export set is unchanged — buildKiroUsageResult + discoverKiroProfileArn
are re-exported from the kiro leaf (the kiro-* tests import them from
services/usage) and __testing stays wired to the moved claude/kiro internals.
Bodies are verbatim: the code-line multiset of host + leaves equals the original.

Adds tests/unit/usage-families-split.test.ts pinning the leaf surface, the kiro
re-export identity, the __testing wiring, and getClaudePlanLabel's pure logic.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-01 04:13:17 -03:00
committed by GitHub
parent e2d2c2759b
commit d372a9af69
7 changed files with 968 additions and 829 deletions

View File

@@ -2,18 +2,13 @@
* Usage Fetcher - Get usage data from provider APIs
*/
import { buildCodexUsageQuotas } from "./codexUsageQuotas.ts";
import { getGlmQuotaUrl } from "../config/glmProvider.ts";
import { getGitHubCopilotInternalUserHeaders } from "../config/providerHeaderProfiles.ts";
import { safePercentage } from "@/shared/utils/formatting";
import { getDbInstance } from "@/lib/db/core";
import { fetchBailianQuota, type BailianTripleWindowQuota } from "./bailianQuotaFetcher.ts";
import { fetchDeepseekQuota, type DeepseekQuota } from "./deepseekQuotaFetcher.ts";
import { fetchOpencodeQuota, type OpencodeTripleWindowQuota } from "./opencodeQuotaFetcher.ts";
import { getOllamaCloudUsage, getOpenCodeGoUsage } from "./opencodeOllamaUsage.ts";
import { getCodeBuddyCnUsage } from "./usage/codebuddy-cn.ts";
import { CLAUDE_CODE_VERSION, fetchClaudeBootstrap } from "../executors/claudeIdentity.ts";
import { isClaudeOauthUsageCoolingDown, markClaudeOauthUsage429 } from "./claudeUsageCooldown.ts";
import {
extractCodeAssistOnboardTierId,
extractCodeAssistSubscriptionTier,
@@ -55,48 +50,21 @@ import {
mapCodeAssistTierIdToLabel,
mapSubscriptionTierStringToPlanLabel,
} from "./usage/antigravity.ts";
import { getCursorUsage } from "./usage/cursor.ts";
import { getKimiUsage } from "./usage/kimi.ts";
import { getCodexUsage } from "./usage/codex.ts";
import { getClaudeUsage, getClaudePlanLabel } from "./usage/claude.ts";
import { getKiroUsage, buildKiroUsageResult, discoverKiroProfileArn } from "./usage/kiro.ts";
// Re-exported para os testes kiro-* (importam de services/usage).
export { buildKiroUsageResult, discoverKiroProfileArn } from "./usage/kiro.ts";
// Quota / usage upstream URLs (overridable for testing or relays).
const CROF_USAGE_URL = process.env.OMNIROUTE_CROF_USAGE_URL ?? "https://crof.ai/usage_api/";
const CODEWHISPERER_BASE_URL =
process.env.OMNIROUTE_CODEWHISPERER_BASE_URL ?? "https://codewhisperer.us-east-1.amazonaws.com";
// Codex (OpenAI) API config
const CODEX_CONFIG = {
usageUrl: "https://chatgpt.com/backend-api/wham/usage",
};
// Claude API config
const CLAUDE_CONFIG = {
oauthUsageUrl: "https://api.anthropic.com/api/oauth/usage",
usageUrl: "https://api.anthropic.com/v1/organizations/{org_id}/usage",
settingsUrl: "https://api.anthropic.com/v1/settings",
apiVersion: "2023-06-01",
};
// Kimi Coding API config
const KIMI_CONFIG = {
baseUrl: "https://api.kimi.com/coding/v1",
usageUrl: "https://api.kimi.com/coding/v1/usages",
apiVersion: "2023-06-01",
};
const NANOGPT_CONFIG = {
usageUrl: "https://nano-gpt.com/api/subscription/v1/usage",
};
// Cursor dashboard usage API config
// The endpoint that powers https://cursor.com/dashboard/spending. Validates the WorkOS
// session via the WorkosCursorSessionToken cookie (format: `${userId}::${jwt}`) and
// rejects requests without a matching Origin/Referer (Invalid origin for state-changing request).
const CURSOR_USAGE_CONFIG = {
usageUrl: "https://cursor.com/api/dashboard/get-current-period-usage",
origin: "https://cursor.com",
referer: "https://cursor.com/dashboard/spending",
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
};
type JsonRecord = Record<string, unknown>;
type UsageProviderConnection = JsonRecord & {
id?: string;
@@ -114,57 +82,6 @@ function shouldDisplayGitHubQuota(quota: UsageQuota | null): quota is UsageQuota
return quota.total > 0 || quota.remainingPercentage !== undefined;
}
function isKiroOverageEnabled(data: JsonRecord): boolean {
const overageConfiguration = toRecord(data.overageConfiguration);
const overageStatus = String(overageConfiguration.overageStatus || "")
.trim()
.toUpperCase();
return (
overageStatus === "ENABLED" ||
data.overageEnabled === true ||
overageConfiguration.overageEnabled === true
);
}
function buildKiroQuota(
used: number,
total: number,
resetAt: string | null,
overageEnabled: boolean
): UsageQuota {
const remaining = total - used;
if (!overageEnabled) {
return { used, total, remaining, resetAt, unlimited: false };
}
return {
used,
total,
remaining,
remainingPercentage: 100,
resetAt,
unlimited: true,
};
}
function getClaudePlanLabel(...candidates: Array<string | null | undefined>): string | null {
for (const candidate of candidates) {
if (typeof candidate !== "string") continue;
const trimmed = candidate.trim();
if (
!trimmed ||
trimmed.toLowerCase() === "claude code" ||
trimmed.toLowerCase() === "unknown"
) {
continue;
}
return trimmed;
}
return null;
}
// CrofAI surfaces a tiny endpoint with two signals:
// GET https://crof.ai/usage_api/ → { usable_requests: number|null, credits: number }
// `usable_requests` is the daily request bucket on a subscription plan; `null`
@@ -545,143 +462,6 @@ async function getNanoGptUsage(apiKey: string) {
}
}
/**
* Decode the `sub` claim of a Cursor JWT (the WorkOS user id).
* Returns null if the token is not a parseable JWT.
*/
function decodeCursorJwtSub(token: string): string | null {
if (!token || typeof token !== "string") return null;
const parts = token.split(".");
if (parts.length !== 3) return null;
try {
let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
while (payload.length % 4 !== 0) payload += "=";
const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
const sub = decoded?.sub;
return typeof sub === "string" && sub.length > 0 ? sub : null;
} catch {
return null;
}
}
/**
* Cursor Pro Plan Usage
* Fetches current-billing-cycle spend from the cursor.com dashboard API and exposes three
* windows that mirror the cursor.com/dashboard/spending UI: Total / Auto + Composer / API.
*/
async function getCursorUsage(accessToken: string, providerSpecificData?: unknown) {
if (!accessToken) {
return { message: "Cursor access token missing. Re-import the connection from Cursor IDE." };
}
const storedUserId = (() => {
const raw = toRecord(providerSpecificData).userId;
return typeof raw === "string" && raw.length > 0 ? raw : null;
})();
const userId = storedUserId || decodeCursorJwtSub(accessToken);
if (!userId) {
return {
message: "Cursor token missing user id. Re-import the connection from Cursor IDE.",
};
}
try {
const response = await fetch(CURSOR_USAGE_CONFIG.usageUrl, {
method: "POST",
redirect: "manual",
headers: {
Cookie: `WorkosCursorSessionToken=${userId}::${accessToken}`,
Origin: CURSOR_USAGE_CONFIG.origin,
Referer: CURSOR_USAGE_CONFIG.referer,
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent": CURSOR_USAGE_CONFIG.userAgent,
},
body: "{}",
});
// 3xx redirect to WorkOS authkit means the session cookie was rejected.
if (response.status >= 300 && response.status < 400) {
return {
plan: "Cursor",
message: "Cursor session expired. Re-import the token from Cursor IDE.",
};
}
if (!response.ok) {
const errorText = (await response.text()).slice(0, 200);
if (response.status === 401 || response.status === 403) {
return {
plan: "Cursor",
message: "Cursor session unauthorized. Re-import the token from Cursor IDE.",
};
}
return {
plan: "Cursor",
message: `Cursor usage endpoint error (${response.status}): ${errorText}`,
};
}
const data = toRecord(await response.json());
const planUsage = toRecord(data.planUsage);
if (Object.keys(planUsage).length === 0) {
return {
plan: "Cursor",
message: "Cursor connected. No active plan usage returned.",
};
}
const limitCents = Math.max(0, toNumber(planUsage.limit, 0));
const totalSpendCents = Math.max(0, toNumber(planUsage.totalSpend, 0));
const autoPercentUsed = clampPercentage(toNumber(planUsage.autoPercentUsed, 0));
const apiPercentUsed = clampPercentage(toNumber(planUsage.apiPercentUsed, 0));
const totalPercentUsed = clampPercentage(toNumber(planUsage.totalPercentUsed, 0));
// billingCycleEnd is a numeric-string in ms; coerce so parseResetTime sees a number.
const billingCycleEndMs = toNumber(data.billingCycleEnd, 0);
const resetAt = billingCycleEndMs > 0 ? parseResetTime(billingCycleEndMs) : null;
// Convert cents → dollars rounded to 2 decimal places.
const toDollars = (cents: number) => Math.round(cents) / 100;
const limitDollars = toDollars(limitCents);
const buildWindow = (percentUsed: number, usedCentsOverride?: number): UsageQuota => {
const usedCents =
typeof usedCentsOverride === "number"
? usedCentsOverride
: Math.round((limitCents * percentUsed) / 100);
const used = toDollars(Math.min(usedCents, limitCents));
const remaining = toDollars(Math.max(limitCents - Math.min(usedCents, limitCents), 0));
return {
used,
total: limitDollars,
remaining,
remainingPercentage: clampPercentage(100 - percentUsed),
resetAt,
unlimited: false,
};
};
const quotas: Record<string, UsageQuota> = {
Total: buildWindow(totalPercentUsed, totalSpendCents),
"Auto + Composer": buildWindow(autoPercentUsed),
API: buildWindow(apiPercentUsed),
};
return {
plan: "Cursor Pro",
quotas,
};
} catch (error) {
return {
plan: "Cursor",
message: `Cursor connected. Unable to fetch usage: ${(error as Error).message}`,
};
}
}
/**
* Single source of truth for which providers have a `getUsageForProvider`
* implementation. Consumers like `genericQuotaFetcher.ts` reference this so
@@ -1011,421 +791,6 @@ function inferGitHubPlanName(data: JsonRecord, premiumQuota: UsageQuota | null):
return "GitHub Copilot";
}
/**
* Claude Usage - Try to fetch from Anthropic API
*/
async function getClaudeUsage(accessToken?: string) {
if (!accessToken) {
return { message: "Claude connected. Access token not available.", bootstrap: null };
}
// Refresh bootstrap in parallel; best-effort, failure non-fatal.
const bootstrapPromise = fetchClaudeBootstrap(accessToken).catch(() => null);
// Skip OAuth usage call while this token is cooling down from a recent 429
// (chat with the same token still works — only the quota endpoint is throttled).
if (isClaudeOauthUsageCoolingDown(accessToken)) {
const legacy = await getClaudeUsageLegacy(accessToken);
return { ...legacy, bootstrap: await bootstrapPromise };
}
try {
// Real CLI uses axios here, not Stainless — UA is `claude-code/<version>`
// (not `claude-cli/...`) and the shape is simpler than /v1/messages.
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 10_000);
let oauthResponse;
try {
oauthResponse = await fetch(CLAUDE_CONFIG.oauthUsageUrl, {
method: "GET",
headers: {
Accept: "application/json, text/plain, */*",
"Accept-Encoding": "gzip, compress, deflate, br",
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": `claude-code/${CLAUDE_CODE_VERSION}`,
"anthropic-beta": "oauth-2025-04-20",
},
signal: ctrl.signal,
});
} finally {
clearTimeout(timer);
}
if (oauthResponse.ok) {
const data = toRecord(await oauthResponse.json());
const quotas: Record<string, UsageQuota> = {};
// utilization = percentage USED (e.g., 90 means 90% used, 10% remaining)
// Confirmed via user report #299: Claude.ai shows 87% used = OmniRoute must show 13% remaining.
const hasUtilization = (window: JsonRecord) =>
window && typeof window === "object" && safePercentage(window.utilization) !== undefined;
const createQuotaObject = (window: JsonRecord) => {
const used = safePercentage(window.utilization) as number; // utilization = % used
const remaining = Math.max(0, 100 - used);
return {
used,
total: 100,
remaining,
resetAt: parseResetTime(window.resets_at),
remainingPercentage: remaining,
unlimited: false,
};
};
const fiveHour = toRecord(data.five_hour);
if (hasUtilization(fiveHour)) {
quotas["session (5h)"] = createQuotaObject(fiveHour);
}
const sevenDay = toRecord(data.seven_day);
if (hasUtilization(sevenDay)) {
quotas["weekly (7d)"] = createQuotaObject(sevenDay);
}
// Map Anthropic's internal codenames (e.g., omelette → Designer) for display.
const MODEL_DISPLAY_NAMES: Record<string, string> = {
omelette: "designer",
};
for (const [key, value] of Object.entries(data)) {
const valueRecord = toRecord(value);
if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(valueRecord)) {
const codename = key.replace("seven_day_", "");
const modelName = MODEL_DISPLAY_NAMES[codename] || codename;
quotas[`weekly ${modelName} (7d)`] = createQuotaObject(valueRecord);
}
}
const bootstrap = await bootstrapPromise;
const plan =
getClaudePlanLabel(
typeof data.tier === "string" ? data.tier : null,
typeof data.plan === "string" ? data.plan : null,
typeof data.subscription_type === "string" ? data.subscription_type : null,
bootstrap?.organization_rate_limit_tier
) ?? undefined;
return {
...(plan ? { plan } : {}),
quotas,
extraUsage: data.extra_usage ?? null,
bootstrap,
};
}
// Cool down OAuth usage polling after a 429 (quota endpoint only — chat is unaffected).
if (oauthResponse.status === 429) {
markClaudeOauthUsage429(accessToken);
}
// Fallback: OAuth endpoint returned non-OK, try legacy settings/org endpoint
console.warn(
`[Claude Usage] OAuth endpoint returned ${oauthResponse.status}, falling back to legacy`
);
const legacy = await getClaudeUsageLegacy(accessToken);
return { ...legacy, bootstrap: await bootstrapPromise };
} catch (error) {
return {
message: `Claude connected. Unable to fetch usage: ${(error as Error).message}`,
bootstrap: await bootstrapPromise,
};
}
}
/**
* Legacy Claude usage fetcher for API key / org admin users.
* Uses /v1/settings + /v1/organizations/{org_id}/usage endpoints.
*/
async function getClaudeUsageLegacy(accessToken?: string) {
try {
const settingsResponse = await fetch(CLAUDE_CONFIG.settingsUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"anthropic-version": CLAUDE_CONFIG.apiVersion,
},
});
if (settingsResponse.ok) {
const settings = toRecord(await settingsResponse.json());
const organizationId =
typeof settings.organization_id === "string" ? settings.organization_id : "";
if (organizationId) {
const usageResponse = await fetch(
CLAUDE_CONFIG.usageUrl.replace("{org_id}", organizationId),
{
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"anthropic-version": CLAUDE_CONFIG.apiVersion,
},
}
);
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 as Error).message}` };
}
}
/**
* Codex (OpenAI) Usage - Fetch from ChatGPT backend API
* IMPORTANT: Uses persisted workspaceId from OAuth to ensure correct workspace binding.
* No fallback to other workspaces - strict binding to user's selected workspace.
*/
async function getCodexUsage(
accessToken?: string,
providerSpecificData: Record<string, unknown> = {}
) {
try {
// Use persisted workspace ID from OAuth - NO FALLBACK
const accountId =
typeof providerSpecificData.workspaceId === "string"
? providerSpecificData.workspaceId
: null;
const headers: Record<string, string> = {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
Accept: "application/json",
};
if (accountId) {
headers["chatgpt-account-id"] = accountId;
}
const response = await fetch(CODEX_CONFIG.usageUrl, {
method: "GET",
headers,
});
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
return {
message: `Codex token expired or access denied. Please re-authenticate the connection.`,
};
}
throw new Error(`Codex API error: ${response.status}`);
}
const data = await response.json();
const { rateLimit, quotas } = buildCodexUsageQuotas(data);
return {
plan: String(getFieldValue(data, "plan_type", "planType") || "unknown"),
limitReached: Boolean(getFieldValue(rateLimit, "limit_reached", "limitReached")),
quotas,
};
} catch (error) {
return { message: `Failed to fetch Codex usage: ${(error as Error).message}` };
}
}
/**
* Build the Kiro usage result from a GetUsageLimits response. When the account returns no
* usage breakdown (some AWS IAM / Builder ID accounts don't expose per-resource quota via
* GetUsageLimits), return an informative message instead of empty `quotas:{}` — otherwise the
* dashboard renders a blank quota card with no explanation (#3506). Exported for testing.
*/
export function buildKiroUsageResult(
data: JsonRecord
): { plan: string; quotas: Record<string, UsageQuota> } | { message: string } {
const usageList = Array.isArray(data.usageBreakdownList) ? data.usageBreakdownList : [];
const quotaInfo: Record<string, UsageQuota> = {};
const resetAt = parseResetTime(data.nextDateReset || data.resetDate);
const overageEnabled = isKiroOverageEnabled(data);
usageList.forEach((breakdownValue: unknown) => {
const breakdown = toRecord(breakdownValue);
const resourceType =
typeof breakdown.resourceType === "string" ? breakdown.resourceType.toLowerCase() : "unknown";
const used = toNumber(breakdown.currentUsageWithPrecision, 0);
const total = toNumber(breakdown.usageLimitWithPrecision, 0);
quotaInfo[resourceType] = buildKiroQuota(used, total, resetAt, overageEnabled);
const freeTrialInfo = toRecord(breakdown.freeTrialInfo);
if (Object.keys(freeTrialInfo).length > 0) {
const freeUsed = toNumber(freeTrialInfo.currentUsageWithPrecision, 0);
const freeTotal = toNumber(freeTrialInfo.usageLimitWithPrecision, 0);
quotaInfo[`${resourceType}_freetrial`] = buildKiroQuota(
freeUsed,
freeTotal,
resetAt,
overageEnabled
);
}
});
if (Object.keys(quotaInfo).length === 0) {
return {
message:
"Kiro connected, but the account returned no usage breakdown. Some AWS IAM / Builder ID accounts don't expose per-resource quota via GetUsageLimits.",
};
}
return {
plan: String(toRecord(data.subscriptionInfo).subscriptionTitle || "").trim() || "Kiro",
quotas: quotaInfo,
};
}
/**
* Discover a Kiro/CodeWhisperer profile ARN for an account that didn't persist one (common for
* AWS IAM Identity Center logins and kiro-cli imports). Calls ListAvailableProfiles on the
* region-matched endpoint and prefers a profile whose ARN is in the same region. Returns
* undefined when no profile is available (e.g. the org/token has no Kiro entitlement).
* Exported for testing.
*/
export async function discoverKiroProfileArn(
accessToken: string,
usageBaseUrl: string,
region: string
): Promise<string | undefined> {
try {
const response = await fetch(usageBaseUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/x-amz-json-1.0",
"x-amz-target": "AmazonCodeWhispererService.ListAvailableProfiles",
Accept: "application/json",
},
body: JSON.stringify({ maxResults: 10 }),
// Don't let a hung profile lookup block the usage/quota refresh indefinitely.
signal: AbortSignal.timeout(10000),
});
if (!response.ok) return undefined;
const data = toRecord(await response.json());
const profiles = Array.isArray(data.profiles) ? data.profiles : [];
const normalizedRegion = region.toLowerCase();
const matched =
profiles.find((profile: unknown) => {
const arn = toRecord(profile).arn;
return typeof arn === "string" && arn.toLowerCase().includes(`:${normalizedRegion}:`);
}) || profiles[0];
const arn = toRecord(matched).arn;
return typeof arn === "string" && arn.length > 0 ? arn : undefined;
} catch {
return undefined;
}
}
/**
* Kiro (AWS CodeWhisperer) Usage
*/
async function getKiroUsage(accessToken?: string, providerSpecificData?: JsonRecord) {
try {
let profileArn =
typeof providerSpecificData?.profileArn === "string"
? providerSpecificData.profileArn
: undefined;
// Enterprise IAM Identity Center accounts are region-bound: the profileArn, token and
// endpoint must all match the region. Derive the region from the stored region (preferred)
// or the profileArn, then route to the regional Amazon Q endpoint (us-east-1 keeps the
// legacy codewhisperer host; codewhisperer.{region} does not resolve for other regions).
const regionFromArn = profileArn
? profileArn.toLowerCase().match(/^arn:aws:codewhisperer:([a-z0-9-]+):/)?.[1]
: undefined;
const region =
(typeof providerSpecificData?.region === "string" &&
providerSpecificData.region.trim().toLowerCase()) ||
regionFromArn ||
"us-east-1";
const usageBaseUrl =
region === "us-east-1" ? CODEWHISPERER_BASE_URL : `https://q.${region}.amazonaws.com`;
// IAM Identity Center logins and kiro-cli imports frequently don't persist a profileArn, which
// previously caused the quota card to show nothing ("0 used"). Discover it on demand from
// ListAvailableProfiles (region-matched) so usage still resolves for those accounts.
if (!profileArn && accessToken) {
profileArn = await discoverKiroProfileArn(accessToken, usageBaseUrl, region);
}
if (!profileArn) {
return { message: "Kiro connected. Profile ARN not available for quota tracking." };
}
// Kiro uses AWS CodeWhisperer GetUsageLimits API
const payload = {
origin: "AI_EDITOR",
profileArn: profileArn,
resourceType: "AGENTIC_REQUEST",
};
const response = await fetch(usageBaseUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/x-amz-json-1.0",
"x-amz-target": "AmazonCodeWhispererService.GetUsageLimits",
Accept: "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
// Social-auth Kiro accounts (added via /api/oauth/kiro/social-exchange with provider
// Google or GitHub) use a different token format that AWS CodeWhisperer's GetUsageLimits
// routinely rejects with 401/403, even when /messages still works. Surface a clear
// "auth expired, chat may still work" message instead of a generic upstream-error blob
// so the quota card matches what users with legacy social-auth accounts already see.
// Inspired by https://github.com/decolua/9router/pull/620.
if (
(response.status === 401 || response.status === 403) &&
isSocialAuthKiroAccount(providerSpecificData)
) {
return {
message: "Kiro quota API authentication expired. Chat may still work.",
quotas: {},
};
}
const errorText = await response.text();
throw new Error(`Kiro API error (${response.status}): ${errorText}`);
}
const data = toRecord(await response.json());
return buildKiroUsageResult(data);
} catch (error) {
throw new Error(`Failed to fetch Kiro usage: ${error.message}`);
}
}
/**
* Was this Kiro connection added via the Google/GitHub social-auth device flow
* (POST /api/oauth/kiro/social-exchange)? That route persists
* `{ authMethod: "imported", provider: "Google" | "Github" }` on the connection.
* Builder-ID / IDC / kiro-cli imports use different markers and should keep the
* existing throw-on-failure behavior.
*/
function isSocialAuthKiroAccount(providerSpecificData?: JsonRecord): boolean {
if (!providerSpecificData || providerSpecificData.authMethod !== "imported") return false;
const provider =
typeof providerSpecificData.provider === "string"
? providerSpecificData.provider.toLowerCase()
: "";
return provider === "google" || provider === "github";
}
/**
* Vertex AI — SELF-TRACKED spend.
*
@@ -1473,193 +838,6 @@ async function getVertexUsage(connectionId: string, provider: string) {
}
}
/**
* Map Kimi membership level to display name
* LEVEL_BASIC = Moderato, LEVEL_INTERMEDIATE = Allegretto,
* LEVEL_ADVANCED = Allegro, LEVEL_STANDARD = Vivace
*/
function getKimiPlanName(level: unknown): string {
if (!level) return "";
const normalizedLevel = String(level);
const levelMap = {
LEVEL_BASIC: "Moderato",
LEVEL_INTERMEDIATE: "Allegretto",
LEVEL_ADVANCED: "Allegro",
LEVEL_STANDARD: "Vivace",
};
return (
levelMap[normalizedLevel as keyof typeof levelMap] ||
normalizedLevel.replace("LEVEL_", "").toLowerCase()
);
}
/**
* Kimi Coding Usage - Fetch quota from Kimi API
* Uses the official /v1/usages endpoint with custom X-Msh-* headers
*/
async function getKimiUsage(accessToken?: string, apiKey?: string) {
// Generate device info for headers (same as OAuth flow)
const deviceId = "kimi-usage-" + Date.now();
const platform = "omniroute";
const version = "2.1.2";
const deviceModel =
typeof process !== "undefined" ? `${process.platform} ${process.arch}` : "unknown";
// API key auth takes precedence — Kimi's /usages endpoint accepts the same
// API key used for /messages (verified live: responds with
// authentication.method = METHOD_API_KEY). OAuth flow falls through to the
// Bearer + device-headers shape used by Kimi Coding OAuth.
const useApiKey = typeof apiKey === "string" && apiKey.length > 0;
const authHeaders: Record<string, string> = useApiKey
? { "x-api-key": apiKey as string }
: {
Authorization: `Bearer ${accessToken}`,
"X-Msh-Platform": platform,
"X-Msh-Version": version,
"X-Msh-Device-Model": deviceModel,
"X-Msh-Device-Id": deviceId,
};
try {
const response = await fetch(KIMI_CONFIG.usageUrl, {
method: "GET",
headers: {
...authHeaders,
"Content-Type": "application/json",
},
});
const responseText = await response.text();
if (!response.ok) {
return {
plan: "Kimi Coding",
message: `Kimi Coding connected. API Error ${response.status}: ${responseText.slice(0, 100)}`,
};
}
let data;
try {
data = JSON.parse(responseText);
} catch {
return {
plan: "Kimi Coding",
message: "Kimi Coding connected. Invalid JSON response from API.",
};
}
const quotas: Record<string, UsageQuota> = {};
const dataObj = toRecord(data);
// Parse Kimi usage response format
// Format: { user: {...}, usage: { limit: "100", used: "92", remaining: "8", resetTime: "..." }, limits: [...] }
const usageObj = toRecord(dataObj.usage);
// Check for Kimi's actual usage fields (strings, not numbers)
const usageLimit = toNumber(usageObj.limit || usageObj.Limit, 0);
const usageUsed = toNumber(usageObj.used || usageObj.Used, 0);
const usageRemaining = toNumber(usageObj.remaining || usageObj.Remaining, 0);
const usageResetTime =
usageObj.resetTime || usageObj.ResetTime || usageObj.reset_at || usageObj.resetAt;
if (usageLimit > 0) {
const percentRemaining = usageLimit > 0 ? (usageRemaining / usageLimit) * 100 : 0;
quotas["Weekly"] = {
used: usageUsed,
total: usageLimit,
remaining: usageRemaining,
remainingPercentage: percentRemaining,
resetAt: parseResetTime(usageResetTime),
unlimited: false,
};
}
// Also parse limits array for rate limits
const limitsArray = Array.isArray(dataObj.limits) ? dataObj.limits : [];
for (let i = 0; i < limitsArray.length; i++) {
const limitItem = toRecord(limitsArray[i]);
const window = toRecord(limitItem.window);
const detail = toRecord(limitItem.detail);
const limit = toNumber(detail.limit || detail.Limit, 0);
const remaining = toNumber(detail.remaining || detail.Remaining, 0);
const resetTime = detail.resetTime || detail.reset_at || detail.resetAt;
if (limit > 0) {
quotas["Ratelimit"] = {
used: limit - remaining,
total: limit,
remaining,
remainingPercentage: limit > 0 ? (remaining / limit) * 100 : 0,
resetAt: parseResetTime(resetTime),
unlimited: false,
};
}
}
// Check for quota windows (Claude-like format with utilization) as fallback
const hasUtilization = (window: JsonRecord) =>
window && typeof window === "object" && safePercentage(window.utilization) !== undefined;
const createQuotaObject = (window: JsonRecord) => {
const remaining = safePercentage(window.utilization) as number;
const used = 100 - remaining;
return {
used,
total: 100,
remaining,
resetAt: parseResetTime(window.resets_at),
remainingPercentage: remaining,
unlimited: false,
};
};
if (hasUtilization(toRecord(dataObj.five_hour))) {
quotas["session (5h)"] = createQuotaObject(toRecord(dataObj.five_hour));
}
if (hasUtilization(toRecord(dataObj.seven_day))) {
quotas["weekly (7d)"] = createQuotaObject(toRecord(dataObj.seven_day));
}
// Check for model-specific quotas
for (const [key, value] of Object.entries(dataObj)) {
const valueRecord = toRecord(value);
if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(valueRecord)) {
const modelName = key.replace("seven_day_", "");
quotas[`weekly ${modelName} (7d)`] = createQuotaObject(valueRecord);
}
}
if (Object.keys(quotas).length > 0) {
const userRecord = toRecord(dataObj.user);
const membershipLevel = toRecord(userRecord.membership).level;
const planName = getKimiPlanName(membershipLevel);
return {
plan: planName || "Kimi Coding",
quotas,
};
}
// No quota data in response
const userRecord = toRecord(dataObj.user);
const membershipLevel = toRecord(userRecord.membership).level;
const planName = getKimiPlanName(membershipLevel);
return {
plan: planName || "Kimi Coding",
message: "Kimi Coding connected. Usage tracked per request.",
};
} catch (error) {
return {
message: `Kimi Coding connected. Unable to fetch usage: ${(error as Error).message}`,
};
}
}
/**
* Qwen Usage
*/

View File

@@ -0,0 +1,216 @@
/**
* usage/claude.ts — Claude (Anthropic OAuth + legacy org) usage fetcher + plan-label helper.
*
* Extracted from services/usage.ts (god-file decomposition): the Claude family — the API
* config, the plan-label picker, the OAuth usage fetcher (getClaudeUsage) with its legacy
* settings/org fallback (getClaudeUsageLegacy). Depends only on the sibling scalar/quota
* leaves + Claude identity/cooldown helpers + safePercentage — no host coupling — so it
* lives as a co-located provider leaf. usage.ts imports getClaudeUsage (dispatcher) +
* getClaudePlanLabel (__testing). Behavior-preserving move.
*/
import { safePercentage } from "@/shared/utils/formatting";
import { CLAUDE_CODE_VERSION, fetchClaudeBootstrap } from "../../executors/claudeIdentity.ts";
import { isClaudeOauthUsageCoolingDown, markClaudeOauthUsage429 } from "../claudeUsageCooldown.ts";
import { toRecord } from "./scalars.ts";
import { type UsageQuota, parseResetTime } from "./quota.ts";
type JsonRecord = Record<string, unknown>;
// Claude API config
const CLAUDE_CONFIG = {
oauthUsageUrl: "https://api.anthropic.com/api/oauth/usage",
usageUrl: "https://api.anthropic.com/v1/organizations/{org_id}/usage",
settingsUrl: "https://api.anthropic.com/v1/settings",
apiVersion: "2023-06-01",
};
export function getClaudePlanLabel(...candidates: Array<string | null | undefined>): string | null {
for (const candidate of candidates) {
if (typeof candidate !== "string") continue;
const trimmed = candidate.trim();
if (
!trimmed ||
trimmed.toLowerCase() === "claude code" ||
trimmed.toLowerCase() === "unknown"
) {
continue;
}
return trimmed;
}
return null;
}
/**
* Claude Usage - Try to fetch from Anthropic API
*/
export async function getClaudeUsage(accessToken?: string) {
if (!accessToken) {
return { message: "Claude connected. Access token not available.", bootstrap: null };
}
// Refresh bootstrap in parallel; best-effort, failure non-fatal.
const bootstrapPromise = fetchClaudeBootstrap(accessToken).catch(() => null);
// Skip OAuth usage call while this token is cooling down from a recent 429
// (chat with the same token still works — only the quota endpoint is throttled).
if (isClaudeOauthUsageCoolingDown(accessToken)) {
const legacy = await getClaudeUsageLegacy(accessToken);
return { ...legacy, bootstrap: await bootstrapPromise };
}
try {
// Real CLI uses axios here, not Stainless — UA is `claude-code/<version>`
// (not `claude-cli/...`) and the shape is simpler than /v1/messages.
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 10_000);
let oauthResponse;
try {
oauthResponse = await fetch(CLAUDE_CONFIG.oauthUsageUrl, {
method: "GET",
headers: {
Accept: "application/json, text/plain, */*",
"Accept-Encoding": "gzip, compress, deflate, br",
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": `claude-code/${CLAUDE_CODE_VERSION}`,
"anthropic-beta": "oauth-2025-04-20",
},
signal: ctrl.signal,
});
} finally {
clearTimeout(timer);
}
if (oauthResponse.ok) {
const data = toRecord(await oauthResponse.json());
const quotas: Record<string, UsageQuota> = {};
// utilization = percentage USED (e.g., 90 means 90% used, 10% remaining)
// Confirmed via user report #299: Claude.ai shows 87% used = OmniRoute must show 13% remaining.
const hasUtilization = (window: JsonRecord) =>
window && typeof window === "object" && safePercentage(window.utilization) !== undefined;
const createQuotaObject = (window: JsonRecord) => {
const used = safePercentage(window.utilization) as number; // utilization = % used
const remaining = Math.max(0, 100 - used);
return {
used,
total: 100,
remaining,
resetAt: parseResetTime(window.resets_at),
remainingPercentage: remaining,
unlimited: false,
};
};
const fiveHour = toRecord(data.five_hour);
if (hasUtilization(fiveHour)) {
quotas["session (5h)"] = createQuotaObject(fiveHour);
}
const sevenDay = toRecord(data.seven_day);
if (hasUtilization(sevenDay)) {
quotas["weekly (7d)"] = createQuotaObject(sevenDay);
}
// Map Anthropic's internal codenames (e.g., omelette → Designer) for display.
const MODEL_DISPLAY_NAMES: Record<string, string> = {
omelette: "designer",
};
for (const [key, value] of Object.entries(data)) {
const valueRecord = toRecord(value);
if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(valueRecord)) {
const codename = key.replace("seven_day_", "");
const modelName = MODEL_DISPLAY_NAMES[codename] || codename;
quotas[`weekly ${modelName} (7d)`] = createQuotaObject(valueRecord);
}
}
const bootstrap = await bootstrapPromise;
const plan =
getClaudePlanLabel(
typeof data.tier === "string" ? data.tier : null,
typeof data.plan === "string" ? data.plan : null,
typeof data.subscription_type === "string" ? data.subscription_type : null,
bootstrap?.organization_rate_limit_tier
) ?? undefined;
return {
...(plan ? { plan } : {}),
quotas,
extraUsage: data.extra_usage ?? null,
bootstrap,
};
}
// Cool down OAuth usage polling after a 429 (quota endpoint only — chat is unaffected).
if (oauthResponse.status === 429) {
markClaudeOauthUsage429(accessToken);
}
// Fallback: OAuth endpoint returned non-OK, try legacy settings/org endpoint
console.warn(
`[Claude Usage] OAuth endpoint returned ${oauthResponse.status}, falling back to legacy`
);
const legacy = await getClaudeUsageLegacy(accessToken);
return { ...legacy, bootstrap: await bootstrapPromise };
} catch (error) {
return {
message: `Claude connected. Unable to fetch usage: ${(error as Error).message}`,
bootstrap: await bootstrapPromise,
};
}
}
/**
* Legacy Claude usage fetcher for API key / org admin users.
* Uses /v1/settings + /v1/organizations/{org_id}/usage endpoints.
*/
async function getClaudeUsageLegacy(accessToken?: string) {
try {
const settingsResponse = await fetch(CLAUDE_CONFIG.settingsUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"anthropic-version": CLAUDE_CONFIG.apiVersion,
},
});
if (settingsResponse.ok) {
const settings = toRecord(await settingsResponse.json());
const organizationId =
typeof settings.organization_id === "string" ? settings.organization_id : "";
if (organizationId) {
const usageResponse = await fetch(
CLAUDE_CONFIG.usageUrl.replace("{org_id}", organizationId),
{
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"anthropic-version": CLAUDE_CONFIG.apiVersion,
},
}
);
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 as Error).message}` };
}
}

View File

@@ -0,0 +1,70 @@
/**
* usage/codex.ts — Codex (OpenAI / ChatGPT backend) usage fetcher.
*
* Extracted from services/usage.ts (god-file decomposition): the Codex family — the ChatGPT
* backend usage-API config and the getCodexUsage fetcher that reads the persisted workspace
* binding and shapes quotas via buildCodexUsageQuotas. Depends only on the scalar leaf +
* codexUsageQuotas — no host coupling — so it lives as a co-located provider leaf. usage.ts
* imports getCodexUsage (dispatcher). Behavior-preserving move.
*/
import { buildCodexUsageQuotas } from "../codexUsageQuotas.ts";
import { getFieldValue } from "./scalars.ts";
// Codex (OpenAI) API config
const CODEX_CONFIG = {
usageUrl: "https://chatgpt.com/backend-api/wham/usage",
};
/**
* Codex (OpenAI) Usage - Fetch from ChatGPT backend API
* IMPORTANT: Uses persisted workspaceId from OAuth to ensure correct workspace binding.
* No fallback to other workspaces - strict binding to user's selected workspace.
*/
export async function getCodexUsage(
accessToken?: string,
providerSpecificData: Record<string, unknown> = {}
) {
try {
// Use persisted workspace ID from OAuth - NO FALLBACK
const accountId =
typeof providerSpecificData.workspaceId === "string"
? providerSpecificData.workspaceId
: null;
const headers: Record<string, string> = {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
Accept: "application/json",
};
if (accountId) {
headers["chatgpt-account-id"] = accountId;
}
const response = await fetch(CODEX_CONFIG.usageUrl, {
method: "GET",
headers,
});
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
return {
message: `Codex token expired or access denied. Please re-authenticate the connection.`,
};
}
throw new Error(`Codex API error: ${response.status}`);
}
const data = await response.json();
const { rateLimit, quotas } = buildCodexUsageQuotas(data);
return {
plan: String(getFieldValue(data, "plan_type", "planType") || "unknown"),
limitReached: Boolean(getFieldValue(rateLimit, "limit_reached", "limitReached")),
quotas,
};
} catch (error) {
return { message: `Failed to fetch Codex usage: ${(error as Error).message}` };
}
}

View File

@@ -0,0 +1,161 @@
/**
* usage/cursor.ts — Cursor (Pro) usage fetcher + JWT/config helpers.
*
* Extracted from services/usage.ts (god-file decomposition): the Cursor family — the
* dashboard usage-API config, the WorkOS JWT `sub` decoder, and the getCursorUsage fetcher
* that probes the cursor.com/dashboard/spending endpoint. Depends only on the sibling
* scalar/quota leaves — no host coupling — so it lives as a co-located provider leaf.
* usage.ts imports getCursorUsage (dispatcher). Behavior-preserving move.
*/
import { toRecord, toNumber, clampPercentage } from "./scalars.ts";
import { type UsageQuota, parseResetTime } from "./quota.ts";
// Cursor dashboard usage API config
// The endpoint that powers https://cursor.com/dashboard/spending. Validates the WorkOS
// session via the WorkosCursorSessionToken cookie (format: `${userId}::${jwt}`) and
// rejects requests without a matching Origin/Referer (Invalid origin for state-changing request).
const CURSOR_USAGE_CONFIG = {
usageUrl: "https://cursor.com/api/dashboard/get-current-period-usage",
origin: "https://cursor.com",
referer: "https://cursor.com/dashboard/spending",
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
};
/**
* Decode the `sub` claim of a Cursor JWT (the WorkOS user id).
* Returns null if the token is not a parseable JWT.
*/
function decodeCursorJwtSub(token: string): string | null {
if (!token || typeof token !== "string") return null;
const parts = token.split(".");
if (parts.length !== 3) return null;
try {
let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
while (payload.length % 4 !== 0) payload += "=";
const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
const sub = decoded?.sub;
return typeof sub === "string" && sub.length > 0 ? sub : null;
} catch {
return null;
}
}
/**
* Cursor Pro Plan Usage
* Fetches current-billing-cycle spend from the cursor.com dashboard API and exposes three
* windows that mirror the cursor.com/dashboard/spending UI: Total / Auto + Composer / API.
*/
export async function getCursorUsage(accessToken: string, providerSpecificData?: unknown) {
if (!accessToken) {
return { message: "Cursor access token missing. Re-import the connection from Cursor IDE." };
}
const storedUserId = (() => {
const raw = toRecord(providerSpecificData).userId;
return typeof raw === "string" && raw.length > 0 ? raw : null;
})();
const userId = storedUserId || decodeCursorJwtSub(accessToken);
if (!userId) {
return {
message: "Cursor token missing user id. Re-import the connection from Cursor IDE.",
};
}
try {
const response = await fetch(CURSOR_USAGE_CONFIG.usageUrl, {
method: "POST",
redirect: "manual",
headers: {
Cookie: `WorkosCursorSessionToken=${userId}::${accessToken}`,
Origin: CURSOR_USAGE_CONFIG.origin,
Referer: CURSOR_USAGE_CONFIG.referer,
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent": CURSOR_USAGE_CONFIG.userAgent,
},
body: "{}",
});
// 3xx redirect to WorkOS authkit means the session cookie was rejected.
if (response.status >= 300 && response.status < 400) {
return {
plan: "Cursor",
message: "Cursor session expired. Re-import the token from Cursor IDE.",
};
}
if (!response.ok) {
const errorText = (await response.text()).slice(0, 200);
if (response.status === 401 || response.status === 403) {
return {
plan: "Cursor",
message: "Cursor session unauthorized. Re-import the token from Cursor IDE.",
};
}
return {
plan: "Cursor",
message: `Cursor usage endpoint error (${response.status}): ${errorText}`,
};
}
const data = toRecord(await response.json());
const planUsage = toRecord(data.planUsage);
if (Object.keys(planUsage).length === 0) {
return {
plan: "Cursor",
message: "Cursor connected. No active plan usage returned.",
};
}
const limitCents = Math.max(0, toNumber(planUsage.limit, 0));
const totalSpendCents = Math.max(0, toNumber(planUsage.totalSpend, 0));
const autoPercentUsed = clampPercentage(toNumber(planUsage.autoPercentUsed, 0));
const apiPercentUsed = clampPercentage(toNumber(planUsage.apiPercentUsed, 0));
const totalPercentUsed = clampPercentage(toNumber(planUsage.totalPercentUsed, 0));
// billingCycleEnd is a numeric-string in ms; coerce so parseResetTime sees a number.
const billingCycleEndMs = toNumber(data.billingCycleEnd, 0);
const resetAt = billingCycleEndMs > 0 ? parseResetTime(billingCycleEndMs) : null;
// Convert cents → dollars rounded to 2 decimal places.
const toDollars = (cents: number) => Math.round(cents) / 100;
const limitDollars = toDollars(limitCents);
const buildWindow = (percentUsed: number, usedCentsOverride?: number): UsageQuota => {
const usedCents =
typeof usedCentsOverride === "number"
? usedCentsOverride
: Math.round((limitCents * percentUsed) / 100);
const used = toDollars(Math.min(usedCents, limitCents));
const remaining = toDollars(Math.max(limitCents - Math.min(usedCents, limitCents), 0));
return {
used,
total: limitDollars,
remaining,
remainingPercentage: clampPercentage(100 - percentUsed),
resetAt,
unlimited: false,
};
};
const quotas: Record<string, UsageQuota> = {
Total: buildWindow(totalPercentUsed, totalSpendCents),
"Auto + Composer": buildWindow(autoPercentUsed),
API: buildWindow(apiPercentUsed),
};
return {
plan: "Cursor Pro",
quotas,
};
} catch (error) {
return {
plan: "Cursor",
message: `Cursor connected. Unable to fetch usage: ${(error as Error).message}`,
};
}
}

View File

@@ -0,0 +1,209 @@
/**
* usage/kimi.ts — Kimi Coding (kimi-coding / kimi-coding-apikey) usage fetcher + helpers.
*
* Extracted from services/usage.ts (god-file decomposition): the Kimi family — the coding
* API config, membership-level → display-name mapping, and the getKimiUsage fetcher that
* probes the official /v1/usages endpoint. Depends only on the sibling scalar/quota leaves
* plus safePercentage — no host coupling — so it lives as a co-located provider leaf.
* usage.ts imports getKimiUsage (dispatcher). Behavior-preserving move.
*/
import { safePercentage } from "@/shared/utils/formatting";
import { toRecord, toNumber } from "./scalars.ts";
import { type UsageQuota, parseResetTime } from "./quota.ts";
type JsonRecord = Record<string, unknown>;
// Kimi Coding API config
const KIMI_CONFIG = {
baseUrl: "https://api.kimi.com/coding/v1",
usageUrl: "https://api.kimi.com/coding/v1/usages",
apiVersion: "2023-06-01",
};
/**
* Map Kimi membership level to display name
* LEVEL_BASIC = Moderato, LEVEL_INTERMEDIATE = Allegretto,
* LEVEL_ADVANCED = Allegro, LEVEL_STANDARD = Vivace
*/
function getKimiPlanName(level: unknown): string {
if (!level) return "";
const normalizedLevel = String(level);
const levelMap = {
LEVEL_BASIC: "Moderato",
LEVEL_INTERMEDIATE: "Allegretto",
LEVEL_ADVANCED: "Allegro",
LEVEL_STANDARD: "Vivace",
};
return (
levelMap[normalizedLevel as keyof typeof levelMap] ||
normalizedLevel.replace("LEVEL_", "").toLowerCase()
);
}
/**
* Kimi Coding Usage - Fetch quota from Kimi API
* Uses the official /v1/usages endpoint with custom X-Msh-* headers
*/
export async function getKimiUsage(accessToken?: string, apiKey?: string) {
// Generate device info for headers (same as OAuth flow)
const deviceId = "kimi-usage-" + Date.now();
const platform = "omniroute";
const version = "2.1.2";
const deviceModel =
typeof process !== "undefined" ? `${process.platform} ${process.arch}` : "unknown";
// API key auth takes precedence — Kimi's /usages endpoint accepts the same
// API key used for /messages (verified live: responds with
// authentication.method = METHOD_API_KEY). OAuth flow falls through to the
// Bearer + device-headers shape used by Kimi Coding OAuth.
const useApiKey = typeof apiKey === "string" && apiKey.length > 0;
const authHeaders: Record<string, string> = useApiKey
? { "x-api-key": apiKey as string }
: {
Authorization: `Bearer ${accessToken}`,
"X-Msh-Platform": platform,
"X-Msh-Version": version,
"X-Msh-Device-Model": deviceModel,
"X-Msh-Device-Id": deviceId,
};
try {
const response = await fetch(KIMI_CONFIG.usageUrl, {
method: "GET",
headers: {
...authHeaders,
"Content-Type": "application/json",
},
});
const responseText = await response.text();
if (!response.ok) {
return {
plan: "Kimi Coding",
message: `Kimi Coding connected. API Error ${response.status}: ${responseText.slice(0, 100)}`,
};
}
let data;
try {
data = JSON.parse(responseText);
} catch {
return {
plan: "Kimi Coding",
message: "Kimi Coding connected. Invalid JSON response from API.",
};
}
const quotas: Record<string, UsageQuota> = {};
const dataObj = toRecord(data);
// Parse Kimi usage response format
// Format: { user: {...}, usage: { limit: "100", used: "92", remaining: "8", resetTime: "..." }, limits: [...] }
const usageObj = toRecord(dataObj.usage);
// Check for Kimi's actual usage fields (strings, not numbers)
const usageLimit = toNumber(usageObj.limit || usageObj.Limit, 0);
const usageUsed = toNumber(usageObj.used || usageObj.Used, 0);
const usageRemaining = toNumber(usageObj.remaining || usageObj.Remaining, 0);
const usageResetTime =
usageObj.resetTime || usageObj.ResetTime || usageObj.reset_at || usageObj.resetAt;
if (usageLimit > 0) {
const percentRemaining = usageLimit > 0 ? (usageRemaining / usageLimit) * 100 : 0;
quotas["Weekly"] = {
used: usageUsed,
total: usageLimit,
remaining: usageRemaining,
remainingPercentage: percentRemaining,
resetAt: parseResetTime(usageResetTime),
unlimited: false,
};
}
// Also parse limits array for rate limits
const limitsArray = Array.isArray(dataObj.limits) ? dataObj.limits : [];
for (let i = 0; i < limitsArray.length; i++) {
const limitItem = toRecord(limitsArray[i]);
const window = toRecord(limitItem.window);
const detail = toRecord(limitItem.detail);
const limit = toNumber(detail.limit || detail.Limit, 0);
const remaining = toNumber(detail.remaining || detail.Remaining, 0);
const resetTime = detail.resetTime || detail.reset_at || detail.resetAt;
if (limit > 0) {
quotas["Ratelimit"] = {
used: limit - remaining,
total: limit,
remaining,
remainingPercentage: limit > 0 ? (remaining / limit) * 100 : 0,
resetAt: parseResetTime(resetTime),
unlimited: false,
};
}
}
// Check for quota windows (Claude-like format with utilization) as fallback
const hasUtilization = (window: JsonRecord) =>
window && typeof window === "object" && safePercentage(window.utilization) !== undefined;
const createQuotaObject = (window: JsonRecord) => {
const remaining = safePercentage(window.utilization) as number;
const used = 100 - remaining;
return {
used,
total: 100,
remaining,
resetAt: parseResetTime(window.resets_at),
remainingPercentage: remaining,
unlimited: false,
};
};
if (hasUtilization(toRecord(dataObj.five_hour))) {
quotas["session (5h)"] = createQuotaObject(toRecord(dataObj.five_hour));
}
if (hasUtilization(toRecord(dataObj.seven_day))) {
quotas["weekly (7d)"] = createQuotaObject(toRecord(dataObj.seven_day));
}
// Check for model-specific quotas
for (const [key, value] of Object.entries(dataObj)) {
const valueRecord = toRecord(value);
if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(valueRecord)) {
const modelName = key.replace("seven_day_", "");
quotas[`weekly ${modelName} (7d)`] = createQuotaObject(valueRecord);
}
}
if (Object.keys(quotas).length > 0) {
const userRecord = toRecord(dataObj.user);
const membershipLevel = toRecord(userRecord.membership).level;
const planName = getKimiPlanName(membershipLevel);
return {
plan: planName || "Kimi Coding",
quotas,
};
}
// No quota data in response
const userRecord = toRecord(dataObj.user);
const membershipLevel = toRecord(userRecord.membership).level;
const planName = getKimiPlanName(membershipLevel);
return {
plan: planName || "Kimi Coding",
message: "Kimi Coding connected. Usage tracked per request.",
};
} catch (error) {
return {
message: `Kimi Coding connected. Unable to fetch usage: ${(error as Error).message}`,
};
}
}

View File

@@ -0,0 +1,243 @@
/**
* usage/kiro.ts — Kiro / Amazon Q (AWS CodeWhisperer) usage fetcher + quota helpers.
*
* Extracted from services/usage.ts (god-file decomposition): the Kiro family — overage
* detection, per-resource quota assembly (buildKiroQuota / buildKiroUsageResult), region-aware
* profile-ARN discovery, the social-auth account marker, and the getKiroUsage fetcher that
* calls GetUsageLimits on the region-matched CodeWhisperer endpoint. Depends only on the
* sibling scalar/quota leaves — no host coupling — so it lives as a co-located provider leaf.
* usage.ts imports getKiroUsage (dispatcher) + re-exports buildKiroUsageResult /
* discoverKiroProfileArn (external kiro tests import them from services/usage) and pulls
* getKiroUsage into __testing. Behavior-preserving move.
*/
import { toRecord, toNumber } from "./scalars.ts";
import { type UsageQuota, parseResetTime } from "./quota.ts";
type JsonRecord = Record<string, unknown>;
const CODEWHISPERER_BASE_URL =
process.env.OMNIROUTE_CODEWHISPERER_BASE_URL ?? "https://codewhisperer.us-east-1.amazonaws.com";
function isKiroOverageEnabled(data: JsonRecord): boolean {
const overageConfiguration = toRecord(data.overageConfiguration);
const overageStatus = String(overageConfiguration.overageStatus || "")
.trim()
.toUpperCase();
return (
overageStatus === "ENABLED" ||
data.overageEnabled === true ||
overageConfiguration.overageEnabled === true
);
}
function buildKiroQuota(
used: number,
total: number,
resetAt: string | null,
overageEnabled: boolean
): UsageQuota {
const remaining = total - used;
if (!overageEnabled) {
return { used, total, remaining, resetAt, unlimited: false };
}
return {
used,
total,
remaining,
remainingPercentage: 100,
resetAt,
unlimited: true,
};
}
/**
* Build the Kiro usage result from a GetUsageLimits response. When the account returns no
* usage breakdown (some AWS IAM / Builder ID accounts don't expose per-resource quota via
* GetUsageLimits), return an informative message instead of empty `quotas:{}` — otherwise the
* dashboard renders a blank quota card with no explanation (#3506). Exported for testing.
*/
export function buildKiroUsageResult(
data: JsonRecord
): { plan: string; quotas: Record<string, UsageQuota> } | { message: string } {
const usageList = Array.isArray(data.usageBreakdownList) ? data.usageBreakdownList : [];
const quotaInfo: Record<string, UsageQuota> = {};
const resetAt = parseResetTime(data.nextDateReset || data.resetDate);
const overageEnabled = isKiroOverageEnabled(data);
usageList.forEach((breakdownValue: unknown) => {
const breakdown = toRecord(breakdownValue);
const resourceType =
typeof breakdown.resourceType === "string" ? breakdown.resourceType.toLowerCase() : "unknown";
const used = toNumber(breakdown.currentUsageWithPrecision, 0);
const total = toNumber(breakdown.usageLimitWithPrecision, 0);
quotaInfo[resourceType] = buildKiroQuota(used, total, resetAt, overageEnabled);
const freeTrialInfo = toRecord(breakdown.freeTrialInfo);
if (Object.keys(freeTrialInfo).length > 0) {
const freeUsed = toNumber(freeTrialInfo.currentUsageWithPrecision, 0);
const freeTotal = toNumber(freeTrialInfo.usageLimitWithPrecision, 0);
quotaInfo[`${resourceType}_freetrial`] = buildKiroQuota(
freeUsed,
freeTotal,
resetAt,
overageEnabled
);
}
});
if (Object.keys(quotaInfo).length === 0) {
return {
message:
"Kiro connected, but the account returned no usage breakdown. Some AWS IAM / Builder ID accounts don't expose per-resource quota via GetUsageLimits.",
};
}
return {
plan: String(toRecord(data.subscriptionInfo).subscriptionTitle || "").trim() || "Kiro",
quotas: quotaInfo,
};
}
/**
* Discover a Kiro/CodeWhisperer profile ARN for an account that didn't persist one (common for
* AWS IAM Identity Center logins and kiro-cli imports). Calls ListAvailableProfiles on the
* region-matched endpoint and prefers a profile whose ARN is in the same region. Returns
* undefined when no profile is available (e.g. the org/token has no Kiro entitlement).
* Exported for testing.
*/
export async function discoverKiroProfileArn(
accessToken: string,
usageBaseUrl: string,
region: string
): Promise<string | undefined> {
try {
const response = await fetch(usageBaseUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/x-amz-json-1.0",
"x-amz-target": "AmazonCodeWhispererService.ListAvailableProfiles",
Accept: "application/json",
},
body: JSON.stringify({ maxResults: 10 }),
// Don't let a hung profile lookup block the usage/quota refresh indefinitely.
signal: AbortSignal.timeout(10000),
});
if (!response.ok) return undefined;
const data = toRecord(await response.json());
const profiles = Array.isArray(data.profiles) ? data.profiles : [];
const normalizedRegion = region.toLowerCase();
const matched =
profiles.find((profile: unknown) => {
const arn = toRecord(profile).arn;
return typeof arn === "string" && arn.toLowerCase().includes(`:${normalizedRegion}:`);
}) || profiles[0];
const arn = toRecord(matched).arn;
return typeof arn === "string" && arn.length > 0 ? arn : undefined;
} catch {
return undefined;
}
}
/**
* Kiro (AWS CodeWhisperer) Usage
*/
export async function getKiroUsage(accessToken?: string, providerSpecificData?: JsonRecord) {
try {
let profileArn =
typeof providerSpecificData?.profileArn === "string"
? providerSpecificData.profileArn
: undefined;
// Enterprise IAM Identity Center accounts are region-bound: the profileArn, token and
// endpoint must all match the region. Derive the region from the stored region (preferred)
// or the profileArn, then route to the regional Amazon Q endpoint (us-east-1 keeps the
// legacy codewhisperer host; codewhisperer.{region} does not resolve for other regions).
const regionFromArn = profileArn
? profileArn.toLowerCase().match(/^arn:aws:codewhisperer:([a-z0-9-]+):/)?.[1]
: undefined;
const region =
(typeof providerSpecificData?.region === "string" &&
providerSpecificData.region.trim().toLowerCase()) ||
regionFromArn ||
"us-east-1";
const usageBaseUrl =
region === "us-east-1" ? CODEWHISPERER_BASE_URL : `https://q.${region}.amazonaws.com`;
// IAM Identity Center logins and kiro-cli imports frequently don't persist a profileArn, which
// previously caused the quota card to show nothing ("0 used"). Discover it on demand from
// ListAvailableProfiles (region-matched) so usage still resolves for those accounts.
if (!profileArn && accessToken) {
profileArn = await discoverKiroProfileArn(accessToken, usageBaseUrl, region);
}
if (!profileArn) {
return { message: "Kiro connected. Profile ARN not available for quota tracking." };
}
// Kiro uses AWS CodeWhisperer GetUsageLimits API
const payload = {
origin: "AI_EDITOR",
profileArn: profileArn,
resourceType: "AGENTIC_REQUEST",
};
const response = await fetch(usageBaseUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/x-amz-json-1.0",
"x-amz-target": "AmazonCodeWhispererService.GetUsageLimits",
Accept: "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
// Social-auth Kiro accounts (added via /api/oauth/kiro/social-exchange with provider
// Google or GitHub) use a different token format that AWS CodeWhisperer's GetUsageLimits
// routinely rejects with 401/403, even when /messages still works. Surface a clear
// "auth expired, chat may still work" message instead of a generic upstream-error blob
// so the quota card matches what users with legacy social-auth accounts already see.
// Inspired by https://github.com/decolua/9router/pull/620.
if (
(response.status === 401 || response.status === 403) &&
isSocialAuthKiroAccount(providerSpecificData)
) {
return {
message: "Kiro quota API authentication expired. Chat may still work.",
quotas: {},
};
}
const errorText = await response.text();
throw new Error(`Kiro API error (${response.status}): ${errorText}`);
}
const data = toRecord(await response.json());
return buildKiroUsageResult(data);
} catch (error) {
throw new Error(`Failed to fetch Kiro usage: ${error.message}`);
}
}
/**
* Was this Kiro connection added via the Google/GitHub social-auth device flow
* (POST /api/oauth/kiro/social-exchange)? That route persists
* `{ authMethod: "imported", provider: "Google" | "Github" }` on the connection.
* Builder-ID / IDC / kiro-cli imports use different markers and should keep the
* existing throw-on-failure behavior.
*/
function isSocialAuthKiroAccount(providerSpecificData?: JsonRecord): boolean {
if (!providerSpecificData || providerSpecificData.authMethod !== "imported") return false;
const provider =
typeof providerSpecificData.provider === "string"
? providerSpecificData.provider.toLowerCase()
: "";
return provider === "google" || provider === "github";
}

View File

@@ -0,0 +1,62 @@
// Characterization of the services/usage.ts family split (god-file decomposition): the Cursor,
// Kimi, Codex, Claude, and Kiro usage families moved out of services/usage.ts into
// services/usage/<family>.ts leaves (mirroring the earlier glm/minimax/antigravity leaves).
// Behavior-preserving move; the locks pin each leaf's exported surface, the Claude plan-label
// helper's pure logic, that usage.ts re-exports buildKiroUsageResult + discoverKiroProfileArn
// (the kiro-* tests import them from services/usage), and that __testing stays wired to the leaves.
import { test } from "node:test";
import assert from "node:assert/strict";
const CURSOR = await import("../../open-sse/services/usage/cursor.ts");
const KIMI = await import("../../open-sse/services/usage/kimi.ts");
const CODEX = await import("../../open-sse/services/usage/codex.ts");
const CLAUDE = await import("../../open-sse/services/usage/claude.ts");
const KIRO = await import("../../open-sse/services/usage/kiro.ts");
const HOST = await import("../../open-sse/services/usage.ts");
const kind = (m: unknown, k: string) => typeof (m as Record<string, unknown>)[k];
test("each family leaf exposes its usage fetcher(s)", () => {
assert.equal(kind(CURSOR, "getCursorUsage"), "function");
assert.equal(kind(KIMI, "getKimiUsage"), "function");
assert.equal(kind(CODEX, "getCodexUsage"), "function");
assert.equal(kind(CLAUDE, "getClaudeUsage"), "function");
assert.equal(kind(CLAUDE, "getClaudePlanLabel"), "function");
assert.equal(kind(KIRO, "getKiroUsage"), "function");
assert.equal(kind(KIRO, "buildKiroUsageResult"), "function");
assert.equal(kind(KIRO, "discoverKiroProfileArn"), "function");
});
test("host re-exports the kiro symbols the kiro-* tests import, with the same identity", () => {
assert.equal(
(HOST as Record<string, unknown>).buildKiroUsageResult,
(KIRO as Record<string, unknown>).buildKiroUsageResult
);
assert.equal(
(HOST as Record<string, unknown>).discoverKiroProfileArn,
(KIRO as Record<string, unknown>).discoverKiroProfileArn
);
});
test("host __testing stays wired to the moved claude/kiro internals", () => {
const testing = (HOST as Record<string, Record<string, unknown>>).__testing;
assert.equal(testing.getClaudePlanLabel, (CLAUDE as Record<string, unknown>).getClaudePlanLabel);
assert.equal(testing.getKiroUsage, (KIRO as Record<string, unknown>).getKiroUsage);
});
test("claude getClaudePlanLabel picks the first meaningful candidate, skipping placeholders", () => {
assert.equal(CLAUDE.getClaudePlanLabel("Pro"), "Pro");
assert.equal(CLAUDE.getClaudePlanLabel(" Max "), "Max");
assert.equal(CLAUDE.getClaudePlanLabel("claude code", "Team"), "Team");
assert.equal(CLAUDE.getClaudePlanLabel("unknown", null, "Enterprise"), "Enterprise");
assert.equal(CLAUDE.getClaudePlanLabel(null, undefined, ""), null);
assert.equal(CLAUDE.getClaudePlanLabel(), null);
});
test("host dispatcher + USAGE_FETCHER_PROVIDERS still cover the moved families", () => {
assert.equal(kind(HOST, "getUsageForProvider"), "function");
const providers = (HOST as Record<string, unknown>).USAGE_FETCHER_PROVIDERS as readonly string[];
for (const p of ["cursor", "codex", "claude", "kiro", "kimi-coding"]) {
assert.ok(providers.includes(p), `${p} must remain a usage-fetcher provider`);
}
});