mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-20 00:02:54 +00:00
* fix(usage): keep existing logins visible and explain unavailable quota Credential discovery must preserve accounts whose login exists even when it has expired. Keep vendor failures on the discovered account and isolate discovery failures without inventing provider cards. * test(usage): align credential route tests with account discovery
253 lines
8.8 KiB
TypeScript
253 lines
8.8 KiB
TypeScript
import type { UsageInput } from "../shared/input.js";
|
|
import { promises as fs } from "node:fs";
|
|
import { homedir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { z } from "zod";
|
|
import {
|
|
unavailable,
|
|
type UsageAccount,
|
|
windowFromUsedPct,
|
|
type UsageReport,
|
|
type UsageWindow,
|
|
} from "@getpaseo/plugin/server/usage";
|
|
|
|
const ApiNumberSchema = z.coerce.number().finite();
|
|
const ApiOptionalStringSchema = z.preprocess(
|
|
(value) => (value == null ? undefined : value),
|
|
z.coerce.string().optional(),
|
|
);
|
|
|
|
const MINIMAX_GLOBAL_BASE_URL = "https://api.minimax.io";
|
|
const MINIMAX_CN_BASE_URL = "https://api.minimaxi.com";
|
|
|
|
const MiniMaxModelRemainSchema = z.object({
|
|
model_name: ApiOptionalStringSchema,
|
|
start_time: ApiNumberSchema.optional(),
|
|
end_time: ApiNumberSchema.optional(),
|
|
remains_time: ApiNumberSchema.optional(),
|
|
current_interval_total_count: ApiNumberSchema.optional(),
|
|
current_interval_usage_count: ApiNumberSchema.optional(),
|
|
current_interval_remaining_percent: ApiNumberSchema.optional(),
|
|
current_weekly_total_count: ApiNumberSchema.optional(),
|
|
current_weekly_usage_count: ApiNumberSchema.optional(),
|
|
current_weekly_remaining_percent: ApiNumberSchema.optional(),
|
|
current_interval_status: ApiNumberSchema.optional(),
|
|
current_weekly_status: ApiNumberSchema.optional(),
|
|
weekly_start_time: ApiNumberSchema.optional(),
|
|
weekly_end_time: ApiNumberSchema.optional(),
|
|
weekly_remains_time: ApiNumberSchema.optional(),
|
|
weekly_boost_permille: ApiNumberSchema.optional(),
|
|
});
|
|
|
|
/**
|
|
* MiniMax reports application-level failures inside a 200 response: `base_resp.status_code`
|
|
* is non-zero and `model_remains` comes back as null. An account with no token plan
|
|
* subscription is the common case (status 2062), so both shapes are normal input here.
|
|
*/
|
|
const MiniMaxBaseRespSchema = z.object({
|
|
status_code: ApiNumberSchema.optional(),
|
|
status_msg: ApiOptionalStringSchema,
|
|
});
|
|
|
|
const MiniMaxQuotaResponseSchema = z.object({
|
|
model_remains: z.array(MiniMaxModelRemainSchema).nullish(),
|
|
base_resp: MiniMaxBaseRespSchema.nullish(),
|
|
});
|
|
|
|
const MiniMaxCredentialsSchema = z.object({
|
|
access_token: z.string().optional(),
|
|
refresh_token: z.string().optional(),
|
|
expires_at: ApiOptionalStringSchema,
|
|
resource_url: ApiOptionalStringSchema,
|
|
});
|
|
|
|
const MiniMaxConfigSchema = z.object({
|
|
api_key: z.string().optional(),
|
|
region: z.string().optional(),
|
|
base_url: ApiOptionalStringSchema,
|
|
oauth: MiniMaxCredentialsSchema.optional(),
|
|
});
|
|
|
|
type MiniMaxModelRemain = z.infer<typeof MiniMaxModelRemainSchema>;
|
|
|
|
interface MiniMaxResolvedAuth {
|
|
token: string;
|
|
baseUrl: string;
|
|
}
|
|
|
|
function resolveBaseUrl(input: { baseUrl?: string; region?: string }): string {
|
|
const explicit = input.baseUrl;
|
|
if (explicit && explicit.startsWith("http")) return explicit;
|
|
if (input.region === "cn") return MINIMAX_CN_BASE_URL;
|
|
return MINIMAX_GLOBAL_BASE_URL;
|
|
}
|
|
|
|
function computeUsedPct(
|
|
remaining: number | null | undefined,
|
|
total: number | null | undefined,
|
|
): number | null {
|
|
if (typeof remaining !== "number" || typeof total !== "number") return null;
|
|
if (!Number.isFinite(total) || total <= 0) return null;
|
|
if (!Number.isFinite(remaining)) return null;
|
|
const used = total - remaining;
|
|
return Math.max(0, Math.min(100, (used / total) * 100));
|
|
}
|
|
|
|
function epochMsToIso(value: number | null | undefined): string | null {
|
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
|
|
return new Date(value).toISOString();
|
|
}
|
|
|
|
function toneForStatus(status: number | null | undefined): UsageWindow["tone"] {
|
|
if (status === 2) return "danger";
|
|
if (status === 3) return "default";
|
|
return "ok";
|
|
}
|
|
|
|
function toIntervalWindow(modelName: string, model: MiniMaxModelRemain): UsageWindow | null {
|
|
const total = model.current_interval_total_count ?? null;
|
|
const used = model.current_interval_usage_count ?? null;
|
|
const remainingPercent = model.current_interval_remaining_percent ?? null;
|
|
const usedPct =
|
|
typeof remainingPercent === "number" && Number.isFinite(remainingPercent)
|
|
? Math.max(0, Math.min(100, 100 - remainingPercent))
|
|
: computeUsedPct(
|
|
typeof total === "number" && typeof used === "number" ? total - used : null,
|
|
total,
|
|
);
|
|
if (usedPct === null) return null;
|
|
return windowFromUsedPct({
|
|
id: `interval_${modelName}`,
|
|
label: `${modelName} · Interval`,
|
|
utilizationPct: usedPct,
|
|
resetsAt: epochMsToIso(model.end_time),
|
|
tone: toneForStatus(model.current_interval_status),
|
|
});
|
|
}
|
|
|
|
function toWeeklyWindow(modelName: string, model: MiniMaxModelRemain): UsageWindow | null {
|
|
const total = model.current_weekly_total_count ?? null;
|
|
const used = model.current_weekly_usage_count ?? null;
|
|
const remainingPercent = model.current_weekly_remaining_percent ?? null;
|
|
let usedPct: number | null = null;
|
|
if (typeof remainingPercent === "number" && Number.isFinite(remainingPercent)) {
|
|
usedPct = Math.max(0, Math.min(100, 100 - remainingPercent));
|
|
} else if (typeof total === "number" && typeof used === "number") {
|
|
usedPct = computeUsedPct(total - used, total);
|
|
}
|
|
if (usedPct === null) return null;
|
|
return windowFromUsedPct({
|
|
id: `weekly_${modelName}`,
|
|
label: `${modelName} · Weekly`,
|
|
utilizationPct: usedPct,
|
|
resetsAt: epochMsToIso(model.weekly_end_time),
|
|
tone: toneForStatus(model.current_weekly_status),
|
|
});
|
|
}
|
|
|
|
export async function fetchUsage(
|
|
input: UsageInput,
|
|
fetchApi: typeof fetch = fetch,
|
|
): Promise<UsageReport> {
|
|
const auth = await readAuth(input);
|
|
if (!auth) throw new Error("MiniMax login store no longer exists");
|
|
if (auth.expiresAt && Date.parse(auth.expiresAt) <= Date.now())
|
|
return unavailable({ kind: "expired", expiresAt: new Date(auth.expiresAt).toISOString() });
|
|
|
|
const res = await fetchApi(`${auth.baseUrl}/v1/token_plan/remains`, {
|
|
signal: AbortSignal.timeout(15_000),
|
|
headers: {
|
|
Authorization: `Bearer ${auth.token}`,
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
|
|
if (res.status === 401 || res.status === 403)
|
|
return unavailable({ kind: "rejected", status: res.status });
|
|
if (!res.ok) throw new Error(`MiniMax usage API returned ${res.status}`);
|
|
|
|
const resp = MiniMaxQuotaResponseSchema.parse(await res.json());
|
|
|
|
const statusCode = resp.base_resp?.status_code;
|
|
if (typeof statusCode === "number" && statusCode !== 0) {
|
|
return unavailable({
|
|
kind: "no_quota",
|
|
detail: resp.base_resp?.status_msg ?? "No active coding plan",
|
|
});
|
|
}
|
|
|
|
const models = resp.model_remains ?? [];
|
|
|
|
const windows: UsageWindow[] = [];
|
|
for (const model of models) {
|
|
const name = model.model_name ?? "token-plan";
|
|
const intervalWindow = toIntervalWindow(name, model);
|
|
if (intervalWindow) windows.push(intervalWindow);
|
|
const weeklyWindow = toWeeklyWindow(name, model);
|
|
if (weeklyWindow) windows.push(weeklyWindow);
|
|
}
|
|
|
|
if (windows.length === 0)
|
|
return unavailable({ kind: "no_quota", detail: "No active coding plan" });
|
|
return {
|
|
status: "available",
|
|
planLabel: undefined,
|
|
windows,
|
|
balances: [],
|
|
details: [],
|
|
};
|
|
}
|
|
|
|
async function readAuth(
|
|
input: UsageInput,
|
|
): Promise<(MiniMaxResolvedAuth & { expiresAt?: string }) | undefined> {
|
|
if (input.store === "env") {
|
|
const token = process.env[input.locator];
|
|
return token
|
|
? { token, baseUrl: resolveBaseUrl({ baseUrl: process.env.MINIMAX_BASE_URL }) }
|
|
: undefined;
|
|
}
|
|
try {
|
|
const raw: unknown = JSON.parse(await fs.readFile(input.locator, "utf8"));
|
|
if (input.store === "credentials") {
|
|
const credentials = MiniMaxCredentialsSchema.parse(raw);
|
|
return credentials.access_token
|
|
? {
|
|
token: credentials.access_token,
|
|
baseUrl: resolveBaseUrl({ baseUrl: credentials.resource_url }),
|
|
expiresAt: credentials.expires_at ?? undefined,
|
|
}
|
|
: undefined;
|
|
}
|
|
const config = MiniMaxConfigSchema.parse(raw);
|
|
if (config.api_key)
|
|
return {
|
|
token: config.api_key,
|
|
baseUrl: resolveBaseUrl({ baseUrl: config.base_url, region: config.region }),
|
|
};
|
|
return config.oauth?.access_token
|
|
? {
|
|
token: config.oauth.access_token,
|
|
baseUrl: resolveBaseUrl({
|
|
baseUrl: config.oauth.resource_url ?? config.base_url,
|
|
region: config.region,
|
|
}),
|
|
expiresAt: config.oauth.expires_at ?? undefined,
|
|
}
|
|
: undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
export async function discover(): Promise<UsageAccount[]> {
|
|
const candidates: UsageInput[] = [
|
|
{ store: "env", locator: "MINIMAX_API_KEY" },
|
|
{ store: "credentials", locator: join(homedir(), ".mmx", "credentials.json") },
|
|
{ store: "config", locator: join(homedir(), ".mmx", "config.json") },
|
|
];
|
|
const accounts: UsageAccount[] = [];
|
|
for (const input of candidates)
|
|
if (await readAuth(input)) accounts.push({ key: "default", input });
|
|
return accounts;
|
|
}
|