mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 11:43:10 +03:00
* fix(sse): surface Qwen/Alibaba personal Token Plan quota in dashboard and preflight The personal Token Plan (5-hour / 7-day sliding windows) has no official OpenAPI and the inference API key cannot read it. Add a cookie-authenticated fetcher for the console gateway shared by home.qwencloud.com and the Model Studio console (contract captured live from a logged-in session): - open-sse/services/qwenTokenPlanQuotaFetcher.ts: POST /data/api.json (IntlBroadScopeAspnGateway / sfm_bailian) for usage + quota-config + subscription; sec_token resolved best-effort from the dashboard HTML; per-window parse (fields are omitted while a window is Temporarily Removed); 60s usage cache, 1h tier cache. - usage/qwen-token-plan.ts leaf + registration in the usage dispatcher, USAGE_FETCHER_PROVIDERS, USAGE_SUPPORTED_PROVIDERS, PROVIDER_LIMITS_APIKEY_PROVIDERS and bespoke preflight/monitor windows. - Also adds bailian-coding-plan to USAGE_SUPPORTED_PROVIDERS / PROVIDER_LIMITS_APIKEY_PROVIDERS: the coding-plan fetcher existed but the dashboard filtered those connections out (UI gap). Refs #9603 (Problema 1 — quota missing; the 429 recovery half is a follow-up). * docs(env): document Qwen Token Plan quota env vars + regen omni-settings skill QWEN_CLOUD_COOKIE, QWEN_CLOUD_SEC_TOKEN, QWEN_TOKEN_PLAN_HOST and QWEN_TOKEN_PLAN_DASHBOARD_URL added to .env.example and docs/reference/ENVIRONMENT.md (check:env-doc-sync), with the generated omni-settings skill refreshed (check:agent-skills-sync). Refs #9603 * revert: keep hand-tuned omni-settings thinking-budget section The agent-skills-sync drift predates this PR (hand improvement from #10169 not yet synced into the generator source) — it fails on every open PR and belongs to a base-reds fix, not this branch. Regenerating here would erase the intentional content. * feat(dashboard): add the Qwen/Model Studio console cookie field to the connection modal The Token Plan quota fetcher is cookie-authenticated (the inference API key cannot read the console gateway), but no modal field existed to paste that cookie — so the quota was unconfigurable from the dashboard and the fetcher could only ever return its 'needs a cookie' message. Adds the field for qwen-cloud-token-plan and bailian-coding-plan alongside the existing ollama-cloud / alibaba console-cookie inputs (same password-input, blank-keeps-stored semantics), pre-fills it when editing a connection, and extends the providerSpecificData string/length validation to the two new keys. Tests: tests/unit/qwen-token-plan-cookie-field.test.ts (RED before, GREEN after) covers persistence + trimming, the blank-input no-overwrite rule and schema acceptance/rejection. Refs #9603 * docs(dashboard): correct the Qwen console cookie instructions The placeholder claimed the cookie looks like 'token=...'; the qwencloud portal actually issues 'login_qwencloud_ticket=...' alongside cna/cnaui/aui (mirroring login_aliyunid_ticket on the Alibaba console), so the hint pointed at the wrong value. Replaces the guesswork with the verified retrieval steps in all three places an operator can hit — the modal field hint, the fetcher's 'needs a cookie' message and .env.example/ENVIRONMENT.md: log in to home.qwencloud.com > Billing > Subscription, F12 > Network, reload, filter by api.json, click a request to cs-data.qwencloud.com and copy the WHOLE Cookie request header. Also documents that the value must go on one line (it contains '=' and ';') and that it dies with the browser session. Refs #9603 * fix(dashboard): tolerate partial form objects in the qwen cookie branch Adding bailian-coding-plan to QWEN_TOKEN_PLAN_PROVIDERS routed callers that previously matched NO branch in assignQuotaScrapingProviderData into the new one, which assumed the two new fields are always present. Older callers build a partial form object, so buildAddProviderSpecificData threw: TypeError: Cannot read properties of undefined (reading 'trim') (tests/unit/dashboard/agentrouter-connection-modal-fields.test.ts) Reads the new fields with optional chaining and adds a regression test that calls the helper with those keys deleted for both providers. Refs #9603 * refactor(dashboard): move quota-scraping form logic into a UI-free module tests/unit/qwen-token-plan-cookie-field.test.ts imported QuotaScrapingFields directly, which pulls `@/shared/components` and, through that barrel, untranspiled ESM (@lobehub/icons). The node:test runner cannot parse it and the whole test file died in CI with: SyntaxError: Unexpected token 'export' at @lobehub/icons/es/Ai21/components/Mono.js (It passed locally, so only the CI shard surfaced it.) Extracts the pure pieces — QWEN_TOKEN_PLAN_PROVIDERS, QuotaScrapingFieldValues, EMPTY_QUOTA_SCRAPING_FIELDS and assignQuotaScrapingProviderData — into quotaScrapingFieldValues.ts. The component imports them and re-exports the public names, so every existing importer keeps its current path. The unit test now targets the UI-free module. Refs #9603 * fix(providers): point bailian-coding-plan at the Token Plan endpoint and its console Two independent defects kept this provider unusable with a valid Alibaba Token Plan key (verified live 2026-08-14 with the owner's key and cookie): 1. Wrong inference host. The catalog entry is named "Alibaba Token Plan", links to token-plan-overview and its hint asks for a Token Plan key, but the registry pointed at coding-intl.dashscope.aliyuncs.com — the Coding Plan host, which rejects Token Plan keys with 401 invalid_api_key. The documented Anthropic base URL for Token Plan is token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic (https://www.alibabacloud.com/help/en/model-studio/more-tools). Against the new host the same key returns 200 for all six registry models and a real completion; auth stays on x-api-key. 2. Wrong console identity for quota. The personal Token Plan is sold through two consoles sharing one backend, and the gateway validates the session against the console declared in the request: an Alibaba console cookie (login_aliyunid_ticket) sent with the QwenCloud identity is refused with BailianGateway.Login.NotLogined. resolveConsoleSite() now picks host, cornerstoneParam.consoleSite/domain and Origin/Referer from the cookie's login ticket, falling back to the provider. With that switch the same cookie returns usage/subscription/quota-config. Also routes bailian-coding-plan quota through the Token Plan fetcher (the Coding Plan call returns "Bad Request" for these accounts), keeping the old fetcher as the fallback for real Coding Plan keys, and labels the plan by console ("Alibaba Token Plan (Pro)" vs "Qwen …"). Live validation: inference 200 (qwen3.7-plus answered "FUNCIONA"); quota 12,934/40,000 credits, 67.7% remaining, resets 2026-08-20. Refs #9603 --------- Co-authored-by: Xiangzhe <bakryun0718@proton.me>
287 lines
9.8 KiB
TypeScript
287 lines
9.8 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, 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";
|
|
import { getXaiOauthUsage } from "./usage/xaiOauth.ts";
|
|
import { getGrokCliUsage } from "./usage/grokCli.ts";
|
|
import { getFirecrawlUsage } from "./usage/firecrawl.ts";
|
|
import { getCommandCodeUsage } from "./usage/command-code.ts";
|
|
import { getQwenTokenPlanUsage } from "./usage/qwen-token-plan.ts";
|
|
import { getConolUsage } from "./conolUsage.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 this so
|
|
* the registration list can't drift from the switch statement below.
|
|
*
|
|
* If you add a new provider to the switch, add it here too.
|
|
*/
|
|
export const USAGE_FETCHER_PROVIDERS = [
|
|
"github",
|
|
"antigravity",
|
|
"agy",
|
|
"claude",
|
|
"codex",
|
|
"cursor",
|
|
"kiro",
|
|
"amazon-q",
|
|
"kimi-coding",
|
|
"kimi-coding-apikey",
|
|
"qoder",
|
|
"glm",
|
|
"glm-cn",
|
|
"zai",
|
|
"glmt",
|
|
"opencode-go",
|
|
"ollama-cloud",
|
|
"minimax",
|
|
"minimax-cn",
|
|
"crof",
|
|
"bailian-coding-plan",
|
|
"qwen-cloud-token-plan",
|
|
"nanogpt",
|
|
"deepseek",
|
|
"opencode",
|
|
"opencode-zen",
|
|
"xiaomi-mimo",
|
|
"xai",
|
|
"xai-oauth",
|
|
"xao",
|
|
"grok-cli",
|
|
"vertex",
|
|
"vertex-partner",
|
|
"codebuddy-cn",
|
|
"openrouter",
|
|
// PromptQL playground credits (data.pro.ql.app getCreditSummary)
|
|
"promptql",
|
|
"pql",
|
|
// HyperAgent billing usage (creditBlocks USD)
|
|
"hyperagent",
|
|
"ha",
|
|
// Firecrawl team credits (GET /v2/team/credit-usage)
|
|
"firecrawl",
|
|
// Command Code credits + 5h/weekly windows (GET /alpha/billing/credits)
|
|
"command-code",
|
|
"conol-web",
|
|
"cnl",
|
|
] as const;
|
|
|
|
export type UsageFetcherProvider = (typeof USAGE_FETCHER_PROVIDERS)[number];
|
|
|
|
/**
|
|
* 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 getOpenCodeGoUsage(apiKey || "", providerSpecificData);
|
|
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 "command-code":
|
|
return await getCommandCodeUsage(apiKey || accessToken || "");
|
|
case "conol-web":
|
|
case "cnl":
|
|
return await getConolUsage(apiKey || accessToken, providerSpecificData);
|
|
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,
|
|
};
|