Files
OmniRoute/open-sse/services/usage.ts
Pixma 01b2467d61 feat(sse): add LLM Gateway DevPass quota tracking (#12462)
* feat(sse): add LLM Gateway DevPass quota tracking

Surface the LLM Gateway DevPass allowance (GET /v1/key) in OmniRoute's
quota telemetry, mirroring the OpenRouter API-key fetcher pattern.

- llmgatewayQuotaFetcher.ts: fetch + parse the DevPass /v1/key response
  (decimal-string USD values), exposing two windows — monthly plan
  credits and the 7-day premium-model window — with a 45s TTL cache.
  Pay-as-you-go keys (devPlan "none") and 401/403 fail open (no quota).
- Register in chat.ts before registerGenericQuotaFetchers + register the
  named windows for the dashboard cutoff modal.
- usage/llmgateway.ts leaf + usage.ts dispatch case so the Limits page
  renders the monthly + weekly premium rows.
- Add "llmgateway" to USAGE_FETCHER_PROVIDERS, USAGE_SUPPORTED_PROVIDERS,
  PROVIDER_LIMITS_APIKEY_PROVIDERS, and the dashboard label/order map.
- tests: 21 cases covering the parser, auth fail-open, cache TTL, window
  exhaustion, preflight proceed/block, registration, and the usage leaf.

* docs(sse): add changelog fragment + codebase-doc entry for llmgateway quota

* refactor(sse): register llmgateway quota via quotaTrackersBatch

Move the LLM Gateway fetcher registration out of chat.ts (a frozen
file-size-baseline chokepoint) into quotaTrackersBatch.ts, the dedicated
side-effect module that exists precisely so new fetchers don't grow
chat.ts. The batch import runs at module load, before
registerGenericQuotaFetchers(), so the bespoke fetcher still wins over
the generic path. Fixes the file-size gate (chat.ts must not grow).

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:58:05 -03:00

268 lines
10 KiB
TypeScript

/**
* 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 {
extractCodeAssistOnboardTierId,
extractCodeAssistSubscriptionTier,
} from "./codeAssistSubscription.ts";
import { toDisplayLabel } from "./usage/scalars.ts";
import { parseResetTime, createQuotaFromUsage } from "./usage/quota.ts";
import {
getMiniMaxUsage,
getMiniMaxPlanLabel,
getMiniMaxSessionTotal,
inferMiniMaxPlanLabelFromTotals,
getMiniMaxQuotaResetAt,
isMiniMaxTextQuotaModel,
getMiniMaxWeeklyTotal,
createMiniMaxQuotaFromCount,
createMiniMaxQuotaFromPercent,
getMiniMaxRemainingPercent,
getMiniMaxAuthErrorMessage,
getMiniMaxErrorSummary,
} from "./usage/minimax.ts";
import { getGlmUsage } from "./usage/glm.ts";
// Re-exported para o teste glm-coding-plan-monthly (importa de services/usage).
export { glmMonthlyRemainingPercentage } from "./usage/glm.ts";
import {
getAntigravityUsage,
getAntigravityPlanLabel,
mapCodeAssistSubscriptionToPlanLabel,
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";
import { getAdobeFireflyUsage } from "./usage/adobeFirefly.ts";
import { getOpenrouterUsage } from "./usage/openrouter.ts";
import { getLlmgatewayUsage } from "./usage/llmgateway.ts";
import { getOllamaCloudUsage } 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 { getMoonshotOpenPlatformUsage } from "./moonshotQuotaFetcher.ts";
import { isMoonshotOpenPlatformConnection } from "./usage/moonshotOpenPlatform.ts";
import { getDevinCliUsage } from "./usage/devinCli.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";
import { getXaiOauthUsage } from "./usage/xaiOauth.ts";
import { getGrokCliUsage } from "./usage/grokCli.ts";
import { getFirecrawlUsage } from "./usage/firecrawl.ts";
import { getVolcenginePlanUsage } from "./usage/volcenginePlan.ts";
import { getCommandCodeUsage } from "./usage/command-code.ts";
import { getQwenTokenPlanUsage } from "./usage/qwen-token-plan.ts";
import { getConolUsage } from "./conolUsage.ts";
import { getAgentrouterUsage } from "./usage/agentrouter.ts";
import { getKilocodeUsage } from "./usage/kilocode.ts";
type JsonRecord = Record<string, unknown>;
type UsageProviderConnection = JsonRecord & {
id?: string;
provider?: string;
accessToken?: string;
apiKey?: string;
providerSpecificData?: JsonRecord;
projectId?: string;
email?: string;
};
/**
* Single source of truth for which providers have a `getUsageForProvider`
* implementation — consumers like `genericQuotaFetcher.ts` reference it so the
* registration list can't drift from the switch statement below.
*
* If you add a new provider to the switch, add it to the list too. The list now lives in
* `./usage/fetcherProviders.ts` (a zero-dependency leaf) so that consumers which only need
* to know *whether* a fetcher exists — the provider-plugin manifest — can read it without
* importing this dispatcher. Re-exported here so this stays the public import path.
*/
export { USAGE_FETCHER_PROVIDERS } from "./usage/fetcherProviders.ts";
export type { UsageFetcherProvider } from "./usage/fetcherProviders.ts";
/**
* Get usage data for a provider connection
* @param {Object} connection - Provider connection with accessToken
* @returns {Promise<unknown>} Usage data with quotas
*/
export async function getUsageForProvider(
connection: UsageProviderConnection,
options: { forceRefresh?: boolean } = {}
) {
const { id, provider, accessToken, apiKey, providerSpecificData, projectId, email } = connection;
if (isMoonshotOpenPlatformConnection(connection)) {
return await getMoonshotOpenPlatformUsage(connection);
}
switch (provider) {
case "github":
return await getGitHubUsage(accessToken, providerSpecificData);
case "antigravity":
case "agy":
return await getAntigravityUsage(
provider,
accessToken,
providerSpecificData,
projectId,
id,
options
);
case "claude":
return await getClaudeUsage(accessToken);
case "codex":
return await getCodexUsage(accessToken, providerSpecificData);
case "cursor":
return await getCursorUsage(accessToken || "", providerSpecificData);
case "kiro":
case "amazon-q":
return await getKiroUsage(accessToken, providerSpecificData);
case "vertex":
case "vertex-partner":
return await getVertexUsage(id || "", provider);
case "kimi-coding":
case "kimi-coding-apikey":
return await getKimiUsage(accessToken, apiKey, providerSpecificData);
case "qoder":
// Qoder PATs live in `apiKey` (decrypted) or `providerSpecificData.qoderPat`,
// never in `accessToken`.
return await getQoderUsage(apiKey, providerSpecificData);
case "glm":
case "glm-cn":
case "zai":
case "glmt":
return await getGlmUsage(apiKey || "", {
...(providerSpecificData || {}),
...(provider === "glm-cn" ? { apiRegion: "china" } : {}),
});
case "opencode-go":
return await getOpencodeUsage(id || "", apiKey || "");
case "ollama-cloud":
return await getOllamaCloudUsage(providerSpecificData);
case "minimax":
case "minimax-cn":
return await getMiniMaxUsage(apiKey || "", provider);
case "crof":
return await getCrofUsage(apiKey || "");
case "bailian-coding-plan":
return await getBailianCodingPlanUsage(id || "", apiKey || "", providerSpecificData);
case "qwen-cloud-token-plan":
return await getQwenTokenPlanUsage(id || "", apiKey || "", providerSpecificData);
case "nanogpt":
return await getNanoGptUsage(apiKey || "");
case "deepseek":
return await getDeepseekUsage(id || "", apiKey || "");
case "moonshot":
case "kimi":
return await getMoonshotOpenPlatformUsage(connection);
case "openrouter":
return await getOpenrouterUsage(id || "", apiKey || "", providerSpecificData);
case "llmgateway":
return await getLlmgatewayUsage(id || "", apiKey || "");
case "opencode":
case "opencode-zen":
return await getOpencodeUsage(id || "", apiKey || "");
case "xiaomi-mimo":
return await getXiaomiMimoUsage(id || "");
case "xai":
return await getXaiUsage(id || "");
case "xai-oauth":
case "xao":
return await getXaiOauthUsage(id || "", accessToken, connection);
case "grok-cli":
return await getGrokCliUsage(accessToken);
case "codebuddy-cn":
return await getCodeBuddyCnUsage(accessToken, apiKey, providerSpecificData);
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);
case "adobe-firefly":
case "firefly":
// Cookie or IMS JWT in apiKey/accessToken → GET firefly.adobe.io/v1/credits/balance
return await getAdobeFireflyUsage(apiKey, accessToken, providerSpecificData);
case "hyperagent":
case "ha":
return await getHyperAgentUsage(apiKey || accessToken, providerSpecificData);
case "firecrawl":
return await getFirecrawlUsage(id || "", apiKey, connection);
case "volcengine-agent-plan":
case "volcengine-coding-plan":
return await getVolcenginePlanUsage(apiKey || "", provider, providerSpecificData);
case "command-code":
return await getCommandCodeUsage(apiKey || accessToken || "");
case "conol-web":
case "cnl":
return await getConolUsage(apiKey || accessToken, providerSpecificData);
case "agentrouter":
return await getAgentrouterUsage(id, connection);
case "kilocode":
return await getKilocodeUsage(id, connection);
case "devin-cli":
// Devin CLI tokens live in `accessToken` (oauth import) or `apiKey`.
return await getDevinCliUsage(apiKey || accessToken);
default:
return { message: `Usage API not implemented for ${provider}` };
}
}
export const __testing = {
parseResetTime,
parseQoderUserStatusUsage,
formatGitHubQuotaSnapshot,
inferGitHubPlanName,
getAntigravityPlanLabel,
extractCodeAssistSubscriptionTier,
extractCodeAssistOnboardTierId,
getMiniMaxPlanLabel,
inferMiniMaxPlanLabelFromTotals,
getOpencodeUsage,
getClaudePlanLabel,
createQuotaFromUsage,
getMiniMaxQuotaResetAt,
isMiniMaxTextQuotaModel,
getMiniMaxSessionTotal,
getMiniMaxWeeklyTotal,
createMiniMaxQuotaFromCount,
createMiniMaxQuotaFromPercent,
getMiniMaxRemainingPercent,
getMiniMaxUsage,
getXiaomiMimoUsage,
getXaiUsage,
getXaiOauthUsage,
getFirecrawlUsage,
getCommandCodeUsage,
getVertexUsage,
getMiniMaxAuthErrorMessage,
getMiniMaxErrorSummary,
mapCodeAssistSubscriptionToPlanLabel,
mapCodeAssistTierIdToLabel,
mapSubscriptionTierStringToPlanLabel,
toDisplayLabel,
getKiroUsage,
getKilocodeUsage,
};