fix(usage): improve MiniMax coding-plan usage parsing for model_remains array#52349
Conversation
Greptile SummaryThis PR fixes a real user-facing bug (#52335) where the MiniMax coding-plan quota display showed the wrong window duration ( Key changes:
Issues found:
Confidence Score: 4/5
|
| const name = typeof r.model_name === "string" ? r.model_name : ""; | ||
| const total = parseFiniteNumber(r.current_interval_total_count); | ||
| return ( | ||
| (name.toLowerCase().startsWith("minimax-m") || name === "MiniMax-M*") && |
There was a problem hiding this comment.
Redundant condition in model name check
name === "MiniMax-M*" is already covered by name.toLowerCase().startsWith("minimax-m") since "MiniMax-M*".toLowerCase() is "minimax-m*", which starts with "minimax-m". The || branch is dead code and could be confusing to future readers.
| (name.toLowerCase().startsWith("minimax-m") || name === "MiniMax-M*") && | |
| (name.toLowerCase().startsWith("minimax-m")) && |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/provider-usage.fetch.minimax.ts
Line: 338
Comment:
**Redundant condition in model name check**
`name === "MiniMax-M*"` is already covered by `name.toLowerCase().startsWith("minimax-m")` since `"MiniMax-M*".toLowerCase()` is `"minimax-m*"`, which starts with `"minimax-m"`. The `||` branch is dead code and could be confusing to future readers.
```suggestion
(name.toLowerCase().startsWith("minimax-m")) &&
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Thanks for the catch! This has been fixed in the follow-up commit (f1935c2) — removed the redundant name === "MiniMax-M*" branch, now only using name.toLowerCase().startsWith("minimax-m").
| function deriveWindowLabelFromTimestamps(record: Record<string, unknown>): string | undefined { | ||
| const startTime = parseEpoch(record.start_time ?? record.startTime); | ||
| const endTime = parseEpoch(record.end_time ?? record.endTime); | ||
| if (startTime && endTime && endTime > startTime) { |
There was a problem hiding this comment.
if (startTime && endTime && ...) treats startTime === 0 (Unix epoch) as a missing value. While a real MiniMax window starting at January 1 1970 is practically impossible, using an explicit !== undefined guard is a bit safer and clearer:
| if (startTime && endTime && endTime > startTime) { | |
| if (startTime !== undefined && endTime !== undefined && endTime > startTime) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/provider-usage.fetch.minimax.ts
Line: 264
Comment:
**Falsy guard rejects epoch 0**
`if (startTime && endTime && ...)` treats `startTime === 0` (Unix epoch) as a missing value. While a real MiniMax window starting at January 1 1970 is practically impossible, using an explicit `!== undefined` guard is a bit safer and clearer:
```suggestion
if (startTime !== undefined && endTime !== undefined && endTime > startTime) {
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Good point — fixed in f1935c2. Now using startTime !== undefined && endTime !== undefined instead of the falsy guard.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 246c0df707
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const plan = | ||
| pickString(usageRecord, PLAN_KEYS) ?? | ||
| pickString(payload, PLAN_KEYS) ?? | ||
| (modelName ? `Coding Plan · ${modelName}` : undefined); |
There was a problem hiding this comment.
Append model name even when MiniMax returns
plan_name
For coding_plan/remains payloads that include both model_remains and the usual top-level plan_name/plan fields, this nullish-coalescing chain stops at the existing plan label and never adds modelName. The repo’s own MiniMax fixtures already cover that shape in src/infra/provider-usage.test.ts:180-192, so the output stays as the ambiguous Coding Plan instead of the intended Coding Plan · MiniMax-M* for a common response format.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid observation. In the current real-world MiniMax coding-plan API response, the plan_name/plan field is not present at the top level — the plan info is only derivable from model_remains[].model_name. However, if MiniMax adds an explicit plan_name in the future, this would indeed shadow the model-specific label. I'll keep this in mind for a follow-up if the API shape evolves. For now, the current behavior correctly handles the known response format.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1935c2ce7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const usageSource = chatRemains ?? payload; | ||
| const candidates = collectUsageCandidates(usageSource); |
There was a problem hiding this comment.
Preserve payload-level fallback for hybrid MiniMax responses
When model_remains is present, this narrows candidate discovery to chatRemains and stops scanning the rest of the payload. If MiniMax returns a hybrid shape where the selected chat entry only carries model metadata/timestamps but the actual quota fields still live in a sibling object (for example a top-level usage block), usedPercent now stays null and we return Unsupported response shape, whereas the previous collectUsageCandidates(payload) path would still parse it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. The current MiniMax coding-plan API puts all quota fields (current_interval_usage_count, current_interval_total_count, start_time, end_time) directly inside each model_remains[] entry, so chatRemains is self-contained. If MiniMax introduces a hybrid shape with quota fields split between model_remains and a sibling object, a broader fallback would be needed. The legacy collectUsageCandidates(payload) path is still reachable when model_remains is absent, so backward compatibility is preserved.
| @@ -371,18 +425,26 @@ export async function fetchMinimaxUsage( | |||
| parseEpoch(pickNumber(usageRecord, RESET_KEYS)) ?? | |||
| parseEpoch(pickString(payload, RESET_KEYS)) ?? | |||
| parseEpoch(pickNumber(payload, RESET_KEYS)); | |||
| const windowLabel = chatRemains ? deriveWindowLabel(chatRemains) : deriveWindowLabel(usageRecord); | |||
There was a problem hiding this comment.
Derive the window label from the chosen usage record
collectUsageCandidates(usageSource) can still pick a nested child inside the selected model_remains entry as usageRecord, but this branch always derives the label from the outer chatRemains object whenever model_remains exists. For responses where the nested child carries window_hours/window_minutes and the parent only carries model metadata, the UI regresses to the default 5h label instead of the actual window even though the correct usage record was found.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is a fair trade-off. In the follow-up commit (f1935c2), I changed the logic to use `chatRemains` directly for `deriveWindowLabel` when it's available, since the timestamps (`start_time`/`end_time`) live at the `model_remains[]` entry level. If a future API revision moves `window_hours`/`window_minutes` into a nested child, the fallback chain in `deriveWindowLabel` (explicit hours → explicit minutes → timestamps → default "5h") would still produce the correct result as long as the timestamps remain on `chatRemains`.
|
Hi maintainers 👋 Just a friendly ping — this PR fixes a real user-facing issue where the MiniMax coding-plan usage tracker reports "0% left" despite the account having full balance (see #52335 for the full investigation). All CI checks pass (the only failing check is Happy to make any further changes if needed. Thanks for your time! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e96a308294
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const startTime = parseEpoch(record.start_time ?? record.startTime); | ||
| const endTime = parseEpoch(record.end_time ?? record.endTime); |
There was a problem hiding this comment.
Handle epoch-string timestamps when deriving the window label
deriveWindowLabelFromTimestamps() now reads start_time/end_time straight through parseEpoch(), but parseEpoch() only understands numeric values or ISO-like date strings. A MiniMax payload that serializes those epochs as strings (for example "1736217600") will therefore skip the new timestamp logic and fall back to the hard-coded "5h", even though the actual window is present. The surrounding reset parsing already goes through pickNumber(...) to accept numeric strings, so this new path is inconsistent with the rest of fetchMinimaxUsage() for that common JSON shape.
Useful? React with 👍 / 👎.
|
Quick update: after the branch update, the previous Discord |
|
Maintainer review: this is not obviously stale, but it does not fix the current live blocker behind #52335. What I rechecked on current
So today the failure happens before this PR's parsing path runs. Recommendation:
|
…ns array - Pick the chat model entry (MiniMax-M*) from model_remains instead of using the first BFS candidate, which could be a speech/video/image model with total_count=0. - Derive window label from start_time/end_time timestamps when window_hours/window_minutes fields are absent; fixes the hardcoded 5h default for 4h windows. - Include model name in plan label so users can distinguish free-tier coding-plan quota from paid API balance. Closes openclaw#52335
- Remove redundant name === 'MiniMax-M*' condition (already matched by startsWith) - Use !== undefined guard instead of falsy check in deriveWindowLabelFromTimestamps - Pass chatRemains directly to deriveWindowLabel when available - Remove JSDoc comment style to match codebase conventions
e96a308 to
5742f85
Compare
|
Landed via temp rebase onto Gate:
Land commit: 5742f85 Thanks @IVY-AI-gif! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5742f850d7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return records.find((r) => { | ||
| const total = parseFiniteNumber(r.current_interval_total_count); | ||
| return total !== undefined && total > 0; |
There was a problem hiding this comment.
Keep scanning model_remains when fallback row lacks usage
The fallback here picks the first model_remains record with current_interval_total_count > 0, even if that row has no usable usage fields. In fetchMinimaxUsage, that chosen row becomes usageSource, so later model_remains rows are never evaluated; if a later row contains the actual quota counters, the function now returns Unsupported response shape instead of producing usage data. This is a regression from the previous payload-wide scan for responses that omit minimax-m* rows or place incomplete rows first.
Useful? React with 👍 / 👎.
Summary
Fixes #52335
The MiniMax
coding_plan/remainsAPI returns amodel_remainsarray containing usage entries for multiple model categories (chat, speech, video, image). The existing BFS-based candidate selection picks the first record with a non-null usage percentage, which can cause two problems:Wrong window duration: The window label defaults to
"5h"when the actual window duration (derived fromstart_time/end_timetimestamps) is 4 hours. Users see"5h: 0% left"when the real window is 4h.Irrelevant model shadowing: Models like
speech-hd,image-01, and video models havecurrent_interval_total_count: 0, which makesderiveUsedPercentreturnnullfor them. While the current code happens to skip these, the selection is fragile — any future model entry with non-zero total but different semantics could shadow the chat model's budget.No plan distinction: The display shows
"MiniMax: 5h: 0% left"without indicating this is the free-tier coding plan quota. Users confuse this with their actual paid API balance (which may be full), leading to bug reports like [Bug]: minimax-portal OAuth usage tracker reports "0% left" despite full MiniMax balance — blocks Discord but CLI works #52335.Changes
pickChatModelRemains()— New helper that selects the chat-model entry (MiniMax-M*or any entry starting withminimax-m) from themodel_remainsarray, filtering out speech/video/image models withtotal_count === 0.deriveWindowLabelFromTimestamps()— New helper that calculates window duration fromstart_time/end_timeepoch fields when explicitwindow_hours/window_minutesare absent. Falls back to the existing"5h"default only when timestamps are also missing.deriveWindowLabel()— Updated to callderiveWindowLabelFromTimestamps()before falling back to the"5h"default.fetchMinimaxUsage()— Updated to detect themodel_remainsarray structure, pick the relevant chat-model entry viapickChatModelRemains(), and pass only that entry to the candidate-scoring pipeline. Also includes the model name in the plan label (e.g.,"Coding Plan · MiniMax-M*").Root Cause Analysis
Tested against a live MiniMax OAuth (
minimax-portal) deployment:{ "model_remains": [ { "model_name": "MiniMax-M*", "current_interval_total_count": 600, "current_interval_usage_count": 595, "start_time": 1774180800000, "end_time": 1774195200000 }, { "model_name": "speech-hd", "current_interval_total_count": 0, ... }, { "model_name": "image-01", "current_interval_total_count": 0, ... } ] }"5h"(hardcoded default)"0% left"after rounding"MiniMax 5h: 0% left"and think their paid balance is zeroTest Plan
deriveWindowLabelFromTimestampscorrectly calculates"4h"from the MiniMax timestampspickChatModelRemainsselects theMiniMax-M*entry and skipsspeech-hd,image-01etc."Coding Plan · MiniMax-M*"model_remains(e.g., legacy API format) still work via the existing candidate pipeline