mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 19:02:17 +03:00
* feat(usage): devin-cli agentic quota + openrouter credits in Provider Limits Two provider families with live quota APIs were missing from the Provider Limits dashboard because their list entries were absent: - devin-cli: new usage leaf querying the Codeium seat-management Connect API (exa.seat_management_pb.SeatManagementService/GetUserStatus, protobuf over POST with the raw `Basic <token>-<token>` auth header the CLI itself uses). Surfaces the plan name plus daily/weekly agentic quota percentages with reset timestamps from the GetUserStatus plan_status payload, via a minimal hand-rolled protobuf encoder/reader (no proto dependency warranted for two fixed messages). - openrouter: the /key + /credits quota fetcher (#6842) was already wired into the dispatcher but gated out of the bulk sync — add it to USAGE_SUPPORTED_PROVIDERS and PROVIDER_LIMITS_APIKEY_PROVIDERS so key limits and account credits actually surface. * fix(build): externalize tiktoken so tiktoken_bg.wasm resolves at runtime The vendored ChatGPT Web connector v4.0.7 (#12181) imports tiktoken (get_encoding) at module level. tiktoken's node build reads tiktoken_bg.wasm via a __dirname-relative fs.readFileSync during import; when Next bundles the package the wasm asset is not traced into the server chunk, and page-data collection for every route reaching the tokenizer (e.g. /api/providers/[id]/chatgpt-web-codex-doctor) aborts with "Missing tiktoken_bg.wasm" — breaking the whole standalone build. Externalize it like the other runtime-resolved native/wasm packages (sql.js, sqlite-vec, better-sqlite3): the require stays at runtime, where node_modules/tiktoken/tiktoken_bg.wasm resolves normally. * fix(openrouter): /credits balance survives a /key failure OpenRouter is credit-based, not subscription-based: the authoritative remaining-credits signal is GET /api/v1/credits (total_credits - total_usage, the documented "get remaining credits" endpoint), while the /key limit fields are optional per-key caps that most accounts never set. fetchOpenrouterQuota previously treated /key as mandatory — any /key failure (429 rate limit, transient error, unexpected shape) discarded the whole payload and the Usage dashboard showed "OpenRouter (usage endpoint unreachable)" even though /credits was reachable. Now: - /key unavailable + /credits OK → credits-only quota (creditBalance = total_credits - total_usage) instead of null - /key 401/403 alone no longer means an invalid token; only a double auth-rejection (both endpoints) does - null is returned only when both endpoints fail, and the dashboard label reflects that ("credits endpoint unreachable") * fix(openrouter): render AI Credits as a USD credit count in Provider Limits The Provider Limits card's dollar renderer only activates on isCredits/creditCount rows (QuotaCardExpanded), but openrouter went through parseGeneric — which drops `currency` and never sets those flags — so the credits balance rendered as a meaningless "100% left" (the unlimited-credits row is always 100%) instead of the actual credit count. Route openrouter's `credits` quota through buildCreditsQuota() like the DeepSeek/AgentRouter credits rows: label "AI Credits", dollar-formatted balance. Free-tier request windows keep the generic percentage treatment. * fix(usage): document DEVIN_SEAT_API_URL and split quota parsers Keep fetchOpenrouterQuota and decodeProtoFields under the complexity ratchets, and add the seat-management URL to the env/docs contract. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test(usage): drop duplicated GLM quota-ordering test in provider-limits-ui * test(usage): drop stale openrouter ACCEPTED_DIVERGENCE OpenRouter is now in both USAGE_FETCHER_PROVIDERS and USAGE_SUPPORTED_PROVIDERS, so the recorded aggregator divergence is no longer real. Add the changelog fragment. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
256 lines
10 KiB
TypeScript
256 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 { 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 { 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;
|
|
|
|
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 "openrouter":
|
|
return await getOpenrouterUsage(id || "", apiKey || "", providerSpecificData);
|
|
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,
|
|
};
|