feat: add MiniMax quota fetcher and brand icon (#1662)
* feat(server): add MiniMax quota fetcher Mirror the live multi-provider quota panel (#1278) for MiniMax. Resolves the bearer token from $MINIMAX_API_KEY or the MiniMax CLI config at ~/.mmx/config.json (api_key, oauth.access_token) and credentials at ~/.mmx/credentials.json, then queries the /v1/token_plan/remains endpoint on the configured region (global by default, cn when region='cn'). The response is normalized into ProviderUsage windows per model_remains entry, surfacing the interval and weekly limits with their reset times and a danger tone when the server reports status=2. * feat(app): add MiniMax brand icon for usage page The quota panel resolves the icon via resolveProviderIconName, which falls back to a generic Bot for any provider not in BUILTIN_PROVIDER_ICON_NAMES. Register 'minimax' as a built-in icon and vendor the official 24×24 brand mark (single Path, currentColor) as MiniMaxIcon so the usage card stops showing the generic robot. * refactor(server): flatten nested ternaries in MiniMax quota fetcher Greptile review on #1662 called out two nested ternaries in toIntervalWindow and toWeeklyWindow. Extract a toneForStatus helper that uses an explicit if/else chain so the same status-to-tone mapping is shared between the interval and weekly windows, and add the missing trailing newline. * fix: format
This commit is contained in:
@@ -9,6 +9,7 @@ import { CopilotQuotaProvider } from "./providers/copilot.js";
|
||||
import { CursorQuotaProvider } from "./providers/cursor.js";
|
||||
import { GrokQuotaProvider } from "./providers/grok.js";
|
||||
import { KimiQuotaProvider } from "./providers/kimi.js";
|
||||
import { MiniMaxQuotaProvider } from "./providers/minimax.js";
|
||||
import { ZaiQuotaProvider } from "./providers/zai.js";
|
||||
|
||||
export const PROVIDER_USAGE_FETCHERS: readonly ProviderUsageFetcherManifestEntry[] = [
|
||||
@@ -48,6 +49,10 @@ export const PROVIDER_USAGE_FETCHERS: readonly ProviderUsageFetcherManifestEntry
|
||||
providerId: "kimi",
|
||||
create: (options) => new KimiQuotaProvider({ logger: options.logger, fetch: options.fetch }),
|
||||
},
|
||||
{
|
||||
providerId: "minimax",
|
||||
create: (options) => new MiniMaxQuotaProvider({ logger: options.logger, fetch: options.fetch }),
|
||||
},
|
||||
];
|
||||
|
||||
export function createProviderUsageFetchers(
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import { existsSync, promises as fs } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { Logger } from "pino";
|
||||
import { z } from "zod";
|
||||
import type { ProviderUsage, ProviderUsageWindow } from "../../../server/messages.js";
|
||||
import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
|
||||
import {
|
||||
ApiNumberSchema,
|
||||
ApiOptionalStringSchema,
|
||||
fetchProviderApi,
|
||||
unavailableUsage,
|
||||
windowFromUsedPct,
|
||||
} from "../usage.js";
|
||||
|
||||
const MINIMAX_GLOBAL_BASE_URL = "https://api.minimax.io";
|
||||
const MINIMAX_CN_BASE_URL = "https://api.minimaxi.com";
|
||||
const MINIMAX_CREDENTIALS_PATH = join(homedir(), ".mmx", "credentials.json");
|
||||
const MINIMAX_CONFIG_PATH = join(homedir(), ".mmx", "config.json");
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
const MiniMaxQuotaResponseSchema = z.object({
|
||||
model_remains: z.array(MiniMaxModelRemainSchema).optional(),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
interface MiniMaxQuotaProviderOptions {
|
||||
logger: Logger;
|
||||
fetch?: ProviderApiFetch;
|
||||
configPath?: string;
|
||||
credentialsPath?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
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): ProviderUsageWindow["tone"] {
|
||||
if (status === 2) return "danger";
|
||||
if (status === 3) return "default";
|
||||
return "ok";
|
||||
}
|
||||
|
||||
function toIntervalWindow(
|
||||
modelName: string,
|
||||
model: MiniMaxModelRemain,
|
||||
): ProviderUsageWindow | 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): ProviderUsageWindow | 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 class MiniMaxQuotaProvider implements ProviderUsageFetcher {
|
||||
readonly providerId = "minimax";
|
||||
readonly displayName = "MiniMax";
|
||||
|
||||
private readonly logger: Logger;
|
||||
private readonly fetchApi: ProviderApiFetch;
|
||||
private readonly configPath: string;
|
||||
private readonly credentialsPath: string;
|
||||
private readonly env: NodeJS.ProcessEnv;
|
||||
private readonly now: () => number;
|
||||
|
||||
constructor(options: MiniMaxQuotaProviderOptions) {
|
||||
this.logger = options.logger;
|
||||
this.fetchApi = options.fetch ?? fetch;
|
||||
this.configPath = options.configPath ?? MINIMAX_CONFIG_PATH;
|
||||
this.credentialsPath = options.credentialsPath ?? MINIMAX_CREDENTIALS_PATH;
|
||||
this.env = options.env ?? process.env;
|
||||
this.now = options.now ?? Date.now;
|
||||
}
|
||||
|
||||
async fetchUsage(): Promise<ProviderUsage> {
|
||||
const auth = await this.resolveAuth();
|
||||
if (!auth) return unavailableUsage(this);
|
||||
|
||||
const res = await fetchProviderApi(this.fetchApi, `${auth.baseUrl}/v1/token_plan/remains`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth.token}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
this.logger.debug({ status: res.status }, "MiniMax usage fetch failed");
|
||||
return unavailableUsage(this);
|
||||
}
|
||||
|
||||
const resp = MiniMaxQuotaResponseSchema.parse(await res.json());
|
||||
const models = resp.model_remains ?? [];
|
||||
|
||||
const windows: ProviderUsageWindow[] = [];
|
||||
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);
|
||||
}
|
||||
|
||||
return {
|
||||
providerId: this.providerId,
|
||||
displayName: this.displayName,
|
||||
status: windows.length > 0 ? "available" : "unavailable",
|
||||
planLabel: null,
|
||||
windows,
|
||||
balances: [],
|
||||
details: [],
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
private async resolveAuth(): Promise<MiniMaxResolvedAuth | null> {
|
||||
const envToken = this.env["MINIMAX_API_KEY"];
|
||||
if (envToken) {
|
||||
const envBase = this.env["MINIMAX_BASE_URL"];
|
||||
return {
|
||||
token: envToken,
|
||||
baseUrl: resolveBaseUrl({ baseUrl: envBase }),
|
||||
};
|
||||
}
|
||||
|
||||
const credentials = await this.readCredentials();
|
||||
if (credentials?.access_token && !this.isExpired(credentials.expires_at)) {
|
||||
return {
|
||||
token: credentials.access_token,
|
||||
baseUrl: resolveBaseUrl({ baseUrl: credentials.resource_url }),
|
||||
};
|
||||
}
|
||||
|
||||
const config = await this.readConfig();
|
||||
if (config?.api_key) {
|
||||
return {
|
||||
token: config.api_key,
|
||||
baseUrl: resolveBaseUrl({
|
||||
baseUrl: config.base_url,
|
||||
region: config.region,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (config?.oauth?.access_token && !this.isExpired(config.oauth.expires_at)) {
|
||||
return {
|
||||
token: config.oauth.access_token,
|
||||
baseUrl: resolveBaseUrl({
|
||||
baseUrl: config.oauth.resource_url ?? config.base_url,
|
||||
region: config.region,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private isExpired(expiresAt: string | null | undefined): boolean {
|
||||
if (!expiresAt) return false;
|
||||
const parsed = Date.parse(expiresAt);
|
||||
if (!Number.isFinite(parsed)) return false;
|
||||
return parsed <= this.now();
|
||||
}
|
||||
|
||||
private async readCredentials(): Promise<z.infer<typeof MiniMaxCredentialsSchema> | null> {
|
||||
if (!existsSync(this.credentialsPath)) return null;
|
||||
try {
|
||||
const raw = JSON.parse(await fs.readFile(this.credentialsPath, "utf8"));
|
||||
return MiniMaxCredentialsSchema.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async readConfig(): Promise<z.infer<typeof MiniMaxConfigSchema> | null> {
|
||||
if (!existsSync(this.configPath)) return null;
|
||||
try {
|
||||
const raw = JSON.parse(await fs.readFile(this.configPath, "utf8"));
|
||||
return MiniMaxConfigSchema.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { CopilotQuotaProvider } from "./providers/copilot.js";
|
||||
import { CursorQuotaProvider } from "./providers/cursor.js";
|
||||
import { GrokQuotaProvider } from "./providers/grok.js";
|
||||
import { KimiQuotaProvider } from "./providers/kimi.js";
|
||||
import { MiniMaxQuotaProvider } from "./providers/minimax.js";
|
||||
import { ZaiQuotaProvider } from "./providers/zai.js";
|
||||
import { ProviderUsageService } from "./service.js";
|
||||
|
||||
@@ -50,6 +51,24 @@ function writeKimiCredentials(dir: string, accessToken: string): void {
|
||||
);
|
||||
}
|
||||
|
||||
function writeMiniMaxConfig(dir: string, payload: Record<string, unknown>): void {
|
||||
mkdirSync(join(dir, ".mmx"), { recursive: true });
|
||||
writeFileSync(join(dir, ".mmx", "config.json"), JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function writeMiniMaxCredentials(
|
||||
dir: string,
|
||||
accessToken: string,
|
||||
expiresAt?: string,
|
||||
resourceUrl?: string,
|
||||
): void {
|
||||
mkdirSync(join(dir, ".mmx"), { recursive: true });
|
||||
const payload: Record<string, unknown> = { access_token: accessToken };
|
||||
if (expiresAt !== undefined) payload["expires_at"] = expiresAt;
|
||||
if (resourceUrl !== undefined) payload["resource_url"] = resourceUrl;
|
||||
writeFileSync(join(dir, ".mmx", "credentials.json"), JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function makeClaudeResponse(
|
||||
overrides: Partial<{
|
||||
five_hour: { utilization: number | string; resets_at: string };
|
||||
@@ -315,6 +334,8 @@ describe("real provider usage fetchers", () => {
|
||||
"KIMI_API_KEY",
|
||||
"KIMI_CODE_HOME",
|
||||
"CODEX_HOME",
|
||||
"MINIMAX_API_KEY",
|
||||
"MINIMAX_BASE_URL",
|
||||
]) {
|
||||
delete process.env[key];
|
||||
}
|
||||
@@ -339,6 +360,8 @@ describe("real provider usage fetchers", () => {
|
||||
platform?: typeof process.platform;
|
||||
keychain?: () => Promise<unknown | null>;
|
||||
kimiHomeDir?: string;
|
||||
miniMaxConfigPath?: string;
|
||||
miniMaxCredentialsPath?: string;
|
||||
} = {},
|
||||
) {
|
||||
const logger = createLogger();
|
||||
@@ -365,6 +388,13 @@ describe("real provider usage fetchers", () => {
|
||||
fetch: fetchThroughTestDouble,
|
||||
homeDir: options.kimiHomeDir,
|
||||
}),
|
||||
new MiniMaxQuotaProvider({
|
||||
logger,
|
||||
fetch: fetchThroughTestDouble,
|
||||
configPath: options.miniMaxConfigPath ?? join(homeDir, ".mmx", "config.json"),
|
||||
credentialsPath:
|
||||
options.miniMaxCredentialsPath ?? join(homeDir, ".mmx", "credentials.json"),
|
||||
}),
|
||||
],
|
||||
cacheTtlMs: 0,
|
||||
});
|
||||
@@ -750,4 +780,143 @@ describe("real provider usage fetchers", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches MiniMax usage from MINIMAX_API_KEY against the global endpoint", async () => {
|
||||
process.env["MINIMAX_API_KEY"] = "minimax_test_token";
|
||||
let requestedUrl: string | null = null;
|
||||
let authorization: string | null = null;
|
||||
fetchApi = (async (url: RequestInfo | URL, init?: RequestInit) => {
|
||||
requestedUrl = url.toString();
|
||||
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
|
||||
return jsonResponse({
|
||||
model_remains: [
|
||||
{
|
||||
model_name: "MiniMax-M2.7",
|
||||
end_time: Date.parse("2026-06-19T05:00:00.000Z"),
|
||||
weekly_end_time: Date.parse("2026-06-26T00:00:00.000Z"),
|
||||
current_interval_total_count: 1000,
|
||||
current_interval_usage_count: 250,
|
||||
current_interval_remaining_percent: 75,
|
||||
current_weekly_total_count: 5000,
|
||||
current_weekly_usage_count: 1200,
|
||||
current_weekly_remaining_percent: 76,
|
||||
},
|
||||
],
|
||||
});
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const miniMax = findProvider(await service().listUsage(), "minimax");
|
||||
|
||||
expect(requestedUrl).toBe("https://api.minimax.io/v1/token_plan/remains");
|
||||
expect(authorization).toBe("Bearer minimax_test_token");
|
||||
expect(miniMax).toMatchObject({
|
||||
status: "available",
|
||||
windows: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "interval_MiniMax-M2.7",
|
||||
label: "MiniMax-M2.7 · Interval",
|
||||
usedPct: 25,
|
||||
remainingPct: 75,
|
||||
resetsAt: "2026-06-19T05:00:00.000Z",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "weekly_MiniMax-M2.7",
|
||||
label: "MiniMax-M2.7 · Weekly",
|
||||
usedPct: 24,
|
||||
remainingPct: 76,
|
||||
resetsAt: "2026-06-26T00:00:00.000Z",
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it("returns unavailable MiniMax usage when no credentials are configured", async () => {
|
||||
fetchApi = vi.fn() as never;
|
||||
|
||||
const miniMax = findProvider(await service().listUsage(), "minimax");
|
||||
|
||||
expect(miniMax.status).toBe("unavailable");
|
||||
expect(fetchApi).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reads MiniMax OAuth credentials from the CLI credentials file", async () => {
|
||||
writeMiniMaxCredentials(
|
||||
homeDir,
|
||||
"minimax_oauth_token",
|
||||
"2030-01-01T00:00:00.000Z",
|
||||
"https://account.example.com",
|
||||
);
|
||||
let requestedUrl: string | null = null;
|
||||
fetchApi = (async (url: RequestInfo | URL) => {
|
||||
requestedUrl = url.toString();
|
||||
return jsonResponse({ model_remains: [] });
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
await service().listUsage();
|
||||
|
||||
expect(requestedUrl).toBe("https://account.example.com/v1/token_plan/remains");
|
||||
});
|
||||
|
||||
it("falls back to MiniMax api_key in the CLI config file", async () => {
|
||||
writeMiniMaxConfig(homeDir, {
|
||||
api_key: "minimax_config_key",
|
||||
region: "cn",
|
||||
});
|
||||
let requestedUrl: string | null = null;
|
||||
fetchApi = (async (url: RequestInfo | URL) => {
|
||||
requestedUrl = url.toString();
|
||||
return jsonResponse({ model_remains: [] });
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const miniMax = findProvider(await service().listUsage(), "minimax");
|
||||
|
||||
expect(requestedUrl).toBe("https://api.minimaxi.com/v1/token_plan/remains");
|
||||
expect(miniMax.status).toBe("unavailable");
|
||||
});
|
||||
|
||||
it("marks exhausted MiniMax interval windows with a danger tone", async () => {
|
||||
process.env["MINIMAX_API_KEY"] = "minimax_test_token";
|
||||
fetchApi = mockFetch(
|
||||
new Map([
|
||||
[
|
||||
"https://api.minimax.io/v1/token_plan/remains",
|
||||
() =>
|
||||
jsonResponse({
|
||||
model_remains: [
|
||||
{
|
||||
model_name: "MiniMax-M2.7",
|
||||
end_time: Date.parse("2026-06-19T05:00:00.000Z"),
|
||||
weekly_end_time: Date.parse("2026-06-26T00:00:00.000Z"),
|
||||
current_interval_total_count: 100,
|
||||
current_interval_usage_count: 100,
|
||||
current_interval_remaining_percent: 0,
|
||||
current_interval_status: 2,
|
||||
current_weekly_total_count: 100,
|
||||
current_weekly_usage_count: 10,
|
||||
current_weekly_remaining_percent: 90,
|
||||
current_weekly_status: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
const miniMax = findProvider(await service().listUsage(), "minimax");
|
||||
|
||||
expect(miniMax).toMatchObject({
|
||||
status: "available",
|
||||
windows: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "interval_MiniMax-M2.7",
|
||||
usedPct: 100,
|
||||
tone: "danger",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "weekly_MiniMax-M2.7",
|
||||
tone: "ok",
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user