From 8fc21651c75e5581523e2764ef245480d9d691ed Mon Sep 17 00:00:00 2001 From: Robin Ebers Date: Tue, 2 Jun 2026 14:42:19 +0400 Subject: [PATCH] fix(minimax): prefer displayable CN usage rows Port the MiniMax CN percent-only quota handling from #531 onto the post-#534 Token Plan parser. Co-authored-by: lyizhou --- docs/providers/minimax.md | 31 ++++++++------- plugins/minimax/plugin.js | 26 +++++++++++-- plugins/minimax/plugin.test.js | 71 ++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 17 deletions(-) diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md index 9b78f338..03111c75 100644 --- a/docs/providers/minimax.md +++ b/docs/providers/minimax.md @@ -1,13 +1,13 @@ # MiniMax -> Uses MiniMax Coding Plan remains API with a user-provided API key. +> Uses MiniMax Token Plan remains API with a user-provided API key. ## Overview - **Protocol:** HTTPS (JSON) -- **Endpoint:** `GET https://api.minimax.io/v1/api/openplatform/coding_plan/remains` +- **Endpoint:** `GET https://www.minimax.io/v1/token_plan/remains` - **Auth:** `Authorization: Bearer ` -- **Window model:** dynamic rolling 5-hour limit (per MiniMax Coding Plan docs) +- **Window model:** Token Plan remaining usage, returned as counts or percent ## Authentication @@ -30,22 +30,20 @@ If no key is found after attempting both regions, it throws: Request: ```http -GET /v1/api/openplatform/coding_plan/remains HTTP/1.1 -Host: api.minimax.io +GET /v1/token_plan/remains HTTP/1.1 +Host: www.minimax.io Authorization: Bearer Content-Type: application/json Accept: application/json ``` -Fallbacks: +Global requests use: -- `https://api.minimax.io/v1/coding_plan/remains` -- `https://www.minimax.io/v1/api/openplatform/coding_plan/remains` (legacy fallback; can return Cloudflare HTML) +- `https://www.minimax.io/v1/token_plan/remains` When the selected region is `CN`, requests use: -- `https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains` -- `https://api.minimaxi.com/v1/coding_plan/remains` +- `https://api.minimaxi.com/v1/token_plan/remains` Expected payload fields: @@ -54,6 +52,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 +63,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 missing or too small to display after CN scaling, fall back to a valid `current_interval_remaining_percent`. - 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). @@ -76,9 +76,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 count totals are unavailable: + - `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 35df276f..4186e552 100644 --- a/plugins/minimax/plugin.js +++ b/plugins/minimax/plugin.js @@ -243,16 +243,31 @@ if (!modelRemains || modelRemains.length === 0) return null - let chosen = modelRemains[0] + const displayMultiplierForSelection = endpointSelection === "CN" ? 1 / MODEL_CALLS_PER_PROMPT : 1 + 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 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 } + 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 @@ -264,7 +279,10 @@ // Handle percentage-based response (new Token Plan API) // When total_count is 0 but remaining_percent exists, use percentage mode - if ((total === null || total === 0) && remainingPercent !== null) { + const hasDisplayableCount = + total !== null && total > 0 && Math.round(total * displayMultiplierForSelection) > 0 + + if (!hasDisplayableCount && remainingPercent !== null) { const percentRemaining = remainingPercent const percentUsed = 100 - percentRemaining const startMs = epochToMs(chosen.start_time ?? chosen.startTime) @@ -304,7 +322,7 @@ } } - if (total === null || total <= 0) return null + if (!hasDisplayableCount) return null const usageFieldCount = readNumber(chosen.current_interval_usage_count ?? chosen.currentIntervalUsageCount) const remainingCount = readNumber( diff --git a/plugins/minimax/plugin.test.js b/plugins/minimax/plugin.test.js index ee175658..caf380b6 100644 --- a/plugins/minimax/plugin.test.js +++ b/plugins/minimax/plugin.test.js @@ -948,6 +948,77 @@ describe("minimax plugin", () => { expect(line.format.kind).toBe("percent") }) + it("falls back to CN remaining percent when count totals are unavailable", async () => { + const ctx = makeCtx() + setEnv(ctx, { MINIMAX_CN_API_KEY: "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) + const line = result.lines[0] + + expect(line.used).toBe(6) + expect(line.limit).toBe(100) + expect(line.format.kind).toBe("percent") + expect(line.resetsAt).toBe(new Date(1780297200000).toISOString()) + }) + + it("falls back to CN remaining percent when small count rows come first", async () => { + const ctx = makeCtx() + setEnv(ctx, { MINIMAX_CN_API_KEY: "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, + }, + ], + }), + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + const line = result.lines[0] + + expect(line.used).toBe(6) + expect(line.limit).toBe(100) + expect(line.format.kind).toBe("percent") + }) + it("handles weekly percentage from Token Plan API", async () => { const ctx = makeCtx() setEnv(ctx, { MINIMAX_API_KEY: "mini-key" })