Skip to content
Closed
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: 2 additions & 0 deletions packages/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import {
workspaceList,
quotaList,
quotaUpdate,
quotaDelete,
quotaHistory,
quotaCheck,
permissionList,
Expand Down Expand Up @@ -246,6 +247,7 @@ export const commands: Record<string, AnyCommand> = {
"workspace list": workspaceList,
"quota list": quotaList,
"quota update": quotaUpdate,
"quota delete": quotaDelete,
"quota history": quotaHistory,
"quota check": quotaCheck,
"permission list": permissionList,
Expand Down
71 changes: 71 additions & 0 deletions packages/commands/src/commands/quota/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { defineCommand, detectOutputFormat, modelsLimitsPath } from "bailian-cli-core";
import { emitResult, confirmDangerousAction } from "bailian-cli-runtime";

export default defineCommand({
description: {
"en-US": "Clear all custom rate limits (QPM/TPM) for a model",
"zh-CN": "清除模型的所有自定义限流配置(QPM/TPM)",
},
auth: "apiKey",
usageArgs: "--model <model> [--yes]",
flags: {
model: {
type: "string",
valueHint: "<model>",
description: { "en-US": "Model name (required)", "zh-CN": "模型名称(必填)" },
required: true,
},
yes: {
type: "switch",
description: {
"en-US": "Skip the confirmation prompt",
"zh-CN": "跳过确认提示",
},
},
},
exampleArgs: ["--model qwen-plus", "--model qwen-plus --yes", "--model qwen-plus --output json"],
notes: [
{
"en-US":
"Irreversible — the server-side OVERLAY is reset to defaults, so your custom QPM/TPM configuration is permanently removed.",
"zh-CN":
"该操作不可撤销——服务端 OVERLAY 会重置为默认值,你的自定义 QPM/TPM 配置将被永久删除。",
},
{
"en-US": "Requires confirmation; pass --yes to skip the prompt in scripts.",
"zh-CN": "需要确认;脚本中可加 --yes 跳过交互提示。",
},
],
async run(ctx) {
const { settings, flags } = ctx;
const modelName = flags.model;
const format = detectOutputFormat(settings.output);

const body = { models: [{ model: modelName, operation_type: "DELETE" }] };

if (settings.dryRun) {
emitResult(
{ endpoint: ctx.client.url(modelsLimitsPath()), method: "POST", request: body },
format,
);
return;
}

await confirmDangerousAction(
`Clear all custom rate limits for model ${modelName}.\nYour custom QPM/TPM configuration will be removed.`,
flags.yes ?? false,
);

const result = await ctx.client.requestJson<{ request_id?: string }>({
path: modelsLimitsPath(),
method: "POST",
body,
});

if (format === "json") {
emitResult({ model: modelName, ...result }, format);
return;
}
process.stdout.write(`Rate limits cleared for "${modelName}".\n`);
},
});
68 changes: 15 additions & 53 deletions packages/commands/src/commands/quota/update.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { defineCommand, detectOutputFormat, modelsLimitsPath } from "bailian-cli-core";
import { emitResult, confirmDangerousAction } from "bailian-cli-runtime";
import { emitResult } from "bailian-cli-runtime";
import { formatNumber } from "../shared/format.ts";

const MINUTE_SECONDS = 60;

export default defineCommand({
description: {
"en-US": "Update model rate limits (QPM/TPM), or clear them with --delete",
"zh-CN": "更新模型限流配置(QPM/TPM),或使用 --delete 清除配置",
"en-US": "Update model rate limits (QPM/TPM)",
"zh-CN": "更新模型限流配置(QPM/TPM)",
},
auth: "apiKey",
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--delete] [--yes]",
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>]",
flags: {
model: {
type: "string",
Expand All @@ -34,38 +34,18 @@ export default defineCommand({
"zh-CN": "每分钟最大 Token 数(TPM)",
},
},
delete: {
type: "switch",
description: {
"en-US": "Clear all custom rate limits for the model",
"zh-CN": "清除该模型的所有自定义限流配置",
},
},
yes: {
type: "switch",
description: {
"en-US": "Skip the confirmation prompt for --delete",
"zh-CN": "使用 --delete 时跳过确认提示",
},
},
},
exampleArgs: [
"--model qwen-plus --rpm 60 --tpm 100000",
"--model qwen3-max --tpm 500000",
"--model qwen-plus --delete",
"--model qwen-plus --delete --yes",
"--model qwen-plus --rpm 60 --output json",
],
notes: [
{
"en-US":
"Fields you omit keep their current values (server-side OVERLAY merge); --delete clears all custom limits.",
'Fields you omit keep their current values (server-side OVERLAY merge). Clear all custom limits with the "quota delete" command instead.',
"zh-CN":
"未指定的字段将保留当前值(服务端 OVERLAY 合并);--delete 会清除所有自定义限流配置。",
},
{
"en-US": "--delete requires confirmation; pass --yes to skip the prompt in scripts.",
"zh-CN": "--delete 需要确认;脚本中可加 --yes 跳过交互提示。",
"未指定的字段将保留当前值(服务端 OVERLAY 合并)。清除全部自定义限流配置请改用 “quota delete” 命令。",
},
{
"en-US":
Expand All @@ -74,10 +54,8 @@ export default defineCommand({
},
],
validate: (flags) => {
if (flags.delete && (flags.rpm !== undefined || flags.tpm !== undefined))
return "--delete cannot be combined with --rpm/--tpm.";
if (!flags.delete && flags.rpm === undefined && flags.tpm === undefined)
return "one of --rpm / --tpm / --delete is required.";
if (flags.rpm === undefined && flags.tpm === undefined)
return "one of --rpm / --tpm is required.";
if (flags.rpm !== undefined && flags.rpm < 0) return "--rpm must be a non-negative number.";
if (flags.tpm !== undefined && flags.tpm < 0) return "--tpm must be a non-negative number.";
return undefined;
Expand All @@ -88,17 +66,13 @@ export default defineCommand({
const format = detectOutputFormat(settings.output);

const entry: Record<string, unknown> = { model: modelName };
if (flags.delete) {
entry.operation_type = "DELETE";
} else {
if (flags.rpm !== undefined) {
entry.request_limit = flags.rpm;
entry.request_limit_period = MINUTE_SECONDS;
}
if (flags.tpm !== undefined) {
entry.usage_limit = flags.tpm;
entry.usage_limit_period = MINUTE_SECONDS;
}
if (flags.rpm !== undefined) {
entry.request_limit = flags.rpm;
entry.request_limit_period = MINUTE_SECONDS;
}
if (flags.tpm !== undefined) {
entry.usage_limit = flags.tpm;
entry.usage_limit_period = MINUTE_SECONDS;
}
const body = { models: [entry] };

Expand All @@ -110,13 +84,6 @@ export default defineCommand({
return;
}

if (flags.delete) {
await confirmDangerousAction(
`Clear all custom rate limits for model ${modelName}.\nYour custom QPM/TPM configuration will be removed.`,
flags.yes ?? false,
);
}

const result = await ctx.client.requestJson<{ request_id?: string }>({
path: modelsLimitsPath(),
method: "POST",
Expand All @@ -127,11 +94,6 @@ export default defineCommand({
emitResult({ model: modelName, ...result }, format);
return;
}

if (flags.delete) {
process.stdout.write(`Rate limits cleared for "${modelName}".\n`);
return;
}
const parts: string[] = [];
if (flags.rpm !== undefined) parts.push(`QPM ${formatNumber(flags.rpm)}`);
if (flags.tpm !== undefined) parts.push(`TPM ${formatNumber(flags.tpm)}`);
Expand Down
1 change: 1 addition & 0 deletions packages/commands/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export { default as modelList } from "./commands/model/list.ts";
export { default as workspaceList } from "./commands/workspace/list.ts";
export { default as quotaList } from "./commands/quota/list.ts";
export { default as quotaUpdate } from "./commands/quota/update.ts";
export { default as quotaDelete } from "./commands/quota/delete.ts";
export { default as quotaHistory } from "./commands/quota/history.ts";
export { default as quotaCheck } from "./commands/quota/check.ts";
export { default as permissionList } from "./commands/permission/list.ts";
Expand Down
40 changes: 25 additions & 15 deletions packages/commands/tests/e2e/quota.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,20 @@ describe("e2e: quota", () => {
expect(stderr).toContain("--model");
expect(stderr).toContain("--rpm");
expect(stderr).toContain("--tpm");
expect(stderr).toContain("--delete");
});

test("quota delete --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "delete", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--model");
expect(stderr).toContain("--yes");
expect(stderr).toContain("bl quota delete --model qwen-plus");
});

test("quota request 作为 quota update 的兼容别名可用", async () => {
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "request", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("--rpm");
expect(stderr).toContain("--delete");
});

test("quota history --help 正常退出", async () => {
Expand Down Expand Up @@ -68,36 +73,41 @@ describe("e2e: quota", () => {
expect(stderr).toContain("at least 1 minute");
});

test("quota update 缺少 --rpm/--tpm/--delete 报用法错误", async () => {
test("quota update 缺少 --rpm/--tpm 报用法错误", async () => {
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
"quota",
"update",
"--model",
"qwen-plus",
]);
expect(exitCode).toBe(2);
expect(stderr).toContain("one of --rpm / --tpm / --delete");
expect(stderr).toContain("one of --rpm / --tpm");
});

test("quota update --delete 与 --rpm 互斥", async () => {
test("quota update 不再接受 --delete", async () => {
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
"quota",
"update",
"--model",
"qwen-plus",
"--delete",
"--rpm",
"60",
]);
expect(exitCode).toBe(2);
expect(stderr).toContain("cannot be combined");
expect(stderr).toContain("Unknown flag");
});

test("quota update --delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
test("quota delete 缺少 --model 报用法错误", async () => {
// 裸 `quota delete`(无任何 flag)会渲染 help 并正常退出,需带 flag 触发必填校验
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "delete", "--yes"]);
expect(exitCode).toBe(2);
expect(stderr).toContain("Missing required flag: --model");
});

test("quota delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
// 注入假 key 让 apiKey 鉴权通过;确认门在发任何网络请求前触发
const { stderr, exitCode } = await runCommandE2e(
QUOTA_ROUTES,
["quota", "update", "--model", "qwen-plus", "--delete"],
["quota", "delete", "--model", "qwen-plus"],
{ DASHSCOPE_API_KEY: "sk-e2e-quota-delete" },
);
expect(exitCode).toBe(2);
Expand Down Expand Up @@ -196,13 +206,12 @@ describe("e2e: quota", () => {
expect(entry?.usage_limit_period).toBe(60);
});

test("quota update --delete --dry-run 输出 DELETE 操作", async () => {
test("quota delete --dry-run 输出 DELETE 操作", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
"quota",
"update",
"delete",
"--model",
"qwen-plus",
"--delete",
"--dry-run",
"--output",
"json",
Expand All @@ -211,6 +220,7 @@ describe("e2e: quota", () => {
const data = parseStdoutJson<{
request?: { models?: { model?: string; operation_type?: string }[] };
}>(stdout);
expect(data.request?.models?.[0]?.model).toBe("qwen-plus");
expect(data.request?.models?.[0]?.operation_type).toBe("DELETE");
});

Expand Down Expand Up @@ -285,8 +295,8 @@ describe("e2e: quota", () => {
});
});

// 真实调用 GET /api/v1/models/limits。quota update 只测 --dry-run——live POST
// 会真实改写账号限流,不做 e2e。
// 真实调用 GET /api/v1/models/limits。quota update / quota delete 只测
// --dry-run——live POST 会真实改写账号限流,不做 e2e。
describe.skipIf(!isDashScopeE2EReady())("e2e: quota(DashScope)", () => {
test("quota list 文本输出正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
Expand Down
1 change: 1 addition & 0 deletions packages/commands/tests/e2e/topic-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const ADVISOR_ROUTES: E2eRouteExports = {
export const QUOTA_ROUTES: E2eRouteExports = {
"quota list": "quotaList",
"quota update": "quotaUpdate",
"quota delete": "quotaDelete",
// Backward-compatible alias of "quota update".
"quota request": "quotaUpdate",
"quota history": "quotaHistory",
Expand Down
5 changes: 3 additions & 2 deletions skills/bailian-cli/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ Use this index for the skill-scoped quick index and global flags.
| `bl plugin list` | No Auth | List installed Command Packs and their load status | [plugin.md](plugin.md) |
| `bl plugin remove` | No Auth | Remove an installed Command Pack | [plugin.md](plugin.md) |
| `bl quota check` | Console | Check current usage against rate limits | [quota.md](quota.md) |
| `bl quota delete` | API Key | Clear all custom rate limits (QPM/TPM) for a model | [quota.md](quota.md) |
| `bl quota history` | Console | View quota change history | [quota.md](quota.md) |
| `bl quota list` | API Key | View model rate limits (QPM/TPM, account and workspace level) | [quota.md](quota.md) |
| `bl quota update` | API Key | Update model rate limits (QPM/TPM), or clear them with --delete | [quota.md](quota.md) |
| `bl quota update` | API Key | Update model rate limits (QPM/TPM) | [quota.md](quota.md) |
| `bl search web` | API Key | Search the web using DashScope MCP WebSearch service | [search.md](search.md) |
| `bl skill add` | No Auth | Install skills from the Bailian skill registry into local agents | [skill.md](skill.md) |
| `bl skill init` | No Auth | Install all bailian-\* skills (one-shot bootstrap for new environments) | [skill.md](skill.md) |
Expand Down Expand Up @@ -122,7 +123,7 @@ Use this index for the skill-scoped quick index and global flags.
| `permission` | `grant`, `list`, `revoke` | [permission.md](permission.md) |
| `pipeline` | `run`, `validate` | [pipeline.md](pipeline.md) |
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
| `quota` | `check`, `history`, `list`, `update` | [quota.md](quota.md) |
| `quota` | `check`, `delete`, `history`, `list`, `update` | [quota.md](quota.md) |
| `search` | `web` | [search.md](search.md) |
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
| `text` | `chat` | [text.md](text.md) |
Expand Down
Loading