diff --git a/packages/server/src/services/quota-fetcher/providers/minimax.ts b/packages/server/src/services/quota-fetcher/providers/minimax.ts index 21e0f05810..ffcff7267b 100644 --- a/packages/server/src/services/quota-fetcher/providers/minimax.ts +++ b/packages/server/src/services/quota-fetcher/providers/minimax.ts @@ -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[] = []; diff --git a/packages/server/src/services/quota-fetcher/service.test.ts b/packages/server/src/services/quota-fetcher/service.test.ts index 2c254678fd..a7adb37d3e 100644 --- a/packages/server/src/services/quota-fetcher/service.test.ts +++ b/packages/server/src/services/quota-fetcher/service.test.ts @@ -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(