chore(usage): decompose services/usage.ts into per-provider usage/* leaves (999 → 253) (#8545)

* chore(usage): extract crof, nanogpt, qoder, opencode, deepseek, bailian, vertex, xiaomi-mimo, xai, github usage fetchers into usage/* leaves

Decompose services/usage.ts (god-file phase 1): move the remaining per-provider
usage fetcher/parser logic into co-located leaves under open-sse/services/usage/
so usage.ts becomes a thin dispatcher (imports + USAGE_FETCHER_PROVIDERS +
getUsageForProvider switch + __testing re-exports).

New leaves (each a pure data transform or independent fetcher, no orchestration):
  - usage/github.ts    getGitHubUsage, formatGitHubQuotaSnapshot, inferGitHubPlanName, shouldDisplayGitHubQuota
  - usage/crof.ts      getCrofUsage
  - usage/nanogpt.ts   getNanoGptUsage
  - usage/qoder.ts     getQoderUsage, parseQoderUserStatusUsage
  - usage/opencode.ts  getOpencodeUsage
  - usage/deepseek.ts  getDeepseekUsage
  - usage/bailian.ts   getBailianCodingPlanUsage
  - usage/vertex.ts    getVertexUsage
  - usage/xiaomi-mimo.ts getXiaomiMimoUsage
  - usage/xai.ts       getXaiUsage

usage.ts re-exports parseQoderUserStatusUsage (named) and threads every helper
the existing __testing contract exposes (usage-utils / usage-service-hardening /
qoder-usage-quota / xiaomi-mimo-selftrack / xai-usage / vertex-spend suites read
them from services/usage). External importers unchanged.

open-sse/services/usage.ts: 1065 -> 256 lines (cap 800).
npm run check:file-size: OK. typecheck:core: OK. eslint: clean. check:cycles: OK.

Added characterization tests (tests/unit/usage-<provider>-split.test.ts) that
import each leaf directly and pin its export surface + key edges, mirroring the
existing usage-quota-core-split / usage-scalars-split pattern.

* docs(changelog): add fragment for this PR
This commit is contained in:
MumuTW
2026-07-26 14:52:19 +08:00
committed by GitHub
parent 56185a8a49
commit f0b08f95c3
22 changed files with 1648 additions and 840 deletions

View File

@@ -0,0 +1 @@
- chore(usage): decompose `open-sse/services/usage.ts` (999 → 253 lines) by extracting the crof, nanogpt, qoder, opencode, deepseek, bailian, vertex, xiaomi-mimo, xai and github usage fetchers into `usage/*` leaves — behavior-preserving move, the file is now a thin provider→fetcher dispatcher and the public import surface is unchanged

View File

@@ -1,35 +1,21 @@
/**
* Usage Fetcher - Get usage data from provider APIs
*
* This module is the dispatcher (orchestration) layer: it maps a provider name
* to the per-provider usage fetcher leaf under `./usage/<provider>.ts` and
* shapes the connection into the args each leaf expects. The provider-specific
* fetcher/parser logic itself lives in those leaves so this file stays flat.
* External consumers import `getUsageForProvider` / `USAGE_FETCHER_PROVIDERS`
* (and the re-exported helpers) from here — the leaf split is an internal
* implementation detail.
*/
import { getGitHubCopilotInternalUserHeaders } from "../config/providerHeaderProfiles.ts";
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 { getOpenrouterUsage } from "./usage/openrouter.ts";
import { getOllamaCloudUsage, getOpenCodeGoUsage } from "./opencodeOllamaUsage.ts";
import { getCodeBuddyCnUsage } from "./usage/codebuddy-cn.ts";
import { getPromptQlUsage } from "./usage/promptql.ts";
import { getHyperAgentUsage } from "./usage/hyperagent.ts";
import {
extractCodeAssistOnboardTierId,
extractCodeAssistSubscriptionTier,
} from "./codeAssistSubscription.ts";
import { sanitizeErrorMessage } from "../utils/error.ts";
import { resolveQoderJobToken } from "./qoderCli.ts";
import {
toRecord,
toNumber,
toPercentage,
toTitleCase,
getFieldValue,
clampPercentage,
roundCurrency,
toDisplayLabel,
pickFirstNonEmptyString,
} from "./usage/scalars.ts";
import { type UsageQuota, parseResetTime, createQuotaFromUsage } from "./usage/quota.ts";
import { toDisplayLabel } from "./usage/scalars.ts";
import { parseResetTime, createQuotaFromUsage } from "./usage/quota.ts";
import {
getMiniMaxUsage,
getMiniMaxPlanLabel,
@@ -62,13 +48,23 @@ import { getKiroUsage, buildKiroUsageResult, discoverKiroProfileArn } from "./us
// Re-exported para os testes kiro-* (importam de services/usage).
export { buildKiroUsageResult, discoverKiroProfileArn } from "./usage/kiro.ts";
import { getAdobeFireflyUsage } from "./usage/adobeFirefly.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 NANOGPT_CONFIG = {
usageUrl: "https://nano-gpt.com/api/subscription/v1/usage",
};
import { getOpenrouterUsage } from "./usage/openrouter.ts";
import { getOllamaCloudUsage, getOpenCodeGoUsage } from "./opencodeOllamaUsage.ts";
import { getCodeBuddyCnUsage } from "./usage/codebuddy-cn.ts";
import { getPromptQlUsage } from "./usage/promptql.ts";
import { getHyperAgentUsage } from "./usage/hyperagent.ts";
import { getGitHubUsage, formatGitHubQuotaSnapshot, inferGitHubPlanName } from "./usage/github.ts";
import { getCrofUsage } from "./usage/crof.ts";
import { getNanoGptUsage } from "./usage/nanogpt.ts";
import { getQoderUsage, parseQoderUserStatusUsage } from "./usage/qoder.ts";
// Re-exported para o teste qoder-usage-quota (importa parseQoderUserStatusUsage de services/usage).
export { parseQoderUserStatusUsage } from "./usage/qoder.ts";
import { getOpencodeUsage } from "./usage/opencode.ts";
import { getDeepseekUsage } from "./usage/deepseek.ts";
import { getBailianCodingPlanUsage } from "./usage/bailian.ts";
import { getVertexUsage } from "./usage/vertex.ts";
import { getXiaomiMimoUsage } from "./usage/xiaomi-mimo.ts";
import { getXaiUsage } from "./usage/xai.ts";
type JsonRecord = Record<string, unknown>;
type UsageProviderConnection = JsonRecord & {
@@ -81,429 +77,6 @@ type UsageProviderConnection = JsonRecord & {
email?: string;
};
function shouldDisplayGitHubQuota(quota: UsageQuota | null): quota is UsageQuota {
if (!quota) return false;
if (quota.unlimited && quota.total <= 0) return false;
return quota.total > 0 || quota.remainingPercentage !== undefined;
}
// 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`
// for pay-as-you-go. `credits` is the USD credit balance. We surface both as
// quotas so the Limits & Quotas page can render whichever the account uses.
async function getCrofUsage(apiKey: string) {
if (!apiKey) {
return { message: "CrofAI API key not available. Add a key to view usage." };
}
let response: Response;
try {
response = await fetch(CROF_USAGE_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
});
} catch (error) {
return { message: `CrofAI connected. Unable to fetch usage: ${(error as Error).message}` };
}
const rawText = await response.text();
if (response.status === 401 || response.status === 403) {
return { message: "CrofAI connected. The API key was rejected by /usage_api/." };
}
if (!response.ok) {
return { message: `CrofAI connected. /usage_api/ returned HTTP ${response.status}.` };
}
let payload: JsonRecord = {};
if (rawText) {
try {
payload = toRecord(JSON.parse(rawText));
} catch {
return { message: "CrofAI connected. Unable to parse /usage_api/ response." };
}
}
const usableRequestsRaw = payload["usable_requests"];
const usableRequests =
usableRequestsRaw === null || usableRequestsRaw === undefined
? null
: toNumber(usableRequestsRaw, 0);
const credits = toNumber(payload["credits"], 0);
const quotas: Record<string, UsageQuota> = {};
if (usableRequests !== null) {
// CrofAI's /usage_api/ returns only the remaining count; the daily
// allotment is not exposed. CrofAI Pro plan = 1,000 requests/day per
// their pricing page, so use that as the baseline total. If the user
// is on a plan with a higher cap we widen the total to whatever they
// currently report so we never compute a negative `used`.
// Without this, total=0 makes the dashboard's percentage formula read
// 0% (interpreted as "depleted" → red) even on a fresh bucket.
const CROF_DAILY_BASELINE = 1000;
const remaining = Math.max(0, usableRequests);
const total = Math.max(CROF_DAILY_BASELINE, remaining);
const used = Math.max(0, total - remaining);
// CrofAI also does not return a reset timestamp and the docs only say
// "requests left today". The Crof.ai dashboard shows the daily bucket
// resetting at ~05:00 UTC (verified against the live countdown on
// 2026-04-25), so synthesize the next 05:00 UTC instant to match.
// Swap for a real field if Crof ever exposes one.
const now = new Date();
const RESET_HOUR_UTC = 5;
const todayResetMs = Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
now.getUTCDate(),
RESET_HOUR_UTC
);
const nextResetMs =
todayResetMs > now.getTime() ? todayResetMs : todayResetMs + 24 * 60 * 60 * 1000;
const nextResetIso = new Date(nextResetMs).toISOString();
quotas["Requests Today"] = {
used,
total,
remaining,
resetAt: nextResetIso,
unlimited: false,
displayName: `Requests Today: ${remaining} left`,
};
}
// Credits are an open balance — render as unlimited so the UI shows the
// dollar value rather than a misleading 0/0 bar.
quotas["Credits"] = {
used: 0,
total: 0,
remaining: 0,
resetAt: null,
unlimited: true,
displayName: `Credits: $${credits.toFixed(4)}`,
};
return { quotas };
}
/**
* Bailian (Alibaba Token Plan) Usage
* Fetches triple-window quota (5h, weekly, monthly) and returns worst-case.
*/
async function getBailianCodingPlanUsage(
connectionId: string,
apiKey: string,
providerSpecificData?: Record<string, unknown>
) {
try {
const connection = { apiKey, providerSpecificData };
const quota = await fetchBailianQuota(connectionId, connection);
if (!quota) {
return { message: "Alibaba Token Plan connected. Unable to fetch quota." };
}
const bailianQuota = quota as BailianTripleWindowQuota;
const used = bailianQuota.used;
const total = bailianQuota.total;
const remaining = Math.max(0, total - used);
const remainingPercentage = Math.round(remaining);
return {
plan: "Alibaba Token Plan",
used,
total,
remaining,
remainingPercentage,
resetAt: bailianQuota.resetAt,
unlimited: false,
displayName: "Alibaba Token Plan",
};
} catch (error) {
return { message: `Alibaba Token Plan error: ${(error as Error).message}` };
}
}
/**
* DeepSeek Usage
* Fetches balance from the DeepSeek balance API.
* Returns all balances (USD and CNY) as "credits" for credits-style UI display.
*/
async function getDeepseekUsage(connectionId: string, apiKey: string) {
try {
const connection = { apiKey };
const quota = await fetchDeepseekQuota(connectionId, connection);
if (!quota) {
return { message: "DeepSeek API key not available. Add a key to view usage." };
}
const deepseekQuota = quota as DeepseekQuota;
const { balances, isAvailable, limitReached } = deepseekQuota;
const quotas: Record<string, UsageQuota> = {};
// Show all balances as credits-style entries (e.g., credits_usd, credits_cny)
// The UI will display them as "🪙 Balance (USD) $50.00"
for (const balanceInfo of balances) {
const key = `credits_${balanceInfo.currency.toLowerCase()}`;
quotas[key] = {
used: 0,
total: 0,
remaining: balanceInfo.balance,
remainingPercentage: 100,
resetAt: null,
unlimited: true,
currency: balanceInfo.currency,
grantedBalance: balanceInfo.grantedBalance,
toppedUpBalance: balanceInfo.toppedUpBalance,
};
}
const plan = isAvailable ? "DeepSeek" : "DeepSeek (Insufficient Balance)";
return {
plan,
quotas,
isAvailable,
limitReached,
};
} catch (error) {
return { message: `DeepSeek error: ${(error as Error).message}` };
}
}
// Xiaomi MiMo Token Plan monthly limit (tokens). Keep in sync with the
// "xiaomi-mimo" preset in src/lib/quota/planRegistry.ts.
const XIAOMI_MIMO_MONTHLY_TOKEN_LIMIT = 4_100_000_000;
/**
* Xiaomi MiMo — SELF-TRACKED monthly quota.
*
* Xiaomi exposes plan usage only behind the console session cookie (the API key
* cannot reach the `tokenPlan/usage` endpoint), so there is no upstream usage
* API to call. Instead we count the tokens OmniRoute itself routed to this
* connection in the current UTC month (from `usage_history`) and compare them
* to the known Token Plan monthly limit. This reflects only traffic that went
* through OmniRoute, not the provider's own dashboard figure.
*/
async function getXiaomiMimoUsage(connectionId: string) {
if (!connectionId) {
return { message: "Xiaomi MiMo: connection id unavailable for self-tracked quota." };
}
try {
const { getMonthlyProviderTokensForConnection } = await import("@/lib/usage/usageStats");
const used = getMonthlyProviderTokensForConnection("xiaomi-mimo", connectionId);
const total = XIAOMI_MIMO_MONTHLY_TOKEN_LIMIT;
const now = new Date();
const resetAt = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)
).toISOString();
return {
plan: "Xiaomi MiMo Token Plan (OmniRoute-tracked)",
quotas: {
monthly: createQuotaFromUsage(used, total, resetAt),
},
};
} catch (error) {
return { message: `Xiaomi MiMo self-tracked usage error: ${(error as Error).message}` };
}
}
/**
* xAI (Grok) — SELF-TRACKED cumulative usage.
*
* xAI has no public per-account quota API (the billing console at console.x.ai
* requires a session cookie, not an API key), so — exactly like the Xiaomi
* MiMo self-track pattern above — OmniRoute sums the tokens it itself routed
* to this connection (from `usage_history`) instead of calling an upstream
* endpoint. Unlike Xiaomi MiMo, xAI has no fixed monthly cap, so the
* aggregate is reported as `unlimited: true` with `remaining: 100` — this
* renders the dashboard's green "100%" badge instead of a meaningless
* progress bar against a `total: 0`.
*/
async function getXaiUsage(connectionId: string) {
if (!connectionId) {
return { message: "xAI: connection id unavailable for self-tracked usage." };
}
try {
const { getMonthlyProviderTokensForConnection } = await import("@/lib/usage/usageStats");
const used = getMonthlyProviderTokensForConnection("xai", connectionId);
return {
plan: "xAI / Grok (OmniRoute-tracked)",
quotas: {
monthly: {
used,
total: 0,
remaining: 100,
remainingPercentage: 100,
resetAt: null,
unlimited: true,
} as UsageQuota,
},
};
} catch (error) {
return { message: `xAI self-tracked usage error: ${(error as Error).message}` };
}
}
/**
* OpenCode Go / OpenCode / OpenCode Zen Usage
* Delegates to the dedicated opencodeQuotaFetcher and shapes the result into
* the standard `{ plan, quotas }` usage response expected by the limits page.
*
* Three rolling windows are surfaced: $12/5h, $30/wk, $60/mo.
*/
async function getOpencodeUsage(connectionId: string, apiKey: string) {
if (!apiKey) {
return { message: "OpenCode API key not available. Add a key to view usage." };
}
try {
const quota = (await fetchOpencodeQuota(connectionId, {
apiKey,
})) as OpencodeTripleWindowQuota | null;
if (!quota) {
return { message: "OpenCode connected. Unable to fetch quota data." };
}
const { window5h, windowWeekly, windowMonthly, limitReached } = quota;
const quotas: Record<string, UsageQuota> = {};
// $12 / 5-hour rolling window
quotas["window_5h"] = {
used: window5h.percentUsed * 12,
total: 12,
remaining: (1 - window5h.percentUsed) * 12,
remainingPercentage: (1 - window5h.percentUsed) * 100,
resetAt: window5h.resetAt,
unlimited: false,
displayName: "$12 / 5-hour",
currency: "USD",
};
// $30 / weekly window
quotas["window_weekly"] = {
used: windowWeekly.percentUsed * 30,
total: 30,
remaining: (1 - windowWeekly.percentUsed) * 30,
remainingPercentage: (1 - windowWeekly.percentUsed) * 100,
resetAt: windowWeekly.resetAt,
unlimited: false,
displayName: "$30 / week",
currency: "USD",
};
// $60 / monthly window
quotas["window_monthly"] = {
used: windowMonthly.percentUsed * 60,
total: 60,
remaining: (1 - windowMonthly.percentUsed) * 60,
remainingPercentage: (1 - windowMonthly.percentUsed) * 100,
resetAt: windowMonthly.resetAt,
unlimited: false,
displayName: "$60 / month",
currency: "USD",
};
return {
plan: "OpenCode Go",
quotas,
limitReached,
};
} catch (error) {
return { message: `OpenCode error: ${sanitizeErrorMessage(error)}` };
}
}
/**
* NanoGPT Usage
* Fetches subscription-level quota from the NanoGPT API.
* Returns daily/weekly token limits and daily image limits for PRO accounts.
*/
async function getNanoGptUsage(apiKey: string) {
if (!apiKey) {
return { message: "NanoGPT API key not available. Add a key to view usage." };
}
try {
const res = await fetch(NANOGPT_CONFIG.usageUrl, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) {
if (res.status === 401) return { message: "Invalid NanoGPT API key." };
return { message: `NanoGPT quota API error (${res.status})` };
}
const data = toRecord(await res.json());
const quotas: Record<string, UsageQuota> = {};
// active -> PRO, otherwise FREE
const plan = data.active ? "PRO" : "FREE";
if (data.active) {
// 1. Tokens limit
// dailyInputTokens if exists, else weeklyInputTokens
let tokenQuota = toRecord(data.dailyInputTokens);
let tokenLabel = "Daily Tokens";
if (!tokenQuota.resetAt) {
const weeklyQuota = toRecord(data.weeklyInputTokens);
if (weeklyQuota.remaining !== undefined) {
tokenQuota = weeklyQuota;
tokenLabel = "Weekly Tokens";
}
}
if (tokenQuota.remaining !== undefined) {
const used = toNumber(tokenQuota.used, 0);
const remaining = toNumber(tokenQuota.remaining, 0);
const total = used + remaining;
quotas[tokenLabel] = {
used,
total,
remaining,
remainingPercentage: clampPercentage(100 - toNumber(tokenQuota.percentUsed, 0) * 100),
resetAt: parseResetTime(tokenQuota.resetAt),
unlimited: false,
};
}
// 2. Images limit
const imageQuota = toRecord(data.dailyImages);
if (imageQuota.remaining !== undefined) {
const used = toNumber(imageQuota.used, 0);
const remaining = toNumber(imageQuota.remaining, 0);
const total = used + remaining;
quotas["Daily Images"] = {
used,
total,
remaining,
remainingPercentage: clampPercentage(100 - toNumber(imageQuota.percentUsed, 0) * 100),
resetAt: parseResetTime(imageQuota.resetAt),
unlimited: false,
};
}
if (Object.keys(quotas).length === 0) {
return { plan, message: "NanoGPT connected, but no active limits found." };
}
}
return { plan, quotas };
} catch (error) {
return { message: `NanoGPT 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
@@ -633,11 +206,7 @@ export async function getUsageForProvider(
case "promptql":
case "pql":
// DDN lux JWTs carry projectId only in JWT aud; connection.projectId may be set by sync.
return await getPromptQlUsage(
apiKey || accessToken,
providerSpecificData,
projectId
);
return await getPromptQlUsage(apiKey || accessToken, providerSpecificData, projectId);
case "adobe-firefly":
case "firefly":
// Cookie or IMS JWT in apiKey/accessToken → GET firefly.adobe.io/v1/credits/balance
@@ -650,387 +219,6 @@ export async function getUsageForProvider(
}
}
/**
* Parse reset date/time to ISO string
* Handles multiple formats: Unix timestamp (ms), ISO date string, etc.
*/
/**
* GitHub Copilot Usage
* Uses GitHub accessToken (not copilotToken) to call copilot_internal/user API
*/
async function getGitHubUsage(accessToken?: string, providerSpecificData?: JsonRecord) {
try {
if (!accessToken) {
throw new Error("No GitHub access token available. Please re-authorize the connection.");
}
// copilot_internal/user API requires GitHub OAuth token, not copilotToken
const response = await fetch("https://api.github.com/copilot_internal/user", {
headers: getGitHubCopilotInternalUserHeaders(`token ${accessToken}`),
});
if (!response.ok) {
const error = await response.text();
if (response.status === 401 || response.status === 403) {
return {
message: `GitHub token expired or permission denied. Please re-authenticate the connection.`,
};
}
throw new Error(`GitHub API error: ${error}`);
}
const data = await response.json();
const dataRecord = toRecord(data);
// Handle different response formats (paid vs free)
if (dataRecord.quota_snapshots) {
// Paid plan format
const snapshots = toRecord(dataRecord.quota_snapshots);
const resetAt = parseResetTime(
getFieldValue(dataRecord, "quota_reset_date", "quotaResetDate")
);
const premiumQuota = formatGitHubQuotaSnapshot(snapshots.premium_interactions, resetAt);
const chatQuota = formatGitHubQuotaSnapshot(snapshots.chat, resetAt);
const completionsQuota = formatGitHubQuotaSnapshot(snapshots.completions, resetAt);
const quotas: Record<string, UsageQuota> = {};
if (shouldDisplayGitHubQuota(premiumQuota)) {
quotas.premium_interactions = premiumQuota;
}
if (shouldDisplayGitHubQuota(chatQuota)) {
quotas.chat = chatQuota;
}
if (shouldDisplayGitHubQuota(completionsQuota)) {
quotas.completions = completionsQuota;
}
return {
plan: inferGitHubPlanName(dataRecord, premiumQuota),
resetDate: getFieldValue(dataRecord, "quota_reset_date", "quotaResetDate"),
quotas,
};
} else if (dataRecord.monthly_quotas || dataRecord.limited_user_quotas) {
// Free/limited plan format. NOTE (#2876): the upstream field
// `limited_user_quotas[name]` is the *remaining* count for the month
// (it counts down toward 0 and resets on `limited_user_reset_date`),
// NOT the used count. The pre-3.8.6 implementation inverted this and
// showed "0% when not used / 100% when fully used" on the dashboard.
// Confirmed against three independent upstream parsers:
// - robinebers/openusage docs/providers/copilot.md (Free Tier table)
// - raycast/extensions agent-usage/src/copilot/fetcher.ts (inline comment)
// - looplj/axonhub frontend/src/components/quota-badges.tsx
const monthlyQuotas = toRecord(dataRecord.monthly_quotas);
const remainingQuotas = toRecord(dataRecord.limited_user_quotas);
const resetDate = getFieldValue(
dataRecord,
"limited_user_reset_date",
"limitedUserResetDate"
);
const resetAt = parseResetTime(resetDate);
const quotas: Record<string, UsageQuota> = {};
const addLimitedQuota = (name: string) => {
const total = toNumber(getFieldValue(monthlyQuotas, name, name), 0);
if (total <= 0) return null;
const remainingRaw = Math.max(0, toNumber(getFieldValue(remainingQuotas, name, name), 0));
const remaining = Math.min(remainingRaw, total);
const used = Math.max(total - remaining, 0);
quotas[name] = {
used,
total,
remaining,
remainingPercentage: clampPercentage((remaining / total) * 100),
unlimited: false,
resetAt,
};
return quotas[name];
};
const premiumQuota = addLimitedQuota("premium_interactions");
addLimitedQuota("chat");
addLimitedQuota("completions");
return {
plan: inferGitHubPlanName(dataRecord, premiumQuota),
resetDate,
quotas,
};
}
return { message: "GitHub Copilot connected. Unable to parse quota data." };
} catch (error) {
throw new Error(`Failed to fetch GitHub usage: ${error.message}`);
}
}
function formatGitHubQuotaSnapshot(
quota: unknown,
resetAt: string | null = null
): UsageQuota | null {
const source = toRecord(quota);
if (Object.keys(source).length === 0) return null;
const unlimited = source.unlimited === true;
const entitlement = toNumber(source.entitlement, Number.NaN);
const totalValue = toNumber(source.total, Number.NaN);
const remainingValue = toNumber(source.remaining, Number.NaN);
const usedValue = toNumber(source.used, Number.NaN);
const percentRemainingValue = toNumber(
getFieldValue(source, "percent_remaining", "percentRemaining"),
Number.NaN
);
let total = Number.isFinite(totalValue)
? Math.max(0, totalValue)
: Number.isFinite(entitlement)
? Math.max(0, entitlement)
: 0;
let remaining = Number.isFinite(remainingValue) ? Math.max(0, remainingValue) : undefined;
let used = Number.isFinite(usedValue) ? Math.max(0, usedValue) : undefined;
let remainingPercentage = Number.isFinite(percentRemainingValue)
? clampPercentage(percentRemainingValue)
: undefined;
if (used === undefined && total > 0 && remaining !== undefined) {
used = Math.max(total - remaining, 0);
}
if (remaining === undefined && total > 0 && used !== undefined) {
remaining = Math.max(total - used, 0);
}
if (remainingPercentage === undefined && total > 0 && remaining !== undefined) {
remainingPercentage = clampPercentage((remaining / total) * 100);
}
if (total <= 0 && remainingPercentage !== undefined) {
total = 100;
used = 100 - remainingPercentage;
remaining = remainingPercentage;
}
return {
used: Math.max(0, used ?? 0),
total,
remaining,
remainingPercentage,
resetAt,
unlimited,
};
}
function inferGitHubPlanName(data: JsonRecord, premiumQuota: UsageQuota | null): string {
const rawPlan = getFieldValue(data, "copilot_plan", "copilotPlan");
const rawSku = getFieldValue(data, "access_type_sku", "accessTypeSku");
const planText = typeof rawPlan === "string" ? rawPlan.trim() : "";
const skuText = typeof rawSku === "string" ? rawSku.trim() : "";
const combined = `${skuText} ${planText}`.trim().toUpperCase();
const monthlyQuotas = toRecord(getFieldValue(data, "monthly_quotas", "monthlyQuotas"));
const premiumTotal =
premiumQuota?.total ||
toNumber(getFieldValue(monthlyQuotas, "premium_interactions", "premiumInteractions"), 0);
const chatTotal = toNumber(getFieldValue(monthlyQuotas, "chat", "chat"), 0);
if (combined.includes("PRO+") || combined.includes("PRO_PLUS") || combined.includes("PROPLUS")) {
return "Copilot Pro+";
}
if (combined.includes("ENTERPRISE")) return "Copilot Enterprise";
if (combined.includes("BUSINESS")) return "Copilot Business";
if (combined.includes("STUDENT")) return "Copilot Student";
if (combined.includes("FREE")) return "Copilot Free";
if (combined.includes("PRO")) return "Copilot Pro";
if (premiumTotal >= 1400) return "Copilot Pro+";
if (premiumTotal >= 900) return "Copilot Enterprise";
if (premiumTotal >= 250) {
if (combined.includes("INDIVIDUAL")) return "Copilot Pro";
return "Copilot Business";
}
if (premiumTotal > 0 || chatTotal === 50) return "Copilot Free";
if (skuText) {
const label = toDisplayLabel(skuText);
return label ? `Copilot ${label}` : "GitHub Copilot";
}
if (planText) {
const label = toDisplayLabel(planText);
return label ? `Copilot ${label}` : "GitHub Copilot";
}
return "GitHub Copilot";
}
/**
* Vertex AI — SELF-TRACKED spend.
*
* Vertex AI exposes no usage/quota API for an API key or Service Account (billing/credit balance
* lives behind the Cloud Billing API, which the proxy credential can't reach). Instead we report
* the USD that OmniRoute has spent through this connection since the account was added — summed
* from `usage_history` and priced via the backend pricing table. Returns a `message` (with the $
* figure) plus a `spend` quota entry so the limits cache persists it (a message-only result is
* treated as a transient error and not cached).
*/
async function getVertexUsage(connectionId: string, provider: string) {
if (!connectionId) {
return { message: "Vertex connected. Connection id unavailable for usage tracking." };
}
try {
const { getConnectionSpendUsdSinceAdded } = await import("@/lib/usage/usageStats");
const { costUsd, requests } = await getConnectionSpendUsdSinceAdded(provider, connectionId);
const spend: JsonRecord = {
used: Number(costUsd.toFixed(6)),
displayName: "Spend (USD)",
quotaSource: "localUsageHistory",
resetAt: null,
unlimited: false,
};
if (requests === 0) {
return {
plan: "Vertex AI",
message: "Vertex connected. No usage recorded through OmniRoute yet for this account.",
quotas: { spend },
};
}
const costStr = costUsd >= 1 ? costUsd.toFixed(2) : costUsd.toFixed(4);
return {
plan: "Vertex AI",
message: `$${costStr} used since this account was added \u00b7 ${requests} request${
requests === 1 ? "" : "s"
}`,
quotas: { spend },
};
} catch (error) {
return { message: `Vertex usage tracking error: ${(error as Error).message}` };
}
}
/**
* Qoder Usage
*
* Qoder exposes account plan + quota at `openapi.qoder.sh/api/v3/user/status`,
* the same endpoint the official qodercli reads for its usage badge. The status
* call needs a short-lived `jt-*` job token, so we exchange the PAT the same way
* the chat/validation paths do (see qoderCli.ts::resolveQoderJobToken).
*/
const QODER_USER_STATUS_URL = "https://openapi.qoder.sh/api/v3/user/status";
/** Human-readable plan label from Qoder's `PLAN_TIER_*` enum / `userTag`. */
function prettifyQoderPlan(planRaw: string, userTag: string): string {
const tag = String(userTag || "").trim();
if (tag) return tag;
const stripped = String(planRaw || "")
.trim()
.replace(/^PLAN_TIER_/i, "");
return stripped ? toTitleCase(stripped) : "Qoder";
}
/**
* Map a Qoder `/user/status` payload into the shared `{ plan, quotas }` shape.
* Pure (no I/O) so it can be unit-tested against captured payloads.
*/
export function parseQoderUserStatusUsage(status: JsonRecord): {
plan: string;
quotas: Record<string, UsageQuota>;
} {
const userType = String(status.userType || "")
.trim()
.toLowerCase();
const planLabel = prettifyQoderPlan(String(status.plan || ""), String(status.userTag || ""));
const isExceeded = status.isQuotaExceeded === true;
const quotaNum = toNumber(status.quota, 0);
const resetAt = parseResetTime(status.nextResetAt);
// Team/enterprise seats draw from a pooled org quota rather than a per-user
// counter, so `quota: 0` there means "pooled", not "exhausted".
const isPooled = userType === "teams" || userType === "enterprise";
const quotas: Record<string, UsageQuota> = {};
if (isExceeded) {
// Genuinely out of quota — remainingPercentage 0 lets routing skip it until reset.
quotas["Quota"] = {
used: quotaNum,
total: quotaNum,
remaining: 0,
remainingPercentage: 0,
resetAt,
unlimited: false,
displayName: "Quota exceeded",
};
} else if (isPooled || quotaNum <= 0) {
// Pooled/unlimited seat — MUST report 100% remaining. The quota→routing
// conversion (src/domain/quotaCache.ts) ignores `unlimited` and would treat a
// `total: 0` window as 0% (i.e. exhausted), wrongly 429-ing every request.
quotas["Plan"] = {
used: 0,
total: 0,
remaining: 0,
remainingPercentage: 100,
resetAt,
unlimited: true,
displayName: `${planLabel} plan · pooled quota`,
};
} else {
quotas["Requests"] = {
used: 0,
total: quotaNum,
remaining: quotaNum,
remainingPercentage: 100,
resetAt,
unlimited: false,
displayName: `${quotaNum} requests left`,
};
}
return { plan: planLabel, quotas };
}
async function getQoderUsage(apiKey?: string, providerSpecificData?: JsonRecord) {
const token = (apiKey || "").trim() || String(providerSpecificData?.qoderPat || "").trim();
if (!token) {
return { message: "Qoder connected. Add a Personal Access Token to view quota." };
}
let jobToken: string;
try {
jobToken = await resolveQoderJobToken(token);
} catch {
return { message: "Qoder connected. Unable to resolve a usage token." };
}
let response: Response;
try {
response = await fetch(QODER_USER_STATUS_URL, {
method: "GET",
headers: { Authorization: `Bearer ${jobToken}`, Accept: "application/json" },
// @ts-ignore — AbortSignal.timeout is available on the Node runtime
signal: AbortSignal.timeout(15000),
});
} catch (error) {
return {
message: `Qoder connected. Unable to fetch usage: ${sanitizeErrorMessage((error as Error).message)}`,
};
}
if (response.status === 401 || response.status === 403) {
return {
message: "Qoder connected. The token was rejected by the usage API — re-test the connection.",
};
}
if (!response.ok) {
return { message: `Qoder connected. Usage API returned HTTP ${response.status}.` };
}
let status: JsonRecord;
try {
status = toRecord(await response.json());
} catch {
return { message: "Qoder connected. Unable to parse the usage response." };
}
return parseQoderUserStatusUsage(status);
}
export const __testing = {
parseResetTime,
parseQoderUserStatusUsage,

View File

@@ -0,0 +1,50 @@
/**
* usage/bailian.ts — Bailian (Alibaba Token Plan) usage fetcher.
*
* Extracted from services/usage.ts (god-file decomposition): the Bailian family —
* delegates to the dedicated bailianQuotaFetcher and shapes the triple-window
* (5h, weekly, monthly) worst-case quota into the standard usage response.
* Depends only on the sibling scalar/quota leaves + fetchBailianQuota — no host
* coupling — so it lives as a co-located provider leaf. usage.ts imports
* getBailianCodingPlanUsage (dispatcher). Behavior-preserving move.
*/
import { fetchBailianQuota, type BailianTripleWindowQuota } from "../bailianQuotaFetcher.ts";
/**
* Bailian (Alibaba Token Plan) Usage
* Fetches triple-window quota (5h, weekly, monthly) and returns worst-case.
*/
export async function getBailianCodingPlanUsage(
connectionId: string,
apiKey: string,
providerSpecificData?: Record<string, unknown>
) {
try {
const connection = { apiKey, providerSpecificData };
const quota = await fetchBailianQuota(connectionId, connection);
if (!quota) {
return { message: "Alibaba Token Plan connected. Unable to fetch quota." };
}
const bailianQuota = quota as BailianTripleWindowQuota;
const used = bailianQuota.used;
const total = bailianQuota.total;
const remaining = Math.max(0, total - used);
const remainingPercentage = Math.round(remaining);
return {
plan: "Alibaba Token Plan",
used,
total,
remaining,
remainingPercentage,
resetAt: bailianQuota.resetAt,
unlimited: false,
displayName: "Alibaba Token Plan",
};
} catch (error) {
return { message: `Alibaba Token Plan error: ${(error as Error).message}` };
}
}

View File

@@ -0,0 +1,123 @@
/**
* usage/crof.ts — CrofAI usage fetcher.
*
* Extracted from services/usage.ts (god-file decomposition): the CrofAI family —
* the /usage_api/ endpoint config and the getCrofUsage fetcher that surfaces the
* daily `usable_requests` subscription bucket plus the USD `credits` balance as
* separate quotas. Depends only on the sibling scalar/quota leaves — no host
* coupling — so it lives as a co-located provider leaf. usage.ts imports
* getCrofUsage (dispatcher). Behavior-preserving move.
*/
import { toRecord, toNumber } from "./scalars.ts";
import { type UsageQuota } from "./quota.ts";
type JsonRecord = Record<string, unknown>;
// Quota / usage upstream URLs (overridable for testing or relays).
const CROF_USAGE_URL = process.env.OMNIROUTE_CROF_USAGE_URL ?? "https://crof.ai/usage_api/";
// 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`
// for pay-as-you-go. `credits` is the USD credit balance. We surface both as
// quotas so the Limits & Quotas page can render whichever the account uses.
export async function getCrofUsage(apiKey: string) {
if (!apiKey) {
return { message: "CrofAI API key not available. Add a key to view usage." };
}
let response: Response;
try {
response = await fetch(CROF_USAGE_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
});
} catch (error) {
return { message: `CrofAI connected. Unable to fetch usage: ${(error as Error).message}` };
}
const rawText = await response.text();
if (response.status === 401 || response.status === 403) {
return { message: "CrofAI connected. The API key was rejected by /usage_api/." };
}
if (!response.ok) {
return { message: `CrofAI connected. /usage_api/ returned HTTP ${response.status}.` };
}
let payload: JsonRecord = {};
if (rawText) {
try {
payload = toRecord(JSON.parse(rawText));
} catch {
return { message: "CrofAI connected. Unable to parse /usage_api/ response." };
}
}
const usableRequestsRaw = payload["usable_requests"];
const usableRequests =
usableRequestsRaw === null || usableRequestsRaw === undefined
? null
: toNumber(usableRequestsRaw, 0);
const credits = toNumber(payload["credits"], 0);
const quotas: Record<string, UsageQuota> = {};
if (usableRequests !== null) {
// CrofAI's /usage_api/ returns only the remaining count; the daily
// allotment is not exposed. CrofAI Pro plan = 1,000 requests/day per
// their pricing page, so use that as the baseline total. If the user
// is on a plan with a higher cap we widen the total to whatever they
// currently report so we never compute a negative `used`.
// Without this, total=0 makes the dashboard's percentage formula read
// 0% (interpreted as "depleted" → red) even on a fresh bucket.
const CROF_DAILY_BASELINE = 1000;
const remaining = Math.max(0, usableRequests);
const total = Math.max(CROF_DAILY_BASELINE, remaining);
const used = Math.max(0, total - remaining);
// CrofAI also does not return a reset timestamp and the docs only say
// "requests left today". The Crof.ai dashboard shows the daily bucket
// resetting at ~05:00 UTC (verified against the live countdown on
// 2026-04-25), so synthesize the next 05:00 UTC instant to match.
// Swap for a real field if Crof ever exposes one.
const now = new Date();
const RESET_HOUR_UTC = 5;
const todayResetMs = Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
now.getUTCDate(),
RESET_HOUR_UTC
);
const nextResetMs =
todayResetMs > now.getTime() ? todayResetMs : todayResetMs + 24 * 60 * 60 * 1000;
const nextResetIso = new Date(nextResetMs).toISOString();
quotas["Requests Today"] = {
used,
total,
remaining,
resetAt: nextResetIso,
unlimited: false,
displayName: `Requests Today: ${remaining} left`,
};
}
// Credits are an open balance — render as unlimited so the UI shows the
// dollar value rather than a misleading 0/0 bar.
quotas["Credits"] = {
used: 0,
total: 0,
remaining: 0,
resetAt: null,
unlimited: true,
displayName: `Credits: $${credits.toFixed(4)}`,
};
return { quotas };
}

View File

@@ -0,0 +1,62 @@
/**
* usage/deepseek.ts — DeepSeek usage fetcher.
*
* Extracted from services/usage.ts (god-file decomposition): the DeepSeek family —
* delegates to the dedicated deepseekQuotaFetcher and shapes the balance result
* into the standard `{ plan, quotas }` usage response (all balances surfaced as
* credits-style entries). Depends only on the sibling scalar/quota leaves +
* fetchDeepseekQuota — no host coupling — so it lives as a co-located provider
* leaf. usage.ts imports getDeepseekUsage (dispatcher). Behavior-preserving move.
*/
import { fetchDeepseekQuota, type DeepseekQuota } from "../deepseekQuotaFetcher.ts";
import { type UsageQuota } from "./quota.ts";
/**
* DeepSeek Usage
* Fetches balance from the DeepSeek balance API.
* Returns all balances (USD and CNY) as "credits" for credits-style UI display.
*/
export async function getDeepseekUsage(connectionId: string, apiKey: string) {
try {
const connection = { apiKey };
const quota = await fetchDeepseekQuota(connectionId, connection);
if (!quota) {
return { message: "DeepSeek API key not available. Add a key to view usage." };
}
const deepseekQuota = quota as DeepseekQuota;
const { balances, isAvailable, limitReached } = deepseekQuota;
const quotas: Record<string, UsageQuota> = {};
// Show all balances as credits-style entries (e.g., credits_usd, credits_cny)
// The UI will display them as "🪙 Balance (USD) $50.00"
for (const balanceInfo of balances) {
const key = `credits_${balanceInfo.currency.toLowerCase()}`;
quotas[key] = {
used: 0,
total: 0,
remaining: balanceInfo.balance,
remainingPercentage: 100,
resetAt: null,
unlimited: true,
currency: balanceInfo.currency,
grantedBalance: balanceInfo.grantedBalance,
toppedUpBalance: balanceInfo.toppedUpBalance,
};
}
const plan = isAvailable ? "DeepSeek" : "DeepSeek (Insufficient Balance)";
return {
plan,
quotas,
isAvailable,
limitReached,
};
} catch (error) {
return { message: `DeepSeek error: ${(error as Error).message}` };
}
}

View File

@@ -0,0 +1,230 @@
/**
* usage/github.ts — GitHub Copilot usage fetcher + quota/plan helpers.
*
* Extracted from services/usage.ts (god-file decomposition): the GitHub family —
* the copilot_internal/user fetcher (getGitHubUsage), the paid/limited quota
* snapshot formatter (formatGitHubQuotaSnapshot), the plan-name inference
* (inferGitHubPlanName), and the display gate (shouldDisplayGitHubQuota).
* Depends only on the sibling scalar/quota leaves + the GitHub internal-user
* header profile — no host coupling — so it lives as a co-located provider leaf.
* usage.ts imports getGitHubUsage (dispatcher) + re-exports the helpers via
* __testing (existing usage-utils / usage-service-hardening suites read them
* from there). Behavior-preserving move.
*/
import { getGitHubCopilotInternalUserHeaders } from "../../config/providerHeaderProfiles.ts";
import { toRecord, toNumber, getFieldValue, clampPercentage, toDisplayLabel } from "./scalars.ts";
import { type UsageQuota, parseResetTime } from "./quota.ts";
type JsonRecord = Record<string, unknown>;
export function shouldDisplayGitHubQuota(quota: UsageQuota | null): quota is UsageQuota {
if (!quota) return false;
if (quota.unlimited && quota.total <= 0) return false;
return quota.total > 0 || quota.remainingPercentage !== undefined;
}
/**
* GitHub Copilot Usage
* Uses GitHub accessToken (not copilotToken) to call copilot_internal/user API
*/
export async function getGitHubUsage(accessToken?: string, providerSpecificData?: JsonRecord) {
try {
if (!accessToken) {
throw new Error("No GitHub access token available. Please re-authorize the connection.");
}
// copilot_internal/user API requires GitHub OAuth token, not copilotToken
const response = await fetch("https://api.github.com/copilot_internal/user", {
headers: getGitHubCopilotInternalUserHeaders(`token ${accessToken}`),
});
if (!response.ok) {
const error = await response.text();
if (response.status === 401 || response.status === 403) {
return {
message: `GitHub token expired or permission denied. Please re-authenticate the connection.`,
};
}
throw new Error(`GitHub API error: ${error}`);
}
const data = await response.json();
const dataRecord = toRecord(data);
// Handle different response formats (paid vs free)
if (dataRecord.quota_snapshots) {
// Paid plan format
const snapshots = toRecord(dataRecord.quota_snapshots);
const resetAt = parseResetTime(
getFieldValue(dataRecord, "quota_reset_date", "quotaResetDate")
);
const premiumQuota = formatGitHubQuotaSnapshot(snapshots.premium_interactions, resetAt);
const chatQuota = formatGitHubQuotaSnapshot(snapshots.chat, resetAt);
const completionsQuota = formatGitHubQuotaSnapshot(snapshots.completions, resetAt);
const quotas: Record<string, UsageQuota> = {};
if (shouldDisplayGitHubQuota(premiumQuota)) {
quotas.premium_interactions = premiumQuota;
}
if (shouldDisplayGitHubQuota(chatQuota)) {
quotas.chat = chatQuota;
}
if (shouldDisplayGitHubQuota(completionsQuota)) {
quotas.completions = completionsQuota;
}
return {
plan: inferGitHubPlanName(dataRecord, premiumQuota),
resetDate: getFieldValue(dataRecord, "quota_reset_date", "quotaResetDate"),
quotas,
};
} else if (dataRecord.monthly_quotas || dataRecord.limited_user_quotas) {
// Free/limited plan format. NOTE (#2876): the upstream field
// `limited_user_quotas[name]` is the *remaining* count for the month
// (it counts down toward 0 and resets on `limited_user_reset_date`),
// NOT the used count. The pre-3.8.6 implementation inverted this and
// showed "0% when not used / 100% when fully used" on the dashboard.
// Confirmed against three independent upstream parsers:
// - robinebers/openusage docs/providers/copilot.md (Free Tier table)
// - raycast/extensions agent-usage/src/copilot/fetcher.ts (inline comment)
// - looplj/axonhub frontend/src/components/quota-badges.tsx
const monthlyQuotas = toRecord(dataRecord.monthly_quotas);
const remainingQuotas = toRecord(dataRecord.limited_user_quotas);
const resetDate = getFieldValue(
dataRecord,
"limited_user_reset_date",
"limitedUserResetDate"
);
const resetAt = parseResetTime(resetDate);
const quotas: Record<string, UsageQuota> = {};
const addLimitedQuota = (name: string) => {
const total = toNumber(getFieldValue(monthlyQuotas, name, name), 0);
if (total <= 0) return null;
const remainingRaw = Math.max(0, toNumber(getFieldValue(remainingQuotas, name, name), 0));
const remaining = Math.min(remainingRaw, total);
const used = Math.max(total - remaining, 0);
quotas[name] = {
used,
total,
remaining,
remainingPercentage: clampPercentage((remaining / total) * 100),
unlimited: false,
resetAt,
};
return quotas[name];
};
const premiumQuota = addLimitedQuota("premium_interactions");
addLimitedQuota("chat");
addLimitedQuota("completions");
return {
plan: inferGitHubPlanName(dataRecord, premiumQuota),
resetDate,
quotas,
};
}
return { message: "GitHub Copilot connected. Unable to parse quota data." };
} catch (error) {
throw new Error(`Failed to fetch GitHub usage: ${error.message}`);
}
}
export function formatGitHubQuotaSnapshot(
quota: unknown,
resetAt: string | null = null
): UsageQuota | null {
const source = toRecord(quota);
if (Object.keys(source).length === 0) return null;
const unlimited = source.unlimited === true;
const entitlement = toNumber(source.entitlement, Number.NaN);
const totalValue = toNumber(source.total, Number.NaN);
const remainingValue = toNumber(source.remaining, Number.NaN);
const usedValue = toNumber(source.used, Number.NaN);
const percentRemainingValue = toNumber(
getFieldValue(source, "percent_remaining", "percentRemaining"),
Number.NaN
);
let total = Number.isFinite(totalValue)
? Math.max(0, totalValue)
: Number.isFinite(entitlement)
? Math.max(0, entitlement)
: 0;
let remaining = Number.isFinite(remainingValue) ? Math.max(0, remainingValue) : undefined;
let used = Number.isFinite(usedValue) ? Math.max(0, usedValue) : undefined;
let remainingPercentage = Number.isFinite(percentRemainingValue)
? clampPercentage(percentRemainingValue)
: undefined;
if (used === undefined && total > 0 && remaining !== undefined) {
used = Math.max(total - remaining, 0);
}
if (remaining === undefined && total > 0 && used !== undefined) {
remaining = Math.max(total - used, 0);
}
if (remainingPercentage === undefined && total > 0 && remaining !== undefined) {
remainingPercentage = clampPercentage((remaining / total) * 100);
}
if (total <= 0 && remainingPercentage !== undefined) {
total = 100;
used = 100 - remainingPercentage;
remaining = remainingPercentage;
}
return {
used: Math.max(0, used ?? 0),
total,
remaining,
remainingPercentage,
resetAt,
unlimited,
};
}
export function inferGitHubPlanName(data: JsonRecord, premiumQuota: UsageQuota | null): string {
const rawPlan = getFieldValue(data, "copilot_plan", "copilotPlan");
const rawSku = getFieldValue(data, "access_type_sku", "accessTypeSku");
const planText = typeof rawPlan === "string" ? rawPlan.trim() : "";
const skuText = typeof rawSku === "string" ? rawSku.trim() : "";
const combined = `${skuText} ${planText}`.trim().toUpperCase();
const monthlyQuotas = toRecord(getFieldValue(data, "monthly_quotas", "monthlyQuotas"));
const premiumTotal =
premiumQuota?.total ||
toNumber(getFieldValue(monthlyQuotas, "premium_interactions", "premiumInteractions"), 0);
const chatTotal = toNumber(getFieldValue(monthlyQuotas, "chat", "chat"), 0);
if (combined.includes("PRO+") || combined.includes("PRO_PLUS") || combined.includes("PROPLUS")) {
return "Copilot Pro+";
}
if (combined.includes("ENTERPRISE")) return "Copilot Enterprise";
if (combined.includes("BUSINESS")) return "Copilot Business";
if (combined.includes("STUDENT")) return "Copilot Student";
if (combined.includes("FREE")) return "Copilot Free";
if (combined.includes("PRO")) return "Copilot Pro";
if (premiumTotal >= 1400) return "Copilot Pro+";
if (premiumTotal >= 900) return "Copilot Enterprise";
if (premiumTotal >= 250) {
if (combined.includes("INDIVIDUAL")) return "Copilot Pro";
return "Copilot Business";
}
if (premiumTotal > 0 || chatTotal === 50) return "Copilot Free";
if (skuText) {
const label = toDisplayLabel(skuText);
return label ? `Copilot ${label}` : "GitHub Copilot";
}
if (planText) {
const label = toDisplayLabel(planText);
return label ? `Copilot ${label}` : "GitHub Copilot";
}
return "GitHub Copilot";
}

View File

@@ -0,0 +1,96 @@
/**
* usage/nanogpt.ts — NanoGPT usage fetcher.
*
* Extracted from services/usage.ts (god-file decomposition): the NanoGPT family —
* the subscription usage-API config and the getNanoGptUsage fetcher that reads
* daily/weekly token + daily image limits for PRO accounts. Depends only on the
* sibling scalar/quota leaves — no host coupling — so it lives as a co-located
* provider leaf. usage.ts imports getNanoGptUsage (dispatcher). Behavior-preserving move.
*/
import { toRecord, toNumber, clampPercentage } from "./scalars.ts";
import { type UsageQuota, parseResetTime } from "./quota.ts";
const NANOGPT_CONFIG = {
usageUrl: "https://nano-gpt.com/api/subscription/v1/usage",
};
/**
* NanoGPT Usage
* Fetches subscription-level quota from the NanoGPT API.
* Returns daily/weekly token limits and daily image limits for PRO accounts.
*/
export async function getNanoGptUsage(apiKey: string) {
if (!apiKey) {
return { message: "NanoGPT API key not available. Add a key to view usage." };
}
try {
const res = await fetch(NANOGPT_CONFIG.usageUrl, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) {
if (res.status === 401) return { message: "Invalid NanoGPT API key." };
return { message: `NanoGPT quota API error (${res.status})` };
}
const data = toRecord(await res.json());
const quotas: Record<string, UsageQuota> = {};
// active -> PRO, otherwise FREE
const plan = data.active ? "PRO" : "FREE";
if (data.active) {
// 1. Tokens limit
// dailyInputTokens if exists, else weeklyInputTokens
let tokenQuota = toRecord(data.dailyInputTokens);
let tokenLabel = "Daily Tokens";
if (!tokenQuota.resetAt) {
const weeklyQuota = toRecord(data.weeklyInputTokens);
if (weeklyQuota.remaining !== undefined) {
tokenQuota = weeklyQuota;
tokenLabel = "Weekly Tokens";
}
}
if (tokenQuota.remaining !== undefined) {
const used = toNumber(tokenQuota.used, 0);
const remaining = toNumber(tokenQuota.remaining, 0);
const total = used + remaining;
quotas[tokenLabel] = {
used,
total,
remaining,
remainingPercentage: clampPercentage(100 - toNumber(tokenQuota.percentUsed, 0) * 100),
resetAt: parseResetTime(tokenQuota.resetAt),
unlimited: false,
};
}
// 2. Images limit
const imageQuota = toRecord(data.dailyImages);
if (imageQuota.remaining !== undefined) {
const used = toNumber(imageQuota.used, 0);
const remaining = toNumber(imageQuota.remaining, 0);
const total = used + remaining;
quotas["Daily Images"] = {
used,
total,
remaining,
remainingPercentage: clampPercentage(100 - toNumber(imageQuota.percentUsed, 0) * 100),
resetAt: parseResetTime(imageQuota.resetAt),
unlimited: false,
};
}
if (Object.keys(quotas).length === 0) {
return { plan, message: "NanoGPT connected, but no active limits found." };
}
}
return { plan, quotas };
} catch (error) {
return { message: `NanoGPT connected. Unable to fetch usage: ${(error as Error).message}` };
}
}

View File

@@ -0,0 +1,86 @@
/**
* usage/opencode.ts — OpenCode / OpenCode Zen usage fetcher.
*
* Extracted from services/usage.ts (god-file decomposition): the OpenCode family —
* delegates to the dedicated opencodeQuotaFetcher and shapes the triple-window
* ($12/5h, $30/wk, $60/mo) result into the standard `{ plan, quotas }` usage
* response expected by the limits page. Depends only on the sibling scalar/quota
* leaves + fetchOpencodeQuota + sanitizeErrorMessage — no host coupling — so it
* lives as a co-located provider leaf. usage.ts imports getOpencodeUsage
* (dispatcher + __testing). Behavior-preserving move.
*/
import { fetchOpencodeQuota, type OpencodeTripleWindowQuota } from "../opencodeQuotaFetcher.ts";
import { sanitizeErrorMessage } from "../../utils/error.ts";
import { type UsageQuota } from "./quota.ts";
/**
* OpenCode Go / OpenCode / OpenCode Zen Usage
* Delegates to the dedicated opencodeQuotaFetcher and shapes the result into
* the standard `{ plan, quotas }` usage response expected by the limits page.
*
* Three rolling windows are surfaced: $12/5h, $30/wk, $60/mo.
*/
export async function getOpencodeUsage(connectionId: string, apiKey: string) {
if (!apiKey) {
return { message: "OpenCode API key not available. Add a key to view usage." };
}
try {
const quota = (await fetchOpencodeQuota(connectionId, {
apiKey,
})) as OpencodeTripleWindowQuota | null;
if (!quota) {
return { message: "OpenCode connected. Unable to fetch quota data." };
}
const { window5h, windowWeekly, windowMonthly, limitReached } = quota;
const quotas: Record<string, UsageQuota> = {};
// $12 / 5-hour rolling window
quotas["window_5h"] = {
used: window5h.percentUsed * 12,
total: 12,
remaining: (1 - window5h.percentUsed) * 12,
remainingPercentage: (1 - window5h.percentUsed) * 100,
resetAt: window5h.resetAt,
unlimited: false,
displayName: "$12 / 5-hour",
currency: "USD",
};
// $30 / weekly window
quotas["window_weekly"] = {
used: windowWeekly.percentUsed * 30,
total: 30,
remaining: (1 - windowWeekly.percentUsed) * 30,
remainingPercentage: (1 - windowWeekly.percentUsed) * 100,
resetAt: windowWeekly.resetAt,
unlimited: false,
displayName: "$30 / week",
currency: "USD",
};
// $60 / monthly window
quotas["window_monthly"] = {
used: windowMonthly.percentUsed * 60,
total: 60,
remaining: (1 - windowMonthly.percentUsed) * 60,
remainingPercentage: (1 - windowMonthly.percentUsed) * 100,
resetAt: windowMonthly.resetAt,
unlimited: false,
displayName: "$60 / month",
currency: "USD",
};
return {
plan: "OpenCode Go",
quotas,
limitReached,
};
} catch (error) {
return { message: `OpenCode error: ${sanitizeErrorMessage(error)}` };
}
}

View File

@@ -0,0 +1,145 @@
/**
* usage/qoder.ts — Qoder usage fetcher + status parser.
*
* Extracted from services/usage.ts (god-file decomposition): the Qoder family —
* the /api/v3/user/status endpoint config, the plan-label prettifier, the pure
* status→quotas mapper (parseQoderUserStatusUsage), and the getQoderUsage
* fetcher that exchanges the PAT for a short-lived job token then reads the
* status endpoint. Depends only on the sibling scalar/quota leaves +
* resolveQoderJobToken + sanitizeErrorMessage — no host coupling — so it lives
* as a co-located provider leaf. usage.ts imports getQoderUsage (dispatcher) +
* re-exports parseQoderUserStatusUsage (named export + __testing, used by the
* qoder-usage-quota suite). Behavior-preserving move.
*/
import { sanitizeErrorMessage } from "../../utils/error.ts";
import { resolveQoderJobToken } from "../qoderCli.ts";
import { toRecord, toNumber, toTitleCase } from "./scalars.ts";
import { type UsageQuota, parseResetTime } from "./quota.ts";
type JsonRecord = Record<string, unknown>;
const QODER_USER_STATUS_URL = "https://openapi.qoder.sh/api/v3/user/status";
/** Human-readable plan label from Qoder's `PLAN_TIER_*` enum / `userTag`. */
function prettifyQoderPlan(planRaw: string, userTag: string): string {
const tag = String(userTag || "").trim();
if (tag) return tag;
const stripped = String(planRaw || "")
.trim()
.replace(/^PLAN_TIER_/i, "");
return stripped ? toTitleCase(stripped) : "Qoder";
}
/**
* Map a Qoder `/user/status` payload into the shared `{ plan, quotas }` shape.
* Pure (no I/O) so it can be unit-tested against captured payloads.
*/
export function parseQoderUserStatusUsage(status: JsonRecord): {
plan: string;
quotas: Record<string, UsageQuota>;
} {
const userType = String(status.userType || "")
.trim()
.toLowerCase();
const planLabel = prettifyQoderPlan(String(status.plan || ""), String(status.userTag || ""));
const isExceeded = status.isQuotaExceeded === true;
const quotaNum = toNumber(status.quota, 0);
const resetAt = parseResetTime(status.nextResetAt);
// Team/enterprise seats draw from a pooled org quota rather than a per-user
// counter, so `quota: 0` there means "pooled", not "exhausted".
const isPooled = userType === "teams" || userType === "enterprise";
const quotas: Record<string, UsageQuota> = {};
if (isExceeded) {
// Genuinely out of quota — remainingPercentage 0 lets routing skip it until reset.
quotas["Quota"] = {
used: quotaNum,
total: quotaNum,
remaining: 0,
remainingPercentage: 0,
resetAt,
unlimited: false,
displayName: "Quota exceeded",
};
} else if (isPooled || quotaNum <= 0) {
// Pooled/unlimited seat — MUST report 100% remaining. The quota→routing
// conversion (src/domain/quotaCache.ts) ignores `unlimited` and would treat a
// `total: 0` window as 0% (i.e. exhausted), wrongly 429-ing every request.
quotas["Plan"] = {
used: 0,
total: 0,
remaining: 0,
remainingPercentage: 100,
resetAt,
unlimited: true,
displayName: `${planLabel} plan · pooled quota`,
};
} else {
quotas["Requests"] = {
used: 0,
total: quotaNum,
remaining: quotaNum,
remainingPercentage: 100,
resetAt,
unlimited: false,
displayName: `${quotaNum} requests left`,
};
}
return { plan: planLabel, quotas };
}
/**
* Qoder Usage
*
* Qoder exposes account plan + quota at `openapi.qoder.sh/api/v3/user/status`,
* the same endpoint the official qodercli reads for its usage badge. The status
* call needs a short-lived `jt-*` job token, so we exchange the PAT the same way
* the chat/validation paths do (see qoderCli.ts::resolveQoderJobToken).
*/
export async function getQoderUsage(apiKey?: string, providerSpecificData?: JsonRecord) {
const token = (apiKey || "").trim() || String(providerSpecificData?.qoderPat || "").trim();
if (!token) {
return { message: "Qoder connected. Add a Personal Access Token to view quota." };
}
let jobToken: string;
try {
jobToken = await resolveQoderJobToken(token);
} catch {
return { message: "Qoder connected. Unable to resolve a usage token." };
}
let response: Response;
try {
response = await fetch(QODER_USER_STATUS_URL, {
method: "GET",
headers: { Authorization: `Bearer ${jobToken}`, Accept: "application/json" },
// @ts-ignore — AbortSignal.timeout is available on the Node runtime
signal: AbortSignal.timeout(15000),
});
} catch (error) {
return {
message: `Qoder connected. Unable to fetch usage: ${sanitizeErrorMessage((error as Error).message)}`,
};
}
if (response.status === 401 || response.status === 403) {
return {
message: "Qoder connected. The token was rejected by the usage API — re-test the connection.",
};
}
if (!response.ok) {
return { message: `Qoder connected. Usage API returned HTTP ${response.status}.` };
}
let status: JsonRecord;
try {
status = toRecord(await response.json());
} catch {
return { message: "Qoder connected. Unable to parse the usage response." };
}
return parseQoderUserStatusUsage(status);
}

View File

@@ -0,0 +1,61 @@
/**
* usage/vertex.ts — Vertex AI self-tracked spend usage fetcher.
*
* Extracted from services/usage.ts (god-file decomposition): the Vertex family —
* Vertex AI exposes no usage/quota API for an API key or Service Account, so
* OmniRoute self-tracks the USD it spent through the connection (summed from
* usage_history via getConnectionSpendUsdSinceAdded) and surfaces a `spend`
* quota entry plus a `$X used · N requests` message. Depends only on the
* sibling scalar/quota leaves + the usageStats dynamic import — no host
* coupling — so it lives as a co-located provider leaf. usage.ts imports
* getVertexUsage (dispatcher + __testing). Behavior-preserving move.
*/
type JsonRecord = Record<string, unknown>;
/**
* Vertex AI — SELF-TRACKED spend.
*
* Vertex AI exposes no usage/quota API for an API key or Service Account (billing/credit balance
* lives behind the Cloud Billing API, which the proxy credential can't reach). Instead we report
* the USD that OmniRoute has spent through this connection since the account was added — summed
* from `usage_history` and priced via the backend pricing table. Returns a `message` (with the $
* figure) plus a `spend` quota entry so the limits cache persists it (a message-only result is
* treated as a transient error and not cached).
*/
export async function getVertexUsage(connectionId: string, provider: string) {
if (!connectionId) {
return { message: "Vertex connected. Connection id unavailable for usage tracking." };
}
try {
const { getConnectionSpendUsdSinceAdded } = await import("@/lib/usage/usageStats");
const { costUsd, requests } = await getConnectionSpendUsdSinceAdded(provider, connectionId);
const spend: JsonRecord = {
used: Number(costUsd.toFixed(6)),
displayName: "Spend (USD)",
quotaSource: "localUsageHistory",
resetAt: null,
unlimited: false,
};
if (requests === 0) {
return {
plan: "Vertex AI",
message: "Vertex connected. No usage recorded through OmniRoute yet for this account.",
quotas: { spend },
};
}
const costStr = costUsd >= 1 ? costUsd.toFixed(2) : costUsd.toFixed(4);
return {
plan: "Vertex AI",
message: `$${costStr} used since this account was added \u00b7 ${requests} request${
requests === 1 ? "" : "s"
}`,
quotas: { spend },
};
} catch (error) {
return { message: `Vertex usage tracking error: ${(error as Error).message}` };
}
}

View File

@@ -0,0 +1,53 @@
/**
* usage/xai.ts — xAI (Grok) self-tracked cumulative usage fetcher.
*
* Extracted from services/usage.ts (god-file decomposition): the xAI family —
* xAI has no public per-account quota API (the billing console at console.x.ai
* requires a session cookie, not an API key), so — exactly like the Xiaomi MiMo
* self-track pattern — OmniRoute sums the tokens it itself routed to this
* connection (from `usage_history`) instead of calling an upstream endpoint.
* Unlike Xiaomi MiMo, xAI has no fixed monthly cap, so the aggregate is reported
* as `unlimited: true` with `remaining: 100`. Depends only on the sibling
* scalar/quota leaves + the usageStats dynamic import — no host coupling — so it
* lives as a co-located provider leaf. usage.ts imports getXaiUsage (dispatcher
* + __testing). Behavior-preserving move.
*/
import { type UsageQuota } from "./quota.ts";
/**
* xAI (Grok) — SELF-TRACKED cumulative usage.
*
* xAI has no public per-account quota API (the billing console at console.x.ai
* requires a session cookie, not an API key), so — exactly like the Xiaomi
* MiMo self-track pattern above — OmniRoute sums the tokens it itself routed
* to this connection (from `usage_history`) instead of calling an upstream
* endpoint. Unlike Xiaomi MiMo, xAI has no fixed monthly cap, so the
* aggregate is reported as `unlimited: true` with `remaining: 100` — this
* renders the dashboard's green "100%" badge instead of a meaningless
* progress bar against a `total: 0`.
*/
export async function getXaiUsage(connectionId: string) {
if (!connectionId) {
return { message: "xAI: connection id unavailable for self-tracked usage." };
}
try {
const { getMonthlyProviderTokensForConnection } = await import("@/lib/usage/usageStats");
const used = getMonthlyProviderTokensForConnection("xai", connectionId);
return {
plan: "xAI / Grok (OmniRoute-tracked)",
quotas: {
monthly: {
used,
total: 0,
remaining: 100,
remainingPercentage: 100,
resetAt: null,
unlimited: true,
} as UsageQuota,
},
};
} catch (error) {
return { message: `xAI self-tracked usage error: ${(error as Error).message}` };
}
}

View File

@@ -0,0 +1,51 @@
/**
* usage/xiaomi-mimo.ts — Xiaomi MiMo self-tracked monthly quota fetcher.
*
* Extracted from services/usage.ts (god-file decomposition): the Xiaomi MiMo family —
* Xiaomi exposes plan usage only behind the console session cookie (the API key
* cannot reach the `tokenPlan/usage` endpoint), so OmniRoute self-tracks the
* tokens it routed to the connection in the current UTC month (from usage_history)
* and compares them to the known Token Plan monthly limit. Depends only on the
* sibling scalar/quota leaves + the usageStats dynamic import — no host coupling
* — so it lives as a co-located provider leaf. usage.ts imports getXiaomiMimoUsage
* (dispatcher + __testing). Behavior-preserving move.
*/
import { createQuotaFromUsage } from "./quota.ts";
// Xiaomi MiMo Token Plan monthly limit (tokens). Keep in sync with the
// "xiaomi-mimo" preset in src/lib/quota/planRegistry.ts.
const XIAOMI_MIMO_MONTHLY_TOKEN_LIMIT = 4_100_000_000;
/**
* Xiaomi MiMo — SELF-TRACKED monthly quota.
*
* Xiaomi exposes plan usage only behind the console session cookie (the API key
* cannot reach the `tokenPlan/usage` endpoint), so there is no upstream usage
* API to call. Instead we count the tokens OmniRoute itself routed to this
* connection in the current UTC month (from `usage_history`) and compare them
* to the known Token Plan monthly limit. This reflects only traffic that went
* through OmniRoute, not the provider's own dashboard figure.
*/
export async function getXiaomiMimoUsage(connectionId: string) {
if (!connectionId) {
return { message: "Xiaomi MiMo: connection id unavailable for self-tracked quota." };
}
try {
const { getMonthlyProviderTokensForConnection } = await import("@/lib/usage/usageStats");
const used = getMonthlyProviderTokensForConnection("xiaomi-mimo", connectionId);
const total = XIAOMI_MIMO_MONTHLY_TOKEN_LIMIT;
const now = new Date();
const resetAt = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)
).toISOString();
return {
plan: "Xiaomi MiMo Token Plan (OmniRoute-tracked)",
quotas: {
monthly: createQuotaFromUsage(used, total, resetAt),
},
};
} catch (error) {
return { message: `Xiaomi MiMo self-tracked usage error: ${(error as Error).message}` };
}
}

View File

@@ -0,0 +1,59 @@
// Characterization of the services/usage.ts bailian split (god-file decomposition): the Bailian
// (Alibaba Token Plan) triple-window fetcher (getBailianCodingPlanUsage) moved into
// services/usage/bailian.ts so usage.ts stays a thin dispatcher. Behavior-preserving move — this
// locks the export surface and the worst-case window selection; the full triple-window matrix
// is covered via getUsageForProvider in bailian-usage.test.ts.
import { test } from "node:test";
import assert from "node:assert/strict";
const B = await import("../../open-sse/services/usage/bailian.ts");
test("module exposes getBailianCodingPlanUsage", () => {
assert.equal(typeof B.getBailianCodingPlanUsage, "function");
});
test("getBailianCodingPlanUsage picks the most restrictive window as the surfaced quota", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(
JSON.stringify({
code: "Success",
data: {
codingPlanInstanceInfos: [
{
planName: "Qwen3 Coder Next",
codingPlanQuotaInfo: {
per5HourUsedQuota: 60,
per5HourTotalQuota: 100,
per5HourQuotaNextRefreshTime: 1718304000,
perWeekUsedQuota: 80,
perWeekTotalQuota: 100,
perWeekQuotaNextRefreshTime: 1718563200,
perBillMonthUsedQuota: 40,
perBillMonthTotalQuota: 100,
perBillMonthQuotaNextRefreshTime: 1719772800,
},
},
],
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
try {
const r = (await B.getBailianCodingPlanUsage("conn", "sk-test", { consoleApiKey: "ck" })) as {
plan?: string;
used?: number;
total?: number;
remaining?: number;
remainingPercentage?: number;
unlimited?: boolean;
};
assert.equal(r.plan, "Alibaba Token Plan");
assert.equal(r.unlimited, false);
// weekly 80% is the most restrictive → used/total = 0.8
assert.equal(r.used! / r.total!, 0.8);
assert.equal(r.remainingPercentage, 20);
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -0,0 +1,69 @@
// Characterization of the services/usage.ts crof split (god-file decomposition): the CrofAI
// /usage_api/ fetcher (getCrofUsage) + its overridable URL moved into services/usage/crof.ts so
// usage.ts stays a thin dispatcher. Behavior-preserving move — these locks pin the export surface
// and the no-key fail-open message already covered via getUsageForProvider in crof-usage.test.ts.
import { test } from "node:test";
import assert from "node:assert/strict";
const C = await import("../../open-sse/services/usage/crof.ts");
test("module exposes getCrofUsage", () => {
assert.equal(typeof C.getCrofUsage, "function");
});
test("getCrofUsage returns a friendly message when the api key is missing", async () => {
const r = (await C.getCrofUsage("")) as { message?: string };
assert.match(r.message ?? "", /CrofAI API key not available/);
});
test("getCrofUsage surfaces Requests Today + Credits for a subscription account", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(JSON.stringify({ usable_requests: 450, credits: 12.3456 }), {
status: 200,
headers: { "content-type": "application/json" },
});
try {
const r = (await C.getCrofUsage("test-key")) as {
quotas?: Record<
string,
{ remaining: number; total: number; used: number; displayName?: string; unlimited: boolean }
>;
};
assert.ok(r.quotas?.["Requests Today"]);
assert.equal(r.quotas!["Requests Today"].remaining, 450);
assert.equal(r.quotas!["Requests Today"].total, 1000);
assert.equal(r.quotas!["Requests Today"].used, 550);
assert.ok(r.quotas?.["Credits"]?.unlimited);
assert.match(r.quotas!["Credits"].displayName!, /\$12\.3456/);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getCrofUsage omits Requests Today for pay-as-you-go (usable_requests=null)", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(JSON.stringify({ usable_requests: null, credits: 5.5 }), {
status: 200,
headers: { "content-type": "application/json" },
});
try {
const r = (await C.getCrofUsage("test-key")) as { quotas?: Record<string, unknown> };
assert.equal(r.quotas?.["Requests Today"], undefined);
assert.ok(r.quotas?.["Credits"]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getCrofUsage returns a rejected-key message on 401/403", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response("nope", { status: 401 });
try {
const r = (await C.getCrofUsage("bad-key")) as { message?: string };
assert.match(r.message ?? "", /rejected/);
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -0,0 +1,82 @@
// Characterization of the services/usage.ts deepseek split (god-file decomposition): the DeepSeek
// balance fetcher (getDeepseekUsage) moved into services/usage/deepseek.ts so usage.ts stays a
// thin dispatcher. Behavior-preserving move — this locks the export surface and the
// credits-style shaping; the full balance matrix is covered via getUsageForProvider in
// usage-service-deepseek.test.ts.
import { test } from "node:test";
import assert from "node:assert/strict";
const D = await import("../../open-sse/services/usage/deepseek.ts");
test("module exposes getDeepseekUsage", () => {
assert.equal(typeof D.getDeepseekUsage, "function");
});
test("getDeepseekUsage surfaces USD + CNY balances as unlimited credits entries", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(
JSON.stringify({
is_available: true,
balance_infos: [
{
currency: "CNY",
total_balance: "1000.00",
granted_balance: "0.00",
topped_up_balance: "1000.00",
},
{
currency: "USD",
total_balance: "50.00",
granted_balance: "5.00",
topped_up_balance: "45.00",
},
],
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
try {
const r = (await D.getDeepseekUsage("conn-multi", "k")) as {
plan?: string;
quotas?: Record<string, { remaining: number; currency: string; unlimited: boolean }>;
};
assert.equal(r.plan, "DeepSeek");
assert.equal(r.quotas?.credits_usd.remaining, 50);
assert.equal(r.quotas?.credits_usd.currency, "USD");
assert.equal(r.quotas?.credits_cny.remaining, 1000);
assert.equal(r.quotas?.credits_usd.unlimited, true);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getDeepseekUsage labels the plan '(Insufficient Balance)' when is_available is false", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(
JSON.stringify({
is_available: false,
balance_infos: [
{
currency: "USD",
total_balance: "0.00",
granted_balance: "0.00",
topped_up_balance: "0.00",
},
],
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
try {
const r = (await D.getDeepseekUsage("conn-empty", "k")) as {
plan?: string;
isAvailable?: boolean;
limitReached?: boolean;
};
assert.equal(r.plan, "DeepSeek (Insufficient Balance)");
assert.equal(r.isAvailable, false);
assert.equal(r.limitReached, true);
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -0,0 +1,71 @@
// Characterization of the services/usage.ts github split (god-file decomposition): the GitHub
// Copilot fetcher (getGitHubUsage) + the paid/limited quota snapshot formatter
// (formatGitHubQuotaSnapshot) + the plan-name inference (inferGitHubPlanName) + the display gate
// (shouldDisplayGitHubQuota) moved into services/usage/github.ts so usage.ts stays a thin
// dispatcher. Behavior-preserving move — these locks pin the export surface and the pure-helper
// edges already covered via __testing in usage-utils / usage-service-hardening.
import { test } from "node:test";
import assert from "node:assert/strict";
const G = await import("../../open-sse/services/usage/github.ts");
test("module exposes the four github helpers", () => {
for (const name of [
"getGitHubUsage",
"formatGitHubQuotaSnapshot",
"inferGitHubPlanName",
"shouldDisplayGitHubQuota",
]) {
assert.equal(typeof (G as Record<string, unknown>)[name], "function", `missing ${name}`);
}
});
test("shouldDisplayGitHubQuota hides unlimited-with-zero-total and empty quotas", () => {
assert.equal(G.shouldDisplayGitHubQuota(null), false);
assert.equal(
G.shouldDisplayGitHubQuota({ used: 0, total: 0, resetAt: null, unlimited: true }),
false
);
assert.equal(
G.shouldDisplayGitHubQuota({ used: 0, total: 0, resetAt: null, unlimited: false }),
false
);
assert.equal(
G.shouldDisplayGitHubQuota({ used: 0, total: 100, resetAt: null, unlimited: false }),
true
);
assert.equal(
G.shouldDisplayGitHubQuota({
used: 0,
total: 0,
remainingPercentage: 50,
resetAt: null,
unlimited: false,
}),
true
);
});
test("formatGitHubQuotaSnapshot returns null for empty objects and synthesizes total from percent", () => {
assert.equal(G.formatGitHubQuotaSnapshot({}), null);
const fromPercent = G.formatGitHubQuotaSnapshot({ percent_remaining: 30 })!;
assert.equal(fromPercent.total, 100);
assert.equal(fromPercent.used, 70);
assert.equal(fromPercent.remaining, 30);
assert.equal(fromPercent.remainingPercentage, 30);
});
test("inferGitHubPlanName maps sku/plan strings and entitlement tiers", () => {
assert.equal(
G.inferGitHubPlanName({ access_type_sku: "copilot_business_seat" }, null),
"Copilot Business"
);
assert.equal(
G.inferGitHubPlanName(
{ copilot_plan: "individual" },
{ used: 0, total: 300, resetAt: null, unlimited: false }
),
"Copilot Pro"
);
assert.equal(G.inferGitHubPlanName({}, null), "GitHub Copilot");
});

View File

@@ -0,0 +1,73 @@
// Characterization of the services/usage.ts nanogpt split (god-file decomposition): the NanoGPT
// subscription usage fetcher (getNanoGptUsage) + its API config moved into
// services/usage/nanogpt.ts so usage.ts stays a thin dispatcher. Behavior-preserving move —
// these locks pin the export surface and the no-key / 401 fail-open messages.
import { test } from "node:test";
import assert from "node:assert/strict";
const N = await import("../../open-sse/services/usage/nanogpt.ts");
test("module exposes getNanoGptUsage", () => {
assert.equal(typeof N.getNanoGptUsage, "function");
});
test("getNanoGptUsage returns a friendly message when the api key is missing", async () => {
const r = (await N.getNanoGptUsage("")) as { message?: string };
assert.match(r.message ?? "", /NanoGPT API key not available/);
});
test("getNanoGptUsage returns FREE plan with no quotas for inactive subscriptions", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(JSON.stringify({ active: false }), {
status: 200,
headers: { "content-type": "application/json" },
});
try {
const r = (await N.getNanoGptUsage("k")) as { plan?: string; quotas?: Record<string, unknown> };
assert.equal(r.plan, "FREE");
assert.deepEqual(r.quotas ?? {}, {});
} finally {
globalThis.fetch = originalFetch;
}
});
test("getNanoGptUsage surfaces Daily Tokens + Daily Images for PRO accounts", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(
JSON.stringify({
active: true,
dailyInputTokens: { used: 10, remaining: 90, percentUsed: 0.1, resetAt: 1_700_000_000 },
dailyImages: { used: 2, remaining: 8, percentUsed: 0.2, resetAt: 1_700_000_000 },
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
try {
const r = (await N.getNanoGptUsage("k")) as {
plan?: string;
quotas?: Record<
string,
{ total: number; used: number; remaining: number; remainingPercentage: number }
>;
};
assert.equal(r.plan, "PRO");
assert.ok(r.quotas?.["Daily Tokens"]);
assert.equal(r.quotas!["Daily Tokens"].total, 100);
assert.equal(r.quotas!["Daily Tokens"].remaining, 90);
assert.equal(r.quotas!["Daily Images"].total, 10);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getNanoGptUsage returns Invalid API key message on 401", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response("nope", { status: 401 });
try {
const r = (await N.getNanoGptUsage("bad")) as { message?: string };
assert.match(r.message ?? "", /Invalid NanoGPT API key/);
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -0,0 +1,18 @@
// Characterization of the services/usage.ts opencode split (god-file decomposition): the
// OpenCode / OpenCode Zen triple-window fetcher (getOpencodeUsage) moved into
// services/usage/opencode.ts so usage.ts stays a thin dispatcher. Behavior-preserving move —
// this locks the export surface and the no-key fail-open message; the window-shaping edges are
// exercised via fetchOpencodeQuota stubs in opencode-quota-fetcher tests.
import { test } from "node:test";
import assert from "node:assert/strict";
const O = await import("../../open-sse/services/usage/opencode.ts");
test("module exposes getOpencodeUsage", () => {
assert.equal(typeof O.getOpencodeUsage, "function");
});
test("getOpencodeUsage returns a no-api-key message when the key is missing", async () => {
const r = (await O.getOpencodeUsage("conn", "")) as { message?: string };
assert.match(r.message ?? "", /OpenCode API key not available/);
});

View File

@@ -0,0 +1,61 @@
// Characterization of the services/usage.ts qoder split (god-file decomposition): the Qoder
// /api/v3/user/status fetcher (getQoderUsage) + the pure status→quotas mapper
// (parseQoderUserStatusUsage) + the plan-label prettifier moved into services/usage/qoder.ts so
// usage.ts stays a thin dispatcher. Behavior-preserving move — these locks pin the export surface
// and the pure parser edges already covered via __testing in qoder-usage-quota.test.ts.
import { test } from "node:test";
import assert from "node:assert/strict";
const Q = await import("../../open-sse/services/usage/qoder.ts");
test("module exposes getQoderUsage + parseQoderUserStatusUsage", () => {
assert.equal(typeof Q.getQoderUsage, "function");
assert.equal(typeof Q.parseQoderUserStatusUsage, "function");
});
test("getQoderUsage returns a Personal Access Token prompt when no token is present", async () => {
const r = (await Q.getQoderUsage(undefined, {})) as { message?: string };
assert.match(r.message ?? "", /Personal Access Token/i);
});
test("parseQoderUserStatusUsage maps a Teams/pooled seat to an unlimited plan entry", () => {
const { plan, quotas } = Q.parseQoderUserStatusUsage({
userType: "teams",
userTag: "Teams",
plan: "PLAN_TIER_TEAM",
quota: 0,
isQuotaExceeded: false,
nextResetAt: 1784736000000,
});
assert.equal(plan, "Teams");
assert.deepEqual(Object.keys(quotas), ["Plan"]);
assert.equal(quotas.Plan.unlimited, true);
assert.equal(quotas.Plan.remainingPercentage, 100);
});
test("parseQoderUserStatusUsage maps an individual plan with remaining quota to a Requests entry", () => {
const { plan, quotas } = Q.parseQoderUserStatusUsage({
userType: "individual",
plan: "PLAN_TIER_PRO",
quota: 42,
isQuotaExceeded: false,
nextResetAt: 1784736000000,
});
assert.equal(plan, "Pro");
assert.deepEqual(Object.keys(quotas), ["Requests"]);
assert.equal(quotas.Requests.remaining, 42);
assert.equal(quotas.Requests.unlimited, false);
});
test("parseQoderUserStatusUsage flags an exceeded quota", () => {
const { quotas } = Q.parseQoderUserStatusUsage({
userType: "individual",
plan: "PLAN_TIER_FREE",
quota: 100,
isQuotaExceeded: true,
nextResetAt: 1784736000000,
});
assert.deepEqual(Object.keys(quotas), ["Quota"]);
assert.equal(quotas.Quota.remaining, 0);
assert.match(quotas.Quota.displayName!, /exceeded/i);
});

View File

@@ -0,0 +1,78 @@
// Characterization of the services/usage.ts vertex split (god-file decomposition): the Vertex AI
// self-tracked spend fetcher (getVertexUsage) moved into services/usage/vertex.ts so usage.ts
// stays a thin dispatcher. Behavior-preserving move — this locks the export surface and the
// missing-connection-id fail-open; the spend aggregation + message shape is covered via
// __testing.getVertexUsage in vertex-spend-usage.test.ts.
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import fs from "node:fs";
// DATA_DIR must be set before any module that opens the DB is imported.
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "omni-vertex-split-"));
process.env.DATA_DIR = TMP;
const core = await import("../../src/lib/db/core.ts");
const V = await import("../../open-sse/services/usage/vertex.ts");
function insertUsage(
connectionId: string,
provider: string,
model: string,
tokensIn: number,
tokensOut: number,
success = 1
) {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)`
).run(provider, model, connectionId, tokensIn, tokensOut, success, new Date().toISOString());
}
describe("vertex leaf self-tracked spend", () => {
before(() => {
core.getDbInstance(); // trigger migrations
insertUsage("conn-leaf", "vertex", "gemini-2.5-flash", 1_000_000, 500_000, 1);
});
after(() => {
core.resetDbInstance();
try {
fs.rmSync(TMP, { recursive: true, force: true });
} catch {
// best-effort temp cleanup
}
});
it("module exposes getVertexUsage", () => {
assert.equal(typeof V.getVertexUsage, "function");
});
it("returns a message when connection id is missing", async () => {
const r = (await V.getVertexUsage("", "vertex")) as { message?: string; quotas?: unknown };
assert.ok(r.message && !r.quotas, "no spend quota without a connection id");
});
it("returns a spend quota + $ message for a used connection", async () => {
const r = (await V.getVertexUsage("conn-leaf", "vertex")) as {
plan?: string;
message?: string;
quotas?: Record<string, { used: number; quotaSource?: string; displayName?: string }>;
};
assert.ok(r.quotas?.spend, "spend quota present");
assert.equal(r.quotas!.spend.quotaSource, "localUsageHistory");
assert.ok(r.message && r.message.includes("$"));
assert.ok(r.message!.includes("1 request"));
});
it("reports no-usage cleanly when nothing was routed", async () => {
const r = (await V.getVertexUsage("conn-empty", "vertex")) as {
message?: string;
quotas?: Record<string, { used: number }>;
};
assert.ok(r.message && /no usage/i.test(r.message));
assert.equal(r.quotas?.spend.used, 0);
});
});

View File

@@ -0,0 +1,72 @@
// Characterization of the services/usage.ts xai split (god-file decomposition): the xAI (Grok)
// self-tracked cumulative usage fetcher (getXaiUsage) moved into services/usage/xai.ts so
// usage.ts stays a thin dispatcher. Behavior-preserving move — this locks the export surface and
// the missing-connection-id fail-open; the cumulative unlimited shaping is covered via
// __testing.getXaiUsage in xai-usage.test.ts.
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import fs from "node:fs";
// DATA_DIR must be set before any module that opens the DB is imported.
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "omni-xai-split-"));
process.env.DATA_DIR = TMP;
const core = await import("../../src/lib/db/core.ts");
const X = await import("../../open-sse/services/usage/xai.ts");
function insertUsage(
connectionId: string,
provider: string,
tokensIn: number,
tokensOut: number,
timestamp: string
) {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO usage_history (provider, connection_id, tokens_input, tokens_output, timestamp)
VALUES (?, ?, ?, ?, ?)`
).run(provider, connectionId, tokensIn, tokensOut, timestamp);
}
describe("xai leaf self-tracked usage", () => {
before(() => {
core.getDbInstance(); // trigger migrations
insertUsage("conn-leaf", "xai", 2_000_000, 300_000, new Date().toISOString());
});
after(() => {
core.resetDbInstance();
try {
fs.rmSync(TMP, { recursive: true, force: true });
} catch {
// best-effort temp cleanup
}
});
it("module exposes getXaiUsage", () => {
assert.equal(typeof X.getXaiUsage, "function");
});
it("returns a message when connection id is missing", async () => {
const r = (await X.getXaiUsage("")) as { message?: string; quotas?: unknown };
assert.ok(r.message && !r.quotas, "no quota without a connection id");
});
it("returns a cumulative unlimited quota scoped to the connection", async () => {
const r = (await X.getXaiUsage("conn-leaf")) as {
plan?: string;
quotas?: Record<
string,
{ used: number; total: number; remaining: number; unlimited: boolean }
>;
message?: string;
};
assert.ok(r.quotas, `expected quotas, got message: ${r.message}`);
const m = r.quotas!.monthly;
assert.equal(m.used, 2_300_000);
assert.equal(m.unlimited, true, "xAI has no fixed monthly cap");
assert.equal(m.remaining, 100, "unlimited rows report remaining: 100");
});
});

View File

@@ -0,0 +1,79 @@
// Characterization of the services/usage.ts xiaomi-mimo split (god-file decomposition): the
// Xiaomi MiMo self-tracked monthly quota fetcher (getXiaomiMimoUsage) moved into
// services/usage/xiaomi-mimo.ts so usage.ts stays a thin dispatcher. Behavior-preserving move —
// this locks the export surface and the missing-connection-id fail-open; the monthly aggregation
// + 4.1B limit comparison is covered via __testing.getXiaomiMimoUsage in
// xiaomi-mimo-selftrack-usage.test.ts.
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import fs from "node:fs";
// DATA_DIR must be set before any module that opens the DB is imported.
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "omni-xiaomi-split-"));
process.env.DATA_DIR = TMP;
const core = await import("../../src/lib/db/core.ts");
const X = await import("../../open-sse/services/usage/xiaomi-mimo.ts");
function insertUsage(
connectionId: string,
provider: string,
tokensIn: number,
tokensOut: number,
timestamp: string
) {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO usage_history (provider, connection_id, tokens_input, tokens_output, timestamp)
VALUES (?, ?, ?, ?, ?)`
).run(provider, connectionId, tokensIn, tokensOut, timestamp);
}
describe("xiaomi-mimo leaf self-tracked quota", () => {
before(() => {
core.getDbInstance(); // trigger migrations
insertUsage("conn-leaf", "xiaomi-mimo", 1_000_000, 500_000, new Date().toISOString());
});
after(() => {
core.resetDbInstance();
try {
fs.rmSync(TMP, { recursive: true, force: true });
} catch {
// best-effort temp cleanup
}
});
it("module exposes getXiaomiMimoUsage", () => {
assert.equal(typeof X.getXiaomiMimoUsage, "function");
});
it("returns a message when connection id is missing", async () => {
const r = (await X.getXiaomiMimoUsage("")) as { message?: string; quotas?: unknown };
assert.ok(r.message && !r.quotas, "no quota without a connection id");
});
it("returns a monthly quota against the 4.1B limit", async () => {
const r = (await X.getXiaomiMimoUsage("conn-leaf")) as {
plan?: string;
quotas?: Record<
string,
{
used: number;
total: number;
remaining?: number;
remainingPercentage?: number;
resetAt: string | null;
}
>;
message?: string;
};
assert.ok(r.quotas, `expected quotas, got message: ${r.message}`);
const m = r.quotas!.monthly;
assert.equal(m.total, 4_100_000_000);
assert.equal(m.used, 1_500_000);
assert.ok(m.resetAt && m.resetAt.endsWith("T00:00:00.000Z"), "reset = first of next month UTC");
});
});