Skip to content

Commit cd1435c

Browse files
committed
fix(quota): show "not in plan" instead of misleading 100%, map code 2056 (#173)
A Token Plan that does not bundle a model (e.g. video) makes the server return that model's row as 0/0 with remaining_percent=100, marked current_*_status=3. The table trusted the percent and rendered a misleading "100%", while `video generate` was rejected by the server with code 2056 — a contradiction reported in #173. - quota-table: when current_interval_status / current_weekly_status is 3, render "not in plan" / "不在当前套餐中" instead of the bar+percent. Status is the only reliable signal: in-plan time-based rows are also 0/0 but report status=1. - errors/api: fold code 2056 into the existing quota branch so the user gets a QUOTA exit code with a check-usage / upgrade hint. Builds on #168, which already added the current_*_status fields to QuotaModelRemain; this change consumes them.
1 parent a0d165b commit cd1435c

4 files changed

Lines changed: 62 additions & 4 deletions

File tree

src/errors/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ export function mapApiError(status: number, body: ApiErrorBody, url?: string): C
6969
);
7070
}
7171

72-
// MiniMax insufficient quota
73-
if (apiCode === 1028 || apiCode === 1030) {
72+
// MiniMax insufficient quota / weekly usage limit (2056)
73+
if (apiCode === 1028 || apiCode === 1030 || apiCode === 2056) {
7474
const hint = planHintForUrl(url);
7575
return new CLIError(
7676
`Quota exhausted. ${apiMsg}`,

src/output/quota-table.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ interface Labels {
3030
resetsIn: string;
3131
noData: string;
3232
now: string;
33+
notInPlan: string;
3334
}
3435

35-
const LABELS_EN: Labels = { dashboard: 'TokenPlan Quota', week: 'Week', current: 'Left', weekly: 'Wk left', resetsIn: 'Reset', noData: 'No quota data available.', now: 'now' };
36-
const LABELS_CN: Labels = { dashboard: 'TokenPlan 配额面板', week: '周期', current: '剩余', weekly: '周剩余', resetsIn: '重置', noData: '暂无配额数据', now: '即将' };
36+
const LABELS_EN: Labels = { dashboard: 'TokenPlan Quota', week: 'Week', current: 'Left', weekly: 'Wk left', resetsIn: 'Reset', noData: 'No quota data available.', now: 'now', notInPlan: 'not in plan' };
37+
const LABELS_CN: Labels = { dashboard: 'TokenPlan 配额面板', week: '周期', current: '剩余', weekly: '周剩余', resetsIn: '重置', noData: '暂无配额数据', now: '即将', notInPlan: '不在当前套餐中' };
3738

3839
const MODEL_NAME_CN: Record<string, string> = {
3940
'general': '通用',
@@ -98,13 +99,22 @@ function renderBar(remainingPct: number, color: boolean, barWidth: number = BAR_
9899
return showPct ? `${bar} ${fg}${B}${pctStr}${R}` : bar;
99100
}
100101

102+
// Server status for a model not bundled in the plan: counts read 0/0 with
103+
// percent 100, so only status distinguishes it from an in-plan 0/0 row (=1).
104+
const STATUS_NOT_IN_PLAN = 3;
105+
101106
function renderMetric(
102107
label: string,
103108
remaining: number,
104109
total: number,
105110
percent: number | undefined | null,
106111
color: boolean,
112+
status: number | undefined | null,
113+
notInPlanLabel: string,
107114
): string {
115+
if (status === STATUS_NOT_IN_PLAN) {
116+
return color ? `${D}${label}${R} ${FG_RED}${notInPlanLabel}${R}` : `${label} ${notInPlanLabel}`;
117+
}
108118
const pct = remainingPct(percent, remaining, total);
109119
const bar = renderBar(pct, color, COMPACT_BAR_WIDTH, total <= 0);
110120
if (total > 0) {
@@ -138,13 +148,17 @@ export function renderQuotaTable(models: QuotaModelRemain[], config: Config): vo
138148
m.current_interval_total_count,
139149
m.current_interval_remaining_percent,
140150
useColor,
151+
m.current_interval_status,
152+
L.notInPlan,
141153
);
142154
const weekly = renderMetric(
143155
L.weekly,
144156
m.current_weekly_usage_count,
145157
m.current_weekly_total_count,
146158
m.current_weekly_remaining_percent,
147159
useColor,
160+
m.current_weekly_status,
161+
L.notInPlan,
148162
);
149163
const reset = `${L.resetsIn} ${formatDuration(m.remains_time, L.now)}`;
150164
return { displayName, current, weekly, reset };

test/errors/api.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,17 @@ describe('mapApiError', () => {
4343
const err = mapApiError(500, { base_resp: { status_code: 0, status_msg: 'something broke' } });
4444
expect(err.message).toContain('something broke');
4545
});
46+
47+
it('maps MiniMax usage-limit code 2056 to QUOTA with upgrade hint', () => {
48+
// issue #173: mmx video generate rejected with 2056 / (0/0 used)
49+
const err = mapApiError(
50+
400,
51+
{ base_resp: { status_code: 2056, status_msg: 'weekly usage limit reached (0/0 used)' } },
52+
'https://api.minimaxi.com/v1/video_generation',
53+
);
54+
expect(err.exitCode).toBe(ExitCode.QUOTA);
55+
expect(err.message).toContain('0/0 used');
56+
expect(err.hint).toContain('quota show');
57+
expect(err.hint).toContain('Upgrade plan');
58+
});
4659
});

test/output/quota-table.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,35 @@ describe('renderQuotaTable', () => {
200200
expect(output).toContain('通用');
201201
expect(output).not.toContain('×2');
202202
});
203+
204+
it('renders "not in plan" for status=3 rows instead of a misleading 100%', () => {
205+
// issue #173: a plan without video. The server marks the unavailable row
206+
// status=3 (counts 0/0, percent 100); the in-plan general row stays status=1.
207+
const base = createCodingPlanModels();
208+
const models: QuotaModelRemain[] = [
209+
{ ...base[0]!, current_interval_status: 1, current_weekly_status: 1 },
210+
{ ...base[1]!, current_interval_status: 3, current_weekly_status: 3 },
211+
];
212+
213+
const lines: string[] = [];
214+
const originalLog = console.log;
215+
console.log = (message?: unknown) => {
216+
lines.push(String(message ?? ''));
217+
};
218+
try {
219+
renderQuotaTable(models, { ...createConfig(), region: 'cn', noColor: true });
220+
} finally {
221+
console.log = originalLog;
222+
}
223+
224+
// video (status=3) says "not in plan"; its percent/bar must not leak through
225+
const videoLine = lines.find((l) => l.includes('视频')) ?? '';
226+
expect(videoLine).toContain('不在当前套餐中');
227+
expect(videoLine).not.toContain('100%');
228+
expect(videoLine).not.toContain('[');
229+
// the in-plan general row (status=1) still renders its bar
230+
const generalLine = lines.find((l) => l.includes('通用')) ?? '';
231+
expect(generalLine).not.toContain('不在当前套餐中');
232+
expect(generalLine).toContain('[');
233+
});
203234
});

0 commit comments

Comments
 (0)