diff --git a/packages/app/src/components/icons/minimax-icon.tsx b/packages/app/src/components/icons/minimax-icon.tsx
new file mode 100644
index 000000000..11847d248
--- /dev/null
+++ b/packages/app/src/components/icons/minimax-icon.tsx
@@ -0,0 +1,14 @@
+import Svg, { Path } from "react-native-svg";
+
+interface MiniMaxIconProps {
+ size?: number;
+ color?: string;
+}
+
+export function MiniMaxIcon({ size = 16, color = "currentColor" }: MiniMaxIconProps) {
+ return (
+
+ );
+}
diff --git a/packages/app/src/components/provider-icon-name.test.ts b/packages/app/src/components/provider-icon-name.test.ts
index 80727b59c..956e40d0b 100644
--- a/packages/app/src/components/provider-icon-name.test.ts
+++ b/packages/app/src/components/provider-icon-name.test.ts
@@ -12,6 +12,7 @@ describe("resolveProviderIconName", () => {
expect(resolveProviderIconName("kiro")).toEqual({ kind: "builtin", id: "kiro" });
expect(resolveProviderIconName("claude")).toEqual({ kind: "builtin", id: "claude" });
expect(resolveProviderIconName("omp")).toEqual({ kind: "builtin", id: "omp" });
+ expect(resolveProviderIconName("minimax")).toEqual({ kind: "builtin", id: "minimax" });
});
it("returns the catalog identifier for ACP catalog provider ids that ship an icon", () => {
diff --git a/packages/app/src/components/provider-icons.ts b/packages/app/src/components/provider-icons.ts
index 443abb50e..92d8c1ca3 100644
--- a/packages/app/src/components/provider-icons.ts
+++ b/packages/app/src/components/provider-icons.ts
@@ -4,6 +4,7 @@ import { SvgXml } from "react-native-svg";
import { ClaudeIcon } from "@/components/icons/claude-icon";
import { CodexIcon } from "@/components/icons/codex-icon";
import { CopilotIcon } from "@/components/icons/copilot-icon";
+import { MiniMaxIcon } from "@/components/icons/minimax-icon";
import { OpenCodeIcon } from "@/components/icons/opencode-icon";
import { OmpIcon } from "@/components/icons/omp-icon";
import { PiIcon } from "@/components/icons/pi-icon";
@@ -22,6 +23,7 @@ const BUILTIN_PROVIDER_ICONS: Record = {
codex: CodexIcon as unknown as ProviderIconComponent,
copilot: CopilotIcon as unknown as ProviderIconComponent,
kiro: PackagePlus,
+ minimax: MiniMaxIcon as unknown as ProviderIconComponent,
omp: OmpIcon as unknown as ProviderIconComponent,
opencode: OpenCodeIcon as unknown as ProviderIconComponent,
pi: PiIcon as unknown as ProviderIconComponent,
diff --git a/packages/protocol/src/provider-icon-names.ts b/packages/protocol/src/provider-icon-names.ts
index 1e0819850..43c65685d 100644
--- a/packages/protocol/src/provider-icon-names.ts
+++ b/packages/protocol/src/provider-icon-names.ts
@@ -3,6 +3,7 @@ export const BUILTIN_PROVIDER_ICON_NAMES = [
"codex",
"copilot",
"kiro",
+ "minimax",
"omp",
"opencode",
"pi",
diff --git a/packages/server/src/services/quota-fetcher/manifest.ts b/packages/server/src/services/quota-fetcher/manifest.ts
index 220bd4757..11578eccd 100644
--- a/packages/server/src/services/quota-fetcher/manifest.ts
+++ b/packages/server/src/services/quota-fetcher/manifest.ts
@@ -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(
diff --git a/packages/server/src/services/quota-fetcher/providers/minimax.ts b/packages/server/src/services/quota-fetcher/providers/minimax.ts
new file mode 100644
index 000000000..21e0f0581
--- /dev/null
+++ b/packages/server/src/services/quota-fetcher/providers/minimax.ts
@@ -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;
+
+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 {
+ 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 {
+ 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 | 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 | 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;
+ }
+ }
+}
diff --git a/packages/server/src/services/quota-fetcher/service.test.ts b/packages/server/src/services/quota-fetcher/service.test.ts
index b499926b4..5c23d4237 100644
--- a/packages/server/src/services/quota-fetcher/service.test.ts
+++ b/packages/server/src/services/quota-fetcher/service.test.ts
@@ -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): 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 = { 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;
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 | 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",
+ }),
+ ]),
+ });
+ });
});