Skip to content
Open
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
13 changes: 10 additions & 3 deletions docs/providers/minimax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- 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 +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
Expand Down
115 changes: 77 additions & 38 deletions plugins/minimax/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,21 +244,81 @@

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

const total = readNumber(chosen.current_interval_total_count ?? chosen.currentIntervalTotalCount)
if (total === null || total <= 0) return null
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)
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 (!hasDisplayableCount) {
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(
Expand Down Expand Up @@ -292,35 +352,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,
Expand Down Expand Up @@ -367,12 +398,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

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