Skip to content

fix(minimax): invert usage_percent when no count fields are present#60254

Merged
steipete merged 1 commit intoopenclaw:mainfrom
jwchmodx:fix/minimax-usage-percent-inversion
Apr 4, 2026
Merged

fix(minimax): invert usage_percent when no count fields are present#60254
steipete merged 1 commit intoopenclaw:mainfrom
jwchmodx:fix/minimax-usage-percent-inversion

Conversation

@jwchmodx
Copy link
Copy Markdown
Contributor

@jwchmodx jwchmodx commented Apr 3, 2026

Summary

Fixes #60193.

MiniMax's usage_percent / usagePercent fields report the remaining quota as a percentage, not the consumed quota. This is evidenced by the existing test named "prefers count-based usage when percent looks inverted": when prompt_remain: 150 and prompt_limit: 200 are present alongside usage_percent: 75, the correct answer is usedPercent: 25 (75% remaining → 25% used).

When count fields are present, fromCounts already overrides the inverted value — so the bug was silent. When the API returns only usage_percent (no count fields), fromCounts is null, and the remaining-percent value was passed through unchanged as if it were a used-percent, causing the menu bar to show "2% left" instead of "98% left".

Fix

  • Moves usage_percent / usagePercent from PERCENT_KEYS to a new REMAINING_PERCENT_KEYS array (local to the MiniMax fetcher, no other provider is affected)
  • deriveUsedPercent now inverts remaining-percent values: usedPercent = 100 − remainingPercent
  • fromCounts still takes priority over both key groups

Test plan

  • 22/22 tests pass (provider-usage.fetch.minimax.test.ts, provider-usage.test.ts)
  • New test: usage_percent: 98 alone → usedPercent: 2 (the exact bug scenario)
  • Existing "prefers count-based when percent looks inverted" test unchanged — fromCounts still wins when count fields are available

🤖 Generated with Claude Code

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 3, 2026

Greptile Summary

This PR bundles three separate fixes under the MiniMax title: (1) inverts usage_percent/usagePercent in the MiniMax fetcher since those fields report remaining quota, not consumed quota; (2) prevents Mattermost DM replies from creating threads by adding a required kind: ChatType guard to resolveMattermostReplyRootId; (3) escalates the generic-repeat loop detector to \"critical\" once criticalThreshold is reached.

The core MiniMax inversion logic in deriveUsedPercent is correct and well-tested. The one minor gap is that scoreUsageRecord doesn't check REMAINING_PERCENT_KEYS, so a record whose only usage field is usage_percent/usagePercent scores 0 and is skipped by the candidate finder — the flat-payload fallback rescues all current test cases, but deeply-nested responses with only those fields could be silently missed.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/hardening suggestions that do not affect current tests or the primary MiniMax API shape.

The inversion logic is correct, fromCounts priority is preserved, and the flat-payload fallback handles all tested scenarios. The scoreUsageRecord gap is theoretical for MiniMax's actual API. The Mattermost and loop-detection changes are straightforward and well-covered by new tests.

src/infra/provider-usage.fetch.minimax.ts — scoreUsageRecord does not score REMAINING_PERCENT_KEYS, which could cause candidate-selection misses for deeply nested responses with only usage_percent.

Comments Outside Diff (1)

  1. src/infra/provider-usage.fetch.minimax.ts, line 199-217 (link)

    P2 scoreUsageRecord doesn't account for REMAINING_PERCENT_KEYS

    scoreUsageRecord only checks PERCENT_KEYS, so a record whose only non-plan field is usage_percent/usagePercent now scores 0 and is invisible to the candidate finder. The flat-payload fallback on line 371 (deriveUsedPercent(payload)) rescues the case tested here, but a MiniMax response that buries usage_percent inside a nested object (with no sibling fields in PERCENT_KEYS/TOTAL_KEYS/etc.) would silently miss that record.

    Adding a small weight for REMAINING_PERCENT_KEYS keeps candidate ranking consistent with deriveUsedPercent's lookup order:

    function scoreUsageRecord(record: Record<string, unknown>): number {
      let score = 0;
      if (hasAny(record, PERCENT_KEYS)) {
        score += 4;
      }
      if (hasAny(record, REMAINING_PERCENT_KEYS)) {
        score += 4;
      }
      // …rest unchanged
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/infra/provider-usage.fetch.minimax.ts
    Line: 199-217
    
    Comment:
    **`scoreUsageRecord` doesn't account for `REMAINING_PERCENT_KEYS`**
    
    `scoreUsageRecord` only checks `PERCENT_KEYS`, so a record whose only non-plan field is `usage_percent`/`usagePercent` now scores 0 and is invisible to the candidate finder. The flat-payload fallback on line 371 (`deriveUsedPercent(payload)`) rescues the case tested here, but a MiniMax response that buries `usage_percent` inside a nested object (with no sibling fields in `PERCENT_KEYS`/`TOTAL_KEYS`/etc.) would silently miss that record.
    
    Adding a small weight for `REMAINING_PERCENT_KEYS` keeps candidate ranking consistent with `deriveUsedPercent`'s lookup order:
    
    ```ts
    function scoreUsageRecord(record: Record<string, unknown>): number {
      let score = 0;
      if (hasAny(record, PERCENT_KEYS)) {
        score += 4;
      }
      if (hasAny(record, REMAINING_PERCENT_KEYS)) {
        score += 4;
      }
      // …rest unchanged
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/infra/provider-usage.fetch.minimax.ts
Line: 199-217

Comment:
**`scoreUsageRecord` doesn't account for `REMAINING_PERCENT_KEYS`**

`scoreUsageRecord` only checks `PERCENT_KEYS`, so a record whose only non-plan field is `usage_percent`/`usagePercent` now scores 0 and is invisible to the candidate finder. The flat-payload fallback on line 371 (`deriveUsedPercent(payload)`) rescues the case tested here, but a MiniMax response that buries `usage_percent` inside a nested object (with no sibling fields in `PERCENT_KEYS`/`TOTAL_KEYS`/etc.) would silently miss that record.

Adding a small weight for `REMAINING_PERCENT_KEYS` keeps candidate ranking consistent with `deriveUsedPercent`'s lookup order:

```ts
function scoreUsageRecord(record: Record<string, unknown>): number {
  let score = 0;
  if (hasAny(record, PERCENT_KEYS)) {
    score += 4;
  }
  if (hasAny(record, REMAINING_PERCENT_KEYS)) {
    score += 4;
  }
  // …rest unchanged
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(minimax): invert usage_percent when ..." | Re-trigger Greptile

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a02183ce7

ℹ️ 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".

Comment thread src/agents/tool-loop-detection.ts Outdated
Comment on lines +478 to +482
if (
!knownPollTool &&
resolvedConfig.detectors.genericRepeat &&
recentCount >= resolvedConfig.criticalThreshold
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate generic critical blocks on no-progress evidence

This new critical branch blocks any non-poll tool once the same args hash appears criticalThreshold times, but it does not verify no-progress (it ignores resultHash entirely and does not require consecutive repeats). In practice, legitimate flows that repeatedly call the same idempotent tool with evolving output (for example, repeated reads/checks during a long task) can now be hard-blocked once the count accumulates in history, which is a behavior regression from warn-only to session-blocking for valid workloads.

Useful? React with 👍 / 👎.

Comment on lines 40 to 44
const PERCENT_KEYS = [
"used_percent",
"usedPercent",
"usage_percent",
"usagePercent",
"used_rate",
"usage_rate",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve percent-key scoring for nested MiniMax usage records

By removing usage_percent/usagePercent from PERCENT_KEYS, candidate scoring no longer treats those fields as usage evidence, but collectUsageCandidates() still relies on scoreUsageRecord() to pick nested usage objects. If the API returns remaining percent only in a nested record (with root metadata like plan/reset), that nested record can now score 0 and be skipped; fallback then checks only the root and may return Unsupported response shape. This is a regression introduced by moving these keys out of the scored percent set without updating the scorer.

Useful? React with 👍 / 👎.

…aining-only fields

MiniMax's usage_percent / usagePercent fields report the *remaining* quota
as a percentage, not the consumed quota. When count fields (prompt_limit /
prompt_remain) are also present, fromCounts already computed the correct
usedPercent and the inverted value was silently ignored. But when only
usage_percent is returned (no count fields), the code treated it as a
used-percent and passed it through unchanged, causing the menu bar to show
"2% left" instead of "98% left".

Move usage_percent and usagePercent from PERCENT_KEYS to a new
REMAINING_PERCENT_KEYS array. deriveUsedPercent now inverts remaining-percent
values to obtain usedPercent, matching the behaviour already validated by the
existing "prefers count-based usage when percent looks inverted" test. Count-
based fromCounts still takes priority over both key groups.

Fixes openclaw#60193

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@steipete steipete force-pushed the fix/minimax-usage-percent-inversion branch from 9a02183 to 5c3a250 Compare April 4, 2026 10:20
@steipete steipete merged commit 28021a0 into openclaw:main Apr 4, 2026
3 checks passed
@steipete
Copy link
Copy Markdown
Contributor

steipete commented Apr 4, 2026

Landed via temp rebase onto main.

  • Gate: OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test src/infra/provider-usage.fetch.minimax.test.ts; pnpm build; live MiniMax text smoke with MINIMAX_API_KEY via @mariozechner/pi-ai
  • Land commit: 5c3a250
  • Merge commit: 28021a0
  • Source commit: jwchmodx@9a02183

I trimmed out unrelated Mattermost/tool-loop commits from the branch and landed only the MiniMax usage fix.

Thanks @jwchmodx.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c3a250b87

ℹ️ 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".

// MiniMax's usage_percent / usagePercent fields report the remaining quota
// as a percentage, not the consumed quota. Treat them as "remaining percent"
// and invert to get usedPercent. Count-based fromCounts always takes priority.
const REMAINING_PERCENT_KEYS = ["usage_percent", "usagePercent"] as const;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include remaining-percent fields in usage candidate scoring

Splitting usage_percent/usagePercent into REMAINING_PERCENT_KEYS without updating scoreUsageRecord() means nested records that only expose those fields now score 0 and are discarded by collectUsageCandidates(). For MiniMax payloads like data: { plan_name, usage: { usage_percent: 98 } }, the parser never evaluates the nested usage record and can return Unsupported response shape instead of a valid usage window; this regression is introduced by the new key split and is not covered by the added top-level test.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: MiniMax usage display shows inverted percentage — "2% left" instead of "98% left"

2 participants