mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
committed by
GitHub
parent
8c3b709794
commit
862154541a
14
packages/app/src/components/icons/minimax-icon.tsx
Normal file
14
packages/app/src/components/icons/minimax-icon.tsx
Normal file
@@ -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 (
|
||||||
|
<Svg width={size} height={size} viewBox="0 0 24 24" fill={color} fillRule="evenodd">
|
||||||
|
<Path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" />
|
||||||
|
</Svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ describe("resolveProviderIconName", () => {
|
|||||||
expect(resolveProviderIconName("kiro")).toEqual({ kind: "builtin", id: "kiro" });
|
expect(resolveProviderIconName("kiro")).toEqual({ kind: "builtin", id: "kiro" });
|
||||||
expect(resolveProviderIconName("claude")).toEqual({ kind: "builtin", id: "claude" });
|
expect(resolveProviderIconName("claude")).toEqual({ kind: "builtin", id: "claude" });
|
||||||
expect(resolveProviderIconName("omp")).toEqual({ kind: "builtin", id: "omp" });
|
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", () => {
|
it("returns the catalog identifier for ACP catalog provider ids that ship an icon", () => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { SvgXml } from "react-native-svg";
|
|||||||
import { ClaudeIcon } from "@/components/icons/claude-icon";
|
import { ClaudeIcon } from "@/components/icons/claude-icon";
|
||||||
import { CodexIcon } from "@/components/icons/codex-icon";
|
import { CodexIcon } from "@/components/icons/codex-icon";
|
||||||
import { CopilotIcon } from "@/components/icons/copilot-icon";
|
import { CopilotIcon } from "@/components/icons/copilot-icon";
|
||||||
|
import { MiniMaxIcon } from "@/components/icons/minimax-icon";
|
||||||
import { OpenCodeIcon } from "@/components/icons/opencode-icon";
|
import { OpenCodeIcon } from "@/components/icons/opencode-icon";
|
||||||
import { OmpIcon } from "@/components/icons/omp-icon";
|
import { OmpIcon } from "@/components/icons/omp-icon";
|
||||||
import { PiIcon } from "@/components/icons/pi-icon";
|
import { PiIcon } from "@/components/icons/pi-icon";
|
||||||
@@ -22,6 +23,7 @@ const BUILTIN_PROVIDER_ICONS: Record<string, ProviderIconComponent> = {
|
|||||||
codex: CodexIcon as unknown as ProviderIconComponent,
|
codex: CodexIcon as unknown as ProviderIconComponent,
|
||||||
copilot: CopilotIcon as unknown as ProviderIconComponent,
|
copilot: CopilotIcon as unknown as ProviderIconComponent,
|
||||||
kiro: PackagePlus,
|
kiro: PackagePlus,
|
||||||
|
minimax: MiniMaxIcon as unknown as ProviderIconComponent,
|
||||||
omp: OmpIcon as unknown as ProviderIconComponent,
|
omp: OmpIcon as unknown as ProviderIconComponent,
|
||||||
opencode: OpenCodeIcon as unknown as ProviderIconComponent,
|
opencode: OpenCodeIcon as unknown as ProviderIconComponent,
|
||||||
pi: PiIcon as unknown as ProviderIconComponent,
|
pi: PiIcon as unknown as ProviderIconComponent,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export const BUILTIN_PROVIDER_ICON_NAMES = [
|
|||||||
"codex",
|
"codex",
|
||||||
"copilot",
|
"copilot",
|
||||||
"kiro",
|
"kiro",
|
||||||
|
"minimax",
|
||||||
"omp",
|
"omp",
|
||||||
"opencode",
|
"opencode",
|
||||||
"pi",
|
"pi",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { CopilotQuotaProvider } from "./providers/copilot.js";
|
|||||||
import { CursorQuotaProvider } from "./providers/cursor.js";
|
import { CursorQuotaProvider } from "./providers/cursor.js";
|
||||||
import { GrokQuotaProvider } from "./providers/grok.js";
|
import { GrokQuotaProvider } from "./providers/grok.js";
|
||||||
import { KimiQuotaProvider } from "./providers/kimi.js";
|
import { KimiQuotaProvider } from "./providers/kimi.js";
|
||||||
|
import { MiniMaxQuotaProvider } from "./providers/minimax.js";
|
||||||
import { ZaiQuotaProvider } from "./providers/zai.js";
|
import { ZaiQuotaProvider } from "./providers/zai.js";
|
||||||
|
|
||||||
export const PROVIDER_USAGE_FETCHERS: readonly ProviderUsageFetcherManifestEntry[] = [
|
export const PROVIDER_USAGE_FETCHERS: readonly ProviderUsageFetcherManifestEntry[] = [
|
||||||
@@ -48,6 +49,10 @@ export const PROVIDER_USAGE_FETCHERS: readonly ProviderUsageFetcherManifestEntry
|
|||||||
providerId: "kimi",
|
providerId: "kimi",
|
||||||
create: (options) => new KimiQuotaProvider({ logger: options.logger, fetch: options.fetch }),
|
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(
|
export function createProviderUsageFetchers(
|
||||||
|
|||||||
275
packages/server/src/services/quota-fetcher/providers/minimax.ts
Normal file
275
packages/server/src/services/quota-fetcher/providers/minimax.ts
Normal file
@@ -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 { CursorQuotaProvider } from "./providers/cursor.js";
|
||||||
import { GrokQuotaProvider } from "./providers/grok.js";
|
import { GrokQuotaProvider } from "./providers/grok.js";
|
||||||
import { KimiQuotaProvider } from "./providers/kimi.js";
|
import { KimiQuotaProvider } from "./providers/kimi.js";
|
||||||
|
import { MiniMaxQuotaProvider } from "./providers/minimax.js";
|
||||||
import { ZaiQuotaProvider } from "./providers/zai.js";
|
import { ZaiQuotaProvider } from "./providers/zai.js";
|
||||||
import { ProviderUsageService } from "./service.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(
|
function makeClaudeResponse(
|
||||||
overrides: Partial<{
|
overrides: Partial<{
|
||||||
five_hour: { utilization: number | string; resets_at: string };
|
five_hour: { utilization: number | string; resets_at: string };
|
||||||
@@ -315,6 +334,8 @@ describe("real provider usage fetchers", () => {
|
|||||||
"KIMI_API_KEY",
|
"KIMI_API_KEY",
|
||||||
"KIMI_CODE_HOME",
|
"KIMI_CODE_HOME",
|
||||||
"CODEX_HOME",
|
"CODEX_HOME",
|
||||||
|
"MINIMAX_API_KEY",
|
||||||
|
"MINIMAX_BASE_URL",
|
||||||
]) {
|
]) {
|
||||||
delete process.env[key];
|
delete process.env[key];
|
||||||
}
|
}
|
||||||
@@ -339,6 +360,8 @@ describe("real provider usage fetchers", () => {
|
|||||||
platform?: typeof process.platform;
|
platform?: typeof process.platform;
|
||||||
keychain?: () => Promise<unknown | null>;
|
keychain?: () => Promise<unknown | null>;
|
||||||
kimiHomeDir?: string;
|
kimiHomeDir?: string;
|
||||||
|
miniMaxConfigPath?: string;
|
||||||
|
miniMaxCredentialsPath?: string;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const logger = createLogger();
|
const logger = createLogger();
|
||||||
@@ -365,6 +388,13 @@ describe("real provider usage fetchers", () => {
|
|||||||
fetch: fetchThroughTestDouble,
|
fetch: fetchThroughTestDouble,
|
||||||
homeDir: options.kimiHomeDir,
|
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,
|
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