From ee0dc144a24ef9f99e2fc6cd41c863124748e2b5 Mon Sep 17 00:00:00 2001 From: lyizhou Date: Mon, 1 Jun 2026 12:49:03 +0800 Subject: [PATCH 1/2] fix(minimax): handle CN percent-only quotas --- docs/providers/minimax.md | 2 + plugins/minimax/plugin.js | 98 +++++++++++++++++++++------------- plugins/minimax/plugin.test.js | 36 +++++++++++++ 3 files changed, 99 insertions(+), 37 deletions(-) diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md index 9b78f338..f2edd1ee 100644 --- a/docs/providers/minimax.md +++ b/docs/providers/minimax.md @@ -54,6 +54,7 @@ Expected payload fields: - `model_remains[].current_interval_total_count` - `model_remains[].current_interval_usage_count` - optional remaining aliases (`current_interval_remaining_count`, `current_interval_remains_count`) +- optional remaining percent fields (`current_interval_remaining_percent`) - `model_remains[].start_time` - `model_remains[].end_time` - `model_remains[].remains_time` @@ -64,6 +65,7 @@ Expected payload fields: - Treat `current_interval_usage_count` as remaining prompts (MiniMax remains API behavior). - If only remaining aliases are provided, compute `used = total - remaining`. - If explicit used-count fields are provided, prefer them. +- If count totals are unavailable but a remaining percentage is provided, render a percent progress line. - Plan name is taken from explicit plan/title fields when available. - If plan fields are missing in GLOBAL mode, infer plan tier from known limits (`100/300/1000/2000` prompts or `1500/4500/15000/30000` model-call equivalents). - If plan fields are missing in CN mode, infer only exact known CN limits (`600/1500/4500` model-call counts). diff --git a/plugins/minimax/plugin.js b/plugins/minimax/plugin.js index 2e10476c..99d52425 100644 --- a/plugins/minimax/plugin.js +++ b/plugins/minimax/plugin.js @@ -244,12 +244,13 @@ if (!modelRemains || modelRemains.length === 0) return null + const displayMultiplierForSelection = endpointSelection === "CN" ? 1 / MODEL_CALLS_PER_PROMPT : 1 let chosen = modelRemains[0] for (let i = 0; i < modelRemains.length; i += 1) { const item = modelRemains[i] if (!item || typeof item !== "object") continue const total = readNumber(item.current_interval_total_count ?? item.currentIntervalTotalCount) - if (total !== null && total > 0) { + if (total !== null && total > 0 && Math.round(total * displayMultiplierForSelection) > 0) { chosen = item break } @@ -258,7 +259,51 @@ if (!chosen || typeof chosen !== "object") return null const total = readNumber(chosen.current_interval_total_count ?? chosen.currentIntervalTotalCount) - if (total === null || total <= 0) return null + const startMs = epochToMs(chosen.start_time ?? chosen.startTime) + const endMs = epochToMs(chosen.end_time ?? chosen.endTime) + const remainsRaw = readNumber(chosen.remains_time ?? chosen.remainsTime) + const nowMs = Date.now() + const remainsMs = inferRemainsMs(remainsRaw, endMs, nowMs) + + let resetsAt = endMs !== null ? ctx.util.toIso(endMs) : null + if (!resetsAt && remainsMs !== null) { + resetsAt = ctx.util.toIso(nowMs + remainsMs) + } + + let periodDurationMs = null + if (startMs !== null && endMs !== null && endMs > startMs) { + periodDurationMs = endMs - startMs + } + + const explicitPlanName = normalizePlanName(pickFirstString([ + data.current_subscribe_title, + data.plan_name, + data.plan, + data.current_plan_title, + data.combo_title, + payload.current_subscribe_title, + payload.plan_name, + payload.plan, + ])) + const inferredPlanName = inferPlanNameFromLimit(total, endpointSelection) + const planName = explicitPlanName || inferredPlanName + + if (total === null || total <= 0) { + const remainingPercent = readNumber( + chosen.current_interval_remaining_percent ?? chosen.currentIntervalRemainingPercent + ) + if (remainingPercent !== null && remainingPercent >= 0 && remainingPercent <= 100) { + return { + planName: planName || "Coding Plan", + used: 100 - remainingPercent, + total: 100, + resetsAt, + periodDurationMs, + formatKind: "percent", + } + } + return null + } const usageFieldCount = readNumber(chosen.current_interval_usage_count ?? chosen.currentIntervalUsageCount) const remainingCount = readNumber( @@ -292,35 +337,6 @@ if (used < 0) used = 0 if (used > total) used = total - const startMs = epochToMs(chosen.start_time ?? chosen.startTime) - const endMs = epochToMs(chosen.end_time ?? chosen.endTime) - const remainsRaw = readNumber(chosen.remains_time ?? chosen.remainsTime) - const nowMs = Date.now() - const remainsMs = inferRemainsMs(remainsRaw, endMs, nowMs) - - let resetsAt = endMs !== null ? ctx.util.toIso(endMs) : null - if (!resetsAt && remainsMs !== null) { - resetsAt = ctx.util.toIso(nowMs + remainsMs) - } - - let periodDurationMs = null - if (startMs !== null && endMs !== null && endMs > startMs) { - periodDurationMs = endMs - startMs - } - - const explicitPlanName = normalizePlanName(pickFirstString([ - data.current_subscribe_title, - data.plan_name, - data.plan, - data.current_plan_title, - data.combo_title, - payload.current_subscribe_title, - payload.plan_name, - payload.plan, - ])) - const inferredPlanName = inferPlanNameFromLimit(total, endpointSelection) - const planName = explicitPlanName || inferredPlanName - return { planName, used, @@ -367,12 +383,20 @@ const isCnEndpoint = successfulEndpoint === "CN" const displayMultiplier = isCnEndpoint ? 1 / MODEL_CALLS_PER_PROMPT : 1 - const line = { - label: "Session", - used: Math.round(parsed.used * displayMultiplier), - limit: Math.round(parsed.total * displayMultiplier), - format: { kind: "count", suffix: "prompts" }, - } + const line = + parsed.formatKind === "percent" + ? { + label: "Session", + used: Math.round(parsed.used), + limit: 100, + format: { kind: "percent" }, + } + : { + label: "Session", + used: Math.round(parsed.used * displayMultiplier), + limit: Math.round(parsed.total * displayMultiplier), + format: { kind: "count", suffix: "prompts" }, + } if (parsed.resetsAt) line.resetsAt = parsed.resetsAt if (parsed.periodDurationMs !== null) line.periodDurationMs = parsed.periodDurationMs diff --git a/plugins/minimax/plugin.test.js b/plugins/minimax/plugin.test.js index fa1112a1..de536e9f 100644 --- a/plugins/minimax/plugin.test.js +++ b/plugins/minimax/plugin.test.js @@ -925,6 +925,42 @@ describe("minimax plugin", () => { expect(() => plugin.probe(ctx)).toThrow("Could not parse usage data") }) + it("falls back to CN remaining percent when count totals are unavailable", async () => { + const ctx = makeCtx() + setEnv(ctx, { MINIMAX_CN_API_KEY: "mini-cn-key" }) + ctx.host.http.request.mockReturnValue({ + status: 200, + headers: {}, + bodyText: JSON.stringify({ + base_resp: { status_code: 0 }, + model_remains: [ + { + model_name: "general", + current_interval_total_count: 0, + current_interval_usage_count: 0, + current_interval_remaining_percent: 94, + start_time: 1780279200000, + end_time: 1780297200000, + }, + { + model_name: "video", + current_interval_total_count: 3, + current_interval_usage_count: 3, + current_interval_remaining_percent: 100, + }, + ], + }), + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + + expect(result.plan).toBe("Coding Plan (CN)") + expect(result.lines[0].used).toBe(6) + expect(result.lines[0].limit).toBe(100) + expect(result.lines[0].format).toEqual({ kind: "percent" }) + }) + it("throws parse error when both used and remaining counts are missing", async () => { const ctx = makeCtx() setEnv(ctx, { MINIMAX_API_KEY: "mini-key" }) From ea42413804eef8d89c3b8571394952736270a67d Mon Sep 17 00:00:00 2001 From: lyizhou Date: Mon, 1 Jun 2026 12:57:29 +0800 Subject: [PATCH 2/2] fix(minimax): avoid zero-limit CN rows --- docs/providers/minimax.md | 11 ++++++++--- plugins/minimax/plugin.js | 19 ++++++++++++++++-- plugins/minimax/plugin.test.js | 36 ++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md index f2edd1ee..545020bd 100644 --- a/docs/providers/minimax.md +++ b/docs/providers/minimax.md @@ -78,9 +78,14 @@ Expected payload fields: - **Plan**: best-effort from API payload (normalized to concise label, with ` (CN)` or ` (GLOBAL)` suffix) - **Session** (overview progress line): - `label`: `Session` - - `format`: count (`prompts`) - - `used`: computed used prompts - - `limit`: total prompt limit for current window + - Count format when totals are available: + - `format`: count (`prompts`) + - `used`: computed used prompts + - `limit`: total prompt limit for current window + - Percent format when totals are unavailable but remaining percent is present: + - `format`: percent + - `used`: `100 - current_interval_remaining_percent` + - `limit`: `100` - `resetsAt`: derived from `end_time` or `remains_time` ## Errors diff --git a/plugins/minimax/plugin.js b/plugins/minimax/plugin.js index 99d52425..9be15c4e 100644 --- a/plugins/minimax/plugin.js +++ b/plugins/minimax/plugin.js @@ -245,7 +245,9 @@ if (!modelRemains || modelRemains.length === 0) return null const displayMultiplierForSelection = endpointSelection === "CN" ? 1 / MODEL_CALLS_PER_PROMPT : 1 - let chosen = modelRemains[0] + let chosen = null + let percentFallbackCandidate = null + let generalPercentFallbackCandidate = null for (let i = 0; i < modelRemains.length; i += 1) { const item = modelRemains[i] if (!item || typeof item !== "object") continue @@ -254,11 +256,24 @@ chosen = item break } + const remainingPercent = readNumber( + item.current_interval_remaining_percent ?? item.currentIntervalRemainingPercent + ) + if (remainingPercent !== null && remainingPercent >= 0 && remainingPercent <= 100) { + const modelName = readString(item.model_name ?? item.modelName) + if (!percentFallbackCandidate) percentFallbackCandidate = item + if (!generalPercentFallbackCandidate && modelName === "general") { + generalPercentFallbackCandidate = item + } + } } + if (!chosen) chosen = generalPercentFallbackCandidate || percentFallbackCandidate if (!chosen || typeof chosen !== "object") return null const total = readNumber(chosen.current_interval_total_count ?? chosen.currentIntervalTotalCount) + const hasDisplayableCount = + total !== null && total > 0 && Math.round(total * displayMultiplierForSelection) > 0 const startMs = epochToMs(chosen.start_time ?? chosen.startTime) const endMs = epochToMs(chosen.end_time ?? chosen.endTime) const remainsRaw = readNumber(chosen.remains_time ?? chosen.remainsTime) @@ -288,7 +303,7 @@ const inferredPlanName = inferPlanNameFromLimit(total, endpointSelection) const planName = explicitPlanName || inferredPlanName - if (total === null || total <= 0) { + if (!hasDisplayableCount) { const remainingPercent = readNumber( chosen.current_interval_remaining_percent ?? chosen.currentIntervalRemainingPercent ) diff --git a/plugins/minimax/plugin.test.js b/plugins/minimax/plugin.test.js index de536e9f..887903d8 100644 --- a/plugins/minimax/plugin.test.js +++ b/plugins/minimax/plugin.test.js @@ -961,6 +961,42 @@ describe("minimax plugin", () => { expect(result.lines[0].format).toEqual({ kind: "percent" }) }) + it("falls back to CN remaining percent when small count rows come first", async () => { + const ctx = makeCtx() + setEnv(ctx, { MINIMAX_CN_API_KEY: "mini-cn-key" }) + ctx.host.http.request.mockReturnValue({ + status: 200, + headers: {}, + bodyText: JSON.stringify({ + base_resp: { status_code: 0 }, + model_remains: [ + { + model_name: "video", + current_interval_total_count: 3, + current_interval_usage_count: 3, + current_interval_remaining_percent: 100, + }, + { + model_name: "general", + current_interval_total_count: 0, + current_interval_usage_count: 0, + current_interval_remaining_percent: 94, + start_time: 1780279200000, + end_time: 1780297200000, + }, + ], + }), + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + + expect(result.plan).toBe("Coding Plan (CN)") + expect(result.lines[0].used).toBe(6) + expect(result.lines[0].limit).toBe(100) + expect(result.lines[0].format).toEqual({ kind: "percent" }) + }) + it("throws parse error when both used and remaining counts are missing", async () => { const ctx = makeCtx() setEnv(ctx, { MINIMAX_API_KEY: "mini-key" })