mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
fix(usage): handle unavailable token plan quotas
This commit is contained in:
@@ -6,24 +6,14 @@ const BOX_WIDTH = 76;
|
||||
const PROGRESS_WIDTH = 32;
|
||||
|
||||
interface TokenPlanUsage {
|
||||
per5HourPercentage: number;
|
||||
per5HourPercentage?: number;
|
||||
per5HourResetTime?: number;
|
||||
per1WeekPercentage: number;
|
||||
per1WeekPercentage?: number;
|
||||
per1WeekResetTime?: number;
|
||||
}
|
||||
|
||||
function readUsage(result: unknown): TokenPlanUsage {
|
||||
const response = unwrapResponse(result as Record<string, unknown>);
|
||||
const percentages = [response.per5HourPercentage, response.per1WeekPercentage];
|
||||
|
||||
if (
|
||||
!percentages.every(
|
||||
(percentage) => typeof percentage === "number" && Number.isFinite(percentage),
|
||||
)
|
||||
) {
|
||||
throw new BailianError("Token Plan usage response has an unexpected format.", ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
const usage = {
|
||||
per5HourPercentage: response.per5HourPercentage,
|
||||
per5HourResetTime: response.per5HourResetTime,
|
||||
@@ -31,17 +21,20 @@ function readUsage(result: unknown): TokenPlanUsage {
|
||||
per1WeekResetTime: response.per1WeekResetTime,
|
||||
};
|
||||
|
||||
const resetTimes = [
|
||||
const quotas = [
|
||||
[usage.per5HourPercentage, usage.per5HourResetTime],
|
||||
[usage.per1WeekPercentage, usage.per1WeekResetTime],
|
||||
];
|
||||
const hasValidResetTimes = resetTimes.every(
|
||||
const hasValidQuotas = quotas.every(
|
||||
([percentage, resetTime]) =>
|
||||
(percentage === 0 && resetTime === undefined) ||
|
||||
(typeof resetTime === "number" && Number.isFinite(resetTime)),
|
||||
(percentage === undefined && resetTime === undefined) ||
|
||||
(typeof percentage === "number" &&
|
||||
Number.isFinite(percentage) &&
|
||||
((percentage === 0 && resetTime === undefined) ||
|
||||
(typeof resetTime === "number" && Number.isFinite(resetTime)))),
|
||||
);
|
||||
|
||||
if (!hasValidResetTimes) {
|
||||
if (!hasValidQuotas) {
|
||||
throw new BailianError("Token Plan usage response has an unexpected format.", ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
@@ -101,11 +94,21 @@ function printView(usage: TokenPlanUsage, generatedAt: number): void {
|
||||
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${visibleContent}`));
|
||||
process.stdout.write(`│ ${content}${" ".repeat(padding)}│\n`);
|
||||
};
|
||||
const writeQuota = (label: string, percentage: number, resetTime: number | undefined) => {
|
||||
const writeQuota = (
|
||||
label: string,
|
||||
unlimitedMessage: string,
|
||||
percentage: number | undefined,
|
||||
resetTime: number | undefined,
|
||||
) => {
|
||||
writeLine(color.bold(label), label);
|
||||
if (percentage === undefined) {
|
||||
writeLine(color.dim(unlimitedMessage), unlimitedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const percentageText = formatPercentage(percentage);
|
||||
const bar = progressBar(percentage);
|
||||
const style = progressStyle(percentage, color.green, color.yellow, color.red);
|
||||
writeLine(color.bold(label), label);
|
||||
writeLine(`${percentageText} used ${style(bar)}`, `${percentageText} used ${bar}`);
|
||||
if (resetTime === undefined) {
|
||||
writeLine(
|
||||
@@ -124,9 +127,19 @@ function printView(usage: TokenPlanUsage, generatedAt: number): void {
|
||||
const generatedAtText = `Generated at: ${formatDateTime(generatedAt)} (local time)`;
|
||||
writeLine(color.dim(generatedAtText), generatedAtText);
|
||||
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
|
||||
writeQuota("5-hour quota", usage.per5HourPercentage, usage.per5HourResetTime);
|
||||
writeQuota(
|
||||
"5-hour quota",
|
||||
"5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。",
|
||||
usage.per5HourPercentage,
|
||||
usage.per5HourResetTime,
|
||||
);
|
||||
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
|
||||
writeQuota("1-week quota", usage.per1WeekPercentage, usage.per1WeekResetTime);
|
||||
writeQuota(
|
||||
"1-week quota",
|
||||
"1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。",
|
||||
usage.per1WeekPercentage,
|
||||
usage.per1WeekResetTime,
|
||||
);
|
||||
process.stdout.write(`└${"─".repeat(BOX_WIDTH)}┘\n`);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () =
|
||||
expect(data.data).toEqual({});
|
||||
});
|
||||
|
||||
test("usage token-plan --json 返回百分比与可用的重置时间", async () => {
|
||||
test("usage token-plan --json 返回可用的额度字段", async () => {
|
||||
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan", "--json"]);
|
||||
if (isConsoleAuthFailure(result)) return;
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
@@ -65,12 +65,18 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () =
|
||||
per1WeekPercentage?: number;
|
||||
per1WeekResetTime?: number;
|
||||
}>(result.stdout);
|
||||
expect(data.per5HourPercentage).toBeTypeOf("number");
|
||||
expect(data.per1WeekPercentage).toBeTypeOf("number");
|
||||
if (data.per5HourPercentage === 0) expect(data.per5HourResetTime).toBeUndefined();
|
||||
else expect(data.per5HourResetTime).toBeTypeOf("number");
|
||||
if (data.per1WeekPercentage === 0) expect(data.per1WeekResetTime).toBeUndefined();
|
||||
else expect(data.per1WeekResetTime).toBeTypeOf("number");
|
||||
const quotas = [
|
||||
[data.per5HourPercentage, data.per5HourResetTime],
|
||||
[data.per1WeekPercentage, data.per1WeekResetTime],
|
||||
];
|
||||
for (const [percentage, resetTime] of quotas) {
|
||||
if (percentage === undefined) expect(resetTime).toBeUndefined();
|
||||
else if (percentage === 0) expect(resetTime).toBeUndefined();
|
||||
else {
|
||||
expect(percentage).toBeTypeOf("number");
|
||||
expect(resetTime).toBeTypeOf("number");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("usage token-plan --view 渲染生成时间与两个额度窗口", async () => {
|
||||
|
||||
@@ -13,15 +13,18 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
function makeUsageResponse(
|
||||
per5HourPercentage: number,
|
||||
per5HourPercentage?: number,
|
||||
per1WeekPercentage = per5HourPercentage,
|
||||
): Record<string, unknown> {
|
||||
const usage: Record<string, number> = {
|
||||
per5HourPercentage,
|
||||
per1WeekPercentage,
|
||||
};
|
||||
if (per5HourPercentage !== 0) usage.per5HourResetTime = 1_786_000_000_000;
|
||||
if (per1WeekPercentage !== 0) usage.per1WeekResetTime = 1_786_100_000_000;
|
||||
const usage: Record<string, number> = {};
|
||||
if (per5HourPercentage !== undefined) {
|
||||
usage.per5HourPercentage = per5HourPercentage;
|
||||
if (per5HourPercentage !== 0) usage.per5HourResetTime = 1_786_000_000_000;
|
||||
}
|
||||
if (per1WeekPercentage !== undefined) {
|
||||
usage.per1WeekPercentage = per1WeekPercentage;
|
||||
if (per1WeekPercentage !== 0) usage.per1WeekResetTime = 1_786_100_000_000;
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
@@ -90,4 +93,57 @@ describe("usage token-plan view", () => {
|
||||
expect(renderedOutput).toContain("Resets: not applicable (no usage yet)");
|
||||
expect(renderedOutput).toMatch(/Resets: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
test("renders missing quota windows as possibly unlimited", async () => {
|
||||
const output: string[] = [];
|
||||
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
||||
output.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
|
||||
await tokenPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(makeUsageResponse()) },
|
||||
flags: { json: false, view: true },
|
||||
settings: { dryRun: false },
|
||||
} as never);
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
expect(renderedOutput).toContain("5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
|
||||
expect(renderedOutput).toContain("1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
|
||||
});
|
||||
|
||||
test("renders only the missing quota window as possibly unlimited", async () => {
|
||||
const output: string[] = [];
|
||||
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
||||
output.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
|
||||
await tokenPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(makeUsageResponse(undefined, 0.5)) },
|
||||
flags: { json: false, view: true },
|
||||
settings: { dryRun: false },
|
||||
} as never);
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
expect(renderedOutput).toContain("5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
|
||||
expect(renderedOutput).not.toContain("1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
|
||||
expect(renderedOutput).toMatch(/Resets: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
test("returns an empty JSON object when no quota fields are available", async () => {
|
||||
const output: string[] = [];
|
||||
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
||||
output.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
|
||||
await tokenPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(makeUsageResponse()) },
|
||||
flags: { json: true, view: false },
|
||||
settings: { dryRun: false },
|
||||
} as never);
|
||||
|
||||
expect(output.join("").trim()).toBe("{}");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user