Show MiniMax usage as unavailable when the account has no token plan (#5258)

### Linked issue

Closes #5247

### Type of change

- [x] Bug fix
- [ ] New feature
- [ ] Enhancement
- [ ] Refactor
- [ ] Docs

### Reasoning

A user whose MiniMax account has no active token plan opened the Usage page and saw a red **Error** badge on the MiniMax card with a block of raw JSON as its body: `[{"expected":"array","code":"invalid_type","path":["model_remains"],...}]`.

MiniMax reports application-level failures inside an HTTP 200 body — `base_resp.status_code` is non-zero and `model_remains` comes back as `null`. Two things went wrong with that response:

- `MiniMaxQuotaResponseSchema` declared `model_remains` as `z.array(...).optional()`, which admits a *missing* key but not `null`, so `.parse()` threw on a perfectly ordinary no-subscription response.
- `fetchUsage()` only checked `res.ok`, so an API-level error returned with HTTP 200 was never inspected in the first place.

The rejection propagated to `ProviderUsageService`, which stores `reason.message` as the card's error text. For a `ZodError`, that message *is* the serialized issue list — which is how the JSON blob reached the card.

This accepts `null` for `model_remains`, parses `base_resp`, and returns the plain unavailable result when the status code is non-zero, matching how a failed HTTP response is already handled in the same method. The API's own `status_msg` goes to the debug log rather than to the card: an account without a token plan is a normal state, not an error, so it should read as "Unavailable" like an unconfigured provider does — not as a red Error.

### Goals

- A MiniMax account with no active token plan renders as **Unavailable** with no error text.
- `model_remains: null` is accepted wherever it appears, with or without a `base_resp`.
- Application-level errors carried in an HTTP 200 body are detected rather than treated as success.
- Accounts that do have a plan keep rendering their interval and weekly windows unchanged.

### Non-goals

- **Generic error rendering in the Usage card.** The issue also asks that structured errors (ZodError and friends) be mapped to a concise message in the UI. That changes what every provider's card shows on schema drift, so it is a design decision for the maintainer rather than part of this fix. With the producer corrected, no ZodError arises on this path; if another provider's response shape drifts, raw issue JSON can still reach the card. Left open for the maintainer.
- Surfacing MiniMax's `status_msg` in the UI. It is logged at debug instead, so the card does not turn red for a normal no-subscription account.

### QA

Reproduced and verified through the real `ProviderUsageService` with the exact payload from the report, in `packages/server/src/services/quota-fetcher/service.test.ts`.

**Before** — `reports MiniMax accounts with no active token plan as unavailable` fails, and the failure output is the reported symptom verbatim:

```
- Expected                          + Received
-   "status": "unavailable",        +   "status": "error",
-   "error": null,                  +   "error": "[
                                    +   {
                                    +     \"expected\": \"array\",
                                    +     \"code\": \"invalid_type\",
                                    +     \"path\": [ \"model_remains\" ],
                                    +     \"message\": \"Invalid input: expected array, received null\"
                                    +   }
                                    + ]",
```

**After** — the full quota-fetcher suite passes: `Tests 71 passed (71)`.

Three tests were added, two of which are red without the provider change (confirmed by stashing only `minimax.ts`):

- `reports MiniMax accounts with no active token plan as unavailable` — the reported response (`model_remains: null` + `base_resp` 2062) → `status: "unavailable"`, `error: null`.
- `reports a null MiniMax token plan as unavailable even without base_resp` — `model_remains: null` alone.
- `still reads MiniMax windows when base_resp reports success` — guards against the new status check rejecting healthy accounts.

Also run locally: `npm run typecheck --workspace=@getpaseo/server`, `npx oxlint packages/server/src/services/quota-fetcher/`, `npx oxfmt --check packages/server/src/services/quota-fetcher/` — all clean.

No UI change, so no screenshots: with the provider fixed, the card takes its existing "Unavailable" rendering, the same one an unconfigured provider already gets.

### Checklist

- [x] Plugin changes follow the SDK import boundaries (if applicable)
- [x] One focused change
- [x] `npm run typecheck` passes
- [x] `npm run lint` passes
- [x] `npm run format` passes
- [x] QA evidence
- [x] Tests added or updated where it made sense

## Supersedes

- [PR #5252 — fix(quota): handle MiniMax accounts with no active token plan (#5247)](https://github.com/getpaseo/paseo/pull/5252) — Same diagnosis and same reported workflow. That PR returns `status: "error"` carrying MiniMax's `status_msg`, which keeps the red Error badge on the card for an account that is simply unsubscribed; this branch returns the plain unavailable result the issue asks for. It also adds cross-provider `ZodError` summarizing in `ProviderUsageService`, which is the design decision left as a non-goal above.

Co-authored-by: Matteo Pietro Dazzi <ilteoood@gmail.com>
This commit is contained in:
paseo-bot[bot]
2026-09-23 12:46:21 +02:00
committed by GitHub
co-authored by Matteo Pietro Dazzi
parent 6d37f7fd9f
commit 3054f80050
2 changed files with 91 additions and 1 deletions
@@ -37,8 +37,19 @@ const MiniMaxModelRemainSchema = z.object({
weekly_boost_permille: ApiNumberSchema.optional(),
});
/**
* MiniMax reports application-level failures inside a 200 response: `base_resp.status_code`
* is non-zero and `model_remains` comes back as null. An account with no token plan
* subscription is the common case (status 2062), so both shapes are normal input here.
*/
const MiniMaxBaseRespSchema = z.object({
status_code: ApiNumberSchema.optional(),
status_msg: ApiOptionalStringSchema,
});
const MiniMaxQuotaResponseSchema = z.object({
model_remains: z.array(MiniMaxModelRemainSchema).optional(),
model_remains: z.array(MiniMaxModelRemainSchema).nullish(),
base_resp: MiniMaxBaseRespSchema.nullish(),
});
const MiniMaxCredentialsSchema = z.object({
@@ -181,6 +192,16 @@ export class MiniMaxQuotaProvider implements ProviderUsageFetcher {
}
const resp = MiniMaxQuotaResponseSchema.parse(await res.json());
const statusCode = resp.base_resp?.status_code;
if (typeof statusCode === "number" && statusCode !== 0) {
this.logger.debug(
{ statusCode, statusMsg: resp.base_resp?.status_msg },
"MiniMax usage unavailable",
);
return unavailableUsage(this);
}
const models = resp.model_remains ?? [];
const windows: ProviderUsageWindow[] = [];
@@ -1265,6 +1265,75 @@ describe("real provider usage fetchers", () => {
expect(miniMax.status).toBe("unavailable");
});
it("reports MiniMax accounts with no active token plan as unavailable", async () => {
writeMiniMaxConfig(homeDir, { api_key: "minimax_config_key", region: "cn" });
fetchApi = mockFetch(
new Map([
[
"https://api.minimaxi.com/v1/token_plan/remains",
() =>
jsonResponse({
model_remains: null,
base_resp: { status_code: 2062, status_msg: "no active token plan subscription" },
}),
],
]),
);
const miniMax = findProvider(await service().listUsage(), "minimax");
expect(miniMax).toMatchObject({ status: "unavailable", windows: [], error: null });
});
it("reports a null MiniMax token plan as unavailable even without base_resp", async () => {
process.env["MINIMAX_API_KEY"] = "minimax_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.minimax.io/v1/token_plan/remains",
() => jsonResponse({ model_remains: null }),
],
]),
);
const miniMax = findProvider(await service().listUsage(), "minimax");
expect(miniMax).toMatchObject({ status: "unavailable", windows: [], error: null });
});
it("still reads MiniMax windows when base_resp reports success", async () => {
process.env["MINIMAX_API_KEY"] = "minimax_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.minimax.io/v1/token_plan/remains",
() =>
jsonResponse({
base_resp: { status_code: 0, status_msg: "success" },
model_remains: [
{
model_name: "MiniMax-M2.7",
end_time: Date.parse("2026-06-19T05:00:00.000Z"),
current_interval_total_count: 100,
current_interval_usage_count: 25,
current_interval_remaining_percent: 75,
},
],
}),
],
]),
);
const miniMax = findProvider(await service().listUsage(), "minimax");
expect(miniMax).toMatchObject({
status: "available",
windows: expect.arrayContaining([
expect.objectContaining({ id: "interval_MiniMax-M2.7", usedPct: 25 }),
]),
});
});
it("marks exhausted MiniMax interval windows with a danger tone", async () => {
process.env["MINIMAX_API_KEY"] = "minimax_test_token";
fetchApi = mockFetch(