Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions docs/providers/minimax.md
Original file line number Diff line number Diff line change
@@ -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 <api_key>`
- **Window model:** dynamic rolling 5-hour limit (per MiniMax Coding Plan docs)
- **Window model:** Token Plan remaining usage, returned as counts or percent

## Authentication

Expand All @@ -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 <api_key>
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:

Expand All @@ -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`
Expand All @@ -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).
Expand All @@ -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
Expand Down
26 changes: 22 additions & 4 deletions plugins/minimax/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand Down
71 changes: 71 additions & 0 deletions plugins/minimax/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" })
Expand Down
Loading