diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb07645d..09369cdb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,24 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and - **Parameter validation and messages** — Help and version queries validate supplied flags. Only explicitly declared flag names are accepted, and parsing errors support English and Chinese messages based on the configured language. - **File upload timeout** — Use `--timeout` to set the transfer timeout when uploading local files to temporary OSS storage. +## [2.2.2] - 2026-10-14 + +### Added + +- **Model compression lifecycle** — Added `bl finetune compress templates/create/list/get/watch/logs/cancel/delete` for quantizing supported full-parameter fine-tuned models, including dry-run previews, status polling, log retrieval, and safe deletion confirmation. +- **Pi agent support** — `bl config agent` and `bl mcp connect` / `bl mcp disconnect` now support Pi, writing its native configuration while preserving existing settings and redacting credentials in output. +- **Global flags before the command path** — Global flags such as `--config` can now precede the command (`bl --config token-plan auth status`) as well as follow it; both select the Profile for that invocation only, without changing `active_config`. Command-specific and credential flags must still follow the command path. +- **Decision model inference** — Added `bl decision` for TypeSafe System One requests against workspace-scoped endpoints, including `choice` / `noul` / `score` validation, JSON and text output, training-sample reuse, Beijing/Singapore routing, and dry-run previews. + +### Changed + +- **Decision dataset validation** — Aligned the `decision` schema with the current API: `state` may be a string, object, or array; `instructions` is optional; and question criteria now enforce API limits. +- **Stricter flag validation** — Only flags defined for a command are accepted. Unknown flags, missing values, and invalid values now return usage exit code 2 even when the invocation would otherwise show help, and parser errors follow the Profile language. A bare root or command-group invocation still shows help and returns 0. `--timeout` uses the same positive-finite rule on the command line and in resolved settings. + +### Fixed + +- **Temporary file upload timeout** — Uploads of local files to temporary OSS storage now honor `--timeout` instead of a fixed 120-second limit. + ## [2.2.1] - 2026-10-12 ### Fixed diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md index 36a480b0a..b804b4df6 100644 --- a/CHANGELOG.zh.md +++ b/CHANGELOG.zh.md @@ -20,6 +20,24 @@ - **参数校验与提示** —— 帮助和版本查询会校验已传入的参数;仅接受命令支持的参数名,参数解析错误支持按配置语言显示中英文提示。 - **文件上传超时** —— `--timeout` 可设置本地文件上传至临时 OSS 的传输超时时间。 +## [2.2.2] - 2026-10-14 + +### 新增 + +- **模型压缩完整生命周期** —— 新增 `bl finetune compress templates/create/list/get/watch/logs/cancel/delete`,支持对符合条件的全参调优模型执行量化压缩,并覆盖 dry-run 预览、状态轮询、日志查询和安全删除确认。 +- **支持 Pi Agent** —— `bl config agent` 与 `bl mcp connect` / `bl mcp disconnect` 新增支持 Pi,写入其原生配置并保留已有设置,输出中的凭据会脱敏。 +- **全局参数可前置** —— `--config` 等全局参数现在既可放在命令之后,也可放在命令之前(`bl --config token-plan auth status`);两种写法都只对本次调用选择 Profile,不修改 `active_config`。命令专属参数和凭证域参数仍须放在命令路径之后。 +- **决策模型推理** —— 新增 `bl decision`,通过工作空间专属 Endpoint 调用 TypeSafe System One,支持 `choice` / `noul` / `score` 校验、JSON 与文本输出、复用训练样本、北京/新加坡地域路由及 dry-run 预览。 + +### 变更 + +- **决策数据集校验** —— 与最新 API 对齐:`state` 可为字符串、对象或数组,`instructions` 改为可选,并按接口限制校验各类问题的 criteria。 +- **更严格的参数校验** —— 只接受命令已定义的参数。未知参数、缺少参数值或参数值非法时,即使本次调用原本会显示帮助,也返回用法错误退出码 2,解析错误提示跟随 Profile 语言;不带参数的根命令或命令组仍显示帮助并返回 0。`--timeout` 在命令行和最终生效配置中使用同一套“正有限数”校验规则。 + +### 修复 + +- **临时文件上传超时** —— 本地文件上传到临时 OSS 存储时会遵循 `--timeout`,不再固定为 120 秒。 + ## [2.2.1] - 2026-10-12 ### 修复 diff --git a/README.md b/README.md index 938a2a305..b35b055b0 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Once installed, just describe your task to your AI Agent — no need to assemble | Monitoring & alerts | "Show my model call stats, failures and logs, and create an alert rule." | | Throughput reservations | "List my throughput reservations and capacity instances, then scale, renew or release capacity and wait for the operation to finish." | | Model selection | "Recommend a model for image understanding and customer support." | +| Decision model | "Use the Bailian decision model to route this ticket and assess whether it needs escalation." | | About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." | > More examples and scenarios: [Aliyun Model Studio CLI Site](https://bailian.console.aliyun.com/cli?spm=a2ty07.github) diff --git a/README.zh.md b/README.zh.md index 08aba3dad..4cdebf186 100644 --- a/README.zh.md +++ b/README.zh.md @@ -120,6 +120,7 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex | 监控与告警 | “查看我的模型调用统计、失败明细和调用日志,并创建一条告警规则。” | | 吞吐预留 | “查看我的吞吐预留及容量实例,并对容量进行扩缩、续订或释放,再等待操作完成。” | | 模型选型 | “推荐一个适合图片理解和智能客服的模型。” | +| 决策模型 | “用百炼决策模型给这张工单分流,并判断是否需要升级处理。” | | 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” | > 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?spm=a2ty07.github) diff --git a/packages/cli/README.md b/packages/cli/README.md index 938a2a305..b35b055b0 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -121,6 +121,7 @@ Once installed, just describe your task to your AI Agent — no need to assemble | Monitoring & alerts | "Show my model call stats, failures and logs, and create an alert rule." | | Throughput reservations | "List my throughput reservations and capacity instances, then scale, renew or release capacity and wait for the operation to finish." | | Model selection | "Recommend a model for image understanding and customer support." | +| Decision model | "Use the Bailian decision model to route this ticket and assess whether it needs escalation." | | About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." | > More examples and scenarios: [Aliyun Model Studio CLI Site](https://bailian.console.aliyun.com/cli?spm=a2ty07.github) diff --git a/packages/cli/README.zh.md b/packages/cli/README.zh.md index 08aba3dad..4cdebf186 100644 --- a/packages/cli/README.zh.md +++ b/packages/cli/README.zh.md @@ -120,6 +120,7 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex | 监控与告警 | “查看我的模型调用统计、失败明细和调用日志,并创建一条告警规则。” | | 吞吐预留 | “查看我的吞吐预留及容量实例,并对容量进行扩缩、续订或释放,再等待操作完成。” | | 模型选型 | “推荐一个适合图片理解和智能客服的模型。” | +| 决策模型 | “用百炼决策模型给这张工单分流,并判断是否需要升级处理。” | | 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” | > 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?spm=a2ty07.github) diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 2a8749489..25c26bd24 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -14,6 +14,7 @@ import { videoTaskGet, videoDownload, visionDescribe, + decisionPredict, configShow, configSet, configList, @@ -154,6 +155,14 @@ import { finetuneWatch, finetuneCapability, finetunePrice, + finetuneCompressTemplates, + finetuneCompressCreate, + finetuneCompressList, + finetuneCompressGet, + finetuneCompressWatch, + finetuneCompressLogs, + finetuneCompressCancel, + finetuneCompressDelete, deployTextCreate, deployAudioCreate, deployImageCreate, @@ -298,6 +307,7 @@ export const commands: Record = { "video task get": videoTaskGet, "video download": videoDownload, "vision describe": visionDescribe, + decision: decisionPredict, "config show": configShow, "config set": configSet, "config list": configList, @@ -440,6 +450,14 @@ export const commands: Record = { "finetune watch": finetuneWatch, "finetune capability": finetuneCapability, "finetune price": finetunePrice, + "finetune compress templates": finetuneCompressTemplates, + "finetune compress create": finetuneCompressCreate, + "finetune compress list": finetuneCompressList, + "finetune compress get": finetuneCompressGet, + "finetune compress watch": finetuneCompressWatch, + "finetune compress logs": finetuneCompressLogs, + "finetune compress cancel": finetuneCompressCancel, + "finetune compress delete": finetuneCompressDelete, "deploy text create": deployTextCreate, "deploy audio create": deployAudioCreate, "deploy image create": deployImageCreate, diff --git a/packages/commands/src/commands/dataset/upload.ts b/packages/commands/src/commands/dataset/upload.ts index 48677ba2c..d22284cba 100644 --- a/packages/commands/src/commands/dataset/upload.ts +++ b/packages/commands/src/commands/dataset/upload.ts @@ -6,6 +6,7 @@ import { formatIssue, MAX_DATASET_BYTES, MAX_CPT_BYTES, + MAX_DECISION_DATASET_BYTES, MAX_MEDIA_ZIP_BYTES, BailianError, ExitCode, @@ -18,9 +19,10 @@ const UPLOAD_FLAGS = { type: "string", valueHint: "", description: { - "en-US": "Local dataset file (.jsonl or .zip; ≤200MB SFT/DPO, ≤300MB CPT, ≤2GB media zip)", + "en-US": + "Local dataset file (.jsonl or .zip; ≤200MB SFT/DPO, ≤300MB CPT/decision, ≤2GB media zip)", "zh-CN": - "本地数据集文件(.jsonl 或 .zip;SFT/DPO 不超过 200MB,CPT 不超过 300MB,媒体 ZIP 不超过 2GB)", + "本地数据集文件(.jsonl 或 .zip;SFT/DPO 不超过 200MB,CPT/决策模型不超过 300MB,媒体 ZIP 不超过 2GB)", }, required: true, }, @@ -37,9 +39,9 @@ const UPLOAD_FLAGS = { valueHint: "", description: { "en-US": - 'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.', + 'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "decision" (state/questions), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.', "zh-CN": - '记录 Schema:"chatml"(SFT)、"dpo"(chosen/rejected)、"cpt"(原始文本)、"tts"(音频)、"image"(图片生成)或 "video"(视频生成)。默认逐条自动识别。', + '记录 Schema:"chatml"(SFT)、"dpo"(chosen/rejected)、"cpt"(原始文本)、"decision"(state/questions 决策数据)、"tts"(音频)、"image"(图片生成)或 "video"(视频生成)。默认逐条自动识别。', }, }, noValidate: { @@ -65,12 +67,13 @@ export default defineCommand({ }, auth: "apiKey", usageArgs: - "--file [--purpose ] [--schema ] [--no-validate] [--full-validate]", + "--file [--purpose ] [--schema ] [--no-validate] [--full-validate]", flags: UPLOAD_FLAGS, exampleArgs: [ "--file train.jsonl", "--file dpo.jsonl --schema dpo", "--file cpt.jsonl --schema cpt", + "--file decision.jsonl --schema decision", "--file audio.zip --schema tts", "--file eval.jsonl --purpose evaluation", "--file train.jsonl --full-validate", @@ -79,21 +82,21 @@ export default defineCommand({ notes: [ { "en-US": - 'Supports .jsonl (text) and .zip (audio/image/video archives with a data.jsonl manifest). Six record schemas are recognized: chatml = {messages:[...]} (SFT); dpo = {messages:[...], chosen, rejected}; cpt = {text:"..."} (continual pre-training, raw text); tts = {wav_fn:"train/xxx.wav", text:"..."} (audio fine-tuning); image = {img_path:"..."} (image generation); video = {first_frame_path:...} (video generation).', + 'Supports .jsonl (text/decision) and .zip (audio/image/video archives with a data.jsonl manifest). Seven record schemas are recognized: chatml = {messages:[...]} (SFT); dpo = {messages:[...], chosen, rejected}; cpt = {text:"..."}; decision = {state:{content}, questions:{...}}; tts = {wav_fn:"train/xxx.wav", text:"..."}; image = {img_path:"..."}; video = {first_frame_path:...}.', "zh-CN": - '支持 .jsonl(文本)和 .zip(包含 data.jsonl 清单的音频、图片或视频归档)。可识别六种记录 Schema:chatml = {messages:[...]}(SFT);dpo = {messages:[...], chosen, rejected};cpt = {text:"..."}(持续预训练,原始文本);tts = {wav_fn:"train/xxx.wav", text:"..."}(音频微调);image = {img_path:"..."}(图片生成);video = {first_frame_path:...}(视频生成)。', + '支持 .jsonl(文本/决策)和 .zip(包含 data.jsonl 清单的音频、图片或视频归档)。可识别七种记录 Schema:chatml = {messages:[...]}(SFT);dpo = {messages:[...], chosen, rejected};cpt = {text:"..."};decision = {state:{content}, questions:{...}};tts = {wav_fn:"train/xxx.wav", text:"..."};image = {img_path:"..."};video = {first_frame_path:...}。', }, { "en-US": - "With no --schema, a record carrying wav_fn is validated as TTS, img_path as image, chosen/rejected as DPO, text (no messages) as CPT, otherwise ChatML.", + "With no --schema, state/questions is validated as decision data, wav_fn as TTS, img_path as image, chosen/rejected as DPO, text (no messages) as CPT, otherwise ChatML.", "zh-CN": - "未指定 --schema 时,包含 wav_fn 的记录按 TTS 验证,包含 img_path 的按 image 验证,包含 chosen/rejected 的按 DPO 验证,仅含 text(无 messages)的按 CPT 验证,其他记录按 ChatML 验证。", + "未指定 --schema 时,state/questions 记录按 decision 验证,包含 wav_fn 的按 TTS 验证,包含 img_path 的按 image 验证,包含 chosen/rejected 的按 DPO 验证,仅含 text(无 messages)的按 CPT 验证,其他记录按 ChatML 验证。", }, { "en-US": - "Upload cap: 200MB SFT/DPO text, 300MB CPT, 2GB media zip. Upload uses the OpenAI-compatible /compatible-mode/v1/files endpoint so the purpose tag is persisted (the DashScope-native /api/v1/files drops it).", + "Upload cap: 200MB SFT/DPO text, 300MB CPT/decision data, 2GB media zip. For decision files over 200MB, pass --schema decision so the correct cap is applied. Upload uses the OpenAI-compatible /compatible-mode/v1/files endpoint so the purpose tag is persisted.", "zh-CN": - "上传上限:SFT/DPO 文本 200MB、CPT 300MB、媒体 ZIP 2GB。上传使用 OpenAI 兼容的 /compatible-mode/v1/files Endpoint,以便保留 purpose 标签;DashScope 原生 /api/v1/files 会丢弃该标签。", + "上传上限:SFT/DPO 文本 200MB、CPT/决策模型数据 300MB、媒体 ZIP 2GB。决策数据超过 200MB 时请显式传入 --schema decision,以应用正确上限。上传使用 OpenAI 兼容的 /compatible-mode/v1/files 接口,以确保持久化 purpose 标签。", }, ], async run(ctx) { @@ -101,13 +104,15 @@ export default defineCommand({ const filePath = flags.file; const purpose = flags.purpose || "fine-tune"; const schema = parseDatasetSchemaFlag(flags.schema); - // Size caps differ per training type: SFT/DPO 200MB, CPT 300MB, media ZIP 2GB. + // 大小上限按平台数据类型区分;自动识别仍采用保守的文本上限,避免放宽已有格式。 const isMediaSchema = schema === "image" || schema === "video"; const maxBytes = isMediaSchema ? MAX_MEDIA_ZIP_BYTES - : schema === "cpt" - ? MAX_CPT_BYTES - : MAX_DATASET_BYTES; + : schema === "decision" + ? MAX_DECISION_DATASET_BYTES + : schema === "cpt" + ? MAX_CPT_BYTES + : MAX_DATASET_BYTES; if (!flags.noValidate) { const result = await validateDataset(filePath, { diff --git a/packages/commands/src/commands/dataset/validate.ts b/packages/commands/src/commands/dataset/validate.ts index 055a21037..fa34fcff5 100644 --- a/packages/commands/src/commands/dataset/validate.ts +++ b/packages/commands/src/commands/dataset/validate.ts @@ -2,6 +2,7 @@ import { defineCommand, validateDataset, parseDatasetSchemaFlag, + MAX_DECISION_DATASET_BYTES, BailianError, ExitCode, type FlagsDef, @@ -30,9 +31,9 @@ const VALIDATE_FLAGS = { valueHint: "", description: { "en-US": - 'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.', + 'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "decision" (state/questions), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.', "zh-CN": - '记录 Schema:"chatml"(SFT)、"dpo"(chosen/rejected)、"cpt"(原始文本)、"tts"(音频)、"image"(图片生成)或 "video"(视频生成)。默认逐条自动识别。', + '记录 Schema:"chatml"(SFT)、"dpo"(chosen/rejected)、"cpt"(原始文本)、"decision"(state/questions 决策数据)、"tts"(音频)、"image"(图片生成)或 "video"(视频生成)。默认逐条自动识别。', }, }, } satisfies FlagsDef; @@ -44,12 +45,13 @@ export default defineCommand({ }, // 纯本地校验,不触网、不需 API key(与 `pipeline validate` 一致)。 auth: "none", - usageArgs: "--file [--full-validate] [--schema ]", + usageArgs: "--file [--full-validate] [--schema ]", flags: VALIDATE_FLAGS, exampleArgs: [ "--file train.jsonl", "--file dpo.jsonl --schema dpo", "--file cpt.jsonl --schema cpt", + "--file decision.jsonl --schema decision", "--file audio.zip --schema tts", "--file wan-i2v-training-dataset.zip --schema video", "--file eval.jsonl --full-validate", @@ -64,15 +66,15 @@ export default defineCommand({ }, { "en-US": - 'Schemas: chatml = {messages:[...]} (SFT); dpo = {messages:[...], chosen, rejected}; cpt = {text:"..."} (continual pre-training, raw text); tts = {wav_fn:"train/xxx.wav", text:"..."} (audio fine-tuning); image = {img_path:"..."} (image generation); video = {first_frame_path:"...", video_path:"..."} (video generation, i2v first-frame or kf2v first+last-frame with last_frame_path).', + 'Schemas: chatml = {messages:[...]}; dpo = {messages:[...], chosen, rejected}; cpt = {text:"..."}; decision = {state:{content}, questions:{...}}; tts = {wav_fn:"train/xxx.wav", text:"..."}; image = {img_path:"..."}; video = {first_frame_path:"...", video_path:"..."}.', "zh-CN": - 'Schema:chatml = {messages:[...]}(SFT);dpo = {messages:[...], chosen, rejected};cpt = {text:"..."}(持续预训练,原始文本);tts = {wav_fn:"train/xxx.wav", text:"..."}(音频微调);image = {img_path:"..."}(图片生成);video = {first_frame_path:"...", video_path:"..."}(视频生成,支持 i2v 首帧或通过 last_frame_path 指定 kf2v 首尾帧)。', + 'Schema:chatml = {messages:[...]};dpo = {messages:[...], chosen, rejected};cpt = {text:"..."};decision = {state:{content}, questions:{...}};tts = {wav_fn:"train/xxx.wav", text:"..."};image = {img_path:"..."};video = {first_frame_path:"...", video_path:"..."}。', }, { "en-US": - "With no --schema, a record carrying wav_fn is validated as TTS, img_path as image, first_frame_path/video_path as video, chosen/rejected as DPO, text (no messages) as CPT, otherwise ChatML. Pass --schema to require a specific shape on every record.", + "With no --schema, state/questions is validated as decision data, wav_fn as TTS, img_path as image, first_frame_path/video_path as video, chosen/rejected as DPO, text (no messages) as CPT, otherwise ChatML.", "zh-CN": - "未指定 --schema 时,包含 wav_fn 的记录按 TTS 验证,包含 img_path 的按 image 验证,包含 first_frame_path/video_path 的按 video 验证,包含 chosen/rejected 的按 DPO 验证,仅含 text(无 messages)的按 CPT 验证,其他记录按 ChatML 验证。使用 --schema 要求每条记录符合指定结构。", + "未指定 --schema 时,state/questions 记录按 decision 验证,包含 wav_fn 的按 TTS 验证,包含 img_path 的按 image 验证,包含 first_frame_path/video_path 的按 video 验证,包含 chosen/rejected 的按 DPO 验证,仅含 text(无 messages)的按 CPT 验证,其他记录按 ChatML 验证。", }, { "en-US": @@ -98,7 +100,11 @@ export default defineCommand({ return; } - const result = await validateDataset(filePath, { fullValidate: flags.fullValidate, schema }); + const result = await validateDataset(filePath, { + fullValidate: flags.fullValidate, + schema, + maxBytes: schema === "decision" ? MAX_DECISION_DATASET_BYTES : undefined, + }); if (settings.quiet) { emitBare(result.valid ? "ok" : "fail"); diff --git a/packages/commands/src/commands/decision/predict.ts b/packages/commands/src/commands/decision/predict.ts new file mode 100644 index 000000000..509a264e4 --- /dev/null +++ b/packages/commands/src/commands/decision/predict.ts @@ -0,0 +1,178 @@ +import { + defineCommand, + detectOutputFormat, + DECISION_REGIONS, + DEFAULT_DECISION_MODEL, + DEFAULT_DECISION_REGION, + type DecisionRequest, + type DecisionResponse, +} from "bailian-cli-core"; +import { emitBare, emitRequestId, emitResult } from "bailian-cli-runtime"; +import { + buildDecisionInput, + decisionInputFlagsError, + formatDecisionAnswers, + formatDecisionStats, + resolveDecisionEndpoint, +} from "./shared.ts"; + +export default defineCommand({ + description: { + "en-US": "Run the decision model: typed choice / noul / score answers with probabilities", + "zh-CN": "调用决策模型:一次返回 choice / noul / score 判定结果及概率分布", + }, + auth: "apiKey", + usageArgs: + "(--state | --state-file ) (--questions | --questions-file ) | --request-file ", + flags: { + model: { + type: "string", + valueHint: "", + description: { + "en-US": `Decision model ID (default: ${DEFAULT_DECISION_MODEL})`, + "zh-CN": `决策模型 ID(默认:${DEFAULT_DECISION_MODEL})`, + }, + }, + state: { + type: "string", + valueHint: "", + description: { + "en-US": "Business state to decide on, sent as a string", + "zh-CN": "待决策的业务状态,按字符串发送", + }, + }, + stateFile: { + type: "string", + valueHint: "", + description: { + "en-US": + "Read state from a file (.json is parsed as an object/array, others as text; - for stdin)", + "zh-CN": "从文件读取 state(.json 解析为对象/数组,其他按文本读取;- 表示 stdin)", + }, + }, + questions: { + type: "string", + valueHint: "", + description: { + "en-US": "Question table JSON, keyed by question ID", + "zh-CN": "问题表 JSON,键为问题 ID", + }, + }, + questionsFile: { + type: "string", + valueHint: "", + description: { + "en-US": 'Read the question table from a JSON file ({...} or {"questions":{...}})', + "zh-CN": '从 JSON 文件读取问题表({...} 或 {"questions":{...}})', + }, + }, + requestFile: { + type: "string", + valueHint: "", + description: { + "en-US": "Full request JSON {state, questions}; label/target are stripped", + "zh-CN": "完整请求 JSON {state, questions};自动去掉 label/target", + }, + }, + workspaceId: { + type: "string", + valueHint: "", + description: { + "en-US": "Workspace ID for the endpoint host (or set BAILIAN_WORKSPACE_ID)", + "zh-CN": "用于拼接请求域名的 Workspace ID(也可设置 BAILIAN_WORKSPACE_ID)", + }, + }, + region: { + type: "string", + valueHint: "", + choices: DECISION_REGIONS, + description: { + "en-US": `Endpoint region: ${DECISION_REGIONS.join(", ")} (default: ${DEFAULT_DECISION_REGION})`, + "zh-CN": `请求地域:${DECISION_REGIONS.join("、")}(默认:${DEFAULT_DECISION_REGION})`, + }, + }, + }, + exampleArgs: [ + { + "en-US": + '--workspace-id --state "Is this review asking for a refund?" --questions \'{"refund":{"type":"noul","instructions":"Asking for a refund?"}}\'', + "zh-CN": + '--workspace-id --state "这段评论是否在索要退款?" --questions \'{"refund":{"type":"noul","instructions":"是否在索要退款?"}}\'', + }, + "--state-file ticket.json --questions-file workload.json", + "--request-file sample.json --model decision-model-preview", + "--region ap-southeast-1 --request-file sample.json", + "--request-file sample.json --dry-run", + ], + notes: [ + { + "en-US": + "Endpoint: configured --base-url, DASHSCOPE_BASE_URL, or profile base_url takes precedence; otherwise https://{workspace}.{region}.maas.aliyuncs.com/compatible-mode/v1/systemone, with workspace from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.", + "zh-CN": + "请求地址:已配置的 --base-url、DASHSCOPE_BASE_URL 或 Profile base_url 优先;否则为 https://{workspace}.{region}.maas.aliyuncs.com/compatible-mode/v1/systemone,工作空间按 --workspace-id、BAILIAN_WORKSPACE_ID、配置项 workspace_id 解析。", + }, + { + "en-US": + "Question types: choice (criteria: option → description, 1-255), noul (optional criteria with true/false descriptions), score (criteria: levels from low to high, 2-255; 3-7 recommended). Keep questions per request at 16 or fewer; latency grows roughly linearly.", + "zh-CN": + "问题类型:choice(criteria 为选项 → 描述,1-255 项)、noul(可选 criteria,含 true/false 描述)、score(criteria 为从低到高的等级,2-255 级,建议 3-7 级)。建议每次请求不超过 16 个问题,延迟随问题数近线性增长。", + }, + { + "en-US": + "Output defaults to the full JSON response. Use --output text for one human-readable line per question; the request ID and usage summary are written to stderr.", + "zh-CN": + "默认输出完整 JSON 响应。使用 --output text 可按问题逐行输出易读结果;请求 ID 与用量摘要写入 stderr。", + }, + { + "en-US": + "Training samples can be reused directly: label/target are stripped before sending. Sampling parameters (temperature, max_tokens, ...) are not supported by this API.", + "zh-CN": + "训练样本可直接复用:发送前自动去掉 label/target。该接口不支持 temperature、max_tokens 等采样参数。", + }, + ], + validate: (flags) => { + const inputError = decisionInputFlagsError(flags); + if (typeof inputError === "string" || !inputError) return inputError; + return `${inputError["en-US"]} / ${inputError["zh-CN"]}`; + }, + async run(ctx) { + const { settings, flags } = ctx; + const input = buildDecisionInput(flags, ctx.localize); + const body: DecisionRequest = { + model: flags.model || input.model || DEFAULT_DECISION_MODEL, + state: input.state, + questions: input.questions, + }; + const endpoint = resolveDecisionEndpoint({ + workspaceId: flags.workspaceId || settings.workspaceId, + region: flags.region ?? DEFAULT_DECISION_REGION, + binName: ctx.identity.binName, + client: ctx.client, + localize: ctx.localize, + }); + const format = settings.outputExplicit ? detectOutputFormat(settings.output) : "json"; + + if (settings.dryRun) { + emitResult({ endpoint, request: body }, format); + return; + } + + const response = await ctx.client.requestJson({ + path: endpoint, + method: "POST", + body, + }); + + if (format !== "text") { + emitResult(response, format); + return; + } + + emitBare(formatDecisionAnswers(response)); + if (!settings.quiet) { + const stats = formatDecisionStats(response); + if (stats) process.stderr.write(`${stats}\n`); + } + emitRequestId(response.request_id, settings.quiet); + }, +}); diff --git a/packages/commands/src/commands/decision/shared.ts b/packages/commands/src/commands/decision/shared.ts new file mode 100644 index 000000000..c2d4a83f1 --- /dev/null +++ b/packages/commands/src/commands/decision/shared.ts @@ -0,0 +1,270 @@ +import { extname } from "path"; +import { + BailianError, + ExitCode, + decisionPath, + inspectDecisionQuestions, + inspectDecisionState, + isDecisionObject, + readTextFromPathOrStdin, + stripDecisionTrainingFields, + unwrapDecisionQuestions, + workspaceMaaSBaseUrl, + type Client, + type DecisionAnswer, + type DecisionQuestion, + type DecisionResponse, + type DecisionState, + type LocalizedText, +} from "bailian-cli-core"; + +type Localize = (text: LocalizedText) => string; + +export interface DecisionInputFlags { + state?: string; + stateFile?: string; + questions?: string; + questionsFile?: string; + requestFile?: string; +} + +export interface DecisionInput { + state: DecisionState; + questions: Record; + /** `--request-file` 内携带的 model,`--model` 未指定时使用。 */ + model?: string; +} + +function usageError(localize: Localize, text: LocalizedText, hint?: LocalizedText): BailianError { + return new BailianError(localize(text), ExitCode.USAGE, hint ? localize(hint) : undefined); +} + +function parseJson(raw: string, source: string, localize: Localize): unknown { + try { + return JSON.parse(raw); + } catch (error) { + const reason = error instanceof Error ? error.message : String(error); + throw usageError(localize, { + "en-US": `${source} is not valid JSON: ${reason}`, + "zh-CN": `${source} 不是合法的 JSON:${reason}`, + }); + } +} + +function countProvided(...values: Array): number { + return values.filter((value) => value !== undefined).length; +} + +/** 返回输入来源组合错误;供 command validate 在鉴权前执行。 */ +export function decisionInputFlagsError(flags: DecisionInputFlags): LocalizedText | undefined { + if (flags.requestFile !== undefined) { + if (countProvided(flags.state, flags.stateFile, flags.questions, flags.questionsFile) > 0) { + return { + "en-US": + "--request-file cannot be combined with --state, --state-file, --questions, or --questions-file.", + "zh-CN": + "--request-file 不能与 --state、--state-file、--questions、--questions-file 同时使用。", + }; + } + return undefined; + } + + const stateSources = countProvided(flags.state, flags.stateFile); + if (stateSources !== 1) { + return { + "en-US": + stateSources === 0 + ? "Provide --state, --state-file, or --request-file." + : "Use only one of --state or --state-file.", + "zh-CN": + stateSources === 0 + ? "请提供 --state、--state-file 或 --request-file。" + : "--state 与 --state-file 只能选择一个。", + }; + } + + const questionSources = countProvided(flags.questions, flags.questionsFile); + if (questionSources !== 1) { + return { + "en-US": + questionSources === 0 + ? "Provide --questions or --questions-file." + : "Use only one of --questions or --questions-file.", + "zh-CN": + questionSources === 0 + ? "请提供 --questions 或 --questions-file。" + : "--questions 与 --questions-file 只能选择一个。", + }; + } + return undefined; +} + +/** 校验输入来源组合:--request-file 独占;否则 state 与 questions 各选一种来源。 */ +export function validateDecisionInputFlags(flags: DecisionInputFlags, localize: Localize): void { + const inputError = decisionInputFlagsError(flags); + if (inputError) throw usageError(localize, inputError); +} + +function readStateFile(path: string, localize: Localize): unknown { + const raw = readTextFromPathOrStdin(path); + return extname(path).toLowerCase() === ".json" ? parseJson(raw, path, localize) : raw; +} + +function readRawInput( + flags: DecisionInputFlags, + localize: Localize, +): { state: unknown; questions: unknown; model?: string } { + if (flags.requestFile !== undefined) { + const parsed = parseJson( + readTextFromPathOrStdin(flags.requestFile), + flags.requestFile, + localize, + ); + if (!isDecisionObject(parsed)) { + throw usageError(localize, { + "en-US": `${flags.requestFile} must be a JSON object with "state" and "questions".`, + "zh-CN": `${flags.requestFile} 必须是包含 "state" 和 "questions" 的 JSON 对象。`, + }); + } + return { + state: parsed.state, + questions: parsed.questions, + model: typeof parsed.model === "string" && parsed.model ? parsed.model : undefined, + }; + } + + const state = + flags.stateFile !== undefined ? readStateFile(flags.stateFile, localize) : flags.state; + const questions = + flags.questionsFile !== undefined + ? parseJson(readTextFromPathOrStdin(flags.questionsFile), flags.questionsFile, localize) + : parseJson(flags.questions ?? "", "--questions", localize); + return { state, questions: unwrapDecisionQuestions(questions) }; +} + +/** + * 按 flag 组装推理输入:读取文件、解包问题文件、去掉训练字段(label / target), + * 再按决策模型 API 规则做本地结构校验;不通过时抛 USAGE 错误,不发请求。 + */ +export function buildDecisionInput(flags: DecisionInputFlags, localize: Localize): DecisionInput { + validateDecisionInputFlags(flags, localize); + const raw = readRawInput(flags, localize); + const questions = stripDecisionTrainingFields(raw.questions); + + const issues = [...inspectDecisionState(raw.state), ...inspectDecisionQuestions(questions)]; + if (issues.length > 0) { + throw usageError( + localize, + { + "en-US": "Invalid decision request:", + "zh-CN": "决策请求不合法:", + }, + { + "en-US": issues.map((issue) => `- ${issue.message["en-US"]}`).join("\n"), + "zh-CN": issues.map((issue) => `- ${issue.message["zh-CN"]}`).join("\n"), + }, + ); + } + + return { + state: raw.state as DecisionState, + questions: questions as Record, + model: raw.model, + }; +} + +/** + * 解析请求地址:显式配置的 Base URL(--base-url / DASHSCOPE_BASE_URL / Profile base_url)优先; + * 否则按 --workspace-id → BAILIAN_WORKSPACE_ID → 配置项 workspace_id 拼 workspace 域名。 + */ +export function resolveDecisionEndpoint(ctx: { + workspaceId?: string; + region: string; + binName: string; + client: Pick; + localize: Localize; +}): string { + return ctx.client.url(decisionPath(), () => { + if (!ctx.workspaceId) { + throw usageError( + ctx.localize, + { + "en-US": "Workspace ID is required for the decision model endpoint.", + "zh-CN": "调用决策模型需要 Workspace ID。", + }, + { + "en-US": `Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: ${ctx.binName} config set workspace_id . You can also override the endpoint with --base-url.`, + "zh-CN": `请传入 --workspace-id、设置环境变量 BAILIAN_WORKSPACE_ID,或执行:${ctx.binName} config set workspace_id 。也可以用 --base-url 覆盖请求地址。`, + }, + ); + } + return workspaceMaaSBaseUrl(ctx.workspaceId, ctx.region); + }); +} + +function formatProbability(value: unknown): string { + return typeof value === "number" ? String(Number(value.toFixed(4))) : String(value); +} + +function formatProbabilities(probabilities: Record | undefined): string { + if (!probabilities) return ""; + return Object.entries(probabilities) + .map(([optionName, probability]) => `${optionName}=${formatProbability(probability)}`) + .join(" "); +} + +function topLevel(answer: DecisionAnswer): string | undefined { + const entries = Object.entries(answer.probabilities ?? {}); + if (entries.length === 0) return undefined; + const [levelKey] = entries.reduce((best, entry) => (entry[1] > best[1] ? entry : best)); + const description = answer.legend?.[levelKey]; + return description ? `${levelKey} ${description}` : levelKey; +} + +function formatAnswer(answer: DecisionAnswer): string { + if (answer.type === "choice" && answer.choice !== undefined) { + return [ + `choice=${answer.choice}`, + answer.confidence !== undefined ? `confidence=${formatProbability(answer.confidence)}` : "", + formatProbabilities(answer.probabilities), + ] + .filter(Boolean) + .join(" "); + } + if (answer.type === "noul" && answer.noul !== undefined) { + return `noul P(yes)=${formatProbability(answer.noul)}`; + } + if (answer.type === "score" && answer.score !== undefined) { + const level = topLevel(answer); + return [ + `score=${formatProbability(answer.score)}`, + answer.confidence !== undefined ? `confidence=${formatProbability(answer.confidence)}` : "", + level ? `top=${level}` : "", + ] + .filter(Boolean) + .join(" "); + } + return JSON.stringify(answer); +} + +/** 文本模式:每个问题一行;未知结构回落为该答案的 JSON。 */ +export function formatDecisionAnswers(response: DecisionResponse): string { + const answers = Object.entries(response.answers ?? {}); + if (answers.length === 0) return JSON.stringify(response); + const idWidth = Math.max(...answers.map(([questionId]) => questionId.length)); + return answers + .map( + ([questionId, answer]) => + `${questionId.padEnd(idWidth)} ${isDecisionObject(answer) ? formatAnswer(answer) : JSON.stringify(answer)}`, + ) + .join("\n"); +} + +/** 文本模式统计行(写 stderr):input_tokens / latency_ms。 */ +export function formatDecisionStats(response: DecisionResponse): string | undefined { + const parts = [ + response.usage?.input_tokens !== undefined ? `input_tokens=${response.usage.input_tokens}` : "", + response.latency_ms !== undefined ? `latency_ms=${response.latency_ms}` : "", + ].filter(Boolean); + return parts.length > 0 ? parts.join(" ") : undefined; +} diff --git a/packages/commands/src/commands/finetune/compress-cancel.ts b/packages/commands/src/commands/finetune/compress-cancel.ts new file mode 100644 index 000000000..6d15ea310 --- /dev/null +++ b/packages/commands/src/commands/finetune/compress-cancel.ts @@ -0,0 +1,27 @@ +import { defineCommand, cancelCompressJob } from "bailian-cli-core"; +import { emitBare, emitResult } from "bailian-cli-runtime"; +import { COMPRESS_JOB_ID_FLAGS } from "./compress-shared.ts"; + +export default defineCommand({ + description: { "en-US": "Cancel a compression job", "zh-CN": "取消压缩任务" }, + auth: "apiKey", + usageArgs: "--job-id ", + flags: COMPRESS_JOB_ID_FLAGS, + exampleArgs: ["--job-id quant-xxx", "--job-id quant-xxx --dry-run"], + notes: [ + { + "en-US": "Only PENDING, QUEUING, or RUNNING jobs can be cancelled.", + "zh-CN": "只有 PENDING、QUEUING 或 RUNNING 状态的任务可以取消。", + }, + ], + async run(ctx) { + const jobId = ctx.flags.jobId; + if (ctx.settings.dryRun) { + emitResult({ action: "finetune.compress.cancel", job_id: jobId }, "json"); + return; + } + const response = await cancelCompressJob(ctx.client, jobId); + if (ctx.settings.quiet) emitBare(jobId); + else emitResult(response, "json"); + }, +}); diff --git a/packages/commands/src/commands/finetune/compress-create.ts b/packages/commands/src/commands/finetune/compress-create.ts new file mode 100644 index 000000000..331b1a524 --- /dev/null +++ b/packages/commands/src/commands/finetune/compress-create.ts @@ -0,0 +1,116 @@ +import { + defineCommand, + createCompressJob, + type CreateCompressJobRequest, + type FlagsDef, +} from "bailian-cli-core"; +import { emitBare, emitResult } from "bailian-cli-runtime"; +import { parseCommaSeparated, parseCompressHyperParameters } from "./compress-shared.ts"; + +const CREATE_FLAGS = { + model: { + type: "string", + valueHint: "", + description: { + "en-US": "Source custom model ID (required)", + "zh-CN": "源自定义模型 ID(必填)", + }, + required: true, + }, + templateId: { + type: "string", + valueHint: "", + description: { "en-US": "Compression template ID (required)", "zh-CN": "压缩模板 ID(必填)" }, + required: true, + }, + jobName: { + type: "string", + valueHint: "", + description: { + "en-US": "Unique job name (max 50 characters)", + "zh-CN": "唯一任务名称(最多 50 字符)", + }, + }, + description: { + type: "string", + valueHint: "", + description: { + "en-US": "Job description (max 200 characters)", + "zh-CN": "任务描述(最多 200 字符)", + }, + }, + hparam: { + type: "array", + valueHint: "", + description: { + "en-US": "Hyper-parameter override; repeat for multiple values", + "zh-CN": "超参数覆盖;可重复传入多个值", + }, + }, + calibrationFiles: { + type: "string", + valueHint: "", + description: { + "en-US": "Comma-separated calibration dataset file IDs", + "zh-CN": "逗号分隔的校准数据集文件 ID", + }, + }, + suffix: { + type: "string", + valueHint: "", + description: { + "en-US": "Output model suffix (1–8 lowercase letters or digits)", + "zh-CN": "产出模型后缀(1–8 个小写字母或数字)", + }, + }, +} satisfies FlagsDef; + +export default defineCommand({ + description: { "en-US": "Create a model compression job", "zh-CN": "创建模型压缩任务" }, + auth: "apiKey", + usageArgs: "--model --template-id [flags]", + flags: CREATE_FLAGS, + validate(flags) { + if (flags.jobName !== undefined && flags.jobName.length > 50) + return "--job-name must be at most 50 characters. / --job-name 最多 50 个字符。"; + if (flags.description !== undefined && flags.description.length > 200) + return "--description must be at most 200 characters. / --description 最多 200 个字符。"; + if (flags.suffix !== undefined && !/^[a-z0-9]{1,8}$/.test(flags.suffix)) + return "--suffix must contain 1–8 lowercase letters or digits. / --suffix 必须为 1–8 个小写字母或数字。"; + return undefined; + }, + exampleArgs: [ + "--model qwen3.5-flash-2026-02-23-ft-xxx --template-id quant-flash-nvfp4-mlp-nomtp", + "--model qwen3.5-flash-2026-02-23-ft-xxx --template-id quant-flash-nvfp4-mlp-nomtp --suffix test --hparam calib_input=true", + ], + notes: [ + { + "en-US": "Run `finetune compress templates` first to get a compatible template ID.", + "zh-CN": "请先运行 `finetune compress templates` 获取兼容的模板 ID。", + }, + { + "en-US": "Model compression APIs are currently available only in the Beijing region.", + "zh-CN": "模型压缩 API 当前仅在北京 Region 开放。", + }, + ], + async run(ctx) { + const hyperParameters = parseCompressHyperParameters(ctx.flags.hparam); + const calibrationFileIds = parseCommaSeparated(ctx.flags.calibrationFiles); + const body: CreateCompressJobRequest = { + model: ctx.flags.model, + template_id: ctx.flags.templateId, + ...(ctx.flags.jobName ? { job_name: ctx.flags.jobName } : {}), + ...(ctx.flags.description ? { job_description: ctx.flags.description } : {}), + ...(Object.keys(hyperParameters).length ? { hyper_parameters: hyperParameters } : {}), + ...(calibrationFileIds ? { custom_calibration_file_ids: calibrationFileIds } : {}), + ...(ctx.flags.suffix ? { output_model_suffix: ctx.flags.suffix } : {}), + }; + if (ctx.settings.dryRun) { + emitResult({ action: "finetune.compress.create", request: body }, "json"); + return; + } + const response = await createCompressJob(ctx.client, body); + if (ctx.settings.quiet) emitBare(response.output?.job_id ?? ""); + else emitResult(response, "json"); + }, +}); diff --git a/packages/commands/src/commands/finetune/compress-delete.ts b/packages/commands/src/commands/finetune/compress-delete.ts new file mode 100644 index 000000000..fe1bc14bd --- /dev/null +++ b/packages/commands/src/commands/finetune/compress-delete.ts @@ -0,0 +1,35 @@ +import { defineCommand, deleteCompressJob } from "bailian-cli-core"; +import { emitBare, emitResult } from "bailian-cli-runtime"; +import { COMPRESS_JOB_ID_FLAGS } from "./compress-shared.ts"; + +export default defineCommand({ + description: { "en-US": "Delete a compression job record", "zh-CN": "删除压缩任务记录" }, + auth: "apiKey", + risk: { + level: "high", + message: { + "en-US": "This permanently deletes the compression job record and cannot be undone.", + "zh-CN": "该操作会永久删除压缩任务记录,且无法撤销。", + }, + }, + usageArgs: "--job-id ", + flags: COMPRESS_JOB_ID_FLAGS, + exampleArgs: ["--job-id quant-xxx", "--job-id quant-xxx --dry-run", "--job-id quant-xxx --yes"], + notes: [ + { + "en-US": + "Only terminal jobs can be deleted. Deleting a job record does not delete its quantized output model.", + "zh-CN": "只有终态任务可以删除;删除任务记录不会删除其产出的量化模型。", + }, + ], + async run(ctx) { + const jobId = ctx.flags.jobId; + if (ctx.settings.dryRun) { + emitResult({ action: "finetune.compress.delete", job_id: jobId }, "json"); + return; + } + const response = await deleteCompressJob(ctx.client, jobId); + if (ctx.settings.quiet) emitBare(jobId); + else emitResult(response, "json"); + }, +}); diff --git a/packages/commands/src/commands/finetune/compress-get.ts b/packages/commands/src/commands/finetune/compress-get.ts new file mode 100644 index 000000000..8d0279284 --- /dev/null +++ b/packages/commands/src/commands/finetune/compress-get.ts @@ -0,0 +1,19 @@ +import { defineCommand, getCompressJob } from "bailian-cli-core"; +import { emitResult } from "bailian-cli-runtime"; +import { COMPRESS_JOB_ID_FLAGS } from "./compress-shared.ts"; + +export default defineCommand({ + description: { "en-US": "Get a compression job", "zh-CN": "查询压缩任务详情" }, + auth: "apiKey", + usageArgs: "--job-id ", + flags: COMPRESS_JOB_ID_FLAGS, + exampleArgs: ["--job-id quant-xxx"], + async run(ctx) { + const jobId = ctx.flags.jobId; + if (ctx.settings.dryRun) { + emitResult({ action: "finetune.compress.get", job_id: jobId }, "json"); + return; + } + emitResult(await getCompressJob(ctx.client, jobId), "json"); + }, +}); diff --git a/packages/commands/src/commands/finetune/compress-list.ts b/packages/commands/src/commands/finetune/compress-list.ts new file mode 100644 index 000000000..4539654f7 --- /dev/null +++ b/packages/commands/src/commands/finetune/compress-list.ts @@ -0,0 +1,148 @@ +import { defineCommand, listCompressJobs, type FlagsDef } from "bailian-cli-core"; +import { emitResult } from "bailian-cli-runtime"; + +const LIST_FLAGS = { + status: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by job status", "zh-CN": "按任务状态筛选" }, + }, + model: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by source model ID", "zh-CN": "按源模型 ID 筛选" }, + }, + templateId: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by template ID", "zh-CN": "按模板 ID 筛选" }, + }, + quantSpec: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by quantization specification", "zh-CN": "按量化规格筛选" }, + }, + quantMethod: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by quantization method", "zh-CN": "按量化算法筛选" }, + }, + startTime: { + type: "string", + valueHint: "