mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
refactor(usage): align token-plan with --output convention and tolerant quota reading
This commit is contained in:
@@ -24,6 +24,14 @@ export function formatDate(ts: number): string {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export function formatDateTime(ts: number): string {
|
||||
const date = new Date(ts);
|
||||
const hour = String(date.getHours()).padStart(2, "0");
|
||||
const minute = String(date.getMinutes()).padStart(2, "0");
|
||||
const second = String(date.getSeconds()).padStart(2, "0");
|
||||
return `${formatDate(ts)} ${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
export function requireWorkspaceId(settings: Settings, binName: string): string {
|
||||
if (settings.workspaceId) return settings.workspaceId;
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { BailianError, ExitCode, defineCommand, unwrapResponse } from "bailian-cli-core";
|
||||
import { ansi, displayWidth, emitResult, type TextStyle } from "bailian-cli-runtime";
|
||||
import { defineCommand, detectOutputFormat, unwrapResponse } from "bailian-cli-core";
|
||||
import {
|
||||
ansi,
|
||||
displayWidth,
|
||||
emitResult,
|
||||
type AnsiStyles,
|
||||
type TextStyle,
|
||||
} from "bailian-cli-runtime";
|
||||
import { formatDateTime } from "./shared.ts";
|
||||
|
||||
const TOKEN_PLAN_USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
|
||||
const BOX_WIDTH = 76;
|
||||
@@ -12,50 +19,36 @@ interface TokenPlanUsage {
|
||||
per1WeekResetTime?: number;
|
||||
}
|
||||
|
||||
interface QuotaWindow {
|
||||
percentage?: number;
|
||||
resetTime?: number;
|
||||
}
|
||||
|
||||
/** Accept only finite numbers; anything else counts as absent (possibly unlimited). */
|
||||
function readNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function readUsage(result: unknown): TokenPlanUsage {
|
||||
const response = unwrapResponse(result as Record<string, unknown>);
|
||||
const usage = {
|
||||
per5HourPercentage: response.per5HourPercentage,
|
||||
per5HourResetTime: response.per5HourResetTime,
|
||||
per1WeekPercentage: response.per1WeekPercentage,
|
||||
per1WeekResetTime: response.per1WeekResetTime,
|
||||
};
|
||||
const usage: TokenPlanUsage = {};
|
||||
|
||||
const quotas = [
|
||||
[usage.per5HourPercentage, usage.per5HourResetTime],
|
||||
[usage.per1WeekPercentage, usage.per1WeekResetTime],
|
||||
];
|
||||
const hasValidQuotas = quotas.every(
|
||||
([percentage, resetTime]) =>
|
||||
(percentage === undefined && resetTime === undefined) ||
|
||||
(typeof percentage === "number" &&
|
||||
Number.isFinite(percentage) &&
|
||||
((percentage === 0 && resetTime === undefined) ||
|
||||
(typeof resetTime === "number" && Number.isFinite(resetTime)))),
|
||||
);
|
||||
const per5HourPercentage = readNumber(response.per5HourPercentage);
|
||||
if (per5HourPercentage !== undefined) usage.per5HourPercentage = per5HourPercentage;
|
||||
const per5HourResetTime = readNumber(response.per5HourResetTime);
|
||||
if (per5HourResetTime !== undefined) usage.per5HourResetTime = per5HourResetTime;
|
||||
const per1WeekPercentage = readNumber(response.per1WeekPercentage);
|
||||
if (per1WeekPercentage !== undefined) usage.per1WeekPercentage = per1WeekPercentage;
|
||||
const per1WeekResetTime = readNumber(response.per1WeekResetTime);
|
||||
if (per1WeekResetTime !== undefined) usage.per1WeekResetTime = per1WeekResetTime;
|
||||
|
||||
if (!hasValidQuotas) {
|
||||
throw new BailianError("Token Plan usage response has an unexpected format.", ExitCode.GENERAL);
|
||||
}
|
||||
|
||||
return usage as TokenPlanUsage;
|
||||
return usage;
|
||||
}
|
||||
|
||||
function formatPercentage(ratio: number): string {
|
||||
return `${(ratio * 100).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
function formatDateTime(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hour = String(date.getHours()).padStart(2, "0");
|
||||
const minute = String(date.getMinutes()).padStart(2, "0");
|
||||
const second = String(date.getSeconds()).padStart(2, "0");
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
function formatRemainingTime(resetTime: number, now: number): string {
|
||||
const remainingMs = Math.max(0, resetTime - now);
|
||||
const totalMinutes = Math.floor(remainingMs / 60_000);
|
||||
@@ -77,102 +70,74 @@ function progressBar(ratio: number): string {
|
||||
return `[${"█".repeat(filled)}${"░".repeat(PROGRESS_WIDTH - filled)}]`;
|
||||
}
|
||||
|
||||
function progressStyle(
|
||||
percentage: number,
|
||||
green: TextStyle,
|
||||
yellow: TextStyle,
|
||||
red: TextStyle,
|
||||
): TextStyle {
|
||||
if (percentage >= 0.9) return red;
|
||||
if (percentage >= 0.75) return yellow;
|
||||
return green;
|
||||
function progressStyle(percentage: number, color: AnsiStyles): TextStyle {
|
||||
if (percentage >= 0.9) return color.red;
|
||||
if (percentage >= 0.75) return color.yellow;
|
||||
return color.green;
|
||||
}
|
||||
|
||||
function printView(usage: TokenPlanUsage, generatedAt: number): void {
|
||||
const color = ansi(process.stdout);
|
||||
const writeLine = (content = "", visibleContent = content) => {
|
||||
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${visibleContent}`));
|
||||
process.stdout.write(`│ ${content}${" ".repeat(padding)}│\n`);
|
||||
const writeLine = (text = "", style?: TextStyle) => {
|
||||
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${text}`));
|
||||
process.stdout.write(`│ ${style ? style(text) : text}${" ".repeat(padding)}│\n`);
|
||||
};
|
||||
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);
|
||||
const writeQuota = (label: string, unlimitedMessage: string, window: QuotaWindow) => {
|
||||
writeLine(label, color.bold);
|
||||
if (window.percentage === undefined) {
|
||||
writeLine(unlimitedMessage, color.dim);
|
||||
return;
|
||||
}
|
||||
|
||||
const percentageText = formatPercentage(percentage);
|
||||
const bar = progressBar(percentage);
|
||||
const style = progressStyle(percentage, color.green, color.yellow, color.red);
|
||||
writeLine(`${percentageText} used ${style(bar)}`, `${percentageText} used ${bar}`);
|
||||
if (resetTime === undefined) {
|
||||
writeLine(
|
||||
color.dim("Resets: not applicable (no usage yet)"),
|
||||
"Resets: not applicable (no usage yet)",
|
||||
);
|
||||
const percentageText = formatPercentage(window.percentage);
|
||||
const bar = progressBar(window.percentage);
|
||||
writeLine(`${percentageText} used ${bar}`, progressStyle(window.percentage, color));
|
||||
if (window.resetTime === undefined) {
|
||||
writeLine("Resets: not applicable (no usage yet)", color.dim);
|
||||
return;
|
||||
}
|
||||
|
||||
const resetText = `Resets: ${formatDateTime(resetTime)} (in ${formatRemainingTime(resetTime, generatedAt)})`;
|
||||
writeLine(color.dim(resetText), resetText);
|
||||
const resetText = `Resets: ${formatDateTime(window.resetTime)} (in ${formatRemainingTime(window.resetTime, generatedAt)})`;
|
||||
writeLine(resetText, color.dim);
|
||||
};
|
||||
|
||||
process.stdout.write(`┌${"─".repeat(BOX_WIDTH)}┐\n`);
|
||||
writeLine(color.cyan("Token Plan Usage"), "Token Plan Usage");
|
||||
const generatedAtText = `Generated at: ${formatDateTime(generatedAt)} (local time)`;
|
||||
writeLine(color.dim(generatedAtText), generatedAtText);
|
||||
writeLine("Token Plan Usage", color.cyan);
|
||||
writeLine(`Generated at: ${formatDateTime(generatedAt)} (local time)`, color.dim);
|
||||
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
|
||||
writeQuota(
|
||||
"5-hour quota",
|
||||
"5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。",
|
||||
usage.per5HourPercentage,
|
||||
usage.per5HourResetTime,
|
||||
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
{ percentage: usage.per5HourPercentage, resetTime: usage.per5HourResetTime },
|
||||
);
|
||||
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
|
||||
writeQuota(
|
||||
"1-week quota",
|
||||
"1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。",
|
||||
usage.per1WeekPercentage,
|
||||
usage.per1WeekResetTime,
|
||||
"The 1-week limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
{ percentage: usage.per1WeekPercentage, resetTime: usage.per1WeekResetTime },
|
||||
);
|
||||
process.stdout.write(`└${"─".repeat(BOX_WIDTH)}┘\n`);
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description: "Show Token Plan quota usage as core JSON or a human-readable view",
|
||||
description: "Show Token Plan quota usage",
|
||||
auth: "console",
|
||||
usageArgs: "<--json | --view> [flags]",
|
||||
flags: {
|
||||
json: {
|
||||
type: "switch",
|
||||
description: "Output only the four core usage fields as JSON",
|
||||
},
|
||||
view: {
|
||||
type: "switch",
|
||||
description: "Render a compact human-readable quota view",
|
||||
},
|
||||
},
|
||||
exampleArgs: ["--json", "--view"],
|
||||
validate: (flags) =>
|
||||
flags.json === flags.view ? "Choose exactly one of --json or --view." : undefined,
|
||||
usageArgs: "[flags]",
|
||||
exampleArgs: ["", "--output json"],
|
||||
async run(ctx) {
|
||||
const { flags, settings } = ctx;
|
||||
const { settings } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitResult({ api: TOKEN_PLAN_USAGE_API, data: {} }, "json");
|
||||
emitResult({ api: TOKEN_PLAN_USAGE_API, data: {} }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ctx.client.console(TOKEN_PLAN_USAGE_API, {});
|
||||
const usage = readUsage(result);
|
||||
|
||||
if (flags.json) {
|
||||
emitResult(usage, "json");
|
||||
if (format === "json") {
|
||||
emitResult(usage, format);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,39 +15,28 @@ describe("e2e: usage token-plan", () => {
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--json|--view|Token Plan/i);
|
||||
expect(stderr).toMatch(/Token Plan|quota/i);
|
||||
});
|
||||
|
||||
test("usage token-plan 未选择输出形式时退出为用法错误", async () => {
|
||||
test("usage token-plan --help 包含 --output json 示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"token-plan",
|
||||
"--quiet",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toContain("Choose exactly one of --json or --view.");
|
||||
});
|
||||
|
||||
test("usage token-plan 同时选择两种输出形式时退出为用法错误", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"token-plan",
|
||||
"--json",
|
||||
"--view",
|
||||
"--quiet",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toContain("Choose exactly one of --json or --view.");
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl usage token-plan --output json");
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () => {
|
||||
test("usage token-plan --json --dry-run 输出网关请求计划", async () => {
|
||||
test("usage token-plan --dry-run 输出网关请求计划", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"token-plan",
|
||||
"--json",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ api?: string; data?: Record<string, unknown> }>(stdout);
|
||||
@@ -55,8 +44,8 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () =
|
||||
expect(data.data).toEqual({});
|
||||
});
|
||||
|
||||
test("usage token-plan --json 返回可用的额度字段", async () => {
|
||||
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan", "--json"]);
|
||||
test("usage token-plan --output json 返回可用的额度字段", async () => {
|
||||
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan", "--output", "json"]);
|
||||
if (isConsoleAuthFailure(result)) return;
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
@@ -65,22 +54,19 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () =
|
||||
per1WeekPercentage?: number;
|
||||
per1WeekResetTime?: number;
|
||||
}>(result.stdout);
|
||||
const quotas = [
|
||||
[data.per5HourPercentage, data.per5HourResetTime],
|
||||
[data.per1WeekPercentage, data.per1WeekResetTime],
|
||||
const fields = [
|
||||
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");
|
||||
}
|
||||
for (const field of fields) {
|
||||
if (field !== undefined) expect(field).toBeTypeOf("number");
|
||||
}
|
||||
});
|
||||
|
||||
test("usage token-plan --view 渲染生成时间与两个额度窗口", async () => {
|
||||
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan", "--view"]);
|
||||
test("usage token-plan 默认渲染生成时间与两个额度窗口", async () => {
|
||||
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan"]);
|
||||
if (isConsoleAuthFailure(result)) return;
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(result.stdout).toContain("Generated at:");
|
||||
|
||||
@@ -12,6 +12,23 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function captureStdout(): string[] {
|
||||
const output: string[] = [];
|
||||
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
||||
output.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
async function runTokenPlan(response: Record<string, unknown>, output?: string): Promise<void> {
|
||||
await tokenPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(response) },
|
||||
flags: {},
|
||||
settings: { dryRun: false, output },
|
||||
} as never);
|
||||
}
|
||||
|
||||
function makeUsageResponse(
|
||||
per5HourPercentage?: number,
|
||||
per1WeekPercentage = per5HourPercentage,
|
||||
@@ -26,6 +43,10 @@ function makeUsageResponse(
|
||||
if (per1WeekPercentage !== 0) usage.per1WeekResetTime = 1_786_100_000_000;
|
||||
}
|
||||
|
||||
return wrapResponse(usage);
|
||||
}
|
||||
|
||||
function wrapResponse(usage: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
data: {
|
||||
DataV2: {
|
||||
@@ -45,49 +66,25 @@ describe("usage token-plan view", () => {
|
||||
])("uses ANSI color %s for %s", async (percentage, colorCode) => {
|
||||
delete process.env.NO_COLOR;
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true });
|
||||
const output: string[] = [];
|
||||
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
||||
output.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
const output = captureStdout();
|
||||
|
||||
await tokenPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(makeUsageResponse(percentage)) },
|
||||
flags: { json: false, view: true },
|
||||
settings: { dryRun: false },
|
||||
} as never);
|
||||
await runTokenPlan(makeUsageResponse(percentage));
|
||||
|
||||
expect(output.join("")).toContain(`\u001B[${colorCode}m[`);
|
||||
expect(output.join("")).toContain(`\u001B[${colorCode}m`);
|
||||
});
|
||||
|
||||
test("accepts missing reset times when the quota usage is zero", async () => {
|
||||
const output: string[] = [];
|
||||
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
||||
output.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
const output = captureStdout();
|
||||
|
||||
await tokenPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(makeUsageResponse(0)) },
|
||||
flags: { json: false, view: true },
|
||||
settings: { dryRun: false },
|
||||
} as never);
|
||||
await runTokenPlan(makeUsageResponse(0));
|
||||
|
||||
expect(output.join("")).toContain("Resets: not applicable (no usage yet)");
|
||||
});
|
||||
|
||||
test("allows one unused quota window without masking another reset time", async () => {
|
||||
const output: string[] = [];
|
||||
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
||||
output.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
const output = captureStdout();
|
||||
|
||||
await tokenPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(makeUsageResponse(0, 0.5)) },
|
||||
flags: { json: false, view: true },
|
||||
settings: { dryRun: false },
|
||||
} as never);
|
||||
await runTokenPlan(makeUsageResponse(0, 0.5));
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
expect(renderedOutput).toContain("Resets: not applicable (no usage yet)");
|
||||
@@ -95,55 +92,91 @@ describe("usage token-plan view", () => {
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
const output = captureStdout();
|
||||
|
||||
await tokenPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(makeUsageResponse()) },
|
||||
flags: { json: false, view: true },
|
||||
settings: { dryRun: false },
|
||||
} as never);
|
||||
await runTokenPlan(makeUsageResponse());
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
expect(renderedOutput).toContain("5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
|
||||
expect(renderedOutput).toContain("1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
|
||||
expect(renderedOutput).toContain(
|
||||
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
expect(renderedOutput).toContain(
|
||||
"The 1-week limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
const output = captureStdout();
|
||||
|
||||
await tokenPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(makeUsageResponse(undefined, 0.5)) },
|
||||
flags: { json: false, view: true },
|
||||
settings: { dryRun: false },
|
||||
} as never);
|
||||
await runTokenPlan(makeUsageResponse(undefined, 0.5));
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
expect(renderedOutput).toContain("5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
|
||||
expect(renderedOutput).not.toContain("1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
|
||||
expect(renderedOutput).toContain(
|
||||
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
expect(renderedOutput).not.toContain(
|
||||
"The 1-week limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
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;
|
||||
});
|
||||
test("renders a window with a missing percentage as possibly unlimited even when its reset time is present", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await tokenPlanUsage.run({
|
||||
client: { console: vi.fn().mockResolvedValue(makeUsageResponse()) },
|
||||
flags: { json: true, view: false },
|
||||
settings: { dryRun: false },
|
||||
} as never);
|
||||
await runTokenPlan(wrapResponse({ per5HourResetTime: 1_786_000_000_000 }));
|
||||
|
||||
expect(output.join("")).toContain(
|
||||
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
});
|
||||
|
||||
test("treats non-numeric quota fields as absent instead of failing", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(
|
||||
wrapResponse({ per5HourPercentage: "not-a-number", per1WeekPercentage: Number.NaN }),
|
||||
);
|
||||
|
||||
const renderedOutput = output.join("");
|
||||
expect(renderedOutput).toContain(
|
||||
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
expect(renderedOutput).toContain(
|
||||
"The 1-week limit may be unlimited; verify in the Bailian Token Plan console.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("usage token-plan json", () => {
|
||||
test("outputs the four core usage fields with --output json", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(makeUsageResponse(0.5, 0.25), "json");
|
||||
|
||||
expect(JSON.parse(output.join(""))).toEqual({
|
||||
per5HourPercentage: 0.5,
|
||||
per5HourResetTime: 1_786_000_000_000,
|
||||
per1WeekPercentage: 0.25,
|
||||
per1WeekResetTime: 1_786_100_000_000,
|
||||
});
|
||||
});
|
||||
|
||||
test("returns an empty JSON object when no quota fields are available", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(makeUsageResponse(), "json");
|
||||
|
||||
expect(output.join("").trim()).toBe("{}");
|
||||
});
|
||||
|
||||
test("omits non-numeric quota fields from the JSON output", async () => {
|
||||
const output = captureStdout();
|
||||
|
||||
await runTokenPlan(
|
||||
wrapResponse({ per5HourPercentage: "not-a-number", per1WeekPercentage: 0 }),
|
||||
"json",
|
||||
);
|
||||
|
||||
expect(JSON.parse(output.join(""))).toEqual({ per1WeekPercentage: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,6 +71,7 @@ Use this table only after the decision table in [`bailian-protocol`](../bailian-
|
||||
| Bailian pipeline workflow (a step in a bl flow) | `bl pipeline run` / `validate` | JSON/YAML workflow definitions |
|
||||
| Bailian rate limits / quota | `bl quota list` / `check` / `request` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Bailian free tier / usage stats | `bl usage free` / `stats` / `freetier` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Bailian Token Plan quota usage | `bl usage token-plan` | Console auth; class 2 — ask which product first if unnamed |
|
||||
| Console API (advanced) | `bl console call` | Console auth |
|
||||
| Bailian workspace listing | `bl workspace list` | Console auth |
|
||||
| Image / video / speech / omni / vision | → skill `bailian-gen` | Fallback: `bl image\|video\|speech\|omni\|vision --help` |
|
||||
|
||||
@@ -66,7 +66,7 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| `bl usage freetier` | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable | [usage.md](usage.md) |
|
||||
| `bl usage stats` | Query model usage statistics | [usage.md](usage.md) |
|
||||
| `bl usage summary` | Show a unified usage summary: free-tier quota and recent usage overview | [usage.md](usage.md) |
|
||||
| `bl usage token-plan` | Show Token Plan quota usage as core JSON or a human-readable view | [usage.md](usage.md) |
|
||||
| `bl usage token-plan` | Show Token Plan quota usage | [usage.md](usage.md) |
|
||||
| `bl workspace init` | Initialize Bailian workspace and activate postpaid services | [workspace.md](workspace.md) |
|
||||
| `bl workspace list` | List all workspaces | [workspace.md](workspace.md) |
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Index: [index.md](index.md)
|
||||
| `bl usage freetier` | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable |
|
||||
| `bl usage stats` | Query model usage statistics |
|
||||
| `bl usage summary` | Show a unified usage summary: free-tier quota and recent usage overview |
|
||||
| `bl usage token-plan` | Show Token Plan quota usage as core JSON or a human-readable view |
|
||||
| `bl usage token-plan` | Show Token Plan quota usage |
|
||||
|
||||
## Command details
|
||||
|
||||
@@ -203,18 +203,16 @@ bl usage summary --output json
|
||||
|
||||
### `bl usage token-plan`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------- |
|
||||
| **Name** | `usage token-plan` |
|
||||
| **Description** | Show Token Plan quota usage as core JSON or a human-readable view |
|
||||
| **Usage** | `bl usage token-plan <--json \| --view> [flags]` |
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------- |
|
||||
| **Name** | `usage token-plan` |
|
||||
| **Description** | Show Token Plan quota usage |
|
||||
| **Usage** | `bl usage token-plan [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--json` | switch | no | Output only the four core usage fields as JSON |
|
||||
| `--view` | switch | no | Render a compact human-readable quota view |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
@@ -223,9 +221,9 @@ bl usage summary --output json
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl usage token-plan --json
|
||||
bl usage token-plan
|
||||
```
|
||||
|
||||
```bash
|
||||
bl usage token-plan --view
|
||||
bl usage token-plan --output json
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user