diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 1d1278e3c..af1d0ef4a 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -484,3 +484,16 @@ If a provider speaks Chat Completions, the `openai-chat` adapter handles it — dashboard or `custom` in `ocx init` and enter the base URL. See the [Configuration reference](/reference/configuration/) for every provider field (`headers`, `noReasoningModels`, `noVisionModels`, `models`, …). + +## Rate limits in the providers overview + +The **Rate limits** section of the Providers overview shows live utilization +bars refreshed from each provider's own usage/billing endpoint when one exists. +The bars show how much of a window (5-hour, weekly, monthly, or +provider-specific) is already consumed. + +Providers with a live probe: OpenAI/Codex, Anthropic, xAI, Cursor, Kimi, +Google Antigravity, OpenRouter, DeepSeek, ClinePass, Z.AI, MiniMax, +Moonshot, Venice, Synthetic, DeepInfra, Neuralwatt, and any a6api-backed +custom provider. + diff --git a/gui/bun.lock b/gui/bun.lock index 1c7e030ba..bbf5a3ad1 100644 --- a/gui/bun.lock +++ b/gui/bun.lock @@ -14,12 +14,13 @@ "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.3", + "@vitejs/plugin-react": "^6.0.5", "eslint": "^10.3.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", "happy-dom": "20.11.1", + "react-refresh": "^0.18.0", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.1.0", @@ -183,7 +184,7 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA=="], "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], @@ -361,6 +362,8 @@ "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + "rolldown": ["rolldown@1.1.3", "", { "dependencies": { "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.3", "@rolldown/binding-darwin-arm64": "1.1.3", "@rolldown/binding-darwin-x64": "1.1.3", "@rolldown/binding-freebsd-x64": "1.1.3", "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", "@rolldown/binding-linux-arm64-gnu": "1.1.3", "@rolldown/binding-linux-arm64-musl": "1.1.3", "@rolldown/binding-linux-ppc64-gnu": "1.1.3", "@rolldown/binding-linux-s390x-gnu": "1.1.3", "@rolldown/binding-linux-x64-gnu": "1.1.3", "@rolldown/binding-linux-x64-musl": "1.1.3", "@rolldown/binding-openharmony-arm64": "1.1.3", "@rolldown/binding-wasm32-wasi": "1.1.3", "@rolldown/binding-win32-arm64-msvc": "1.1.3", "@rolldown/binding-win32-x64-msvc": "1.1.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], diff --git a/gui/package.json b/gui/package.json index 1556402dc..7f62cdc78 100644 --- a/gui/package.json +++ b/gui/package.json @@ -23,12 +23,13 @@ "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.3", + "@vitejs/plugin-react": "^6.0.5", "eslint": "^10.3.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", "happy-dom": "20.11.1", + "react-refresh": "^0.18.0", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.1.0" diff --git a/src/providers/derive.ts b/src/providers/derive.ts index 87b3a0e15..b63c9fa7d 100644 --- a/src/providers/derive.ts +++ b/src/providers/derive.ts @@ -1,5 +1,9 @@ import type { CodexAccountMode, OcxProviderConfig } from "../types"; -import { PROVIDER_REGISTRY, providerMatchesRegistryTransport, type ProviderRegistryEntry } from "./registry"; +import { + PROVIDER_REGISTRY, + providerMatchesRegistryTransport, + type ProviderRegistryEntry, +} from "./registry"; export interface DerivedKeyLoginProvider { label: string; diff --git a/src/providers/quota.ts b/src/providers/quota.ts index 1e5b46fb5..3ff9396c8 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -34,6 +34,16 @@ const REQUEST_TIMEOUT_MS = 8_000; const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1"; const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`; const A6API_BASE_URL = "https://api.a6api.com"; +const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"; +const DEEPSEEK_BASE_URL = "https://api.deepseek.com"; +const CLINE_BASE_URL = "https://api.cline.bot"; +const ZAI_BASE_URL = "https://api.z.ai"; +const MINIMAX_REMAINS_URL = "https://www.minimax.io/v1/token_plan/remains"; +const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1"; +const VENICE_BASE_URL = "https://api.venice.ai/api/v1"; +const SYNTHETIC_BASE_URL = "https://api.synthetic.new/v2"; +const DEEPINFRA_BASE_URL = "https://api.deepinfra.com"; +const NEURALWATT_BASE_URL = "https://api.neuralwatt.com/v1"; /** Keep a failed probe's previous row at most this long before dropping it. */ const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS; const nativeMainReportGenerations = new WeakMap(); @@ -243,6 +253,54 @@ function isCanonicalA6apiBaseUrl(baseUrl: string): boolean { return normalized === A6API_BASE_URL || normalized === `${A6API_BASE_URL}/v1`; } +function isCanonicalOpenRouterBaseUrl(baseUrl: string): boolean { + const normalized = normalizedBaseUrl(baseUrl); + return normalized === OPENROUTER_BASE_URL; +} + +function isCanonicalDeepSeekBaseUrl(baseUrl: string): boolean { + const normalized = normalizedBaseUrl(baseUrl); + return normalized === DEEPSEEK_BASE_URL || normalized === `${DEEPSEEK_BASE_URL}/v1`; +} + +function isCanonicalClineBaseUrl(baseUrl: string): boolean { + const normalized = normalizedBaseUrl(baseUrl); + return normalized === CLINE_BASE_URL || normalized === `${CLINE_BASE_URL}/api/v1`; +} + +function isCanonicalZaiBaseUrl(baseUrl: string): boolean { + const normalized = normalizedBaseUrl(baseUrl); + return normalized === ZAI_BASE_URL || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`; +} + +function isCanonicalMinimaxBaseUrl(baseUrl: string): boolean { + const normalized = normalizedBaseUrl(baseUrl); + return normalized === "https://api.minimax.io/v1" || normalized === "https://api.minimaxi.com/v1"; +} + +function isCanonicalMoonshotBaseUrl(baseUrl: string): boolean { + const normalized = normalizedBaseUrl(baseUrl); + return normalized === MOONSHOT_BASE_URL || normalized === "https://api.moonshot.cn/v1"; +} + +function isCanonicalVeniceBaseUrl(baseUrl: string): boolean { + return normalizedBaseUrl(baseUrl) === VENICE_BASE_URL; +} + +function isCanonicalSyntheticBaseUrl(baseUrl: string): boolean { + const normalized = normalizedBaseUrl(baseUrl); + return normalized === SYNTHETIC_BASE_URL || normalized === "https://api.synthetic.new/openai/v1"; +} + +function isCanonicalDeepInfraBaseUrl(baseUrl: string): boolean { + const normalized = normalizedBaseUrl(baseUrl); + return normalized === DEEPINFRA_BASE_URL || normalized === `${DEEPINFRA_BASE_URL}/v1/openai`; +} + +function isCanonicalNeuralwattBaseUrl(baseUrl: string): boolean { + return normalizedBaseUrl(baseUrl) === NEURALWATT_BASE_URL; +} + function a6apiPayload(value: unknown): Record | null { const body = asRecord(value); return asRecord(body?.data) ?? body; @@ -309,6 +367,470 @@ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Pro }); } +/** + * OpenRouter `GET /api/v1/key` — the key's own credit balance and optional + * per-key spending cap. `limit` is the configured cap (absent = uncapped); + * `usage` is lifetime spend; `limit_remaining` is what is left of the cap. + * When no cap is set there is no hard limit to meter against, so no bar is + * produced — the provider falls back to its documented reference. + */ +async function fetchOpenRouterQuota(provider: string, config: OcxProviderConfig): Promise { + // Never send a configured API key to a lookalike host or through a redirect. + if (!isCanonicalOpenRouterBaseUrl(config.baseUrl)) return null; + const apiKey = resolveEnvValue(config.apiKey)?.trim(); + if (!apiKey) return null; + const response = await fetch(`${OPENROUTER_BASE_URL}/key`, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const body = asRecord(await response.json().catch(() => null)); + const data = asRecord(body?.data) ?? body; + if (!data) return null; + const limit = toFiniteNumber(data.limit); + const limitRemaining = toFiniteNumber(data.limit_remaining); + const usage = toFiniteNumber(data.usage); + // A successful no-cap response is a DELIBERATE change, not a transient + // failure: the old capped row must be dropped, not preserved as last-good. + if (limit === undefined || limit <= 0) return TERMINAL_QUOTA_FAILURE; + // Prefer the authoritative remaining-cap value when present: `usage` is + // lifetime accumulated spend and overstates a reset or re-capped key. + const used = limitRemaining !== undefined + ? Math.max(0, limit - limitRemaining) + : usage !== undefined && usage >= 0 ? usage : undefined; + if (used === undefined) return null; + const percent = normalizePercent((used / limit) * 100); + if (percent === undefined) return null; + const remaining = Math.max(0, limit - used); + const label = `API credits ($${remaining.toFixed(2)} of $${limit.toFixed(2)} remaining)`; + return report(provider, "openrouter:key-info", { + customWindows: [{ label, percent }], + updatedAt: Date.now(), + }); +} + +/** + * DeepSeek `GET /user/balance` — the account's granted + topped-up credit + * balance. The payload places `total_balance` / `granted_balance` inside + * entries of `balance_infos` (one row per currency); the row for the account's + * currency is selected by preference. `granted_balance` is a CURRENT balance + * component, not the original grant ceiling, so no consumed percentage is + * fabricated — the balance is reported as a balance-only window. + */ +async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig): Promise { + if (!isCanonicalDeepSeekBaseUrl(config.baseUrl)) return null; + const apiKey = resolveEnvValue(config.apiKey)?.trim(); + if (!apiKey) return null; + const response = await fetch(`${DEEPSEEK_BASE_URL}/user/balance`, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const body = asRecord(await response.json().catch(() => null)); + // The payload nests balances under `balance_infos` rows keyed by currency; + // prefer a USD row, then CNY, then the first row that parses. + const infos = Array.isArray(body?.balance_infos) ? body.balance_infos as unknown[] : null; + const rows = infos + ? infos.map((raw): Record | null => asRecord(raw)).filter((r): r is Record => r !== null) + : []; + const pick = (currency: string): Record | null => + rows.find(row => String(row.currency ?? "").toUpperCase() === currency) ?? null; + const preferred = pick("USD") ?? pick("CNY") ?? rows[0] ?? null; + if (!preferred) return null; + const totalBalance = toFiniteNumber(preferred.total_balance); + const grantedBalance = toFiniteNumber(preferred.granted_balance); + const toppedUp = toFiniteNumber(preferred.topped_up_balance); + const balance = totalBalance ?? grantedBalance ?? toppedUp; + if (balance === undefined || balance < 0) return null; + const label = grantedBalance !== undefined && grantedBalance > 0 + ? `API balance ($${balance.toFixed(2)} total, $${grantedBalance.toFixed(2)} granted)` + : `API balance ($${balance.toFixed(2)})`; + return report(provider, "deepseek:balance", { + customWindows: [{ label, percent: 0 }], + updatedAt: Date.now(), + }); +} + +/** + * ClinePass `GET /api/v1/users/me/plan/usage-limits` — the subscription's + * rolling five-hour, weekly, and monthly utilization, matching the existing + * ProviderQuota windows directly. The endpoint 404s (or returns a null plan) + * for accounts without an active ClinePass, which is a no-report, not an error. + */ +async function fetchClineQuota(provider: string, config: OcxProviderConfig): Promise { + if (!isCanonicalClineBaseUrl(config.baseUrl)) return null; + const apiKey = resolveEnvValue(config.apiKey)?.trim(); + if (!apiKey) return null; + const response = await fetch(`${CLINE_BASE_URL}/api/v1/users/me/plan/usage-limits`, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + // 404 = no active plan; a plain "no plan" is a no-report, everything else + // 4xx (except 408/429) is a credential/contract problem. + if (response.status === 404) return null; + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const body = asRecord(await response.json().catch(() => null)); + const data = asRecord(body?.data) ?? body; + const limits = Array.isArray(data?.limits) ? data.limits : null; + if (!limits) return null; + const quota: ProviderQuota = { updatedAt: Date.now() }; + let windows = 0; + for (const raw of limits) { + const row = asRecord(raw); + if (!row) continue; + const percent = normalizePercent(row.percentUsed); + if (percent === undefined) continue; + const resetAt = normalizeResetAt(row.resetsAt); + if (row.type === "five_hour") { + quota.fiveHourPercent = percent; + if (resetAt !== undefined) quota.fiveHourResetAt = resetAt; + windows += 1; + } else if (row.type === "weekly") { + quota.weeklyPercent = percent; + if (resetAt !== undefined) quota.weeklyResetAt = resetAt; + windows += 1; + } else if (row.type === "monthly") { + quota.monthlyPercent = percent; + if (resetAt !== undefined) quota.monthlyResetAt = resetAt; + windows += 1; + } + } + return windows > 0 ? report(provider, "cline:plan-usage-limits", quota) : null; +} + +/** + * Z.AI GLM Coding Plan `GET /api/monitor/usage/quota/limit` — the coding-plan + * subscription's 5-hour token cycle, weekly quota, and monthly MCP usage. + * Authenticates with the API key as a Bearer token per Z.AI's API reference. + */ +async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise { + if (!isCanonicalZaiBaseUrl(config.baseUrl)) return null; + const apiKey = resolveEnvValue(config.apiKey)?.trim(); + if (!apiKey) return null; + const response = await fetch(`${ZAI_BASE_URL}/api/monitor/usage/quota/limit`, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const body = asRecord(await response.json().catch(() => null)); + if (!body || body.success === false) return null; + const data = asRecord(body.data) ?? body; + // The plugin renders a 5h token window, a weekly window, and a monthly MCP + // window. Look for percent fields with window identifiers. + const quota: ProviderQuota = { updatedAt: Date.now() }; + let windows = 0; + const percentAt = (key: string): number | undefined => { + const value = normalizePercent(data?.[key]); + if (value !== undefined) return value; + const nested = asRecord(data?.quota); + return nested ? normalizePercent(nested[key]) : undefined; + }; + const fiveHour = percentAt("fiveHourPercent") ?? percentAt("fiveHourUsage") ?? percentAt("fiveHourUsed"); + const weekly = percentAt("weeklyPercent") ?? percentAt("weeklyUsage") ?? percentAt("weeklyUsed"); + const monthly = percentAt("monthlyPercent") ?? percentAt("mcpPercent") ?? percentAt("monthlyMCPUsage"); + if (fiveHour !== undefined) { + quota.fiveHourPercent = fiveHour; + windows += 1; + } + if (weekly !== undefined) { + quota.weeklyPercent = weekly; + windows += 1; + } + if (monthly !== undefined) { + quota.monthlyPercent = monthly; + windows += 1; + } + return windows > 0 ? report(provider, "zai:quota-limit", quota) : null; +} + +/** + * MiniMax Token Plan `GET /v1/token_plan/remains` — the subscription's + * remaining quota as a countdown-time value (ms). The endpoint does not expose + * the plan's total duration, so no percentage is fabricated from a presumed + * window: the remaining time is reported as a duration-only window. When the + * API supplies a total (`total_time` / `plan_duration_ms`), a consumed share + * is derived from it. Region selects the host: `minimax` → www.minimax.io, + * `minimax-cn` → api.minimaxi.com. + */ +async function fetchMinimaxQuota(provider: string, config: OcxProviderConfig): Promise { + if (!isCanonicalMinimaxBaseUrl(config.baseUrl)) return null; + const apiKey = resolveEnvValue(config.apiKey)?.trim(); + if (!apiKey) return null; + const cnHost = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.minimaxi.com"); + const remainsUrl = cnHost ? "https://api.minimaxi.com/v1/token_plan/remains" : MINIMAX_REMAINS_URL; + const response = await fetch(remainsUrl, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const body = asRecord(await response.json().catch(() => null)); + if (!body || body.success === false) return null; + const data = asRecord(body.data) ?? body; + const remainsMs = toFiniteNumber(data.remains_time ?? data.remainsTime); + if (remainsMs === undefined || remainsMs < 0) return null; + const hours = Math.floor(remainsMs / 3_600_000); + const label = `Token Plan remaining (${hours}h)`; + // Only derive a consumed share when the API actually reports the plan total; + // a presumed window (e.g. 30 days) would fabricate utilization. A valid + // response that omits the total after a prior refresh had it is a DELIBERATE + // contract change — the old row must be dropped (terminal), not preserved as + // a transient last-good. + const totalMs = toFiniteNumber(data.total_time ?? data.plan_duration_ms ?? data.total_duration_ms); + if (totalMs === undefined || totalMs <= 0) return TERMINAL_QUOTA_FAILURE; + const consumed = Math.max(0, totalMs - remainsMs); + const percent = normalizePercent((consumed / totalMs) * 100); + if (percent === undefined) return null; + return report(provider, "minimax:token-plan-remains", { + customWindows: [{ label, percent }], + updatedAt: Date.now(), + }); +} + +/** + * Moonshot/Kimi `GET /v1/users/me/balance` — the account's available balance + * (voucher + cash). Renders a single balance window against the sum of + * voucher + cash when positive (there is no per-window rate limit to meter). + */ +async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig): Promise { + if (!isCanonicalMoonshotBaseUrl(config.baseUrl)) return null; + const apiKey = resolveEnvValue(config.apiKey)?.trim(); + if (!apiKey) return null; + const host = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.moonshot.cn") ? "https://api.moonshot.cn/v1" : MOONSHOT_BASE_URL; + const response = await fetch(`${host}/users/me/balance`, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const body = asRecord(await response.json().catch(() => null)); + const data = asRecord(body?.data) ?? body; + if (!data) return null; + const available = toFiniteNumber(data.available_balance); + const voucher = toFiniteNumber(data.voucher_balance); + const cash = toFiniteNumber(data.cash_balance); + if (available === undefined || available < 0) return null; + // Moonshot exposes no per-window quota ceiling, only a balance — report it + // as a balance-only window (percent 0) rather than a fabricated utilization. + const label = voucher !== undefined && cash !== undefined + ? `Balance ($${available.toFixed(2)} available, $${voucher.toFixed(2)} voucher)` + : `Balance ($${available.toFixed(2)} available)`; + return report(provider, "moonshot:balance", { + customWindows: [{ label, percent: 0 }], + updatedAt: Date.now(), + }); +} + +/** + * Venice `GET /api/v1/billing/balance` — DIEM (native credits) or USD balance. + * Shows the remaining balance; epoch allocation progress when present. + */ +async function fetchVeniceQuota(provider: string, config: OcxProviderConfig): Promise { + if (!isCanonicalVeniceBaseUrl(config.baseUrl)) return null; + const apiKey = resolveEnvValue(config.apiKey)?.trim(); + if (!apiKey) return null; + const response = await fetch(`${VENICE_BASE_URL}/billing/balance`, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const body = asRecord(await response.json().catch(() => null)); + const data = asRecord(body?.data) ?? body; + if (!data) return null; + const diemBalance = toFiniteNumber(data.balance); + const usdBalance = toFiniteNumber(data.balance_usd); + const epochUsed = toFiniteNumber(data.diem_epoch_used); + const epochAllocated = toFiniteNumber(data.diem_epoch_allocated); + if (diemBalance === undefined && usdBalance === undefined) return null; + const label = diemBalance !== undefined + ? `DIEM balance (${Math.round(diemBalance)})` + : `USD balance ($${usdBalance?.toFixed(2) ?? "?"})`; + if (epochAllocated !== undefined && epochAllocated > 0 && epochUsed !== undefined) { + const percent = normalizePercent((epochUsed / epochAllocated) * 100); + if (percent === undefined) return null; + return report(provider, "venice:billing-balance", { + customWindows: [{ label, percent }], + updatedAt: Date.now(), + }); + } + return report(provider, "venice:billing-balance", { + customWindows: [{ label, percent: 0 }], + updatedAt: Date.now(), + }); +} + +/** + * Synthetic `GET /v2/quotas` — the known quota lanes (rolling 5-hour, + * weekly token, search-hourly) mapped onto the quota windows. + */ +async function fetchSyntheticQuota(provider: string, config: OcxProviderConfig): Promise { + if (!isCanonicalSyntheticBaseUrl(config.baseUrl)) return null; + const apiKey = resolveEnvValue(config.apiKey)?.trim(); + if (!apiKey) return null; + const response = await fetch(`${SYNTHETIC_BASE_URL}/quotas`, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const body = asRecord(await response.json().catch(() => null)); + const data = asRecord(body?.data) ?? body; + const quota: ProviderQuota = { updatedAt: Date.now() }; + let windows = 0; + const percentAt = (key: string): number | undefined => { + const value = normalizePercent(data?.[key]); + if (value !== undefined) return value; + const nested = asRecord(data?.quota) ?? asRecord(data?.quotas); + return nested ? normalizePercent(nested[key]) : undefined; + }; + const fiveHour = percentAt("rollingFiveHourLimit"); + const weekly = percentAt("weeklyTokenLimit"); + if (fiveHour !== undefined) { + quota.fiveHourPercent = fiveHour; + windows += 1; + } + if (weekly !== undefined) { + quota.weeklyPercent = weekly; + windows += 1; + } + const search = asRecord(data?.search); + const searchHourly = search ? normalizePercent(search.hourly) : undefined; + if (searchHourly !== undefined) { + quota.customWindows = [...(quota.customWindows ?? []), { label: "Search hourly", percent: searchHourly }]; + windows += 1; + } + return windows > 0 ? report(provider, "synthetic:quotas", quota) : null; +} + +/** + * DeepInfra `GET /payment/checklist?compute_owed=true` — prepaid balance, + * recent spend, spending limit, and suspension state. Renders a balance + * window (prepaid funds are a negative `stripe_balance` → positive available). + */ +async function fetchDeepInfraQuota(provider: string, config: OcxProviderConfig): Promise { + if (!isCanonicalDeepInfraBaseUrl(config.baseUrl)) return null; + const apiKey = resolveEnvValue(config.apiKey)?.trim(); + if (!apiKey) return null; + const response = await fetch(`${DEEPINFRA_BASE_URL}/payment/checklist?compute_owed=true`, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const body = asRecord(await response.json().catch(() => null)); + const data = asRecord(body?.data) ?? body; + if (!data) return null; + const stripeBalance = toFiniteNumber(data.stripe_balance); + const spendLimit = toFiniteNumber(data.spending_limit); + const total = toFiniteNumber(data.total_amount_due); + if (stripeBalance === undefined) return null; + // Prepaid funds are negative; a positive value is money owed. + const available = stripeBalance < 0 ? -stripeBalance : 0; + if (spendLimit !== undefined && spendLimit > 0) { + const spent = total !== undefined && total > 0 ? total : Math.max(0, spendLimit - available); + const percent = normalizePercent((spent / spendLimit) * 100); + if (percent === undefined) return null; + return report(provider, "deepinfra:billing-checklist", { + customWindows: [{ label: `Billing cycle spend ($${spent.toFixed(2)} of $${spendLimit.toFixed(2)})`, percent }], + updatedAt: Date.now(), + }); + } + return report(provider, "deepinfra:billing-checklist", { + customWindows: [{ label: `Prepaid balance ($${available.toFixed(2)})`, percent: 0 }], + updatedAt: Date.now(), + }); +} + +/** + * Neuralwatt `GET /v1/quota` — subscription kWh usage (primary window) and + * prepaid USD credit balance (secondary). + */ +async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig): Promise { + if (!isCanonicalNeuralwattBaseUrl(config.baseUrl)) return null; + const apiKey = resolveEnvValue(config.apiKey)?.trim(); + if (!apiKey) return null; + const response = await fetch(`${NEURALWATT_BASE_URL}/quota`, { + headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` }, + redirect: "error", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429 + ? TERMINAL_QUOTA_FAILURE + : null; + } + const body = asRecord(await response.json().catch(() => null)); + const data = asRecord(body?.data) ?? body; + const quota: ProviderQuota = { updatedAt: Date.now() }; + let windows = 0; + const subscription = asRecord(data?.subscription); + const kwhUsed = subscription ? toFiniteNumber(subscription.kwh_used) : undefined; + const kwhIncluded = subscription ? toFiniteNumber(subscription.kwh_included) : undefined; + if (kwhUsed !== undefined && kwhIncluded !== undefined && kwhIncluded > 0) { + const percent = normalizePercent((kwhUsed / kwhIncluded) * 100); + if (percent !== undefined) { + quota.fiveHourPercent = percent; + const periodEnd = subscription ? normalizeResetAt(subscription.current_period_end) : undefined; + if (periodEnd !== undefined) quota.fiveHourResetAt = periodEnd; + windows += 1; + } + } + const balance = asRecord(data?.balance); + const totalCredits = balance ? toFiniteNumber(balance.total_credits_usd) : undefined; + const remainingCredits = balance ? toFiniteNumber(balance.credits_remaining_usd) : undefined; + if (totalCredits !== undefined && totalCredits > 0 && remainingCredits !== undefined) { + // Utilization is CONSUMED credits, not the remaining share. + const used = Math.max(0, totalCredits - remainingCredits); + const percent = normalizePercent((used / totalCredits) * 100); + if (percent !== undefined) { + quota.customWindows = [...(quota.customWindows ?? []), { label: "Prepaid credits", percent }]; + windows += 1; + } + } + return windows > 0 ? report(provider, "neuralwatt:quota", quota) : null; +} + function report( provider: string, source: string, @@ -1179,6 +1701,36 @@ async function maybeFetchProviderQuota( if ((provider.authMode ?? "key") === "key" && isCanonicalA6apiBaseUrl(provider.baseUrl)) { return fetchA6apiQuota(name, provider); } + if ((provider.authMode ?? "key") === "key" && name === "openrouter") { + return fetchOpenRouterQuota(name, provider); + } + if ((provider.authMode ?? "key") === "key" && name === "deepseek") { + return fetchDeepSeekQuota(name, provider); + } + if ((provider.authMode ?? "key") === "key" && name === "cline-pass") { + return fetchClineQuota(name, provider); + } + if ((provider.authMode ?? "key") === "key" && name === "zai") { + return fetchZaiQuota(name, provider); + } + if ((provider.authMode ?? "key") === "key" && (name === "minimax" || name === "minimax-cn")) { + return fetchMinimaxQuota(name, provider); + } + if ((provider.authMode ?? "key") === "key" && name === "moonshot") { + return fetchMoonshotQuota(name, provider); + } + if ((provider.authMode ?? "key") === "key" && name === "venice") { + return fetchVeniceQuota(name, provider); + } + if ((provider.authMode ?? "key") === "key" && name === "synthetic") { + return fetchSyntheticQuota(name, provider); + } + if ((provider.authMode ?? "key") === "key" && name === "deepinfra") { + return fetchDeepInfraQuota(name, provider); + } + if ((provider.authMode ?? "key") === "key" && name === "neuralwatt") { + return fetchNeuralwattQuota(name, provider); + } return null; } catch { return null; diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts index 353c54b7a..873a7af54 100644 --- a/tests/provider-quota.test.ts +++ b/tests/provider-quota.test.ts @@ -518,6 +518,619 @@ describe("fetchProviderQuotaReports", () => { expect(rejectedRefresh.reports).toEqual([]); }); + function keyQuotaConfig(name: string, baseUrl: string): OcxConfig { + return { + defaultProvider: name, + providers: { + [name]: { adapter: "openai-chat", authMode: "key", baseUrl, apiKey: `${name}-secret` }, + }, + } as OcxConfig; + } + + test("OpenRouter quota renders a credit window against the per-key cap", async () => { + const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const headers = init?.headers as Record | undefined; + seen.push({ url, authorization: headers?.Authorization, redirect: init?.redirect }); + return new Response(JSON.stringify({ + data: { label: "openrouter", usage: 5, limit: 20, limit_remaining: 15, is_free_tier: false }, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("openrouter", "https://openrouter.ai/api/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.source).toBe("openrouter:key-info"); + expect(result.reports[0]?.quota.customWindows).toEqual([{ + label: "API credits ($15.00 of $20.00 remaining)", + percent: 25, + }]); + expect(seen).toHaveLength(1); + expect(seen[0]?.url).toBe("https://openrouter.ai/api/v1/key"); + expect(seen[0]?.authorization).toBe("Bearer openrouter-secret"); + expect(seen[0]?.redirect).toBe("error"); + }); + + test("OpenRouter quota never sends the key to a non-canonical base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response("unexpected", { status: 500 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("openrouter", "https://attacker.example/api/v1"), + true, + ); + + expect(result.reports).toEqual([]); + expect(seen).toEqual([]); + }); + + test("OpenRouter quota drops a key with no spending cap (terminal, not transient)", async () => { + // A successful no-cap response is a DELIBERATE cap removal — the old + // capped row must be suppressed, not preserved as a last-good transient. + let capped = true; + globalThis.fetch = (async () => new Response(JSON.stringify( + capped + ? { data: { usage: 5, limit: 20, limit_remaining: 15 } } + : { data: { usage: 3, is_free_tier: false } }, + ), { status: 200 })) as typeof fetch; + const config = keyQuotaConfig("openrouter", "https://openrouter.ai/api/v1"); + + const valid = await fetchProviderQuotaReports(config, true); + capped = false; + const uncapped = await fetchProviderQuotaReports(config, true); + + expect(valid.reports).toHaveLength(1); + expect(uncapped.reports).toEqual([]); + }); + + test("OpenRouter quota prefers limit_remaining over accumulated usage for reset keys", async () => { + // A reset key can report large accumulated `usage` while most of the + // current cap remains; utilization must come from limit_remaining. + globalThis.fetch = (async () => new Response(JSON.stringify({ + data: { usage: 90, limit: 20, limit_remaining: 18 }, + }), { status: 200 })) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("openrouter", "https://openrouter.ai/api/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.quota.customWindows?.[0]).toEqual({ + label: "API credits ($18.00 of $20.00 remaining)", + percent: 10, + }); + }); + + test("OpenRouter quota reports zero consumption for a capped key with usage 0", async () => { + // A valid capped response with `usage: 0` and no limit_remaining must + // still render: 0% consumed, full cap remaining. + globalThis.fetch = (async () => new Response(JSON.stringify({ + data: { usage: 0, limit: 20 }, + }), { status: 200 })) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("openrouter", "https://openrouter.ai/api/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.quota.customWindows?.[0]).toEqual({ + label: "API credits ($20.00 of $20.00 remaining)", + percent: 0, + }); + }); + + test("OpenRouter quota treats a terminal 401 as invalid (drops last-good)", async () => { + let rejected = false; + globalThis.fetch = (async () => { + if (rejected) return new Response("unauthorized", { status: 401 }); + return new Response(JSON.stringify({ + data: { usage: 5, limit: 20, limit_remaining: 15 }, + }), { status: 200 }); + }) as typeof fetch; + const config = keyQuotaConfig("openrouter", "https://openrouter.ai/api/v1"); + + const valid = await fetchProviderQuotaReports(config, true); + rejected = true; + const invalid = await fetchProviderQuotaReports(config, true); + + expect(valid.reports).toHaveLength(1); + expect(invalid.reports).toEqual([]); + }); + + test("OpenRouter quota keeps the last-good row on a transient 429", async () => { + let throttled = false; + globalThis.fetch = (async () => { + if (throttled) return new Response("rate limited", { status: 429 }); + return new Response(JSON.stringify({ + data: { usage: 5, limit: 20, limit_remaining: 15 }, + }), { status: 200 }); + }) as typeof fetch; + const config = keyQuotaConfig("openrouter", "https://openrouter.ai/api/v1"); + + const valid = await fetchProviderQuotaReports(config, true); + throttled = true; + const throttledRefresh = await fetchProviderQuotaReports(config, true); + + expect(throttledRefresh.reports).toEqual(valid.reports); + }); + + test("DeepSeek quota renders a balance-only window from balance_infos", async () => { + const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const headers = init?.headers as Record | undefined; + seen.push({ url, authorization: headers?.Authorization, redirect: init?.redirect }); + return new Response(JSON.stringify({ + is_available: true, + // The real payload nests balances per currency inside balance_infos. + balance_infos: [{ currency: "CNY", total_balance: "6", granted_balance: "4", topped_up_balance: "2" }], + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("deepseek", "https://api.deepseek.com"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.source).toBe("deepseek:balance"); + expect(result.reports[0]?.quota.customWindows).toEqual([{ + label: "API balance ($6.00 total, $4.00 granted)", + percent: 0, + }]); + expect(seen).toHaveLength(1); + expect(seen[0]?.url).toBe("https://api.deepseek.com/user/balance"); + expect(seen[0]?.authorization).toBe("Bearer deepseek-secret"); + expect(seen[0]?.redirect).toBe("error"); + }); + + test("DeepSeek quota never sends the key to a non-canonical base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response("unexpected", { status: 500 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("deepseek", "https://attacker.example"), + true, + ); + + expect(result.reports).toEqual([]); + expect(seen).toEqual([]); + }); + + test("DeepSeek quota accepts the canonical /v1 base URL and probes the root endpoint", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response(JSON.stringify({ + is_available: true, + balance_infos: [{ currency: "CNY", total_balance: "6" }], + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("deepseek", "https://api.deepseek.com/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(seen[0]).toBe("https://api.deepseek.com/user/balance"); + }); + + test("DeepSeek quota drops a payload with no balance_infos rows", async () => { + globalThis.fetch = (async () => new Response(JSON.stringify({ + is_available: true, + total_balance: "50", + granted_balance: "0", + }), { status: 200 })) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("deepseek", "https://api.deepseek.com"), true); + + expect(result.reports).toEqual([]); + }); + + test("DeepSeek quota treats a terminal 401 as invalid (drops last-good)", async () => { + let rejected = false; + globalThis.fetch = (async () => { + if (rejected) return new Response("unauthorized", { status: 401 }); + return new Response(JSON.stringify({ + is_available: true, + balance_infos: [{ currency: "CNY", total_balance: "6", granted_balance: "4" }], + }), { status: 200 }); + }) as typeof fetch; + const config = keyQuotaConfig("deepseek", "https://api.deepseek.com"); + + const valid = await fetchProviderQuotaReports(config, true); + rejected = true; + const invalid = await fetchProviderQuotaReports(config, true); + + expect(valid.reports).toHaveLength(1); + expect(invalid.reports).toEqual([]); + }); + + test("ClinePass quota maps five-hour/weekly/monthly utilization windows", async () => { + const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const headers = init?.headers as Record | undefined; + seen.push({ url, authorization: headers?.Authorization, redirect: init?.redirect }); + return new Response(JSON.stringify({ + success: true, + data: { limits: [ + { type: "five_hour", percentUsed: 40.5 }, + { type: "weekly", percentUsed: 52, resetsAt: "2026-08-09T00:00:00Z" }, + { type: "monthly", percentUsed: 12.3 }, + ] }, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("cline-pass", "https://api.cline.bot/api/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.source).toBe("cline:plan-usage-limits"); + expect(result.reports[0]?.quota).toMatchObject({ + fiveHourPercent: 40.5, + weeklyPercent: 52, + monthlyPercent: 12.3, + }); + expect(seen).toHaveLength(1); + expect(seen[0]?.url).toBe("https://api.cline.bot/api/v1/users/me/plan/usage-limits"); + expect(seen[0]?.authorization).toBe("Bearer cline-pass-secret"); + expect(seen[0]?.redirect).toBe("error"); + }); + + test("ClinePass quota treats a 404 (no active plan) as a no-report, not terminal", async () => { + globalThis.fetch = (async () => new Response("no plan", { status: 404 })) as typeof fetch; + const config = keyQuotaConfig("cline-pass", "https://api.cline.bot/api/v1"); + + const result = await fetchProviderQuotaReports(config, true); + + expect(result.reports).toEqual([]); + }); + + test("ClinePass quota never sends the key to a non-canonical base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response("unexpected", { status: 500 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("cline-pass", "https://attacker.example/api/v1"), + true, + ); + + expect(result.reports).toEqual([]); + expect(seen).toEqual([]); + }); + + test("Z.AI quota sends the key as a Bearer token and maps plan windows", async () => { + const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const headers = init?.headers as Record | undefined; + seen.push({ url, authorization: headers?.Authorization, redirect: init?.redirect }); + return new Response(JSON.stringify({ + success: true, + data: { fiveHourPercent: 40.5, weeklyPercent: 52, monthlyMCPUsage: 12.3 }, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("zai", "https://api.z.ai/api/coding/paas/v4"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.source).toBe("zai:quota-limit"); + expect(result.reports[0]?.quota).toMatchObject({ + fiveHourPercent: 40.5, + weeklyPercent: 52, + monthlyPercent: 12.3, + }); + expect(seen).toHaveLength(1); + expect(seen[0]?.url).toBe("https://api.z.ai/api/monitor/usage/quota/limit"); + expect(seen[0]?.authorization).toBe("Bearer zai-secret"); + expect(seen[0]?.redirect).toBe("error"); + }); + + test("Z.AI quota treats an unsuccessful payload as a no-report", async () => { + globalThis.fetch = (async () => new Response(JSON.stringify({ + code: 1001, success: false, msg: "Authentication parameter not received", + }), { status: 200 })) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("zai", "https://api.z.ai/api/coding/paas/v4"), true); + + expect(result.reports).toEqual([]); + }); + + test("Z.AI quota never sends the token to a non-canonical base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response("unexpected", { status: 500 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("zai", "https://attacker.example/api/coding/paas/v4"), + true, + ); + + expect(result.reports).toEqual([]); + expect(seen).toEqual([]); + }); + + test("MiniMax quota drops the row when the API omits the plan total after having it", async () => { + // A valid row (with total) exists; a later valid response omitting the + // total is a DELIBERATE contract change — the stale row must be dropped + // (terminal), not preserved as a transient last-good. + let withTotal = true; + const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const headers = init?.headers as Record | undefined; + seen.push({ url, authorization: headers?.Authorization, redirect: init?.redirect }); + return new Response(JSON.stringify(withTotal + ? { success: true, data: { remains_time: 750_000_000, total_time: 1_000_000_000 } } + : { success: true, data: { remains_time: 1_000_000_000 } }), { status: 200 }); + }) as typeof fetch; + const config = keyQuotaConfig("minimax", "https://api.minimax.io/v1"); + + const valid = await fetchProviderQuotaReports(config, true); + withTotal = false; + const noTotal = await fetchProviderQuotaReports(config, true); + + expect(valid.reports).toHaveLength(1); + expect(noTotal.reports).toEqual([]); + expect(seen[0]?.url).toBe("https://www.minimax.io/v1/token_plan/remains"); + expect(seen[0]?.authorization).toBe("Bearer minimax-secret"); + expect(seen[0]?.redirect).toBe("error"); + }); + + test("MiniMax quota derives a consumed share when the API reports the plan total", async () => { + globalThis.fetch = (async () => new Response(JSON.stringify({ + success: true, + data: { remains_time: 750_000_000, total_time: 1_000_000_000 }, + }), { status: 200 })) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("minimax", "https://api.minimax.io/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.quota.customWindows?.[0]?.percent).toBe(25); + }); + + test("MiniMax CN quota probes the minimaxi.com host", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response(JSON.stringify({ + success: true, + data: { remains_time: 750_000_000, total_time: 1_000_000_000 }, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("minimax-cn", "https://api.minimaxi.com/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(seen[0]).toBe("https://api.minimaxi.com/v1/token_plan/remains"); + }); + + test("MiniMax quota never sends the key to a non-canonical base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response("unexpected", { status: 500 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("minimax", "https://attacker.example/v1"), + true, + ); + + expect(result.reports).toEqual([]); + expect(seen).toEqual([]); + }); + + test("Moonshot quota renders a balance-only window from the account balance", async () => { + const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const headers = init?.headers as Record | undefined; + seen.push({ url, authorization: headers?.Authorization, redirect: init?.redirect }); + return new Response(JSON.stringify({ + data: { available_balance: 8, voucher_balance: 2, cash_balance: 6 }, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("moonshot", "https://api.moonshot.ai/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.source).toBe("moonshot:balance"); + // Balance-only: no fabricated utilization percentage. + expect(result.reports[0]?.quota.customWindows?.[0]).toMatchObject({ + label: "Balance ($8.00 available, $2.00 voucher)", + percent: 0, + }); + expect(seen).toHaveLength(1); + expect(seen[0]?.url).toBe("https://api.moonshot.ai/v1/users/me/balance"); + expect(seen[0]?.authorization).toBe("Bearer moonshot-secret"); + expect(seen[0]?.redirect).toBe("error"); + }); + + test("Moonshot quota probes the CN host for a China-region base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response(JSON.stringify({ + data: { available_balance: 5, voucher_balance: 0, cash_balance: 5 }, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("moonshot", "https://api.moonshot.cn/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(seen[0]).toBe("https://api.moonshot.cn/v1/users/me/balance"); + }); + + test("Moonshot quota never sends the key to a non-canonical base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response("unexpected", { status: 500 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("moonshot", "https://attacker.example/v1"), + true, + ); + + expect(result.reports).toEqual([]); + expect(seen).toEqual([]); + }); + + test("Venice quota renders a DIEM epoch allocation window when present", async () => { + const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const headers = init?.headers as Record | undefined; + seen.push({ url, authorization: headers?.Authorization, redirect: init?.redirect }); + return new Response(JSON.stringify({ + data: { balance: 250, diem_epoch_used: 30, diem_epoch_allocated: 100 }, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("venice", "https://api.venice.ai/api/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.source).toBe("venice:billing-balance"); + expect(result.reports[0]?.quota.customWindows?.[0]?.label).toContain("DIEM balance (250)"); + expect(result.reports[0]?.quota.customWindows?.[0]?.percent).toBe(30); + expect(seen[0]?.url).toBe("https://api.venice.ai/api/v1/billing/balance"); + expect(seen[0]?.authorization).toBe("Bearer venice-secret"); + }); + + test("Synthetic quota maps rolling 5-hour and weekly token lanes", async () => { + globalThis.fetch = (async () => new Response(JSON.stringify({ + data: { rollingFiveHourLimit: 40.5, weeklyTokenLimit: 52, search: { hourly: 12 } }, + }), { status: 200 })) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("synthetic", "https://api.synthetic.new/v2"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.source).toBe("synthetic:quotas"); + expect(result.reports[0]?.quota).toMatchObject({ + fiveHourPercent: 40.5, + weeklyPercent: 52, + }); + expect(result.reports[0]?.quota.customWindows?.[0]).toMatchObject({ label: "Search hourly", percent: 12 }); + }); + + test("Synthetic quota accepts the preset /openai/v1 base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response(JSON.stringify({ + data: { rollingFiveHourLimit: 10, weeklyTokenLimit: 20 }, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("synthetic", "https://api.synthetic.new/openai/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(seen[0]).toBe("https://api.synthetic.new/v2/quotas"); + }); + + test("Synthetic quota never sends the key to a non-canonical base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response("unexpected", { status: 500 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("synthetic", "https://attacker.example/v2"), + true, + ); + + expect(result.reports).toEqual([]); + expect(seen).toEqual([]); + }); + + test("DeepInfra quota renders a billing-cycle spend window when a limit is set", async () => { + const seen: Array<{ url: string; authorization?: string; redirect?: RequestRedirect }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const headers = init?.headers as Record | undefined; + seen.push({ url, authorization: headers?.Authorization, redirect: init?.redirect }); + return new Response(JSON.stringify({ + stripe_balance: -10, spending_limit: 50, total_amount_due: 5, + }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("deepinfra", "https://api.deepinfra.com/v1/openai"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.source).toBe("deepinfra:billing-checklist"); + expect(result.reports[0]?.quota.customWindows?.[0]?.label).toContain("$5.00 of $50.00"); + expect(seen[0]?.url).toContain("/payment/checklist"); + expect(seen[0]?.authorization).toBe("Bearer deepinfra-secret"); + }); + + test("DeepInfra quota never sends the key to a non-canonical base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response("unexpected", { status: 500 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("deepinfra", "https://attacker.example/v1/openai"), + true, + ); + + expect(result.reports).toEqual([]); + expect(seen).toEqual([]); + }); + + test("DeepInfra quota accepts the root base URL and probes the payment checklist", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response(JSON.stringify({ stripe_balance: -10, spending_limit: 50, total_amount_due: 5 }), { status: 200 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("deepinfra", "https://api.deepinfra.com"), true); + + expect(result.reports).toHaveLength(1); + expect(seen[0]).toBe("https://api.deepinfra.com/payment/checklist?compute_owed=true"); + }); + + test("Neuralwatt quota renders subscription kWh + prepaid credits windows", async () => { + globalThis.fetch = (async () => new Response(JSON.stringify({ + data: { + subscription: { kwh_used: 5, kwh_included: 20, current_period_end: "2026-08-31T00:00:00Z" }, + balance: { total_credits_usd: 10, credits_remaining_usd: 7 }, + }, + }), { status: 200 })) as typeof fetch; + + const result = await fetchProviderQuotaReports(keyQuotaConfig("neuralwatt", "https://api.neuralwatt.com/v1"), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.source).toBe("neuralwatt:quota"); + expect(result.reports[0]?.quota.fiveHourPercent).toBe(25); + // Utilization is CONSUMED credits: (10 − 7) / 10 = 30%, not the 70% remaining. + expect(result.reports[0]?.quota.customWindows?.[0]).toMatchObject({ label: "Prepaid credits", percent: 30 }); + }); + + test("Neuralwatt quota never sends the key to a non-canonical base URL", async () => { + const seen: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + seen.push(String(input)); + return new Response("unexpected", { status: 500 }); + }) as typeof fetch; + + const result = await fetchProviderQuotaReports( + keyQuotaConfig("neuralwatt", "https://attacker.example/v1"), + true, + ); + + expect(result.reports).toEqual([]); + expect(seen).toEqual([]); + }); + test("Kimi quota never sends OAuth credentials to a non-canonical base URL", async () => { await saveCredential("kimi", { access: "kimi-access-secret", refresh: "kimi-refresh-secret", expires: Date.now() + 3600_000 }); const seen: string[] = [];