Skip to content
Merged
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
33 changes: 33 additions & 0 deletions apps/web/src/lib/coding-plans/minimax-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,39 @@ describe('MiniMax managed usage transport', () => {
});
});

it.each([
['null', null],
['empty', []],
])(
'reports an inactive provider plan when MiniMax returns %s quota rows',
async (_label, rows) => {
jest.spyOn(global, 'fetch').mockResolvedValue(
jsonResponse({
base_resp: { status_code: 0, status_msg: 'provider message' },
model_remains: rows,
})
);

await expect(getMiniMaxUsage(API_KEY)).rejects.toMatchObject({
code: 'provider_plan_inactive',
message: 'Coding Plan usage is temporarily unavailable.',
});
}
);

it('rejects a zero-status response that omits the quota rows as invalid', async () => {
jest
.spyOn(global, 'fetch')
.mockResolvedValue(
jsonResponse({ base_resp: { status_code: 0, status_msg: 'provider message' } })
);

await expect(getMiniMaxUsage(API_KEY)).rejects.toMatchObject({
code: 'invalid_response',
message: 'Coding Plan usage is temporarily unavailable.',
});
});

it.each([
[
'missing aggregate pool',
Expand Down
18 changes: 13 additions & 5 deletions apps/web/src/lib/coding-plans/minimax-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ const MiniMaxModelRemainsSchema = z.object({
current_weekly_status: NativeIntegerSchema.optional(),
});

// `model_remains` is optional so a non-zero application status without quota
// rows still parses and maps to an `application` failure instead of
// `invalid_response`.
// MiniMax returns `model_remains: null` with a zero status code when it holds
// no quota rows for the managed key. The field stays optional so a non-zero
// application status without rows still maps to `application`; a zero status
// with the field absent is a response-shape problem and is rejected below.
const MiniMaxUsageResponseSchema = z.object({
base_resp: z.object({
status_code: NativeIntegerSchema,
}),
model_remains: z.array(MiniMaxModelRemainsSchema).max(64).optional(),
model_remains: z.array(MiniMaxModelRemainsSchema).max(64).nullish(),
Comment thread
iscekic marked this conversation as resolved.
});

type MiniMaxModelRemains = z.infer<typeof MiniMaxModelRemainsSchema>;
Expand Down Expand Up @@ -133,8 +134,15 @@ export async function getMiniMaxUsage(apiKey: string) {
if (result.data.base_resp.status_code !== 0) {
throw new CodingPlanUsageError('application');
}
const rows = result.data.model_remains;
if (rows === undefined) {
throw new CodingPlanUsageError('invalid_response');
}
if (rows === null || rows.length === 0) {
throw new CodingPlanUsageError('provider_plan_inactive');
}
return {
fetchedAt: new Date().toISOString(),
windows: normalizeUsage(result.data.model_remains ?? []),
windows: normalizeUsage(rows),
};
}
3 changes: 2 additions & 1 deletion apps/web/src/lib/coding-plans/usage-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export type CodingPlanUsageErrorCode =
| 'timeout'
| 'http'
| 'invalid_response'
| 'application';
| 'application'
| 'provider_plan_inactive';

// Thrown by provider usage adapters. The message is a safe generic user-facing
// string; `code` is a non-secret failure category for logging.
Expand Down