feat(usage): MiniMax + MiniMax-CN quota tracking in provider limits dashboard (#1516)

Integrated into release/v3.7.0
This commit is contained in:
Prakersh Maheshwari
2026-04-23 04:05:14 +05:30
committed by GitHub
parent 16b0499192
commit 331e1b7d61
6 changed files with 398 additions and 1 deletions

View File

@@ -71,6 +71,21 @@ const CURSOR_USAGE_CONFIG = {
clientVersion: CURSOR_REGISTRY_VERSION,
};
const MINIMAX_USAGE_CONFIG = {
minimax: {
usageUrls: [
"https://www.minimax.io/v1/token_plan/remains",
"https://api.minimax.io/v1/api/openplatform/coding_plan/remains",
],
},
"minimax-cn": {
usageUrls: [
"https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains",
"https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains",
],
},
} as const;
type JsonRecord = Record<string, unknown>;
type UsageQuota = {
used: number;
@@ -126,6 +141,257 @@ function shouldDisplayGitHubQuota(quota: UsageQuota | null): quota is UsageQuota
return quota.total > 0 || quota.remainingPercentage !== undefined;
}
function createQuotaFromUsage(
usedValue: unknown,
totalValue: unknown,
resetValue: unknown
): UsageQuota {
const total = Math.max(0, toNumber(totalValue, 0));
const used = total > 0 ? Math.min(Math.max(0, toNumber(usedValue, 0)), total) : 0;
const remaining = total > 0 ? Math.max(total - used, 0) : 0;
return {
used,
total,
remaining,
remainingPercentage: total > 0 ? clampPercentage((remaining / total) * 100) : 0,
resetAt: parseResetTime(resetValue),
unlimited: false,
};
}
function getMiniMaxQuotaResetAt(
model: JsonRecord,
capturedAtMs: number,
remainsTimeSnakeKey: string,
remainsTimeCamelKey: string,
endTimeSnakeKey: string,
endTimeCamelKey: string
): string | null {
const remainsMs = toNumber(getFieldValue(model, remainsTimeSnakeKey, remainsTimeCamelKey), 0);
if (remainsMs > 0) {
return new Date(capturedAtMs + remainsMs).toISOString();
}
return parseResetTime(getFieldValue(model, endTimeSnakeKey, endTimeCamelKey));
}
function isMiniMaxTextQuotaModel(modelName: string): boolean {
const normalized = modelName.trim().toLowerCase();
return normalized.startsWith("minimax-m") || normalized.startsWith("coding-plan");
}
function getMiniMaxSessionTotal(model: JsonRecord): number {
return Math.max(
0,
toNumber(getFieldValue(model, "current_interval_total_count", "currentIntervalTotalCount"), 0)
);
}
function getMiniMaxWeeklyTotal(model: JsonRecord): number {
return Math.max(
0,
toNumber(getFieldValue(model, "current_weekly_total_count", "currentWeeklyTotalCount"), 0)
);
}
function pickMiniMaxRepresentativeModel(
models: JsonRecord[],
getTotal: (model: JsonRecord) => number
): JsonRecord | null {
const withQuota = models.filter((model) => getTotal(model) > 0);
const pool = withQuota.length > 0 ? withQuota : models;
if (pool.length === 0) return null;
return pool.reduce((best, current) => (getTotal(current) > getTotal(best) ? current : best));
}
function getMiniMaxAuthErrorMessage(message: string): string {
const normalized = message.toLowerCase();
if (
normalized.includes("token plan") ||
normalized.includes("coding plan") ||
normalized.includes("active period") ||
normalized.includes("invalid api key") ||
normalized.includes("invalid key") ||
normalized.includes("subscription")
) {
return "MiniMax Token Plan API key invalid or inactive. Use an active Token Plan key.";
}
return "MiniMax access denied. Confirm the key is an active Token Plan API key.";
}
function getMiniMaxErrorSummary(status: number, message: string): string {
const compact = message.replace(/\s+/g, " ").trim();
if (!compact) {
return `MiniMax usage endpoint error (${status}).`;
}
if (compact.length <= 160) {
return `MiniMax usage endpoint error (${status}): ${compact}`;
}
return `MiniMax usage endpoint error (${status}): ${compact.slice(0, 157)}...`;
}
async function getMiniMaxUsage(apiKey: string, provider: "minimax" | "minimax-cn") {
if (!apiKey) {
return { message: "MiniMax API key not available. Add a Token Plan API key." };
}
const usageUrls = MINIMAX_USAGE_CONFIG[provider].usageUrls;
let lastErrorMessage = "";
for (let index = 0; index < usageUrls.length; index += 1) {
const usageUrl = usageUrls[index];
const canFallback = index < usageUrls.length - 1;
try {
const response = await fetch(usageUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
"Content-Type": "application/json",
},
});
const rawText = await response.text();
let payload: JsonRecord = {};
if (rawText) {
try {
payload = toRecord(JSON.parse(rawText));
} catch {
payload = {};
}
}
const baseResp = toRecord(getFieldValue(payload, "base_resp", "baseResp"));
const apiStatusCode = toNumber(getFieldValue(baseResp, "status_code", "statusCode"), 0);
const apiStatusMessage = String(
getFieldValue(baseResp, "status_msg", "statusMsg") ?? ""
).trim();
const combinedMessage = `${apiStatusMessage} ${rawText}`.trim();
const authLikeMessage =
/token plan|coding plan|invalid api key|invalid key|unauthorized|inactive/i;
if (
response.status === 401 ||
response.status === 403 ||
apiStatusCode === 1004 ||
authLikeMessage.test(combinedMessage)
) {
return { message: getMiniMaxAuthErrorMessage(combinedMessage) };
}
if (!response.ok) {
lastErrorMessage = getMiniMaxErrorSummary(response.status, combinedMessage);
if (
(response.status === 404 || response.status === 405 || response.status >= 500) &&
canFallback
) {
continue;
}
return { message: `MiniMax connected. ${lastErrorMessage}` };
}
if (rawText && Object.keys(payload).length === 0) {
return { message: "MiniMax connected. Unable to parse usage response." };
}
if (apiStatusCode !== 0) {
if (apiStatusMessage) {
return { message: `MiniMax connected. ${apiStatusMessage}` };
}
return { message: "MiniMax connected. Upstream quota API returned an error." };
}
const capturedAtMs = Date.now();
const modelRemains = getFieldValue(payload, "model_remains", "modelRemains");
const allModels = Array.isArray(modelRemains)
? modelRemains.map((item) => toRecord(item))
: [];
const textModels = allModels.filter((model) => {
const modelName = String(getFieldValue(model, "model_name", "modelName") ?? "");
return isMiniMaxTextQuotaModel(modelName);
});
if (textModels.length === 0) {
return { message: "MiniMax connected. No text quota data was returned." };
}
const quotas: Record<string, UsageQuota> = {};
const sessionModel = pickMiniMaxRepresentativeModel(textModels, getMiniMaxSessionTotal);
if (sessionModel) {
const total = getMiniMaxSessionTotal(sessionModel);
const remain = Math.max(
0,
toNumber(
getFieldValue(
sessionModel,
"current_interval_usage_count",
"currentIntervalUsageCount"
),
0
)
);
quotas["session (5h)"] = createQuotaFromUsage(
Math.max(total - remain, 0),
total,
getMiniMaxQuotaResetAt(
sessionModel,
capturedAtMs,
"remains_time",
"remainsTime",
"end_time",
"endTime"
)
);
}
const weeklyModel = pickMiniMaxRepresentativeModel(textModels, getMiniMaxWeeklyTotal);
if (weeklyModel && getMiniMaxWeeklyTotal(weeklyModel) > 0) {
const total = getMiniMaxWeeklyTotal(weeklyModel);
const remain = Math.max(
0,
toNumber(
getFieldValue(weeklyModel, "current_weekly_usage_count", "currentWeeklyUsageCount"),
0
)
);
quotas["weekly (7d)"] = createQuotaFromUsage(
Math.max(total - remain, 0),
total,
getMiniMaxQuotaResetAt(
weeklyModel,
capturedAtMs,
"weekly_remains_time",
"weeklyRemainsTime",
"weekly_end_time",
"weeklyEndTime"
)
);
}
if (Object.keys(quotas).length === 0) {
return { message: "MiniMax connected. Unable to extract text quota usage." };
}
return { quotas };
} catch (error) {
lastErrorMessage = (error as Error).message;
if (!canFallback) {
break;
}
}
}
return {
message: lastErrorMessage
? `MiniMax connected. Unable to fetch usage: ${lastErrorMessage}`
: "MiniMax connected. Unable to fetch usage.",
};
}
async function getGlmUsage(apiKey: string, providerSpecificData?: Record<string, unknown>) {
const quotaUrl = getGlmQuotaUrl(providerSpecificData);
@@ -240,6 +506,9 @@ export async function getUsageForProvider(connection) {
case "glm":
case "glmt":
return await getGlmUsage(apiKey, providerSpecificData);
case "minimax":
case "minimax-cn":
return await getMiniMaxUsage(apiKey, provider);
case "cursor":
return await getCursorUsage(accessToken);
case "bailian-coding-plan":

View File

@@ -37,6 +37,8 @@ const PROVIDER_CONFIG = {
glm: { label: "GLM (Z.AI)", color: "#4A90D9" },
glmt: { label: "GLM Thinking", color: "#2563EB" },
"kimi-coding": { label: "Kimi Coding", color: "#1E3A8A" },
minimax: { label: "MiniMax", color: "#7C3AED" },
"minimax-cn": { label: "MiniMax CN", color: "#DC2626" },
};
const TIER_FILTERS = [
@@ -293,6 +295,8 @@ export default function ProviderLimits() {
glm: 7,
glmt: 8,
"kimi-coding": 9,
minimax: 10,
"minimax-cn": 11,
};
return [...filteredConnections].sort(
(a, b) => (priority[a.provider] || 9) - (priority[b.provider] || 9)

View File

@@ -43,7 +43,7 @@ interface ProviderConnectionLike {
backoffLevel?: number;
}
const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set(["glm", "glmt"]);
const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set(["glm", "glmt", "minimax", "minimax-cn"]);
const DEFAULT_PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES = 70;
const PROVIDER_LIMITS_AUTO_SYNC_SETTING_KEY = "provider_limits_auto_sync_last_run";

View File

@@ -1226,6 +1226,8 @@ export const USAGE_SUPPORTED_PROVIDERS = [
"kimi-coding",
"glm",
"glmt",
"minimax",
"minimax-cn",
];
// ── Zod validation at module load (Phase 7.2) ──

View File

@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
const providerLimitUtils =
await import("../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx");
const providerConstants = await import("../../src/shared/constants/providers.ts");
test("provider plan fallbacks normalize to Unknown instead of repeating provider labels", () => {
const tier = providerLimitUtils.normalizePlanTier("Claude Code");
@@ -53,3 +54,42 @@ test("quota labels normalize session and weekly windows while preserving readabl
assert.equal(providerLimitUtils.formatQuotaLabel("weekly sonnet (7d)"), "Weekly Sonnet");
assert.equal(providerLimitUtils.formatQuotaLabel("code_review"), "Code Review");
});
test("MiniMax providers are exposed to the limits dashboard support list", () => {
assert.ok(providerConstants.USAGE_SUPPORTED_PROVIDERS.includes("minimax"));
assert.ok(providerConstants.USAGE_SUPPORTED_PROVIDERS.includes("minimax-cn"));
});
test("MiniMax quota payloads use generic provider parsing and stale resets still refill", () => {
const future = new Date(Date.now() + 5 * 60_000).toISOString();
const past = new Date(Date.now() - 5 * 60_000).toISOString();
const parsed = providerLimitUtils.parseQuotaData("minimax", {
quotas: {
"session (5h)": {
used: 400,
total: 1500,
remaining: 1100,
remainingPercentage: 73.3,
resetAt: future,
},
"weekly (7d)": {
used: 1200,
total: 15000,
remaining: 13800,
remainingPercentage: 92,
resetAt: past,
},
},
});
assert.equal(parsed.length, 2);
assert.equal(parsed[0].name, "session (5h)");
assert.equal(parsed[0].used, 400);
assert.equal(parsed[0].total, 1500);
assert.equal(parsed[1].name, "weekly (7d)");
assert.equal(parsed[1].used, 0);
assert.equal(parsed[1].remainingPercentage, 100);
assert.equal(providerLimitUtils.formatQuotaLabel(parsed[0].name), "Session");
assert.equal(providerLimitUtils.formatQuotaLabel(parsed[1].name), "Weekly");
});

View File

@@ -854,6 +854,88 @@ test("usage service covers Qwen, Qoder, GLM and GLMT branches", async () => {
);
});
test("usage service covers MiniMax usage parsing, documented endpoint fallback and auth errors", async () => {
const calls: any[] = [];
const beforeCall = Date.now();
globalThis.fetch = async (url, init = {}) => {
calls.push({ url: String(url), init });
if (String(url) === "https://www.minimax.io/v1/token_plan/remains") {
return new Response("missing", { status: 404 });
}
if (String(url) === "https://api.minimax.io/v1/api/openplatform/coding_plan/remains") {
assert.equal((init as any).headers.Authorization, "Bearer minimax-key");
assert.equal((init as any).headers.Accept, "application/json");
return new Response(
JSON.stringify({
base_resp: { status_code: 0, status_msg: "ok" },
model_remains: [
{
model_name: "MiniMax-M2.7",
remains_time: 300_000,
current_interval_total_count: 1500,
current_interval_usage_count: 1100,
current_weekly_total_count: 15000,
current_weekly_usage_count: 13800,
weekly_remains_time: 1_800_000,
},
{
model_name: "image-01",
remains_time: 86_400_000,
current_interval_total_count: 50,
current_interval_usage_count: 45,
},
],
}),
{ status: 200 }
);
}
if (String(url) === "https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains") {
return new Response(
JSON.stringify({
base_resp: {
status_code: 1004,
status_msg: "token plan api key invalid",
},
}),
{ status: 403 }
);
}
throw new Error(`unexpected fetch: ${url}`);
};
const usage: any = await usageService.getUsageForProvider({
provider: "minimax",
apiKey: "minimax-key",
});
assert.deepEqual(
calls.map((call) => call.url),
[
"https://www.minimax.io/v1/token_plan/remains",
"https://api.minimax.io/v1/api/openplatform/coding_plan/remains",
]
);
assert.equal(usage.quotas["session (5h)"].used, 400);
assert.equal(usage.quotas["session (5h)"].total, 1500);
assert.equal(usage.quotas["session (5h)"].remaining, 1100);
assert.equal(usage.quotas["weekly (7d)"].used, 1200);
assert.equal(usage.quotas["weekly (7d)"].total, 15000);
assert.equal(usage.quotas["weekly (7d)"].remainingPercentage, 92);
assert.ok(Date.parse(usage.quotas["session (5h)"].resetAt) >= beforeCall + 240_000);
const invalid: any = await usageService.getUsageForProvider({
provider: "minimax-cn",
apiKey: "bad-minimax-key",
});
assert.match(invalid.message, /Token Plan API key/i);
});
test("usage service parses Cursor team quotas and clamps on-demand ratio", async () => {
const calls: any[] = [];
globalThis.fetch = async (url, init = {}) => {