Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2815,6 +2815,23 @@ APP_LOG_TO_FILE=true
# QWEN_TOKEN_PLAN_HOST=
# QWEN_TOKEN_PLAN_DASHBOARD_URL=

# ── Xiaomi MiMo Token Plan quota ──
# Console-cookie live fetch + self-tracked fallback (issue #15753). Used by:
# open-sse/services/usage/xiaomi-mimo.ts. Prefer the per-connection Dashboard
# fields (xiaomiMimoConsoleCookie / monthlyTokenLimit) — these env vars are
# global fallbacks. The cookie is a SENSITIVE session credential.
# Getting the cookie: log in to platform.xiaomimimo.com, open the Token Plan
# page, press F12 > Network, reload, click any request to
# platform.xiaomimimo.com and copy the WHOLE Cookie value from Request Headers
# (it contains api-platform_serviceToken and userId). Paste it on ONE line —
# the value may contain '=' and ';'. It expires with the browser session;
# re-paste it when the dashboard reports an expired session.
# XIAOMI_MIMO_CONSOLE_COOKIE=
# Monthly budget (Credits) for the OmniRoute-tracked fallback without a cookie.
# Default: 4100000000 (Lite tier). Standard 11000000000 / Pro 38000000000 /
# Max 82000000000.
# XIAOMI_MIMO_MONTHLY_TOKEN_LIMIT=

# ── Alibaba Model Studio free-tier quota sync ──
# Console front-end path overrides for the free-tier quota fetcher. Used by:
# open-sse/services/alibabaFreeTierQuotaFetcher.ts. When unset, the fetcher
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(providers):** Xiaomi MiMo quota now reads the real Token Plan usage from the MiMo console API (plan label, used/limit Credits, `currentPeriodEnd` reset) through a new per-connection console-cookie field, and the self-tracked fallback budget becomes configurable (`providerSpecificData.monthlyTokenLimit` / `XIAOMI_MIMO_MONTHLY_TOKEN_LIMIT`, default 4.1B Lite Credits) — no more permanent `Exhausted` after a plan upgrade ([#15753](https://github.com/diegosouzapw/OmniRoute/issues/15753)) — thanks @LazyGatto
2 changes: 2 additions & 0 deletions docs/reference/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,8 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
| `QWEN_CLOUD_SEC_TOKEN` | _(unset)_ | `open-sse/services/qwenTokenPlanQuotaFetcher.ts` | Manual `sec_token` override for the Token Plan console gateway. Sensitive; when unset the fetcher resolves it from the dashboard HTML using the cookie. |
| `QWEN_TOKEN_PLAN_HOST` | `https://cs-data.qwencloud.com` | `open-sse/services/qwenTokenPlanQuotaFetcher.ts` | Gateway host override for the personal Token Plan quota fetcher (e.g. `bailian-singapore-cs.alibabacloud.com` for the Model Studio console). |
| `QWEN_TOKEN_PLAN_DASHBOARD_URL` | `https://home.qwencloud.com/` | `open-sse/services/qwenTokenPlanQuotaFetcher.ts` | Dashboard URL used to resolve `sec_token` from the logged-in HTML. |
| `XIAOMI_MIMO_CONSOLE_COOKIE` | _(unset)_ | `open-sse/services/usage/xiaomi-mimo.ts` | Console session cookie for the Xiaomi MiMo Token Plan quota API (`platform.xiaomimimo.com/api/v1/tokenPlan/usage` + `tokenPlan/detail` — the inference API key cannot read it). Copy the whole `Cookie` request header — it contains `api-platform_serviceToken` and `userId` — from any request to `platform.xiaomimimo.com` on the Token Plan page (F12 › Network). Sensitive and session-scoped; prefer the per-connection `xiaomiMimoConsoleCookie` Dashboard field. |
| `XIAOMI_MIMO_MONTHLY_TOKEN_LIMIT` | `4100000000` | `open-sse/services/usage/xiaomi-mimo.ts` | Monthly budget (Credits) for the OmniRoute-tracked Xiaomi MiMo fallback used when no console cookie is configured. Defaults to the Lite tier's 4.1B; set it to the plan's Credits (Standard 11B / Pro 38B / Max 82B). A per-connection `providerSpecificData.monthlyTokenLimit` override wins over this env var. |
| `ALIBABA_FREE_TIER_VISION_FE_PATH` | `/costing-balance/free-quota-image-video` | `open-sse/services/alibabaFreeTierQuotaFetcher.ts` | Console front-end path override for fetching Alibaba Model Studio free-tier vision/media quota. |
| `ALIBABA_FREE_TIER_MULTIMODAL_FE_PATH` | `/costing-balance/free-quota-multimodal` | `open-sse/services/alibabaFreeTierQuotaFetcher.ts` | Console front-end path override for fetching Alibaba Model Studio free-tier multimodal quota. |
| `ALIBABA_FREE_TIER_AUDIO_FE_PATH` | `/costing-balance/free-quota-audio` | `open-sse/services/alibabaFreeTierQuotaFetcher.ts` | Console front-end path override for fetching Alibaba Model Studio free-tier audio quota. |
Expand Down
4 changes: 2 additions & 2 deletions open-sse/services/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,9 @@ export async function getUsageForProvider(
case "opencode-zen":
return await getOpencodeUsage(id || "", apiKey || "");
case "xiaomi-mimo":
return await getXiaomiMimoUsage(id || "");
return await getXiaomiMimoUsage(id || "", "xiaomi-mimo", providerSpecificData);
case "xiaomi-mimo-token-plan":
return await getXiaomiMimoUsage(id || "", "xiaomi-mimo-token-plan");
return await getXiaomiMimoUsage(id || "", "xiaomi-mimo-token-plan", providerSpecificData);
case "xai":
return await getXaiUsage(id || "");
case "xai-oauth":
Expand Down
258 changes: 229 additions & 29 deletions open-sse/services/usage/xiaomi-mimo.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,251 @@
/**
* usage/xiaomi-mimo.ts — Xiaomi MiMo self-tracked monthly quota fetcher.
* usage/xiaomi-mimo.ts — Xiaomi MiMo Token Plan quota fetcher (issue #15753).
*
* Extracted from services/usage.ts (god-file decomposition): the Xiaomi MiMo family —
* Xiaomi exposes plan usage only behind the console session cookie (the API key
* cannot reach the `tokenPlan/usage` endpoint), so OmniRoute self-tracks the
* tokens it routed to the connection in the current UTC month (from usage_history)
* and compares them to the known Token Plan monthly limit. Depends only on the
* sibling scalar/quota leaves + the usageStats dynamic import — no host coupling
* — so it lives as a co-located provider leaf. usage.ts imports getXiaomiMimoUsage
* (dispatcher + __testing). Behavior-preserving move.
* Two layers:
*
* 1. Live console fetch (preferred): when the connection carries a MiMo console
* session cookie (providerSpecificData.xiaomiMimoConsoleCookie / `cookie`, or
* XIAOMI_MIMO_CONSOLE_COOKIE), GET the console Token Plan API —
* `/api/v1/tokenPlan/usage` (monthUsage items: used/limit in Credits) and
* `/api/v1/tokenPlan/detail` (planName + currentPeriodEnd) — and report the
* real plan window. The `tp-`/`mk-` inference key cannot read this: the
* token-plan hosts only serve `/v1/*`, and the console endpoints answer
* 401 + Xiaomi SSO `loginUrl` without the session cookie.
*
* 2. Self-tracked fallback (keeps working without a cookie): count the tokens
* OmniRoute routed to the connection in the current UTC month
* (usage_history) and compare them to a configurable monthly budget —
* providerSpecificData.monthlyTokenLimit, then XIAOMI_MIMO_MONTHLY_TOKEN_LIMIT,
* then 4.1B (the Lite plan's Credits). When the console session is missing
* or expired, the fallback result carries a hint message so the dashboard
* tells the operator how to enable live quota instead of failing hard.
*
* Depends only on the sibling scalar/quota leaves + the quotaFetchThrottle gate
* + the usageStats dynamic import — no host coupling. usage.ts imports
* getXiaomiMimoUsage (dispatcher + __testing).
*/

import { createQuotaFromUsage } from "./quota.ts";
import { createQuotaFromUsage, parseResetTime, type UsageQuota } from "./quota.ts";
import { toRecord, toNumber, toTitleCase } from "./scalars.ts";
import { throttleQuotaFetch } from "../quotaFetchThrottle.ts";

type JsonRecord = Record<string, unknown>;

// Xiaomi MiMo Token Plan monthly limit (tokens) for the Lite tier. Keep in sync
// with the "xiaomi-mimo" preset in src/lib/quota/planRegistry.ts.
const DEFAULT_MONTHLY_TOKEN_LIMIT = 4_100_000_000;

const MIMO_CONSOLE_BASE_URL = "https://platform.xiaomimimo.com/api/v1";
const CONSOLE_FETCH_TIMEOUT_MS = 8_000;

/** providerSpecificData keys scanned for the console `Cookie:` value (first hit wins). */
const CONSOLE_COOKIE_KEYS = ["xiaomiMimoConsoleCookie", "cookie"] as const;

// Xiaomi MiMo Token Plan monthly limit (tokens). Keep in sync with the
// "xiaomi-mimo" preset in src/lib/quota/planRegistry.ts.
const XIAOMI_MIMO_MONTHLY_TOKEN_LIMIT = 4_100_000_000;
const PLAN_LABEL_TRACKED = "Xiaomi MiMo Token Plan (OmniRoute-tracked)";
const PLAN_LABEL_LIVE = "Xiaomi MiMo Token Plan";

const CONSOLE_COOKIE_HINT =
"MiMo console cookie missing or expired — set the connection's 'Xiaomi MiMo console cookie' " +
"field (the Xiaomi SSO session Cookie from platform.xiaomimimo.com containing " +
"api-platform_serviceToken and userId), or set XIAOMI_MIMO_CONSOLE_COOKIE. The tp-/mk- " +
"inference key cannot read Token Plan usage.";

// ─── Config helpers ─────────────────────────────────────────────────────────

function getConsoleCookie(providerSpecificData?: JsonRecord): string {
for (const key of CONSOLE_COOKIE_KEYS) {
const value = providerSpecificData?.[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
return process.env.XIAOMI_MIMO_CONSOLE_COOKIE?.trim() || "";
}

/**
* Xiaomi MiMo — SELF-TRACKED monthly quota.
*
* Xiaomi exposes plan usage only behind the console session cookie (the API key
* cannot reach the `tokenPlan/usage` endpoint), so there is no upstream usage
* API to call. Instead we count the tokens OmniRoute itself routed to this
* connection in the current UTC month (from `usage_history`) and compare them
* to the known Token Plan monthly limit. This reflects only traffic that went
* through OmniRoute, not the provider's own dashboard figure.
* Monthly budget for the self-tracked fallback: the connection override, then
* the env override, then the historical 4.1B Lite-tier default.
*/
export function resolveMonthlyTokenLimit(providerSpecificData?: JsonRecord): number {
const configured = toNumber(providerSpecificData?.monthlyTokenLimit, 0);
if (configured > 0) return configured;
const envLimit = Number(process.env.XIAOMI_MIMO_MONTHLY_TOKEN_LIMIT?.trim());
if (Number.isFinite(envLimit) && envLimit > 0) return envLimit;
return DEFAULT_MONTHLY_TOKEN_LIMIT;
}

function nextMonthStartUtc(): string {
const now = new Date();
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)).toISOString();
}

/**
* The console reports `currentPeriodEnd` as a naive "YYYY-MM-DD HH:mm:ss" wall
* time with no zone info — parse it deterministically as UTC. Anything else
* falls through to the shared reset-time parser (epoch, ISO, Date, …).
*/
export async function getXiaomiMimoUsage(connectionId: string, provider = "xiaomi-mimo") {
export function parseConsoleTime(value: unknown): string | null {
if (typeof value !== "string") return parseResetTime(value);
const match = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/.exec(value.trim());
if (!match) return parseResetTime(value);
const [, year, month, day, hour, minute, second] = match;
return new Date(
Date.UTC(
Number(year),
Number(month) - 1,
Number(day),
Number(hour),
Number(minute),
Number(second)
)
).toISOString();
}

// ─── Console transport ──────────────────────────────────────────────────────

interface ConsoleFetch {
status: number;
body: JsonRecord;
}

async function fetchConsoleJson(path: string, cookie: string): Promise<ConsoleFetch> {
// #6911: space concurrent upstream quota fetches (mirrors qwenTokenPlanQuotaFetcher).
await throttleQuotaFetch();
const response = await fetch(`${MIMO_CONSOLE_BASE_URL}${path}`, {
method: "GET",
headers: {
Accept: "application/json",
Cookie: cookie,
},
signal: AbortSignal.timeout(CONSOLE_FETCH_TIMEOUT_MS),
});
let body: JsonRecord = {};
try {
body = toRecord(await response.json());
} catch {
// Non-JSON body (SSO login redirect page) — treated as a rejected session.
}
return { status: response.status, body };
}

/** 401/403, `code !== 0`, an embedded SSO `loginUrl`, or a non-JSON body ⇒ session rejected. */
function isSessionRejected(status: number, body: JsonRecord): boolean {
if (status === 401 || status === 403) return true;
if (Object.keys(body).length === 0) return true;
const code = body["code"];
if (code !== undefined && code !== 0 && code !== "0") return true;
const data = toRecord(body["data"]);
return typeof body["loginUrl"] === "string" || typeof data["loginUrl"] === "string";
}

// ─── Console payload parsers ────────────────────────────────────────────────

/** `tokenPlan/usage` → the `month_total_token` item (used/limit in Credits). */
export function parseMonthUsage(body: JsonRecord): { used: number; total: number } | null {
const monthUsage = toRecord(toRecord(body["data"])["monthUsage"]);
const items = Array.isArray(monthUsage["items"]) ? monthUsage["items"] : [];
for (const raw of items) {
const item = toRecord(raw);
if (item["name"] !== "month_total_token") continue;
const used = toNumber(item["used"], Number.NaN);
const total = toNumber(item["limit"], Number.NaN);
if (!Number.isFinite(used) || !Number.isFinite(total) || total <= 0) return null;
return { used: Math.max(0, used), total };
}
return null;
}

/** `tokenPlan/detail` → display plan name + the real window end. */
export function parsePlanDetail(body: JsonRecord): {
planName: string | null;
resetAt: string | null;
} {
const data = toRecord(body["data"]);
const planName = typeof data["planName"] === "string" ? data["planName"].trim() : "";
return { planName: planName || null, resetAt: parseConsoleTime(data["currentPeriodEnd"]) };
}

function livePlanLabel(planName: string | null): string {
if (!planName) return PLAN_LABEL_LIVE;
return `${PLAN_LABEL_LIVE} · ${toTitleCase(planName)}`;
}

// ─── Live fetch ─────────────────────────────────────────────────────────────

type LiveQuotaResult = { plan: string; quotas: Record<string, UsageQuota> } | { message: string };

async function fetchLiveTokenPlanUsage(cookie: string): Promise<LiveQuotaResult> {
const [usageFetch, detailFetch] = await Promise.all([
fetchConsoleJson("/tokenPlan/usage", cookie),
fetchConsoleJson("/tokenPlan/detail", cookie),
]);

if (isSessionRejected(usageFetch.status, usageFetch.body)) {
return { message: CONSOLE_COOKIE_HINT };
}

const month = parseMonthUsage(usageFetch.body);
if (!month) {
return {
message: `${PLAN_LABEL_LIVE}: console usage payload unrecognized — cannot map the Token Plan window.`,
};
}

// The detail call only supplies the label + the real window end; when it is
// unavailable the monthly window falls back to the calendar-month reset.
const detail = isSessionRejected(detailFetch.status, detailFetch.body)
? { planName: null, resetAt: null }
: parsePlanDetail(detailFetch.body);

return {
plan: livePlanLabel(detail.planName),
quotas: {
monthly: createQuotaFromUsage(month.used, month.total, detail.resetAt ?? nextMonthStartUtc()),
},
};
}

// ─── Entry point ────────────────────────────────────────────────────────────

export async function getXiaomiMimoUsage(
connectionId: string,
provider = "xiaomi-mimo",
providerSpecificData?: JsonRecord
) {
if (!connectionId) {
return { message: "Xiaomi MiMo: connection id unavailable for self-tracked quota." };
}

let hint = CONSOLE_COOKIE_HINT;
const cookie = getConsoleCookie(providerSpecificData);
if (cookie) {
try {
const live = await fetchLiveTokenPlanUsage(cookie);
if (!("message" in live)) return live;
hint = live.message;
} catch (error) {
hint = `Xiaomi MiMo console usage unavailable: ${(error as Error).message}`;
}
}

// Fallback: self-tracked monthly counter against the configured budget.
try {
const { getMonthlyProviderTokensForConnection } = await import("@/lib/usage/usageStats");
const used = getMonthlyProviderTokensForConnection(provider, connectionId);
const total = XIAOMI_MIMO_MONTHLY_TOKEN_LIMIT;
const now = new Date();
const resetAt = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)
).toISOString();
const total = resolveMonthlyTokenLimit(providerSpecificData);
return {
plan: "Xiaomi MiMo Token Plan (OmniRoute-tracked)",
plan: PLAN_LABEL_TRACKED,
quotas: {
monthly: createQuotaFromUsage(used, total, resetAt),
monthly: createQuotaFromUsage(used, total, nextMonthStartUtc()),
},
message: hint,
};
} catch (error) {
return { message: `Xiaomi MiMo self-tracked usage error: ${(error as Error).message}` };
}
}

export const __testing = {
parseConsoleTime,
parseMonthUsage,
parsePlanDetail,
resolveMonthlyTokenLimit,
};
Loading
Loading