mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd96fd664c |
@@ -6,16 +6,6 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
|
||||
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
## [1.18.2] - 2026-09-01
|
||||
|
||||
### Changed
|
||||
|
||||
- **Confirmation before deleting or clearing resources** — `bl finetune delete`, `bl deploy delete`, `bl dataset delete`, and `bl quota update --delete` now ask for confirmation; pass `--yes` for non-interactive use.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Skill installation reliability** — `bl skill init` now retries transient network failures, and completed Skill updates are no longer reported as failed when backup cleanup is blocked.
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### Removed
|
||||
|
||||
@@ -6,16 +6,6 @@
|
||||
|
||||
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
|
||||
|
||||
## [1.18.2] - 2026-09-01
|
||||
|
||||
### 变更
|
||||
|
||||
- **删除与清除操作增加确认** —— `bl finetune delete`、`bl deploy delete`、`bl dataset delete` 和 `bl quota update --delete` 现在会在执行前要求确认;非交互场景请传入 `--yes`。
|
||||
|
||||
### 修复
|
||||
|
||||
- **Skill 安装可靠性** —— `bl skill init` 现在会重试临时性网络故障;备份清理受阻时,已完成的 Skill 更新不再被误报为失败。
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### 已移除
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.18.2",
|
||||
"version": "1.18.1",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -181,13 +181,7 @@ function atomicSwap(tmpDir, catalogDir) {
|
||||
if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir);
|
||||
throw err;
|
||||
}
|
||||
// Best-effort cleanup (symmetric with core skills/extract.ts): the swap already
|
||||
// succeeded, so a backup deletion failure must not fail the pre-download
|
||||
try {
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* keep the backup on disk rather than report a completed swap as failed */
|
||||
}
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -220,11 +214,7 @@ async function main() {
|
||||
}
|
||||
atomicSwap(tmpDir, catalogDir);
|
||||
} catch (err) {
|
||||
try {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* cleanup must not mask the original error */
|
||||
}
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
throw err;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.18.2",
|
||||
"version": "1.18.1",
|
||||
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -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`);
|
||||
},
|
||||
});
|
||||
@@ -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)}`);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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: quota(DashScope)", () => {
|
||||
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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-core",
|
||||
"version": "1.18.2",
|
||||
"version": "1.18.1",
|
||||
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -37,8 +37,6 @@ const MODELS_FILE = "models.jsonl";
|
||||
const THROTTLE_MS = 12 * 60 * 60 * 1000; // 12h
|
||||
/** Tighter than the interactive default: the silent channel must not stall `bl advisor recommend` */
|
||||
const INDEX_TIMEOUT_MS = 3000;
|
||||
/** No retries on the silent channel: a failed sync simply tries again on the next recommend */
|
||||
const FETCH_ATTEMPTS = 1;
|
||||
|
||||
interface SyncState {
|
||||
lastChecked: number;
|
||||
@@ -110,7 +108,7 @@ function wikiLockNeedsBackfill(contentHash: string): boolean {
|
||||
/** Fetch skills/index.json via the shared registry client and extract the wiki skill entry; returns null on any failure */
|
||||
async function fetchIndexEntry(): Promise<SkillIndexEntry | null> {
|
||||
try {
|
||||
const index = await fetchSkillsIndex(INDEX_TIMEOUT_MS, FETCH_ATTEMPTS);
|
||||
const index = await fetchSkillsIndex(INDEX_TIMEOUT_MS);
|
||||
return index.skills[WIKI_SKILL_NAME] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -162,7 +160,6 @@ export async function maybeSyncWikiData(): Promise<boolean> {
|
||||
entry,
|
||||
detectInstalledAgents(),
|
||||
previousLinks,
|
||||
FETCH_ATTEMPTS,
|
||||
);
|
||||
recordWikiInLock(record.lockEntry);
|
||||
} catch {
|
||||
|
||||
@@ -100,12 +100,5 @@ export function atomicSwap(tmpDir: string, destDir: string): void {
|
||||
if (existsSync(backup) && !existsSync(destDir)) renameSync(backup, destDir);
|
||||
throw err;
|
||||
}
|
||||
// Best-effort cleanup: the swap already succeeded, so a backup deletion failure
|
||||
// (permissions, host safe-delete guards on large dirs) must not fail the install;
|
||||
// leftover .old-* dirs are inert (skill status scans ignore them)
|
||||
try {
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* keep the backup on disk rather than report a completed install as failed */
|
||||
}
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -61,22 +61,12 @@ export async function installSkillFromBuffer(
|
||||
atomicSwap(tmpDir, dest);
|
||||
return { name, path: dest, meta };
|
||||
} finally {
|
||||
// Best-effort cleanup: on failure paths this must not mask the original error,
|
||||
// and on success the dir is already renamed away (existsSync → false)
|
||||
try {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* leave the temp dir rather than hide the real error */
|
||||
}
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Install a single skill by index entry (download + validate + write to disk) */
|
||||
export async function installSkill(
|
||||
name: string,
|
||||
entry: SkillIndexEntry,
|
||||
downloadAttempts?: number,
|
||||
): Promise<InstalledSkill> {
|
||||
export async function installSkill(name: string, entry: SkillIndexEntry): Promise<InstalledSkill> {
|
||||
if (entry.compression && entry.compression !== "tar.br") {
|
||||
throw new BailianError(
|
||||
`Skill ${name} uses unsupported compression format: ${entry.compression}`,
|
||||
@@ -84,7 +74,7 @@ export async function installSkill(
|
||||
"Upgrade bailian-cli to the latest version and retry",
|
||||
);
|
||||
}
|
||||
const buffer = await downloadSkillAsset(name, entry, downloadAttempts);
|
||||
const buffer = await downloadSkillAsset(name, entry);
|
||||
return installSkillFromBuffer(name, buffer, entry.contentHash);
|
||||
}
|
||||
|
||||
@@ -126,17 +116,14 @@ export interface SkillInstallRecord {
|
||||
* entry (batch writeSkillLock for commands, best-effort upsertSkillLockEntry for silent channels).
|
||||
* recordedLinks = the skill's previously recorded fan-out paths from the lock; lets the
|
||||
* fan-out replace copy-fallback artifacts and keeps unvisited paths reclaimable.
|
||||
* downloadAttempts = registry fetch attempts (undefined → interactive default; silent
|
||||
* background channels pass 1 to fail fast instead of stalling the host command).
|
||||
*/
|
||||
export async function installSkillWithFanout(
|
||||
name: string,
|
||||
entry: SkillIndexEntry,
|
||||
agents: AgentTarget[] = detectInstalledAgents(),
|
||||
recordedLinks: string[] = [],
|
||||
downloadAttempts?: number,
|
||||
): Promise<SkillInstallRecord> {
|
||||
await installSkill(name, entry, downloadAttempts);
|
||||
await installSkill(name, entry);
|
||||
const fanout = fanOutSkillToAgents(name, agents, recordedLinks);
|
||||
return {
|
||||
lockEntry: buildSkillLockEntry(entry, fanout.links),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { BailianError } from "../errors/base.ts";
|
||||
import { ExitCode } from "../errors/codes.ts";
|
||||
import { withRetry } from "../utils/retry.ts";
|
||||
import type { SkillIndexEntry, SkillsIndex } from "./types.ts";
|
||||
|
||||
/**
|
||||
@@ -10,10 +9,8 @@ import type { SkillIndexEntry, SkillsIndex } from "./types.ts";
|
||||
*/
|
||||
const DEFAULT_REGISTRY_BASE_URL = "https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/skills";
|
||||
|
||||
const INDEX_TIMEOUT_MS = 30_000;
|
||||
const INDEX_TIMEOUT_MS = 10_000;
|
||||
const ASSET_TIMEOUT_MS = 120_000;
|
||||
/** Interactive channels retry transient failures; silent background channels pass 1 to fail fast */
|
||||
const DEFAULT_ATTEMPTS = 3;
|
||||
|
||||
export function getSkillRegistryBaseUrl(): string {
|
||||
const override = process.env.BAILIAN_SKILL_REGISTRY_URL?.trim();
|
||||
@@ -23,64 +20,55 @@ export function getSkillRegistryBaseUrl(): string {
|
||||
/**
|
||||
* Fetch the remote skill index. No local caching — the diff comparison is always
|
||||
* "live remote index vs local skill-lock.json".
|
||||
* Silent background channels (advisor sync) may pass a tighter timeout and attempts=1
|
||||
* than the interactive defaults.
|
||||
* Silent background channels (advisor sync) may pass a tighter timeout than the interactive default.
|
||||
*/
|
||||
export async function fetchSkillsIndex(
|
||||
timeoutMs: number = INDEX_TIMEOUT_MS,
|
||||
attempts: number = DEFAULT_ATTEMPTS,
|
||||
): Promise<SkillsIndex> {
|
||||
return withRetry(
|
||||
async () => {
|
||||
const url = `${getSkillRegistryBaseUrl()}/index.json`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Cannot access skill registry: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Check network connectivity; if using a private mirror, verify BAILIAN_SKILL_REGISTRY_URL configuration",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Skill registry returned HTTP ${res.status}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "Skill index not yet published or registry URL is incorrect; confirm the publisher has generated index.json"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = await res.json();
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json is not valid JSON",
|
||||
ExitCode.GENERAL,
|
||||
"Remote may be in the middle of publishing, retry later",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
const index = parsed as SkillsIndex;
|
||||
if (
|
||||
typeof index !== "object" ||
|
||||
index === null ||
|
||||
typeof index.skills !== "object" ||
|
||||
index.skills === null
|
||||
) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json has invalid structure",
|
||||
ExitCode.GENERAL,
|
||||
"Retry later or contact the publisher",
|
||||
);
|
||||
}
|
||||
return index;
|
||||
},
|
||||
{ attempts },
|
||||
);
|
||||
export async function fetchSkillsIndex(timeoutMs: number = INDEX_TIMEOUT_MS): Promise<SkillsIndex> {
|
||||
const url = `${getSkillRegistryBaseUrl()}/index.json`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Cannot access skill registry: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Check network connectivity; if using a private mirror, verify BAILIAN_SKILL_REGISTRY_URL configuration",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Skill registry returned HTTP ${res.status}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "Skill index not yet published or registry URL is incorrect; confirm the publisher has generated index.json"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = await res.json();
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json is not valid JSON",
|
||||
ExitCode.GENERAL,
|
||||
"Remote may be in the middle of publishing, retry later",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
const index = parsed as SkillsIndex;
|
||||
if (
|
||||
typeof index !== "object" ||
|
||||
index === null ||
|
||||
typeof index.skills !== "object" ||
|
||||
index.skills === null
|
||||
) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json has invalid structure",
|
||||
ExitCode.GENERAL,
|
||||
"Retry later or contact the publisher",
|
||||
);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,38 +84,29 @@ export function resolveAssetFileName(entry?: SkillIndexEntry): string {
|
||||
}
|
||||
|
||||
/** Download the tar.br archive for a single skill (one skill = one GET) */
|
||||
export async function downloadSkillAsset(
|
||||
name: string,
|
||||
entry?: SkillIndexEntry,
|
||||
attempts: number = DEFAULT_ATTEMPTS,
|
||||
): Promise<Buffer> {
|
||||
return withRetry(
|
||||
async () => {
|
||||
const url = `${getSkillRegistryBaseUrl()}/${name}/${resolveAssetFileName(entry)}`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(ASSET_TIMEOUT_MS) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Network error, retryable",
|
||||
{
|
||||
cause: err,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: HTTP ${res.status}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "index.json and skill object are temporarily inconsistent (publishing in progress), retry later"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
},
|
||||
{ attempts },
|
||||
);
|
||||
export async function downloadSkillAsset(name: string, entry?: SkillIndexEntry): Promise<Buffer> {
|
||||
const url = `${getSkillRegistryBaseUrl()}/${name}/${resolveAssetFileName(entry)}`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(ASSET_TIMEOUT_MS) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Network error, retryable",
|
||||
{
|
||||
cause: err,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: HTTP ${res.status}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "index.json and skill object are temporarily inconsistent (publishing in progress), retry later"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { brotliCompressSync } from "node:zlib";
|
||||
import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "fs";
|
||||
import { createHash } from "crypto";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { brotliCompressSync } from "zlib";
|
||||
import tar from "tar-stream";
|
||||
import { afterEach, expect, test, vi } from "vite-plus/test";
|
||||
import { BailianError } from "../src/errors/base.ts";
|
||||
@@ -10,18 +10,6 @@ import type { AgentTarget } from "../src/skills/agents.ts";
|
||||
import { isSafeEntryName } from "../src/skills/extract.ts";
|
||||
import { installSkillFromBuffer, installSkillWithFanout } from "../src/skills/installer.ts";
|
||||
import { getSkillsDir } from "../src/skills/lock.ts";
|
||||
import { downloadSkillAsset, fetchSkillsIndex } from "../src/skills/registry.ts";
|
||||
|
||||
/** rmSync wrapped in a spy so tests can simulate host deletion guards (e.g. safe-delete) */
|
||||
const fsMocks = vi.hoisted(() => ({
|
||||
rmSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs")>();
|
||||
fsMocks.rmSync.mockImplementation(actual.rmSync);
|
||||
return { ...actual, rmSync: fsMocks.rmSync };
|
||||
});
|
||||
|
||||
/** Run in an isolated temp config dir, restore env afterwards. */
|
||||
async function inTempConfigDir(fn: () => Promise<void>): Promise<void> {
|
||||
@@ -237,83 +225,15 @@ test("fanout install: downloads, links agents, and builds lock entry with merged
|
||||
});
|
||||
});
|
||||
|
||||
test("fanout install: download failure exhausts retries and leaves no canonical dir", async () => {
|
||||
test("fanout install: download failure surfaces as BailianError and leaves no canonical dir", async () => {
|
||||
await inTempConfigDir(async () => {
|
||||
const fetchMock = vi.fn(async () => ({ ok: false, status: 404 }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => ({ ok: false, status: 404 })),
|
||||
);
|
||||
await expect(
|
||||
installSkillWithFanout("demo", { contentHash: "sha256:whatever" }, []),
|
||||
).rejects.toThrow(BailianError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
expect(existsSync(join(getSkillsDir(), "demo"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- host deletion guards (safe-delete): backup cleanup must not fail a completed install ----
|
||||
|
||||
const GUARD_ERROR =
|
||||
'[safe-delete][SAFE_DELETE_BULK_CONFIRM_REQUIRED] {"count":1583,"threshold":500}';
|
||||
|
||||
test("installer: guard blocking backup deletion does not fail the install", async () => {
|
||||
await inTempConfigDir(async () => {
|
||||
await installSkillFromBuffer("demo", await buildTarBr({ "SKILL.md": VALID_SKILL_MD }));
|
||||
fsMocks.rmSync.mockImplementationOnce(() => {
|
||||
throw new Error(GUARD_ERROR);
|
||||
});
|
||||
const v2 = "---\nname: demo\ndescription: demo skill v2\n---\n";
|
||||
const installed = await installSkillFromBuffer("demo", await buildTarBr({ "SKILL.md": v2 }));
|
||||
expect(installed.name).toBe("demo");
|
||||
expect(readFileSync(join(getSkillsDir(), "demo", "SKILL.md"), "utf-8")).toBe(v2);
|
||||
// The blocked backup stays on disk but is inert (status scans ignore .old-*)
|
||||
const leftovers = readdirSync(getSkillsDir()).filter((entry) => entry.startsWith("demo.old-"));
|
||||
expect(leftovers).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("installer: temp cleanup failure does not mask the original error", async () => {
|
||||
await inTempConfigDir(async () => {
|
||||
fsMocks.rmSync.mockImplementationOnce(() => {
|
||||
throw new Error(GUARD_ERROR);
|
||||
});
|
||||
const buf = await buildTarBr({ "SKILL.md": VALID_SKILL_MD, "../evil.txt": "pwned\n" });
|
||||
await expect(installSkillFromBuffer("demo", buf)).rejects.toThrow(/unsafe tar entry/);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- registry retry policy ----
|
||||
|
||||
test("registry: index fetch retries transient failures and succeeds", async () => {
|
||||
const indexPayload = { skills: { demo: { contentHash: "sha256:abc" } } };
|
||||
const fetchMock = vi.fn(async () => {
|
||||
if (fetchMock.mock.calls.length < 3) throw new Error("network down");
|
||||
return { ok: true, status: 200, json: async () => indexPayload };
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const index = await fetchSkillsIndex(1000);
|
||||
expect(index.skills.demo.contentHash).toBe("sha256:abc");
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("registry: attempts=1 keeps the silent channel fail-fast", async () => {
|
||||
const fetchMock = vi.fn(async () => {
|
||||
throw new Error("network down");
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
await expect(fetchSkillsIndex(1000, 1)).rejects.toThrow(BailianError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("registry: asset download retries transient HTTP errors", async () => {
|
||||
const fetchMock = vi.fn(async () => {
|
||||
if (fetchMock.mock.calls.length === 1) return { ok: false, status: 503 };
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||||
};
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const buffer = await downloadSkillAsset("demo");
|
||||
expect([...buffer]).toEqual([1, 2, 3]);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "knowledge-studio-cli",
|
||||
"version": "1.18.2",
|
||||
"version": "1.18.1",
|
||||
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
|
||||
"keywords": [
|
||||
"alibaba-cloud",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-runtime",
|
||||
"version": "1.18.2",
|
||||
"version": "1.18.1",
|
||||
"description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-cli
|
||||
metadata:
|
||||
version: "1.18.2"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-finetune
|
||||
metadata:
|
||||
version: "1.18.2"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-gen
|
||||
metadata:
|
||||
version: "1.18.2"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-managed-agent
|
||||
metadata:
|
||||
version: "1.18.2"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-protocol
|
||||
metadata:
|
||||
version: "1.18.2"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-web-search
|
||||
metadata:
|
||||
version: "1.18.2"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
Reference in New Issue
Block a user