feat(xiaomi-mimo): merge MiMo Desktop support into xiaomi-mimo as dual auth
Adds the Desktop-exclusive Preview models and the Xiaomi account-session
route to the existing xiaomi-mimo provider instead of a separate
xiaomi-desktop provider, so the dashboard shows one MiMo entry rather than
three overlapping ones.
Dual auth, same pattern as kimi — API key (sk-) covers the cloud API,
Desktop/OAuth adds the account session used by the Preview models:
- registry: category oauth, authModes [oauth, apikey], oauth block, the two
mimo-x-*-preview models, and the invite signupUrl
- executor: routes Preview models to the account-service route with a Cookie
session, everything else keeps the sourceFormat-matched transport
- oauth: custom ECDH encrypted-callback flow (X25519 -> SHA256 -> AES-256-GCM)
with a loopback callback proxy, plus one-click import of the local Desktop
auth.json
- usage: weekly quota from the account session
Fixes found while merging:
- the OAuth browser flow was dead: poll-status cleared the session before the
client could POST /exchange, so every exchange returned 400
- a Claude-format client was sent to /v1/chat/completions instead of the
declared /anthropic/v1/messages transport, because buildUrl ignored
runtimeTransport
- stopXiaomiMimoProxy leaked every pending session (each holding an X25519
private key) for the process lifetime
- the OAuth exchange did not persist the Desktop passToken, so the Preview
models could never work after a browser sign-in
Removes dead code: the local engine token minting (mimoEngine, never called
on the request path), the model-catalog and usage routes, engineToken/
engineUrl plumbing, and an unread top-level usage block.
Adds tests/unit/xiaomi-mimo-{executor,oauth-session,oauth-proxy}.test.js —
the provider previously had none.
This commit is contained in:
125
open-sse/services/usage/xiaomi-mimo.js
Normal file
125
open-sse/services/usage/xiaomi-mimo.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Xiaomi MiMo usage — weekly quota from the Xiaomi account session.
|
||||
*
|
||||
* Primary path: GET {mimo-server}/api/user/usage authorized by the account-session
|
||||
* cookie (see shared/mimoAccount.js). Response: { code: 0, data: { percent (remaining
|
||||
* %), resetDate, resetAt } }.
|
||||
*
|
||||
* Fallback: the sk- API key cannot read the quota, so when no account session is
|
||||
* available we surface a graceful message instead of failing.
|
||||
*/
|
||||
|
||||
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
|
||||
import { getMimoAccountUsage } from "../../shared/mimoAccount.js";
|
||||
|
||||
const USAGE_URL = "https://aistudio.xiaomimimo.com/open-apis/v1/user/usage";
|
||||
|
||||
/**
|
||||
* @param {string|null|undefined} accessToken - sk- API key
|
||||
* @param {object|null} providerSpecificData - may contain mimoPassToken, uid, etc.
|
||||
* @param {object|null} proxyOptions
|
||||
*/
|
||||
export async function getXiaomiMimoUsage(accessToken = null, providerSpecificData = null, proxyOptions = null) {
|
||||
// Preferred path: the weekly quota comes from the account service session
|
||||
// (mimo-server /api/user/usage), which the sk- key cannot reach. The session is
|
||||
// derived from MiMo Desktop's persisted passToken via the SSO/sts handshake.
|
||||
const account = await getMimoAccountUsage(providerSpecificData, proxyOptions);
|
||||
if (typeof account.percent === "number" && Number.isFinite(account.percent)) {
|
||||
const remaining = Math.max(0, Math.min(100, Math.round(account.percent)));
|
||||
const used = 100 - remaining;
|
||||
let resetAt = null;
|
||||
if (typeof account.resetAt === "number" && account.resetAt > 0) {
|
||||
resetAt = new Date(account.resetAt * 1000).toISOString();
|
||||
} else if (typeof account.resetDate === "string") {
|
||||
const parsed = new Date(`${account.resetDate}T00:00:00Z`);
|
||||
if (!Number.isNaN(parsed.getTime())) resetAt = parsed.toISOString();
|
||||
}
|
||||
return {
|
||||
plan: "Xiaomi MiMo Desktop",
|
||||
quotas: {
|
||||
Weekly: { used, total: 100, remainingPercentage: remaining, resetAt, unlimited: false },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback: no account session available (Desktop never logged in, or its cookie
|
||||
// store is locked). The sk- key cannot read the quota, so surface a clear message.
|
||||
const key = accessToken || providerSpecificData?.apiKey;
|
||||
if (!key || typeof key !== "string" || !key.trim()) {
|
||||
return { message: "Xiaomi MiMo Desktop not connected. Add credentials to view usage." };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await proxyAwareFetch(
|
||||
USAGE_URL,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${key.trim()}`,
|
||||
"X-Mimo-Source": "mimocode-cli",
|
||||
Accept: "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
},
|
||||
proxyOptions,
|
||||
);
|
||||
|
||||
if (response.status === 401) {
|
||||
return {
|
||||
plan: "Xiaomi MiMo Desktop",
|
||||
message: "Weekly quota requires Xiaomi account session. API key alone is insufficient.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
plan: "Xiaomi MiMo Desktop",
|
||||
message: `Usage API error (${response.status})`,
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => null);
|
||||
if (!data || data.code !== 0 || !data.data) {
|
||||
return {
|
||||
plan: "Xiaomi MiMo Desktop",
|
||||
message: "Usage endpoint returned unexpected response.",
|
||||
};
|
||||
}
|
||||
|
||||
const { percent, resetDate } = data.data;
|
||||
if (typeof percent !== "number" || !Number.isFinite(percent)) {
|
||||
return {
|
||||
plan: "Xiaomi MiMo Desktop",
|
||||
message: "Usage data missing percent field.",
|
||||
};
|
||||
}
|
||||
|
||||
// percent = remaining percentage (e.g. 94 means 94% remaining)
|
||||
const remaining = Math.max(0, Math.min(100, Math.round(percent)));
|
||||
const used = 100 - remaining;
|
||||
|
||||
// Parse resetDate — expected format "2026-09-16"
|
||||
let resetAt = null;
|
||||
if (resetDate && typeof resetDate === "string") {
|
||||
const parsed = new Date(`${resetDate}T00:00:00Z`);
|
||||
if (!Number.isNaN(parsed.getTime())) {
|
||||
resetAt = parsed.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
plan: "Xiaomi MiMo Desktop",
|
||||
quotas: {
|
||||
Weekly: {
|
||||
used,
|
||||
total: 100,
|
||||
remainingPercentage: remaining,
|
||||
resetAt,
|
||||
unlimited: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return { message: `Xiaomi MiMo Desktop usage error: ${error.message}` };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user