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
2 changes: 1 addition & 1 deletion src/client/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function vlmEndpoint(baseUrl: string): string {
export function quotaEndpoint(baseUrl: string): string {
// Quota endpoint uses api subdomain
const host = baseUrl.includes('minimaxi.com') ? 'https://api.minimaxi.com' : 'https://api.minimax.io';
return `${host}/v1/token_plan/remains`;
return `${host}/v1/api/openplatform/coding_plan/remains`;
}

export function fileUploadEndpoint(baseUrl: string): string {
Expand Down
2 changes: 1 addition & 1 deletion src/config/detect-region.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { REGIONS, type Region } from "./schema";
import { readConfigFile, writeConfigFile } from "./loader";

const QUOTA_PATH = "/v1/token_plan/remains";
const QUOTA_PATH = "/v1/api/openplatform/coding_plan/remains";

function quotaUrl(region: Region): string {
return REGIONS[region] + QUOTA_PATH;
Expand Down
130 changes: 88 additions & 42 deletions src/output/quota-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,34 @@ const BG_YELLOW = '\x1b[48;2;202;138;4m';
const BG_RED = '\x1b[48;2;220;38;38m';
const BG_EMPTY = '\x1b[48;2;55;65;81m';

function usageColors(usedPct: number): [string, string] {
if (usedPct < 50) return [FG_GREEN, BG_GREEN];
if (usedPct <= 80) return [FG_YELLOW, BG_YELLOW];
function remainingColors(remainingPct: number): [string, string] {
if (remainingPct >= 50) return [FG_GREEN, BG_GREEN];
if (remainingPct >= 20) return [FG_YELLOW, BG_YELLOW];
return [FG_RED, BG_RED];
}

interface Labels {
dashboard: string;
week: string;
current: string;
weekly: string;
resetsIn: string;
noData: string;
now: string;
}

const LABELS_EN: Labels = { dashboard: 'TokenPlan Quota', week: 'Week', weekly: 'Weekly', resetsIn: 'Resets in', noData: 'No quota data available.', now: 'now' };
const LABELS_CN: Labels = { dashboard: 'TokenPlan 配额面板', week: '周期', weekly: '每周', resetsIn: '重置于', noData: '暂无配额数据', now: '即将' };
const LABELS_EN: Labels = { dashboard: 'TokenPlan Quota', week: 'Week', current: 'Left', weekly: 'Wk left', resetsIn: 'Reset', noData: 'No quota data available.', now: 'now' };
const LABELS_CN: Labels = { dashboard: 'TokenPlan 配额面板', week: '周期', current: '剩余', weekly: '周剩余', resetsIn: '重置', noData: '暂无配额数据', now: '即将' };

const MODEL_NAME_CN: Record<string, string> = {
'general': '通用',
'video': '视频',
};

function displayModelName(name: string, region: string): string {
if (region !== 'cn') return name;
return MODEL_NAME_CN[name] ?? name;
}

function formatDuration(ms: number, nowLabel: string): string {
if (ms <= 0) return nowLabel;
Expand Down Expand Up @@ -60,15 +71,47 @@ function displayWidth(s: string): number {
}

const BAR_WIDTH = 16;
const COMPACT_BAR_WIDTH = 10;

function renderBar(usedPct: number, color: boolean): string {
const ratio = Math.max(0, Math.min(100, usedPct)) / 100;
const filled = Math.round(BAR_WIDTH * ratio);
const empty = BAR_WIDTH - filled;
const pctStr = `${usedPct}%`.padStart(4);
if (!color) return `[${'█'.repeat(filled)}${'.'.repeat(empty)}] ${pctStr}`;
const [fg, bg] = usageColors(usedPct);
return `${bg}${' '.repeat(filled)}${R}${BG_EMPTY}${' '.repeat(empty)}${R} ${fg}${B}${pctStr}${R}`;
function clampPct(value: number): number {
return Math.max(0, Math.min(100, Math.round(value)));
}

function remainingPct(percent: number | undefined | null, remaining: number, total: number): number {
return percent !== undefined && percent !== null
? clampPct(percent)
: total > 0 ? clampPct((remaining / total) * 100) : 0;
}

function renderBar(remainingPct: number, color: boolean, barWidth: number = BAR_WIDTH, showPct: boolean = true): string {
const pct = clampPct(remainingPct);
const ratio = pct / 100;
const filled = Math.round(barWidth * ratio);
const empty = barWidth - filled;
const pctStr = `${pct}%`.padStart(4);
if (!color) {
const bar = `[${'█'.repeat(filled)}${'.'.repeat(empty)}]`;
return showPct ? `${bar} ${pctStr}` : bar;
}
const [fg, bg] = remainingColors(pct);
const bar = `${bg}${' '.repeat(filled)}${R}${BG_EMPTY}${' '.repeat(empty)}${R}`;
return showPct ? `${bar} ${fg}${B}${pctStr}${R}` : bar;
}

function renderMetric(
label: string,
remaining: number,
total: number,
percent: number | undefined | null,
color: boolean,
): string {
const pct = remainingPct(percent, remaining, total);
const bar = renderBar(pct, color, COMPACT_BAR_WIDTH, total <= 0);
if (total > 0) {
const count = `${remaining.toLocaleString()} / ${total.toLocaleString()}`;
return color ? `${D}${label}${R} ${bar} ${remainingColors(pct)[0]}${count}${R}` : `${label} ${bar} ${count}`;
}
return `${label} ${bar}`;
}

function boxLine(w: number, l: string, f: string, r: string, c: boolean): string {
Expand All @@ -84,9 +127,31 @@ export function renderQuotaTable(models: QuotaModelRemain[], config: Config): vo
const useColor = !config.noColor && process.stdout.isTTY === true;
const L = config.region === 'cn' ? LABELS_CN : LABELS_EN;

const maxNameLen = models.length > 0 ? Math.max(...models.map(m => m.model_name.length)) : 16;
const barVisLen = useColor ? BAR_WIDTH + 5 : BAR_WIDTH + 7;
const W = Math.max(68, maxNameLen + 2 + 15 + 2 + barVisLen + 2);
const rows = models.map((m) => {
const displayName = displayModelName(m.model_name, config.region);
const current = renderMetric(
L.current,
m.current_interval_usage_count,
m.current_interval_total_count,
m.current_interval_remaining_percent,
useColor,
);
const weekly = renderMetric(
L.weekly,
m.current_weekly_usage_count,
m.current_weekly_total_count,
m.current_weekly_remaining_percent,
useColor,
);
const reset = `${L.resetsIn} ${formatDuration(m.remains_time, L.now)}`;
return { displayName, current, weekly, reset };
});

const nameWidth = Math.max(6, ...rows.map(r => displayWidth(r.displayName)));
const currentWidth = Math.max(...rows.map(r => displayWidth(r.current)), 18);
const weeklyWidth = Math.max(...rows.map(r => displayWidth(r.weekly)), 18);
const resetWidth = Math.max(...rows.map(r => displayWidth(r.reset)), 10);
const W = Math.max(72, nameWidth + 2 + currentWidth + 2 + weeklyWidth + 2 + resetWidth + 4);

const weekRange = models.length > 0
? `${formatDate(models[0]!.weekly_start_time)} — ${formatDate(models[0]!.weekly_end_time)}`
Expand All @@ -110,34 +175,15 @@ export function renderQuotaTable(models: QuotaModelRemain[], config: Config): vo
return;
}

for (const m of models) {
for (const row of rows) {
console.log(boxLine(W, '├', '─', '┤', useColor));

const used = m.current_interval_usage_count;
const limit = m.current_interval_total_count;
const usedPct = limit > 0 ? Math.round((used / limit) * 100) : 0;
const weekUsed = m.current_weekly_usage_count;
const weekLimit = m.current_weekly_total_count;
const resets = formatDuration(m.remains_time, L.now);

const nameStr = m.model_name.padEnd(maxNameLen);
const usageFrac = `${used.toLocaleString()} / ${limit.toLocaleString()}`;
const bar = renderBar(usedPct, useColor);
const line1VisLen = maxNameLen + 2 + 15 + 2 + barVisLen;

const line1 = useColor
? `${B}${nameStr}${R} ${usageColors(usedPct)[0]}${usageFrac.padStart(15)}${R} ${bar}`
: `${nameStr} ${usageFrac.padStart(15)} ${renderBar(usedPct, false)}`;
console.log(boxRow(line1, W, line1VisLen, useColor));

const subLeft = `└ ${L.weekly} ${weekUsed.toLocaleString()} / ${weekLimit.toLocaleString()}`;
const subRight = `${L.resetsIn} ${resets}`;
const subGap = Math.max(2, (W - 2) - 2 - displayWidth(subLeft) - displayWidth(subRight));
const subVisLen = 2 + displayWidth(subLeft) + subGap + displayWidth(subRight);
const sub = useColor
? ` ${D}${subLeft}${' '.repeat(subGap)}${subRight}${R}`
: ` ${subLeft}${' '.repeat(subGap)}${subRight}`;
console.log(boxRow(sub, W, subVisLen, useColor));
const name = useColor ? `${B}${row.displayName}${R}` : row.displayName;
const line = `${name}${' '.repeat(Math.max(1, nameWidth - displayWidth(row.displayName) + 2))}` +
`${row.current}${' '.repeat(Math.max(1, currentWidth - displayWidth(row.current) + 2))}` +
`${row.weekly}${' '.repeat(Math.max(1, weeklyWidth - displayWidth(row.weekly) + 2))}` +
row.reset;
console.log(boxRow(line, W, displayWidth(line), useColor));
}

console.log(boxLine(W, '╰', '─', '╯', useColor));
Expand Down
2 changes: 2 additions & 0 deletions src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,10 @@ export interface QuotaModelRemain {
remains_time: number;
current_interval_total_count: number;
current_interval_usage_count: number;
current_interval_remaining_percent?: number;
current_weekly_total_count: number;
current_weekly_usage_count: number;
current_weekly_remaining_percent?: number;
weekly_start_time: number;
weekly_end_time: number;
weekly_remains_time: number;
Expand Down
6 changes: 3 additions & 3 deletions test/auth/timeout-fix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('detect-region: probeRegion auth style fallback', () => {
it('succeeds when endpoint only accepts Bearer token', async () => {
server = createMockServer({
routes: {
'/v1/token_plan/remains': (req) => {
'/v1/api/openplatform/coding_plan/remains': (req) => {
if (req.headers.get('Authorization') === 'Bearer bearer-only-key') {
return jsonResponse({ base_resp: { status_code: 0 } });
}
Expand All @@ -55,7 +55,7 @@ describe('detect-region: probeRegion auth style fallback', () => {
it('succeeds when endpoint only accepts x-api-key header', async () => {
server = createMockServer({
routes: {
'/v1/token_plan/remains': (req) => {
'/v1/api/openplatform/coding_plan/remains': (req) => {
if (req.headers.get('x-api-key') === 'xapikey-only-key') {
return jsonResponse({ base_resp: { status_code: 0 } });
}
Expand All @@ -80,7 +80,7 @@ describe('detect-region: probeRegion auth style fallback', () => {
it('falls back to global when key is invalid for all auth styles and regions', async () => {
server = createMockServer({
routes: {
'/v1/token_plan/remains': () =>
'/v1/api/openplatform/coding_plan/remains': () =>
jsonResponse({ error: 'unauthorized' }, 401),
},
});
Expand Down
8 changes: 4 additions & 4 deletions test/client/endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { describe, it, expect } from 'bun:test';
import { quotaEndpoint } from '../../src/client/endpoints';

describe('quotaEndpoint', () => {
it('uses token_plan/remains for global', () => {
expect(quotaEndpoint('https://api.minimax.io')).toBe('https://api.minimax.io/v1/token_plan/remains');
it('uses coding_plan/remains for global', () => {
expect(quotaEndpoint('https://api.minimax.io')).toBe('https://api.minimax.io/v1/api/openplatform/coding_plan/remains');
});

it('uses token_plan/remains for cn', () => {
expect(quotaEndpoint('https://api.minimaxi.com')).toBe('https://api.minimaxi.com/v1/token_plan/remains');
it('uses coding_plan/remains for cn', () => {
expect(quotaEndpoint('https://api.minimaxi.com')).toBe('https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains');
});
});
64 changes: 64 additions & 0 deletions test/output/quota-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,41 @@ function createModel(): QuotaModelRemain {
};
}

function createCodingPlanModels(): QuotaModelRemain[] {
return [
{
model_name: 'general',
start_time: Date.UTC(2026, 4, 31, 0, 0, 0),
end_time: Date.UTC(2026, 4, 31, 2, 0, 0),
remains_time: 2 * 60 * 60 * 1000,
current_interval_total_count: 0,
current_interval_usage_count: 0,
current_interval_remaining_percent: 94,
current_weekly_total_count: 0,
current_weekly_usage_count: 0,
current_weekly_remaining_percent: 98,
weekly_start_time: Date.UTC(2026, 4, 31, 0, 0, 0),
weekly_end_time: Date.UTC(2026, 5, 7, 0, 0, 0),
weekly_remains_time: 6 * 24 * 60 * 60 * 1000,
},
{
model_name: 'video',
start_time: Date.UTC(2026, 4, 31, 0, 0, 0),
end_time: Date.UTC(2026, 5, 1, 0, 0, 0),
remains_time: 6 * 60 * 60 * 1000,
current_interval_total_count: 3,
current_interval_usage_count: 3,
current_interval_remaining_percent: 100,
current_weekly_total_count: 21,
current_weekly_usage_count: 21,
current_weekly_remaining_percent: 100,
weekly_start_time: Date.UTC(2026, 4, 31, 0, 0, 0),
weekly_end_time: Date.UTC(2026, 5, 7, 0, 0, 0),
weekly_remains_time: 6 * 24 * 60 * 60 * 1000,
},
];
}

describe('renderQuotaTable', () => {
it('does not force model names to white in color mode', () => {
const lines: string[] = [];
Expand Down Expand Up @@ -65,4 +100,33 @@ describe('renderQuotaTable', () => {
expect(output).toContain('MiniMax-M2');
expect(output).not.toContain(WHITE_ANSI);
});

it('renders coding plan remaining quotas without deriving counts from percent', () => {
const lines: string[] = [];
const originalLog = console.log;

console.log = (message?: unknown) => {
lines.push(String(message ?? ''));
};

try {
renderQuotaTable(createCodingPlanModels(), {
...createConfig(),
region: 'cn',
noColor: true,
});
} finally {
console.log = originalLog;
}

const output = lines.join('\n');

expect(output).toContain('通用');
expect(output).toContain('剩余 [█████████.] 94%');
expect(output).toContain('周剩余 [██████████] 98%');
expect(output).toContain('视频');
expect(output).toContain('3 / 3');
expect(output).toContain('21 / 21');
expect(output).not.toContain('0 / 3');
});
});
3 changes: 2 additions & 1 deletion test/sdk/quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MiniMaxSDK } from '../../src/sdk';
describe('MiniMaxSDK.quota', () => {
it('should get quota info successfully', async () => {
const mockFetch = mock(async (url: string) => {
if (url.includes('/v1/token_plan/remains')) {
if (url.includes('/v1/api/openplatform/coding_plan/remains')) {
return new Response(JSON.stringify({
model_remains: [
{
Expand All @@ -14,6 +14,7 @@ describe('MiniMaxSDK.quota', () => {
remains_time: 1000,
current_interval_total_count: 1000,
current_interval_usage_count: 500,
current_interval_remaining_percent: 50,
current_weekly_total_count: 5000,
current_weekly_usage_count: 2000,
weekly_start_time: 0,
Expand Down
Loading