Compare commits

...

1 Commits

Author SHA1 Message Date
故璃 fd96fd664c feat(quota): split --delete into a dedicated quota delete command
The destructive --delete flag sat on "quota update", but the --yes
high-risk confirmation guards command paths rather than individual flags.
Promote it to its own "quota delete" command with a --yes guardrail, and
reduce "quota update" to QPM/TPM updates only.
2026-09-01 17:35:46 +08:00
8 changed files with 176 additions and 101 deletions
+2
View File
@@ -85,6 +85,7 @@ import {
workspaceList,
quotaList,
quotaUpdate,
quotaDelete,
quotaHistory,
quotaCheck,
permissionList,
@@ -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,
@@ -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`);
},
});
+15 -53
View File
@@ -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",
@@ -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":
@@ -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;
@@ -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] };
@@ -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",
@@ -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)}`);
+1
View File
@@ -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";
+25 -15
View File
@@ -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 () => {
@@ -68,7 +73,7 @@ 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",
@@ -76,28 +81,33 @@ describe("e2e: quota", () => {
"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);
@@ -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",
@@ -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");
});
@@ -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: quotaDashScope", () => {
test("quota list 文本输出正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
@@ -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",
+3 -2
View File
@@ -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) |
@@ -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) |
+58 -31
View File
@@ -7,12 +7,13 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Authentication | Description |
| ------------------ | -------------- | --------------------------------------------------------------- |
| `bl quota check` | Console | Check current usage against rate limits |
| `bl quota history` | Console | View quota change history |
| `bl quota list` | API Key | View model rate limits (QPM/TPM, account and workspace level) |
| `bl quota update` | API Key | Update model rate limits (QPM/TPM), or clear them with --delete |
| Command | Authentication | Description |
| ------------------ | -------------- | ------------------------------------------------------------- |
| `bl quota check` | Console | Check current usage against rate limits |
| `bl quota delete` | API Key | Clear all custom rate limits (QPM/TPM) for a model |
| `bl quota history` | Console | View quota change history |
| `bl quota list` | API Key | View model rate limits (QPM/TPM, account and workspace level) |
| `bl quota update` | API Key | Update model rate limits (QPM/TPM) |
## Command details
@@ -58,6 +59,43 @@ bl quota check --model qwen3.6-plus,qwen-turbo
bl quota check --output json
```
### `bl quota delete`
| Field | Value |
| ------------------ | -------------------------------------------------- |
| **Name** | `quota delete` |
| **Description** | Clear all custom rate limits (QPM/TPM) for a model |
| **Authentication** | API Key |
| **Usage** | `bl quota delete --model <model> [--yes]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | ---------------------------- |
| `--model <model>` | string | yes | Model name (required) |
| `--yes` | switch | no | Skip the confirmation prompt |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Irreversible — the server-side OVERLAY is reset to defaults, so your custom QPM/TPM configuration is permanently removed.
- Requires confirmation; pass --yes to skip the prompt in scripts.
#### Examples
```bash
bl quota delete --model qwen-plus
```
```bash
bl quota delete --model qwen-plus --yes
```
```bash
bl quota delete --model qwen-plus --output json
```
### `bl quota history`
| Field | Value |
@@ -149,29 +187,26 @@ bl quota list --output json
### `bl quota update`
| Field | Value |
| ------------------ | ---------------------------------------------------------------------------- |
| **Name** | `quota update` |
| **Description** | Update model rate limits (QPM/TPM), or clear them with --delete |
| **Authentication** | API Key |
| **Usage** | `bl quota update --model <model> [--rpm <n>] [--tpm <n>] [--delete] [--yes]` |
| Field | Value |
| ------------------ | --------------------------------------------------------- |
| **Name** | `quota update` |
| **Description** | Update model rate limits (QPM/TPM) |
| **Authentication** | API Key |
| **Usage** | `bl quota update --model <model> [--rpm <n>] [--tpm <n>]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | ------------------------------------------ |
| `--model <model>` | string | yes | Model name (required) |
| `--rpm <n>` | number | no | Max requests per minute (QPM) |
| `--tpm <n>` | number | no | Max tokens per minute (TPM) |
| `--delete` | switch | no | Clear all custom rate limits for the model |
| `--yes` | switch | no | Skip the confirmation prompt for --delete |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | ----------------------------- |
| `--model <model>` | string | yes | Model name (required) |
| `--rpm <n>` | number | no | Max requests per minute (QPM) |
| `--tpm <n>` | number | no | Max tokens per minute (TPM) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Fields you omit keep their current values (server-side OVERLAY merge); --delete clears all custom limits.
- --delete requires confirmation; pass --yes to skip the prompt in scripts.
- Fields you omit keep their current values (server-side OVERLAY merge). Clear all custom limits with the "quota delete" command instead.
- Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together.
#### Examples
@@ -184,14 +219,6 @@ bl quota update --model qwen-plus --rpm 60 --tpm 100000
bl quota update --model qwen3-max --tpm 500000
```
```bash
bl quota update --model qwen-plus --delete
```
```bash
bl quota update --model qwen-plus --delete --yes
```
```bash
bl quota update --model qwen-plus --rpm 60 --output json
```