Compare commits

...

19 Commits

Author SHA1 Message Date
故璃 1c76749ee5 feat: update datalist validate 2026-08-11 13:33:31 +08:00
故璃 5d1b7aac3a Merge branch 'main' into feat/cli-skill-sync 2026-08-11 10:47:45 +08:00
gujieye 2389681ad6 Merge pull request #144 from modelstudioai/feat/add-version-tag
feat: add version 1.14.2
2026-08-07 18:02:08 +08:00
故璃 946b7029c6 feat: add version 1.14.2 2026-08-07 17:42:31 +08:00
gujieye b9ecd5c43b Merge pull request #143 from modelstudioai/feat/skill-init-commend
feat: add skill init & opt commend flags
2026-08-07 17:26:21 +08:00
Gong Shiqi 978f332fea Merge pull request #142 from modelstudioai/docs/update-readme-and-agent-guides
docs: refresh READMEs and auth maintenance guidance
2026-08-07 17:24:52 +08:00
故璃 4502424200 feat: add skill init & opt commend flags 2026-08-07 17:17:27 +08:00
若麒 03839766bc docs: update READMEs 2026-08-07 17:15:26 +08:00
故璃 5007b9b574 feat: change output to json 2026-08-07 16:35:47 +08:00
若麒 1f8b9ace7e docs: refine auth maintenance guidance 2026-08-07 15:27:46 +08:00
故璃 8286a74fb6 test: remove sync pipeline verification marker 2026-08-07 14:40:33 +08:00
故璃 9eb2acbb65 test: trigger skills sync pipeline 2026-08-07 14:38:40 +08:00
故璃 1d35326c86 ci: read FC trigger url from variables 2026-08-07 14:36:42 +08:00
故璃 eb6c2b8e2a Merge branch 'feat/model-finetune-opt' into feat/cli-skill-sync 2026-08-07 14:26:38 +08:00
故璃 ebd6226a9f ci: rename trigger secret to FC_TRIGGER_URL 2026-08-07 14:25:49 +08:00
故璃 e25d3b0b8e ci: add workflow to publish skills to OSS 2026-08-07 14:13:47 +08:00
故璃 3c64461cca feat: video finetune/deploy/invoke full pipeline + training cost calculation
- Add finetune video create subcommand (wan2.7/2.5/2.2 i2v + kf2v)
- Align video hyperparams with official docs (n_epochs=50, per-model batch_size/max_pixels)
- Add --last-frame flag to video generate for kf2v (image2video endpoint)
- Fix wan2.1-2.6 i2v input format (img_url instead of media[])
- Add training_cost field to finetune get/watch (catalog ft price, API-key domain only)
- Add --aigc-* flags to deploy create (optional, for video LoRA prompt config)
2026-08-07 07:03:01 +08:00
故璃 f30fff9065 feat(finetune): clarify model flags; add price estimate & actual cost
- Rename for clarity: finetune --model → --base-model (create/price/
  capability/list); deploy create --model → --model-name, --name →
  --display-name
- Add `finetune price` (console domain) for pre-training cost estimate
  (sft/dpo/cpt)
- Add actual training cost (fee.ts) enriched into finetune get/watch
  from catalog price × reported usage
2026-08-06 15:08:02 +08:00
故璃 a7245c0f62 feat(deploy): add pause/resume commands; JSON-only output for dataset/finetune/deploy
- Add `bl deploy pause` and `bl deploy resume` (console domain, first
  console-auth commands in deploy group) via modelInstance start/stop APIs
- Add core deploy/lifecycle.ts with input-wrapped console gateway calls
- Switch all dataset/finetune/deploy commands to JSON-only output, removing
  text formatting logic
- Expose usage/charge_type in finetune get, model_name/expire_time in
  finetune checkpoints with near-expiry warning
- Update deploy delete hint to suggest `bl deploy pause`
2026-08-06 11:34:48 +08:00
71 changed files with 2227 additions and 1503 deletions
+27
View File
@@ -0,0 +1,27 @@
# Poke the FC publish-skills flow after skills/ changes land.
# The FC side reconciles this repo's skills/ directory against OSS
# (bailian-wiki/skills/) using the repo HEAD snapshot as the only
# source of truth — the request itself carries no content. Both the
# repo and branch params are validated against FC-side whitelists
# (PUBLISH_REPOS / PUBLISH_BRANCHES).
#
# feat/cli-skill-sync is temporary for end-to-end testing; remove it
# (here and from the FC PUBLISH_BRANCHES whitelist) once the sync
# link is verified on main.
name: Publish skills to OSS
on:
push:
branches:
- main
- feat/cli-skill-sync
paths:
- "skills/**"
jobs:
poke:
runs-on: ubuntu-latest
steps:
- name: Trigger FC publish-skills
run: |
curl -sf -X POST "${{ vars.FC_TRIGGER_URL }}/publish-skills?repo=modelstudioai/cli&branch=${{ github.ref_name }}"
+73 -137
View File
@@ -13,8 +13,9 @@
---
_Chat with Qwen, generate images & videos, understand images, call agents,_
_manage memory, search the web — all from your terminal._
_Chat with Qwen, generate and edit images and videos, understand images, synthesize_
_and recognize speech, call apps, manage memory, retrieve knowledge, search the web —_
_every AI capability, one command away._
_Built for AI Agents. Every command works as a structured tool call._
@@ -22,28 +23,16 @@ _Built for AI Agents. Every command works as a structured tool call._
## Features
Equip your AI Agent out-of-the-box with these capabilities, composable across complex tasks:
- **Model generation** — Full-modality generation across text, image, video, and speech, with editing and reference-based generation
- **Asset understanding** — Parse and ask questions about images, documents, audio, and long videos
- **App orchestration** — Call Managed Agents, agents, and workflows published on Aliyun Model Studio, wired to knowledge bases, memory, web search, and MCP tools
- **Training & deployment** — Validate and upload datasets, fine-tune models, deploy dedicated models as endpoints
- **Account operations** — Login, UI-based configuration, model marketplace, usage and quota, rate-limit increases, team seat management
- **Plan onboarding** — Connect subscription plans such as Token Plan to the CLI and common coding agents in one step
- **Text chat** — Qwen3.8-max: major gains in agentic coding, frontend coding, and vibe coding
- **Multimodal (Omni)** — Full omni-modal support across text + image + audio + video
- **Image generation & editing** — Qwen-Image 3.0: pro text rendering, photorealism, strong semantic adherence, multi-image composition
- **Video generation & editing** — happyhorse-1.1 series: text-/image-/reference-to-video and natural-language video editing (up to 9-image reference)
- **Speech synthesis & recognition** — CosyVoice streaming TTS, voice cloning from 520s samples; FunAudio-ASR covers 30 languages including 7 Chinese dialects and 20+ Mandarin accents
- **Image & video understanding** — Qwen-VL: long-form video analysis, chart/document parsing, visual reasoning, multilingual OCR
- **Coding agent setup** — Configure Claude Code, Qwen Code, OpenCode, OpenClaw, Hermes Agent, or Codex to use DashScope with `bl config agent`
> **Note:** App orchestration, training & deployment, account operations, and plan onboarding are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
> **Note:** The features below are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
- **Knowledge base & memory** — Multimodal RAG retrieval and cross-session memory for personalized, coherent dialogue
- **App calls** — Invoke agents and workflows already published on Aliyun Model Studio
- **MCP integration** — Orchestrate Bailian MCP servers: list services, inspect tools, and invoke any tool directly from the terminal
- **Web search** — Real-time internet retrieval for up-to-date, accurate answers
- **Model recommendation** — Describe your scenario and get best-fit model suggestions; supports scoped search, model comparison, and alternative discovery
- **Fine-tuning & deployment** — Upload datasets, create text/audio/image fine-tune jobs (`finetune text|audio|image create`; text covers SFT/LoRA/DPO/CPT), probe job status non-blockingly (`finetune watch`), query per-model training capability (`finetune capability`), and deploy trained models as endpoints (`deploy text|audio|image create`)
- **Console capabilities** — Browse the model marketplace (`model list`) and Bailian apps (`app list`), review a unified usage view (`usage summary`), check free-tier quota (`usage free`), view model usage statistics (`usage stats`), manage workspaces (`workspace list`), and manage rate limits (`quota list/request/check/history`)
- **Local file auto-upload** — Every URL parameter accepts a local path; uploaded to free temp storage with 48-hour validity
## Showcase: One-Sentence Cinematic Video
## Showcase 1: A Cinematic Short Film from One Sentence
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -56,129 +45,77 @@ Equip your AI Agent out-of-the-box with these capabilities, composable across co
A complete **2-minute, 16:9 cinematic short film** — produced end-to-end from a single natural-language sentence, with **zero manual editing**. This showcase demonstrates how an AI Agent can compose a multi-step creative pipeline by orchestrating three primitives:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — the agentic coding model that interprets the user's intent and drives the workflow
- **[Aliyun Model Studio CLI](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** — handles scene decomposition, storyboarding, shot continuity, and final stitching
### The single prompt
> _"Generate a roughly 2-minute video in Japanese cinematic style — a sweet, innocent first-love story about a high-school girl. The plot should be heart-fluttering enough to make viewers want to fall in love. Aspect ratio: 16:9."_
>
> _(Original: "帮我生成一段日系影视风格高中女生的青涩初恋故事剧情高甜让人看了想谈恋爱2分钟左右的视频尺寸是16:9")_
### How it works
## Showcase 2: A Short-Film Director Managed Agent from One Sentence
1. **Qwen Code** parses the request, plans the narrative beats, and decides which tools to call.
2. The **spark-video Skill** breaks the story into shots, writes per-shot prompts, and enforces visual continuity (characters, lighting, palette, lens language).
3. **`bl video generate`** dispatches each shot to **HappyHorse 1.1** in parallel.
4. The skill stitches all clips back together into a single 16:9 / ~2-min deliverable.
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="Click to play the demo video" width="720" />
</a>
</p>
No timeline scrubbing. No frame-by-frame editing. Just one sentence → one video.
<p align="center"><i>👆 Click the cover to play the full demo</i></p>
One sentence builds a reusable cloud-side short-film director for storyboarding, storyboard image generation, and video creation:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — understands the requirement and generates the agent configuration
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — validates the configuration, previews the changes, and completes the deployment
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** — runs the director role along with its skills and tools in the cloud
### The single prompt
> _"Build me a Managed Agent app that can produce short films — a director expert that generates videos and can also design the matching storyboards."_
## Installation
**Agent install (recommended)**
Send the following to your Agent — it will detect your environment, then install and verify the CLI for you:
```text
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
```
**Manual install (npm)**
```bash
# Recommended — no Node required
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
# Windows (PowerShell)
irm https://bailian.aliyun.com/cli/install.ps1 | iex
# Node users / developers (Node.js >= 18.17)
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```
> Binary install does not require Node.js. `npm install -g` remains fully supported.
> Requires Node.js >= 18.17.
## Quick Start
```bash
# Authenticate, recommended
bl auth login --console
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
# Or authenticate with an API key
bl auth login --api-key sk-xxxxx
# Or use Token Plan (Base URL built in; the key is tested during login)
bl auth login --config token-plan --api-key sk-sp-xxxxx
# Configure a coding agent to use DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# Chat with Qwen
bl text chat --message "What is DashScope?"
# Multimodal chat (text + image + audio + video)
bl omni --message "Describe this image" --image ./photo.jpg
# Generate an image
bl image generate --prompt "A cat in a spacesuit" --out-dir ./images/
# Generate a video from local image
bl video generate --image ./cat.png --prompt "Make the cat move" --download cat.mp4
# Model recommendation — find the best model for your use case
bl advisor recommend --message "I need a visual-understanding chatbot"
# Compare specific models
bl advisor recommend --message "qwen-max vs deepseek-v3 for code generation"
# Browser login (required for console capability commands)
bl auth login --console
# Fine-tune & deploy — a one-shot train-to-serve workflow
bl dataset upload --file ./train.jsonl # Upload a .jsonl dataset (validated first)
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # Local paths auto-upload
bl finetune watch --job-id ft-xxx --output json # Non-blocking probe (running/succeeded return 0; failed/canceled report an error)
bl finetune capability --model qwen3-8b # Which training types a model supports
bl deploy text create --model qwen3-8b --name my-svc --plan mu # Deploy the trained model as an endpoint
# Browse models / apps / free-tier quota / usage statistics / workspaces
bl model list # Browse model families and pricing
bl app list
bl usage summary # Unified view: free-tier quota + recent usage overview
bl usage free # Free-tier quota across models (add --model/--expiring/--sort)
bl usage stats --workspace-id <id> # Model usage statistics (add --model for per-model)
bl workspace list # List all workspaces
# Rate limit management (list / check / request / history)
bl quota list # View RPM/TPM limits (add --model to filter)
bl quota check # Current usage vs rate limits (add --model/--period)
bl quota request --model qwen3.6-plus --tpm 6000000 # Request a temporary TPM increase
bl quota history # View quota-change history
# Token Plan team management (requires AK/SK, see auth below)
bl token-plan list-seats # View subscription seat details
bl token-plan add-member --account-name dev --org-id org_xxx
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
```
| Scenario | What to say to your Agent |
| ------------------------ | --------------------------------------------------------------------------------- |
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
| Model selection | "Recommend a model for image understanding and customer support." |
| 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?source_channel=cli_github&)
## Authentication
### DashScope API Key
### API Key
Required for most commands. Get your key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key).
```bash
# Option 1: Environment variable
export DASHSCOPE_API_KEY=sk-xxxxx
# Option 2: Login command (persisted to ~/.bailian/config.json)
bl auth login --api-key sk-xxxxx
# Option 3: Per-command flag
bl text chat --api-key sk-xxxxx --message "Hello"
```
### Token Plan API Key
Get or copy the API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
The CLI has the default Token Plan Base URL built in. Login tests the key first, then saves and activates the `token-plan` config only when validation succeeds.
Get or copy your Token Plan API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
@@ -186,26 +123,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### Console Login (OAuth)
Required for console capability commands (`model list`, `app list`, `usage summary/free/stats`, `workspace list`, `quota list/request/check/history`). Opens the Bailian console in your browser to sign in.
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). Opens the Bailian console in your browser to sign in.
```bash
bl auth login --console
```
### Alibaba Cloud OpenAPI AK/SK (Token Plan only)
### Alibaba Cloud OpenAPI AK/SK
Required for the `token-plan` command group. Get your AccessKey from [RAM Console](https://ram.console.aliyun.com/manage/ak).
Token Plan seat and member management requires an Alibaba Cloud AccessKey. Get yours from the [RAM Console](https://ram.console.aliyun.com/manage/ak).
> Recommended: create a RAM sub-account with minimum privileges instead of using the root account's AK/SK.
```bash
# Option 1: Login command (persisted to ~/.bailian/config.json)
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# Option 2: Environment variables
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## Configuration
@@ -214,18 +145,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
# View current config
bl config show
# Set defaults
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# List all config profiles
bl config list
# Self-update to latest or a specific version
bl update
bl update --to 0.1.14
# Switch config profile
bl config use --name token-plan
```
Config file location: `~/.bailian/config.json`
## Update
```bash
bl update
```
Upgrades the CLI to the latest version and refreshes the installed Agent Skills. Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
Scan the QR code to join the Aliyun Model Studio CLI DingTalk user group for usage help, troubleshooting, bug reports, and tips from other users.
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="Aliyun Model Studio CLI DingTalk user group" width="240" />
## Links
| Resource | URL |
@@ -237,11 +181,3 @@ Config file location: `~/.bailian/config.json`
| Get API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| Get Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| Get AccessKey | https://ram.console.aliyun.com/manage/ak |
## Changelog
Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
+73 -138
View File
@@ -22,28 +22,16 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
## 功能特性
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
- **素材理解** — 图像、文档、音频、长视频的解析与问答
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流接入知识库、记忆库、联网搜索与 MCP 工具
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
- **文本对话** — Qwen3.8-maxAgentic coding、前端编程、Vibe coding 等能力显著增强
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
- **图像生成与编辑** — Qwen-Image 3.0:专业文字渲染、真实质感、强语义遵循、多图合成
- **视频生成与编辑** — happyhorse-1.1 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
- **语音合成与识别** — CosyVoice 实时流式合成5-20s 样本即可克隆FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
- **图像与视频理解** — Qwen-VL长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
- **Coding Agent 配置** — 使用 `bl config agent` 将 Claude Code、Qwen Code、OpenCode、OpenClaw、Hermes Agent 或 Codex 配置为使用 DashScope
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站aliyun.com账号暂不支持国际站 / 全球站账号。
> **注意:** 以下功能目前仅对中国站aliyun.com账号开放国际站 / 全球站账号暂不支持。
- **知识库与记忆库** — 多模态 RAG 检索 + 跨会话记忆,提供个性化连贯对话体验
- **应用调用** — 调用已发布在阿里云百炼平台上的智能体与工作流应用
- **MCP 集成** — 统一调度百炼 MCP 服务:列出服务、查看工具、直接在终端调用任意工具
- **联网搜索** — 实时互联网信息检索,提升回答准确性及时效性
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
- **微调与部署** — 上传数据集、创建文本/音频/图像调优任务(`finetune text|audio|image create`;文本涵盖 SFT/LoRA/DPO/CPT、非阻塞探测任务状态`finetune watch`)、按模型查训练能力(`finetune capability`),并把训练好的模型部署为推理服务(`deploy text|audio|image create`
- **控制台能力** — 浏览模型市场(`model list`)和百炼应用(`app list`),查看统一用量视图(`usage summary`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
## 示例:一句话生成一部电影短片
## 示例 1一句话生成一部电影短片
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -53,130 +41,80 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
<p align="center"><i>👆 点击封面播放完整 2 分钟演示</i></p>
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**百炼的文生/图生/参考生视频模型
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** —— 负责场景拆分、分镜设计、镜头连贯性和最终拼接
### 唯一的提示词
> _"帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9"_
> _帮我生成一段日系影视风格高中女生的青涩初恋故事剧情高甜让人看了想谈恋爱2 分钟左右的视频尺寸是 16:9。”_
### 工作流程
## 示例 2一句话构建短片导演 Managed Agent
1. **Qwen Code** 解析需求、规划叙事节奏,决定要调用哪些工具。
2. **spark-video Skill** 把故事拆成镜头、为每个镜头写提示词,并保证视觉连贯性(角色、光线、色调、镜头语言)。
3. **`bl video generate`** 把每个镜头并行下发给 **HappyHorse 1.1**
4. Skill 把所有片段拼成最终的 16:9 / 约 2 分钟成片。
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="点击播放演示视频" width="720" />
</a>
</p>
没有时间线拖拽,没有逐帧剪辑。一句话 → 一部短片。
<p align="center"><i>👆 点击封面播放完整演示</i></p>
一句话构建一个可复用的云端短片导演,用于分镜设计、分镜图生成和视频创作:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— 理解需求并生成 Agent 配置
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 校验配置、预览变更并完成部署
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** —— 在云端运行导演角色及其 Skill 和工具
### 唯一的提示词
> _“帮我构建一个 managedagent 应用能够实现短片拍摄导演专家生成视频然后也能进行设计对应的分镜图。”_
## 安装
**Agent 安装(推荐)**
把下面这句话发给你的 Agent它会自行判断环境并完成安装与校验
```text
请阅读https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
```
**手动安装npm**
```bash
# 推荐 — 无需本机 Node.js
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
# WindowsPowerShell
irm https://bailian.aliyun.com/cli/install.ps1 | iex
# Node 用户 / 开发者(需要 Node.js >= 18.17
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```
> 二进制安装不依赖 Node.js。`npm install -g` 长期保留
> 需要预先安装 Node.js >= 18.17
## 快速开始
```bash
# 认证(推荐浏览器登录)
bl auth login --console
安装完成后,直接在 AI Agent 中描述你的任务,无需手动拼接命令。
# 或使用 API key 认证
bl auth login --api-key sk-xxxxx
# 或使用 Token Plan已内置 Base URL登录时自动测试 Key
bl auth login --config token-plan --api-key sk-sp-xxxxx
# 配置 Coding Agent 使用 DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# 和通义千问对话
bl text chat --message "你好,介绍一下阿里云百炼平台"
# 多模态对话(文本 + 图片 + 音频 + 视频)
bl omni --message "描述这张图片" --image ./photo.jpg
# 生成图片
bl image generate --prompt "一只穿太空服的猫在火星上" --out-dir ./images/
# 图生视频(本地文件自动上传)
bl video generate --image ./cat.png --prompt "让画面中的猫动起来" --download cat.mp4
# 模型推荐 — 根据场景推荐最适合的模型
bl advisor recommend --message "我要做一个能理解图片的客服机器人"
# 对比特定模型
bl advisor recommend --message "qwen-max 和 deepseek-v3 哪个更适合做代码生成"
# 浏览器登录(控制台能力相关命令需要)
bl auth login --console
# 微调与部署 — 从训练到服务的一站式流程
bl dataset upload --file ./train.jsonl # 上传 .jsonl 数据集(先校验)
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # 本地路径自动上传
bl finetune watch --job-id ft-xxx --output json # 非阻塞探测(运行中/成功返回 0失败/取消报错)
bl finetune capability --model qwen3-8b # 查询模型支持哪些训练方式
bl deploy text create --model qwen3-8b --name my-svc --plan mu # 把训练好的模型部署为推理服务
# 浏览模型 / 应用 / 免费额度 / 用量统计 / 业务空间
bl model list # 浏览模型系列与价格信息
bl app list
bl usage summary # 统一视图:免费额度 + 近期用量概览
bl usage free # 各模型免费额度(可加 --model/--expiring/--sort
bl usage stats --workspace-id <id> # 模型用量统计(加 --model 查单模型)
bl workspace list # 列出所有业务空间
# 限流管理与提额list / check / request / history
bl quota list # 查看 RPM/TPM 限额(加 --model 过滤)
bl quota check # 当前用量 vs 限流阈值(加 --model/--period
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
bl quota history # 查看提额历史记录
# Token Plan 团队版管理(需 AK/SK见下方认证说明
bl token-plan list-seats # 查看订阅席位明细
bl token-plan add-member --account-name dev --org-id org_xxx
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
```
| 场景 | 可以这样对 Agent 说 |
| ---------------- | ----------------------------------------------------------------------- |
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
> 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
## 认证方式
### DashScope API Key
### API Key
大部分命令均需要 API Key。前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取。
```bash
# 方式一:环境变量
export DASHSCOPE_API_KEY=sk-xxxxx
# 方式二:登录命令(持久化到 ~/.bailian/config.json
bl auth login --api-key sk-xxxxx
# 方式三:命令行参数
bl text chat --api-key sk-xxxxx --message "你好"
```
### Token Plan API Key
前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制 API Key。
CLI 已内置 Token Plan 的默认 Base URL登录命令会先测试 Key通过后才保存并激活 `token-plan` 配置。
Token Plan API Key 前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制。
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
@@ -184,26 +122,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### 控制台登录OAuth
控制台能力命令(`model list``app list``usage summary/free/stats``workspace list``quota list/request/check/history`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```bash
bl auth login --console
```
### 阿里云 OpenAPI AK/SK(仅 Token Plan
### 阿里云 OpenAPI AK/SK
`token-plan` 命令组需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
Token Plan 的席位与成员管理需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
```bash
# 方式一:登录命令(持久化到 ~/.bailian/config.json
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# 方式二:环境变量
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## 配置
@@ -212,20 +144,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
# 查看当前配置
bl config show
# 设置默认值
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# 查看全部配置档
bl config list
# 自更新到最新版本
bl update
# 安装指定版本
bl update --to 0.1.14
# 切换配置档
bl config use --name token-plan
```
配置文件位置:`~/.bailian/config.json`
## 更新
```bash
bl update
```
升级 CLI 至最新版本,并同步更新已安装的 Agent Skills。每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
欢迎扫码加入阿里云百炼 CLI 钉钉用户交流群获取使用答疑、问题排查、Bug 反馈和使用经验交流支持。
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="阿里云百炼 CLI 钉钉用户交流群" width="240" />
## 相关链接
| 资源 | 地址 |
@@ -237,11 +180,3 @@ bl update --to 0.1.14
| 获取 API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| 获取 Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| 获取 AccessKey | https://ram.console.aliyun.com/manage/ak |
## 更新日志
每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
+10 -3
View File
@@ -25,7 +25,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
当前 command 鉴权域(`AuthRequirement`):
- `apiKey` — DashScope / OpenAI-compatible 模型域,用 API key 与 model base URL
- `console` — Bailian Console Gateway,用 console access token + region/site/switchAgent/workspace
- `console` — Bailian Console Gateway,用 console access token + region/site/switchAgent;`workspace_id` 是独立的 Settings 作用域,不属于 credential
- `openapi` — 阿里云 OpenAPI 签名域,用 AccessKey ID/Secret 调用 Token Plan 等 OpenAPI
- `none` — 本地命令、登录/配置类命令、无需 credential 的命令
@@ -35,7 +35,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
- `bl auth login --api-key ...` 只更新 `api_key` / `base_url`
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi``console` 域凭证
- `bl auth logout --console` 只清 `access_token`
- `bl auth logout --open-api` 只清 `access_key_id` / `access_key_secret` / `security_token`
- `bl auth logout``api_key` + `base_url` + `access_token` + `access_key_*`
@@ -78,6 +78,9 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
- 如新增鉴权域,扩展 `AuthRequirement`
- 更新 `credentialFlagDefs()` 暴露该域可见的 flag
- 必要时新增 `*_AUTH_FLAGS`
- `workspace_id` 是作用域字段而非 credential,不要把它放进 `ConsoleCredential`;读取方式按命令 `auth` 域区分:
- `auth: "console"` 命令通过 `CONSOLE_AUTH_FLAGS` 自动获得 `--workspace-id`,由 `buildSettings()` 解析到 `settings.workspaceId`,命令统一从 `settings.workspaceId` 读取
- `auth: "apiKey"`/`"openapi"`/`"none"` 命令如需 `--workspace-id`,必须自声明 flag;因它不会进入 credential/global flags,命令从 `ctx.flags.workspaceId` 读取(可回退到 `settings.workspaceId`)
- [ ] `packages/core/src/auth/types.ts`:
- 新增 credential 类型 / source / scope 字段
- [ ] `packages/core/src/auth/resolver.ts`:
@@ -131,6 +134,8 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
## 完成后自查
本仓库同时存在 `bl`(packages/cli) 与 `kscli`(packages/kscli) 两个入口,二者共享 core/runtime 鉴权链路,但暴露的命令不同。如果改动会影响两个入口共用的命令或错误提示,再分别验证它们各自实际暴露的路径;不要假设 `kscli` 也有 `bl auth *` 命令。
```sh
# 各种凭证组合
unset DASHSCOPE_API_KEY ALIBABA_CLOUD_ACCESS_KEY_ID ALIBABA_CLOUD_ACCESS_KEY_SECRET
@@ -150,9 +155,11 @@ Console 登录/网关相关改动:
```sh
pnpm -F bailian-cli exec tsx src/main.ts auth login --console
pnpm -F bailian-cli exec tsx src/main.ts usage stats --dry-run --output json
pnpm -F bailian-cli exec tsx src/main.ts usage stats --dry-run --output json --workspace-id ws-xxx
```
注意:`usage stats --dry-run` 仍会先校验 workspace,必须传入 `--workspace-id`(或 `BAILIAN_WORKSPACE_ID` / config `workspace_id`)。
## 常见漏点
- ✗ 加了新 token 来源但忘了改 resolver 优先级,实际不生效
+73 -137
View File
@@ -13,8 +13,9 @@
---
_Chat with Qwen, generate images & videos, understand images, call agents,_
_manage memory, search the web — all from your terminal._
_Chat with Qwen, generate and edit images and videos, understand images, synthesize_
_and recognize speech, call apps, manage memory, retrieve knowledge, search the web —_
_every AI capability, one command away._
_Built for AI Agents. Every command works as a structured tool call._
@@ -22,28 +23,16 @@ _Built for AI Agents. Every command works as a structured tool call._
## Features
Equip your AI Agent out-of-the-box with these capabilities, composable across complex tasks:
- **Model generation** — Full-modality generation across text, image, video, and speech, with editing and reference-based generation
- **Asset understanding** — Parse and ask questions about images, documents, audio, and long videos
- **App orchestration** — Call Managed Agents, agents, and workflows published on Aliyun Model Studio, wired to knowledge bases, memory, web search, and MCP tools
- **Training & deployment** — Validate and upload datasets, fine-tune models, deploy dedicated models as endpoints
- **Account operations** — Login, UI-based configuration, model marketplace, usage and quota, rate-limit increases, team seat management
- **Plan onboarding** — Connect subscription plans such as Token Plan to the CLI and common coding agents in one step
- **Text chat** — Qwen3.8-max: major gains in agentic coding, frontend coding, and vibe coding
- **Multimodal (Omni)** — Full omni-modal support across text + image + audio + video
- **Image generation & editing** — Qwen-Image 3.0: pro text rendering, photorealism, strong semantic adherence, multi-image composition
- **Video generation & editing** — happyhorse-1.1 series: text-/image-/reference-to-video and natural-language video editing (up to 9-image reference)
- **Speech synthesis & recognition** — CosyVoice streaming TTS, voice cloning from 520s samples; FunAudio-ASR covers 30 languages including 7 Chinese dialects and 20+ Mandarin accents
- **Image & video understanding** — Qwen-VL: long-form video analysis, chart/document parsing, visual reasoning, multilingual OCR
- **Coding agent setup** — Configure Claude Code, Qwen Code, OpenCode, OpenClaw, Hermes Agent, or Codex to use DashScope with `bl config agent`
> **Note:** App orchestration, training & deployment, account operations, and plan onboarding are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
> **Note:** The features below are currently available only to China site (aliyun.com) account holders and are not yet supported for international / global site accounts.
- **Knowledge base & memory** — Multimodal RAG retrieval and cross-session memory for personalized, coherent dialogue
- **App calls** — Invoke agents and workflows already published on Aliyun Model Studio
- **MCP integration** — Orchestrate Bailian MCP servers: list services, inspect tools, and invoke any tool directly from the terminal
- **Web search** — Real-time internet retrieval for up-to-date, accurate answers
- **Model recommendation** — Describe your scenario and get best-fit model suggestions; supports scoped search, model comparison, and alternative discovery
- **Fine-tuning & deployment** — Upload datasets, create text/audio/image fine-tune jobs (`finetune text|audio|image create`; text covers SFT/LoRA/DPO/CPT), probe job status non-blockingly (`finetune watch`), query per-model training capability (`finetune capability`), and deploy trained models as endpoints (`deploy text|audio|image create`)
- **Console capabilities** — Browse the model marketplace (`model list`) and Bailian apps (`app list`), review a unified usage view (`usage summary`), check free-tier quota (`usage free`), view model usage statistics (`usage stats`), manage workspaces (`workspace list`), and manage rate limits (`quota list/request/check/history`)
- **Local file auto-upload** — Every URL parameter accepts a local path; uploaded to free temp storage with 48-hour validity
## Showcase: One-Sentence Cinematic Video
## Showcase 1: A Cinematic Short Film from One Sentence
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -56,129 +45,77 @@ Equip your AI Agent out-of-the-box with these capabilities, composable across co
A complete **2-minute, 16:9 cinematic short film** — produced end-to-end from a single natural-language sentence, with **zero manual editing**. This showcase demonstrates how an AI Agent can compose a multi-step creative pipeline by orchestrating three primitives:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — the agentic coding model that interprets the user's intent and drives the workflow
- **[Aliyun Model Studio CLI](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — invokes **HappyHorse 1.1**, Aliyun Model Studio's text-/image-/reference-to-video generation model
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** — handles scene decomposition, storyboarding, shot continuity, and final stitching
### The single prompt
> _"Generate a roughly 2-minute video in Japanese cinematic style — a sweet, innocent first-love story about a high-school girl. The plot should be heart-fluttering enough to make viewers want to fall in love. Aspect ratio: 16:9."_
>
> _(Original: "帮我生成一段日系影视风格高中女生的青涩初恋故事剧情高甜让人看了想谈恋爱2分钟左右的视频尺寸是16:9")_
### How it works
## Showcase 2: A Short-Film Director Managed Agent from One Sentence
1. **Qwen Code** parses the request, plans the narrative beats, and decides which tools to call.
2. The **spark-video Skill** breaks the story into shots, writes per-shot prompts, and enforces visual continuity (characters, lighting, palette, lens language).
3. **`bl video generate`** dispatches each shot to **HappyHorse 1.1** in parallel.
4. The skill stitches all clips back together into a single 16:9 / ~2-min deliverable.
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="Click to play the demo video" width="720" />
</a>
</p>
No timeline scrubbing. No frame-by-frame editing. Just one sentence → one video.
<p align="center"><i>👆 Click the cover to play the full demo</i></p>
One sentence builds a reusable cloud-side short-film director for storyboarding, storyboard image generation, and video creation:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** — understands the requirement and generates the agent configuration
- **[Aliyun Model Studio CLI](https://github.com/modelstudioai/cli/)** — validates the configuration, previews the changes, and completes the deployment
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** — runs the director role along with its skills and tools in the cloud
### The single prompt
> _"Build me a Managed Agent app that can produce short films — a director expert that generates videos and can also design the matching storyboards."_
## Installation
**Agent install (recommended)**
Send the following to your Agent — it will detect your environment, then install and verify the CLI for you:
```text
Please read https://bailian.aliyun.com/cli/install.md and install the Aliyun Model Studio CLI for me
```
**Manual install (npm)**
```bash
# Recommended — no Node required
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
# Windows (PowerShell)
irm https://bailian.aliyun.com/cli/install.ps1 | iex
# Node users / developers (Node.js >= 18.17)
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```
> Binary install does not require Node.js. `npm install -g` remains fully supported.
> Requires Node.js >= 18.17.
## Quick Start
```bash
# Authenticate, recommended
bl auth login --console
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
# Or authenticate with an API key
bl auth login --api-key sk-xxxxx
# Or use Token Plan (Base URL built in; the key is tested during login)
bl auth login --config token-plan --api-key sk-sp-xxxxx
# Configure a coding agent to use DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# Chat with Qwen
bl text chat --message "What is DashScope?"
# Multimodal chat (text + image + audio + video)
bl omni --message "Describe this image" --image ./photo.jpg
# Generate an image
bl image generate --prompt "A cat in a spacesuit" --out-dir ./images/
# Generate a video from local image
bl video generate --image ./cat.png --prompt "Make the cat move" --download cat.mp4
# Model recommendation — find the best model for your use case
bl advisor recommend --message "I need a visual-understanding chatbot"
# Compare specific models
bl advisor recommend --message "qwen-max vs deepseek-v3 for code generation"
# Browser login (required for console capability commands)
bl auth login --console
# Fine-tune & deploy — a one-shot train-to-serve workflow
bl dataset upload --file ./train.jsonl # Upload a .jsonl dataset (validated first)
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # Local paths auto-upload
bl finetune watch --job-id ft-xxx --output json # Non-blocking probe (running/succeeded return 0; failed/canceled report an error)
bl finetune capability --model qwen3-8b # Which training types a model supports
bl deploy text create --model qwen3-8b --name my-svc --plan mu # Deploy the trained model as an endpoint
# Browse models / apps / free-tier quota / usage statistics / workspaces
bl model list # Browse model families and pricing
bl app list
bl usage summary # Unified view: free-tier quota + recent usage overview
bl usage free # Free-tier quota across models (add --model/--expiring/--sort)
bl usage stats --workspace-id <id> # Model usage statistics (add --model for per-model)
bl workspace list # List all workspaces
# Rate limit management (list / check / request / history)
bl quota list # View RPM/TPM limits (add --model to filter)
bl quota check # Current usage vs rate limits (add --model/--period)
bl quota request --model qwen3.6-plus --tpm 6000000 # Request a temporary TPM increase
bl quota history # View quota-change history
# Token Plan team management (requires AK/SK, see auth below)
bl token-plan list-seats # View subscription seat details
bl token-plan add-member --account-name dev --org-id org_xxx
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
```
| Scenario | What to say to your Agent |
| ------------------------ | --------------------------------------------------------------------------------- |
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
| Model selection | "Recommend a model for image understanding and customer support." |
| 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?source_channel=cli_github&)
## Authentication
### DashScope API Key
### API Key
Required for most commands. Get your key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key).
```bash
# Option 1: Environment variable
export DASHSCOPE_API_KEY=sk-xxxxx
# Option 2: Login command (persisted to ~/.bailian/config.json)
bl auth login --api-key sk-xxxxx
# Option 3: Per-command flag
bl text chat --api-key sk-xxxxx --message "Hello"
```
### Token Plan API Key
Get or copy the API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
The CLI has the default Token Plan Base URL built in. Login tests the key first, then saves and activates the `token-plan` config only when validation succeeds.
Get or copy your Token Plan API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
@@ -186,26 +123,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### Console Login (OAuth)
Required for console capability commands (`model list`, `app list`, `usage summary/free/stats`, `workspace list`, `quota list/request/check/history`). Opens the Bailian console in your browser to sign in.
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). Opens the Bailian console in your browser to sign in.
```bash
bl auth login --console
```
### Alibaba Cloud OpenAPI AK/SK (Token Plan only)
### Alibaba Cloud OpenAPI AK/SK
Required for the `token-plan` command group. Get your AccessKey from [RAM Console](https://ram.console.aliyun.com/manage/ak).
Token Plan seat and member management requires an Alibaba Cloud AccessKey. Get yours from the [RAM Console](https://ram.console.aliyun.com/manage/ak).
> Recommended: create a RAM sub-account with minimum privileges instead of using the root account's AK/SK.
```bash
# Option 1: Login command (persisted to ~/.bailian/config.json)
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# Option 2: Environment variables
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## Configuration
@@ -214,18 +145,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
# View current config
bl config show
# Set defaults
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# List all config profiles
bl config list
# Self-update to latest or a specific version
bl update
bl update --to 0.1.14
# Switch config profile
bl config use --name token-plan
```
Config file location: `~/.bailian/config.json`
## Update
```bash
bl update
```
Upgrades the CLI to the latest version and refreshes the installed Agent Skills. Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
Scan the QR code to join the Aliyun Model Studio CLI DingTalk user group for usage help, troubleshooting, bug reports, and tips from other users.
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="Aliyun Model Studio CLI DingTalk user group" width="240" />
## Links
| Resource | URL |
@@ -237,11 +181,3 @@ Config file location: `~/.bailian/config.json`
| Get API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| Get Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| Get AccessKey | https://ram.console.aliyun.com/manage/ak |
## Changelog
Release notes for every version live in [CHANGELOG.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.md).
## Contributing
Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup, repo layout, and the workflow for adding or changing commands.
+73 -138
View File
@@ -22,28 +22,16 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
## 功能特性
让您的 AI Agent 开箱即具备以下能力,并可在复杂任务中自动组合调用:
- **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成
- **素材理解** — 图像、文档、音频、长视频的解析与问答
- **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流接入知识库、记忆库、联网搜索与 MCP 工具
- **模型训推** — 数据集校验上传、模型精调、专属模型部署上线
- **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理
- **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent
- **文本对话** — Qwen3.8-maxAgentic coding、前端编程、Vibe coding 等能力显著增强
- **全模态对话** — 文本 + 图像 + 音频 + 视频全模态支持
- **图像生成与编辑** — Qwen-Image 3.0:专业文字渲染、真实质感、强语义遵循、多图合成
- **视频生成与编辑** — happyhorse-1.1 系列,支持文生 / 图生 / 参考生(最多 9 张图参考)/ 自然语言视频编辑
- **语音合成与识别** — CosyVoice 实时流式合成5-20s 样本即可克隆FunAudio-ASR 覆盖 30 种语种,含汉语七大方言与 20+ 口音官话
- **图像与视频理解** — Qwen-VL长视频解析、复杂图表与文档识别、视觉推理、多语种 OCR
- **Coding Agent 配置** — 使用 `bl config agent` 将 Claude Code、Qwen Code、OpenCode、OpenClaw、Hermes Agent 或 Codex 配置为使用 DashScope
> **注意:** 应用编排、模型训推、账号运维和套餐接入目前仅支持中国站aliyun.com账号暂不支持国际站 / 全球站账号。
> **注意:** 以下功能目前仅对中国站aliyun.com账号开放国际站 / 全球站账号暂不支持。
- **知识库与记忆库** — 多模态 RAG 检索 + 跨会话记忆,提供个性化连贯对话体验
- **应用调用** — 调用已发布在阿里云百炼平台上的智能体与工作流应用
- **MCP 集成** — 统一调度百炼 MCP 服务:列出服务、查看工具、直接在终端调用任意工具
- **联网搜索** — 实时互联网信息检索,提升回答准确性及时效性
- **模型推荐** — 描述你的场景,智能推荐最适合的模型;支持限定范围搜索、模型对比和替代发现
- **微调与部署** — 上传数据集、创建文本/音频/图像调优任务(`finetune text|audio|image create`;文本涵盖 SFT/LoRA/DPO/CPT、非阻塞探测任务状态`finetune watch`)、按模型查训练能力(`finetune capability`),并把训练好的模型部署为推理服务(`deploy text|audio|image create`
- **控制台能力** — 浏览模型市场(`model list`)和百炼应用(`app list`),查看统一用量视图(`usage summary`),查询模型免费额度(`usage free`),查看模型用量统计(`usage stats`),管理业务空间(`workspace list`),管理限流与提额(`quota list/request/check/history`
- **本地文件自动上传** — 所有 URL 参数同时支持本地路径,免费临时存储 48 小时
## 示例:一句话生成一部电影短片
## 示例 1一句话生成一部电影短片
<p align="center">
<a href="https://cloud.video.taobao.com/vod/dS2F4huqbw5Nfe5L3wwb3grz2q2DNYD3retq8dU-iHo.mp4">
@@ -53,130 +41,80 @@ _专为 AI Agent 打造每个命令均可作为结构化工具调用。_
<p align="center"><i>👆 点击封面播放完整 2 分钟演示</i></p>
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成,**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线:
一部完整的 **2 分钟、16:9 电影感短片** —— 由一句自然语言端到端生成**全程零手动剪辑**。这个示例展示了 AI Agent 如何把三个基础能力编排成一条多步创作流水线
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型,解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**,百炼的文生/图生/参考生视频模型
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— Agentic coding 模型解析用户意图、驱动整个工作流
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 调用 **HappyHorse 1.1**百炼的文生/图生/参考生视频模型
- **[spark-video Skill](https://github.com/JohnKeating1997/spark-video)** —— 负责场景拆分、分镜设计、镜头连贯性和最终拼接
### 唯一的提示词
> _"帮我生成一段日系影视风格,高中女生的青涩初恋故事,剧情高甜,让人看了想谈恋爱,2 分钟左右的视频,尺寸是 16:9"_
> _帮我生成一段日系影视风格高中女生的青涩初恋故事剧情高甜让人看了想谈恋爱2 分钟左右的视频尺寸是 16:9。”_
### 工作流程
## 示例 2一句话构建短片导演 Managed Agent
1. **Qwen Code** 解析需求、规划叙事节奏,决定要调用哪些工具。
2. **spark-video Skill** 把故事拆成镜头、为每个镜头写提示词,并保证视觉连贯性(角色、光线、色调、镜头语言)。
3. **`bl video generate`** 把每个镜头并行下发给 **HappyHorse 1.1**
4. Skill 把所有片段拼成最终的 16:9 / 约 2 分钟成片。
<p align="center">
<a href="https://cloud.video.taobao.com/vod/2v0GYLbJSQb2saj4iopTJDW3iRIHsintYlK-wTKbhqE.mp4">
<img src="https://img.alicdn.com/imgextra/i4/6000000001674/O1CN01xhzixhxltbH3LxWu_!!6000000001674-0-tbvideo.jpg" alt="点击播放演示视频" width="720" />
</a>
</p>
没有时间线拖拽,没有逐帧剪辑。一句话 → 一部短片。
<p align="center"><i>👆 点击封面播放完整演示</i></p>
一句话构建一个可复用的云端短片导演,用于分镜设计、分镜图生成和视频创作:
- **[Qwen Code](https://github.com/QwenLM/qwen-code)** —— 理解需求并生成 Agent 配置
- **[阿里云百炼 CLI](https://github.com/modelstudioai/cli/)** —— 校验配置、预览变更并完成部署
- **[Managed Agent](https://bailian.console.aliyun.com/cn-beijing/?tab=managed-agents#/managed-agents/quick-start)** —— 在云端运行导演角色及其 Skill 和工具
### 唯一的提示词
> _“帮我构建一个 managedagent 应用能够实现短片拍摄导演专家生成视频然后也能进行设计对应的分镜图。”_
## 安装
**Agent 安装(推荐)**
把下面这句话发给你的 Agent它会自行判断环境并完成安装与校验
```text
请阅读https://bailian.aliyun.com/cli/install.md 并按照说明为我安装阿里云百炼 CLI
```
**手动安装npm**
```bash
# 推荐 — 无需本机 Node.js
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
# WindowsPowerShell
irm https://bailian.aliyun.com/cli/install.ps1 | iex
# Node 用户 / 开发者(需要 Node.js >= 18.17
npm install -g bailian-cli
# Agent skills
npx skills add modelstudioai/cli --all -g
```
> 二进制安装不依赖 Node.js。`npm install -g` 长期保留
> 需要预先安装 Node.js >= 18.17
## 快速开始
```bash
# 认证(推荐浏览器登录)
bl auth login --console
安装完成后,直接在 AI Agent 中描述你的任务,无需手动拼接命令。
# 或使用 API key 认证
bl auth login --api-key sk-xxxxx
# 或使用 Token Plan已内置 Base URL登录时自动测试 Key
bl auth login --config token-plan --api-key sk-sp-xxxxx
# 配置 Coding Agent 使用 DashScope
bl config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus
# 和通义千问对话
bl text chat --message "你好,介绍一下阿里云百炼平台"
# 多模态对话(文本 + 图片 + 音频 + 视频)
bl omni --message "描述这张图片" --image ./photo.jpg
# 生成图片
bl image generate --prompt "一只穿太空服的猫在火星上" --out-dir ./images/
# 图生视频(本地文件自动上传)
bl video generate --image ./cat.png --prompt "让画面中的猫动起来" --download cat.mp4
# 模型推荐 — 根据场景推荐最适合的模型
bl advisor recommend --message "我要做一个能理解图片的客服机器人"
# 对比特定模型
bl advisor recommend --message "qwen-max 和 deepseek-v3 哪个更适合做代码生成"
# 浏览器登录(控制台能力相关命令需要)
bl auth login --console
# 微调与部署 — 从训练到服务的一站式流程
bl dataset upload --file ./train.jsonl # 上传 .jsonl 数据集(先校验)
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft-lora # 本地路径自动上传
bl finetune watch --job-id ft-xxx --output json # 非阻塞探测(运行中/成功返回 0失败/取消报错)
bl finetune capability --model qwen3-8b # 查询模型支持哪些训练方式
bl deploy text create --model qwen3-8b --name my-svc --plan mu # 把训练好的模型部署为推理服务
# 浏览模型 / 应用 / 免费额度 / 用量统计 / 业务空间
bl model list # 浏览模型系列与价格信息
bl app list
bl usage summary # 统一视图:免费额度 + 近期用量概览
bl usage free # 各模型免费额度(可加 --model/--expiring/--sort
bl usage stats --workspace-id <id> # 模型用量统计(加 --model 查单模型)
bl workspace list # 列出所有业务空间
# 限流管理与提额list / check / request / history
bl quota list # 查看 RPM/TPM 限额(加 --model 过滤)
bl quota check # 当前用量 vs 限流阈值(加 --model/--period
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
bl quota history # 查看提额历史记录
# Token Plan 团队版管理(需 AK/SK见下方认证说明
bl token-plan list-seats # 查看订阅席位明细
bl token-plan add-member --account-name dev --org-id org_xxx
bl token-plan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
bl token-plan create-key --account-id acc_xxx --workspace-id ws_xxx
```
| 场景 | 可以这样对 Agent 说 |
| ---------------- | ----------------------------------------------------------------------- |
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
> 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
## 认证方式
### DashScope API Key
### API Key
大部分命令均需要 API Key。前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取。
```bash
# 方式一:环境变量
export DASHSCOPE_API_KEY=sk-xxxxx
# 方式二:登录命令(持久化到 ~/.bailian/config.json
bl auth login --api-key sk-xxxxx
# 方式三:命令行参数
bl text chat --api-key sk-xxxxx --message "你好"
```
### Token Plan API Key
前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制 API Key。
CLI 已内置 Token Plan 的默认 Base URL登录命令会先测试 Key通过后才保存并激活 `token-plan` 配置。
Token Plan API Key 前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制。
```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
@@ -184,26 +122,20 @@ bl auth login --config token-plan --api-key sk-sp-xxxxx
### 控制台登录OAuth
控制台能力命令(`model list``app list``usage summary/free/stats``workspace list``quota list/request/check/history`)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
```bash
bl auth login --console
```
### 阿里云 OpenAPI AK/SK(仅 Token Plan
### 阿里云 OpenAPI AK/SK
`token-plan` 命令组需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
Token Plan 的席位与成员管理需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
```bash
# 方式一:登录命令(持久化到 ~/.bailian/config.json
bl auth login --open-api --access-key-id LTAI5t... --access-key-secret ...
# 方式二:环境变量
export ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5t...
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
export BAILIAN_WORKSPACE_ID=ws-...
```
## 配置
@@ -212,20 +144,31 @@ export BAILIAN_WORKSPACE_ID=ws-...
# 查看当前配置
bl config show
# 设置默认值
bl config set --key base_url --value https://dashscope-us.aliyuncs.com
bl config set --key default_text_model --value qwen-turbo
bl config set --key timeout --value 600
# 查看全部配置档
bl config list
# 自更新到最新版本
bl update
# 安装指定版本
bl update --to 0.1.14
# 切换配置档
bl config use --name token-plan
```
配置文件位置:`~/.bailian/config.json`
## 更新
```bash
bl update
```
升级 CLI 至最新版本,并同步更新已安装的 Agent Skills。每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
欢迎扫码加入阿里云百炼 CLI 钉钉用户交流群获取使用答疑、问题排查、Bug 反馈和使用经验交流支持。
<img src="https://img.alicdn.com/imgextra/i3/O1CN015uuhYGb6j0L12xJZ_!!6000000006304-2-tps-516-485.png" alt="阿里云百炼 CLI 钉钉用户交流群" width="240" />
## 相关链接
| 资源 | 地址 |
@@ -237,11 +180,3 @@ bl update --to 0.1.14
| 获取 API Key | https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key |
| 获取 Token Plan API Key | https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview |
| 获取 AccessKey | https://ram.console.aliyun.com/manage/ak |
## 更新日志
每个版本的变更详情记录在 [CHANGELOG.zh.md](https://github.com/modelstudioai/cli/blob/main/CHANGELOG.zh.md)。
## 参与贡献
欢迎提 Issue、Feature Request 和 PR。开发环境搭建、仓库结构、新增/修改命令的工作流请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.14.1",
"version": "1.14.2",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
+10
View File
@@ -62,6 +62,7 @@ import {
finetuneTextCreate,
finetuneAudioCreate,
finetuneImageCreate,
finetuneVideoCreate,
finetuneList,
finetuneGet,
finetuneCancel,
@@ -71,6 +72,7 @@ import {
finetuneExport,
finetuneWatch,
finetuneCapability,
finetunePrice,
deployTextCreate,
deployAudioCreate,
deployImageCreate,
@@ -80,6 +82,8 @@ import {
deployScale,
deployUpdate,
deployDelete,
deployPause,
deployResume,
tokenPlanListSeats,
tokenPlanCreateKey,
tokenPlanAssignSeats,
@@ -93,6 +97,7 @@ import {
skillUpdate,
skillRemove,
skillList,
skillInit,
managedAgentInit,
managedAgentValidate,
managedAgentPlan,
@@ -180,6 +185,7 @@ export const commands: Record<string, AnyCommand> = {
"finetune text create": finetuneTextCreate,
"finetune audio create": finetuneAudioCreate,
"finetune image create": finetuneImageCreate,
"finetune video create": finetuneVideoCreate,
"finetune list": finetuneList,
"finetune get": finetuneGet,
"finetune cancel": finetuneCancel,
@@ -189,6 +195,7 @@ export const commands: Record<string, AnyCommand> = {
"finetune export": finetuneExport,
"finetune watch": finetuneWatch,
"finetune capability": finetuneCapability,
"finetune price": finetunePrice,
"deploy text create": deployTextCreate,
"deploy audio create": deployAudioCreate,
"deploy image create": deployImageCreate,
@@ -198,6 +205,8 @@ export const commands: Record<string, AnyCommand> = {
"deploy scale": deployScale,
"deploy update": deployUpdate,
"deploy delete": deployDelete,
"deploy pause": deployPause,
"deploy resume": deployResume,
"token-plan list-seats": tokenPlanListSeats,
"token-plan create-key": tokenPlanCreateKey,
"token-plan assign-seats": tokenPlanAssignSeats,
@@ -211,6 +220,7 @@ export const commands: Record<string, AnyCommand> = {
"skill update": skillUpdate,
"skill remove": skillRemove,
"skill list": skillList,
"skill init": skillInit,
"managed-agent init": managedAgentInit,
"managed-agent validate": managedAgentValidate,
"managed-agent plan": managedAgentPlan,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.14.1",
"version": "1.14.2",
"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": {
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, deleteDataset, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const DELETE_FLAGS = {
fileId: {
@@ -19,20 +19,18 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const fileId = flags.fileId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "dataset.delete", file_id: fileId }, format);
emitResult({ action: "dataset.delete", file_id: fileId }, "json");
return;
}
const response = await deleteDataset(ctx.client, fileId);
if (settings.quiet || format === "text") {
emitBare(`Deleted ${fileId}.`);
emitRequestId(response.request_id, settings.quiet);
if (settings.quiet) {
emitBare(fileId);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
+7 -17
View File
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, getDataset, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, getDataset, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const GET_FLAGS = {
fileId: {
@@ -19,10 +19,9 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const fileId = flags.fileId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "dataset.get", file_id: fileId }, format);
emitResult({ action: "dataset.get", file_id: fileId }, "json");
return;
}
@@ -45,19 +44,10 @@ export default defineCommand({
description: file.description ?? "",
};
if (format === "json") {
emitResult({ ...item, request_id: response.request_id }, format);
return;
if (settings.quiet) {
emitBare(item.file_id);
} else {
emitResult({ ...item, request_id: response.request_id }, "json");
}
// text / quiet
emitBare(`file_id: ${item.file_id}`);
emitBare(`name: ${item.name}`);
emitBare(`size: ${item.size}`);
if (item.md5) emitBare(`md5: ${item.md5}`);
if (item.purpose) emitBare(`purpose: ${item.purpose}`);
if (item.created_at) emitBare(`created_at: ${item.created_at}`);
if (item.description) emitBare(`description: ${item.description}`);
emitRequestId(response.request_id, settings.quiet);
},
});
+7 -19
View File
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, listDatasets, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
import { defineCommand, listDatasets, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const LIST_FLAGS = {
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
@@ -23,7 +23,6 @@ export default defineCommand({
exampleArgs: ["", "--purpose fine-tune", "--purpose evaluation --page-size 20", "--output json"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
@@ -33,7 +32,7 @@ export default defineCommand({
page_size: flags.pageSize,
purpose: flags.purpose,
},
format,
"json",
);
return;
}
@@ -46,7 +45,6 @@ export default defineCommand({
const files = response.data?.files ?? [];
const total = response.data?.total;
// Normalize to consistent structure for both text/json output.
const items = files.map((item) => ({
file_id: item.file_id ?? "",
name: item.name ?? "",
@@ -54,20 +52,10 @@ export default defineCommand({
purpose: item.purpose ?? "",
}));
if (format === "json") {
emitResult({ items, total, request_id: response.request_id }, format);
return;
if (settings.quiet) {
for (const item of items) emitBare(item.file_id);
} else {
emitResult({ items, total, request_id: response.request_id }, "json");
}
// text / quiet
if (items.length === 0) {
emitBare("No dataset files found.");
return;
}
const headers = ["FILE_ID", "NAME", "SIZE", "PURPOSE"];
const rows = items.map((i) => [i.file_id, i.name, i.size, i.purpose]);
for (const line of formatTable(headers, rows)) emitBare(line);
if (total !== undefined) emitBare(`\nTotal: ${total}`);
emitRequestId(response.request_id, settings.quiet);
},
});
@@ -1,6 +1,5 @@
import {
defineCommand,
detectOutputFormat,
uploadDataset,
validateDataset,
parseDatasetSchemaFlag,
@@ -11,7 +10,7 @@ import {
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
const UPLOAD_FLAGS = {
file: {
@@ -29,7 +28,7 @@ const UPLOAD_FLAGS = {
type: "string",
valueHint: "<s>",
description:
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), or "image" (image generation). Default auto-detects per record.',
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.',
},
noValidate: {
type: "switch",
@@ -45,7 +44,7 @@ export default defineCommand({
description: "Upload a dataset file (.jsonl or .zip) to Bailian",
auth: "apiKey",
usageArgs:
"--file <path> [--purpose <name>] [--schema <chatml|dpo|cpt|tts|image>] [--no-validate] [--full-validate]",
"--file <path> [--purpose <name>] [--schema <chatml|dpo|cpt|tts|image|video>] [--no-validate] [--full-validate]",
flags: UPLOAD_FLAGS,
exampleArgs: [
"--file train.jsonl",
@@ -73,16 +72,8 @@ export default defineCommand({
const filePath = flags.file;
const purpose = flags.purpose || "fine-tune";
const schema = parseDatasetSchemaFlag(flags.schema);
if (schema === "video") {
throw new BailianError(
`--schema video is not supported.`,
ExitCode.USAGE,
`Supported schemas: chatml, dpo, cpt, tts, image.`,
);
}
const format = detectOutputFormat(settings.output);
// Image schema allows larger ZIPs (1 GB vs 300 MB for text).
const isMediaSchema = schema === "image";
// Image and video schemas allow larger ZIPs (1 GB vs 300 MB for text).
const isMediaSchema = schema === "image" || schema === "video";
if (!flags.noValidate) {
const maxBytes = isMediaSchema ? MAX_MEDIA_ZIP_BYTES : MAX_DATASET_BYTES;
@@ -129,7 +120,7 @@ export default defineCommand({
validate: !flags.noValidate,
schema: schema ?? "auto",
},
format,
"json",
);
return;
}
@@ -142,11 +133,8 @@ export default defineCommand({
if (settings.quiet) {
emitBare(file.file_id);
} else if (format === "text") {
emitBare(`Uploaded ${file.name} → file_id=${file.file_id}`);
emitRequestId(request_id, settings.quiet);
} else {
emitResult({ ...file, request_id }, format);
emitResult({ ...file, request_id }, "json");
}
},
});
@@ -1,26 +1,13 @@
import {
defineCommand,
detectOutputFormat,
validateDataset,
parseDatasetSchemaFlag,
formatIssue,
BailianError,
ExitCode,
type ValidationResult,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
function formatStats(result: ValidationResult): string[] {
const out: string[] = [];
if (result.stats.totalRecords !== undefined) out.push(`records: ${result.stats.totalRecords}`);
if (result.stats.sampledRecords !== undefined)
out.push(`sampled: ${result.stats.sampledRecords}`);
if (result.stats.bytes !== undefined) out.push(`bytes: ${result.stats.bytes}`);
if (result.stats.durationMs !== undefined) out.push(`took: ${result.stats.durationMs}ms`);
return out;
}
const VALIDATE_FLAGS = {
file: {
type: "string",
@@ -36,7 +23,7 @@ const VALIDATE_FLAGS = {
type: "string",
valueHint: "<s>",
description:
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), or "image" (image generation). Default auto-detects per record.',
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.',
},
} satisfies FlagsDef;
@@ -44,13 +31,14 @@ export default defineCommand({
description: "Locally validate a dataset file (.jsonl or .zip) without uploading",
// 纯本地校验,不触网、不需 API key与 `pipeline validate` 一致)。
auth: "none",
usageArgs: "--file <path> [--full-validate] [--schema <chatml|dpo|cpt|tts|image>]",
usageArgs: "--file <path> [--full-validate] [--schema <chatml|dpo|cpt|tts|image|video>]",
flags: VALIDATE_FLAGS,
exampleArgs: [
"--file train.jsonl",
"--file dpo.jsonl --schema dpo",
"--file cpt.jsonl --schema cpt",
"--file audio.zip --schema tts",
"--file wan-i2v-training-dataset.zip --schema video",
"--file eval.jsonl --full-validate",
"--file train.jsonl --output json",
],
@@ -60,27 +48,20 @@ export default defineCommand({
"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). 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. Pass --schema to",
"require a specific shape on every record. ZIP archives (.zip) are",
"validated structurally (data.jsonl present, media references resolve) in",
"addition to per-record content checks. Use --full-validate to JSON.parse",
"every line.",
'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). 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. ZIP archives (.zip) are validated structurally",
"(data.jsonl present, media references resolve) in addition to per-record",
"content checks. Use --full-validate to JSON.parse every line.",
],
async run(ctx) {
const { settings, flags } = ctx;
const filePath = flags.file;
const schema = parseDatasetSchemaFlag(flags.schema);
if (schema === "video") {
throw new BailianError(
`--schema video is not supported.`,
ExitCode.USAGE,
`Supported schemas: chatml, dpo, cpt, tts, image.`,
);
}
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
{
@@ -89,38 +70,17 @@ export default defineCommand({
full: flags.fullValidate,
schema: schema ?? "auto",
},
format,
"json",
);
return;
}
const result = await validateDataset(filePath, { fullValidate: flags.fullValidate, schema });
if (format === "json") {
// For json output we always emit the structured result, exit code conveys validity.
emitResult(result, format);
} else if (settings.quiet) {
if (settings.quiet) {
emitBare(result.valid ? "ok" : "fail");
} else {
const status = result.valid ? "PASSED" : "FAILED";
emitBare(`Dataset validation ${status} for ${result.filePath}`);
const stats = formatStats(result);
if (stats.length) emitBare(` ${stats.join(" · ")}`);
if (result.errors.length) {
emitBare(`Errors (${result.errors.length}):`);
for (const error of result.errors.slice(0, 20)) emitBare(formatIssue(error));
if (result.errors.length > 20) {
emitBare(` … and ${result.errors.length - 20} more.`);
}
}
if (result.warnings.length) {
emitBare(`Warnings (${result.warnings.length}):`);
for (const warning of result.warnings.slice(0, 10)) emitBare(formatIssue(warning));
if (result.warnings.length > 10) {
emitBare(` … and ${result.warnings.length - 10} more.`);
}
}
emitResult(result, "json");
}
if (!result.valid) {
+25 -39
View File
@@ -1,6 +1,5 @@
import {
defineCommand,
detectOutputFormat,
createDeployment,
pickPlanStrategy,
STRATEGIES,
@@ -11,16 +10,16 @@ import {
type CommandContext,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
const CREATE_FLAGS = {
model: {
modelName: {
type: "string",
valueHint: "<name>",
description: "Model name (catalog model or fine-tuned output) (required)",
valueHint: "<model_name>",
description: "Model to deploy — fine-tuned output name or catalog model (required)",
required: true,
},
name: {
displayName: {
type: "string",
valueHint: "<display_name>",
description: "Console display name for the deployment (required)",
@@ -64,7 +63,7 @@ const CREATE_FLAGS = {
} satisfies FlagsDef;
const CREATE_USAGE =
"--model <model_name> --name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]";
"--model-name <model_name> --display-name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]";
const CREATE_NOTES = [
"Plan defaults to `lora` (Token-billed) for text/image and `mu` (model-unit-",
@@ -78,14 +77,11 @@ const CREATE_NOTES = [
"Use `bl deploy models --source base` to inspect available templates.",
"After creation, status starts at PENDING and transitions to RUNNING.",
"Invoke the deployed model with: bl text chat --model <deployed_model>",
"WARNING: --model is overloaded across commands and refers to DIFFERENT",
"values. `bl deploy <modality> create --model` takes the exported model_name",
"(e.g. `qwen3-8b-ft-...`), but the create response also returns a",
"`deployed_model` field (the deployment instance id, e.g.",
"`qwen3-8b-5ecb5f068d79`). The inference call `bl text chat --model` must use",
"the `deployed_model` from the create response — NOT the `model_name` you",
"passed to `deploy <modality> create`. Do not reuse the value across the two",
"commands.",
"NOTE: --model-name is the model being deployed (e.g. `qwen3-8b-ft-...`).",
"The create response also returns a `deployed_model` field — the deployment",
"instance id (e.g. `qwen3-8b-5ecb5f068d79`). Use that id for inference",
"(`bl text chat --model <deployed_model>`) and lifecycle commands",
"(`deploy get/scale/pause/resume/delete --deployed-model <id>`).",
];
/**
@@ -119,10 +115,9 @@ async function runCreate(
ctx: CommandContext<typeof CREATE_FLAGS>,
): Promise<void> {
const { identity, settings, flags } = ctx;
const model = flags.model as string;
const name = flags.name as string;
const model = flags.modelName as string;
const name = flags.displayName as string;
const plan = (flags.plan as string | undefined) || defaultDeployPlan(modality);
const format = detectOutputFormat(settings.output);
// Plan-specific behaviour is owned by core `plans.ts`. The strategy resolves
// the plan-specific body fragment (mu may auto-pick a template from the
@@ -146,7 +141,7 @@ async function runCreate(
};
if (settings.dryRun) {
emitResult({ action: "deploy.create", body }, format);
emitResult({ action: "deploy.create", body }, "json");
return;
}
@@ -155,17 +150,8 @@ async function runCreate(
if (settings.quiet) {
emitBare(deployment?.deployed_model ?? "");
} else if (format === "text") {
emitBare(`Created deployment.`);
if (deployment?.deployed_model) emitBare(` deployed_model: ${deployment.deployed_model}`);
if (deployment?.status) emitBare(` status: ${deployment.status}`);
if (deployment?.plan) emitBare(` plan: ${deployment.plan}`);
emitBare(
`\nNext: track readiness with: ${identity.binName} deploy get --deployed-model ${deployment?.deployed_model ?? "<id>"}`,
);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
}
@@ -176,10 +162,10 @@ export const deployTextCreate = defineCommand({
usageArgs: CREATE_USAGE,
flags: CREATE_FLAGS,
exampleArgs: [
"--model my-qwen-sft --name my-sft-test",
"--model qwen3.6-flash-2026-04-16 --name my-flash --plan ptu --input-tpm 10000 --output-tpm 1000",
"--model qwen3-8b --name my-qwen3-mu --plan mu",
"--model qwen3-8b --name my-qwen3 --plan mu --deploy-spec MU1 --capacity 2",
"--model-name my-qwen-sft --display-name my-sft-test",
"--model-name qwen3.6-flash-2026-04-16 --display-name my-flash --plan ptu --input-tpm 10000 --output-tpm 1000",
"--model-name qwen3-8b --display-name my-qwen3-mu --plan mu",
"--model-name qwen3-8b --display-name my-qwen3 --plan mu --deploy-spec MU1 --capacity 2",
],
notes: CREATE_NOTES,
validate: (flags) => validateCreate("text", flags),
@@ -193,9 +179,9 @@ export const deployAudioCreate = defineCommand({
usageArgs: CREATE_USAGE,
flags: CREATE_FLAGS,
exampleArgs: [
"--model my-cosyvoice-ft --name my-tts",
"--model my-cosyvoice-ft --name my-tts --deploy-spec dps-xxxx --capacity 1",
"--model my-cosyvoice-ft --name my-tts --dry-run",
"--model-name my-cosyvoice-ft --display-name my-tts",
"--model-name my-cosyvoice-ft --display-name my-tts --deploy-spec dps-xxxx --capacity 1",
"--model-name my-cosyvoice-ft --display-name my-tts --dry-run",
],
notes: CREATE_NOTES,
validate: (flags) => validateCreate("audio", flags),
@@ -209,9 +195,9 @@ export const deployImageCreate = defineCommand({
usageArgs: CREATE_USAGE,
flags: CREATE_FLAGS,
exampleArgs: [
"--model my-wan-ft --name my-wan",
"--model my-wan-ft --name my-wan-mu --plan mu",
"--model my-wan-ft --name my-wan --dry-run",
"--model-name my-wan-ft --display-name my-wan",
"--model-name my-wan-ft --display-name my-wan-mu --plan mu",
"--model-name my-wan-ft --display-name my-wan --dry-run",
],
notes: CREATE_NOTES,
validate: (flags) => validateCreate("image", flags),
@@ -1,13 +1,12 @@
import {
defineCommand,
detectOutputFormat,
deleteDeployment,
getDeployment,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
const DELETE_FLAGS = {
deployedModel: {
@@ -38,10 +37,9 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "deploy.delete", deployed_model: deployedModel }, format);
emitResult({ action: "deploy.delete", deployed_model: deployedModel }, "json");
return;
}
@@ -55,7 +53,8 @@ export default defineCommand({
if (status && status !== "STOPPED" && status !== "FAILED") {
throw new BailianError(
`Deployment ${deployedModel} is ${status}. Only STOPPED / FAILED deployments can be deleted. ` +
`Stop it first via the platform console, or pass --skip-precheck to attempt deletion anyway.`,
`Run \`bl deploy pause --deployed-model ${deployedModel}\` to pause it first, ` +
`or pass --skip-precheck to attempt deletion anyway.`,
ExitCode.USAGE,
);
}
@@ -69,11 +68,8 @@ export default defineCommand({
if (settings.quiet) {
emitBare(deployedModel);
} else if (format === "text") {
emitBare(`Deleted ${deployedModel}.`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
+5 -18
View File
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, getDeployment, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, getDeployment, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const GET_FLAGS = {
deployedModel: {
@@ -22,10 +22,9 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "deploy.get", deployed_model: deployedModel }, format);
emitResult({ action: "deploy.get", deployed_model: deployedModel }, "json");
return;
}
@@ -33,7 +32,7 @@ export default defineCommand({
const deployment = response.output ?? response.data;
if (!deployment) {
emitBare(`No data returned for ${deployedModel}`);
emitResult({ deployed_model: deployedModel, request_id: response.request_id }, "json");
return;
}
@@ -57,18 +56,6 @@ export default defineCommand({
if (deployment.gmt_create) item.created_at = deployment.gmt_create;
if (deployment.gmt_modified) item.updated_at = deployment.gmt_modified;
if (format === "json") {
emitResult({ ...item, request_id: response.request_id }, format);
return;
}
// text / quiet — fixed-width label column for alignment
const label = (key: string) => `${key}:`.padEnd(18);
for (const [key, value] of Object.entries(item)) {
if (value === "" || value === undefined) continue;
const display = typeof value === "string" ? value : JSON.stringify(value);
emitBare(`${label(key)}${display}`);
}
emitRequestId(response.request_id, settings.quiet);
emitResult({ ...item, request_id: response.request_id }, "json");
},
});
+4 -31
View File
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
listDeployments,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
import { defineCommand, listDeployments, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const LIST_FLAGS = {
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
@@ -28,13 +23,12 @@ export default defineCommand({
exampleArgs: ["", "--status RUNNING", "--page-size 20 --output json"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const status = flags.status || undefined;
if (settings.dryRun) {
emitResult(
{ action: "deploy.list", page: flags.page, page_size: flags.pageSize, status },
format,
"json",
);
return;
}
@@ -57,27 +51,6 @@ export default defineCommand({
created_at: item.gmt_create ?? "",
}));
if (format === "json") {
emitResult({ items, total, request_id: response.request_id }, format);
return;
}
// text / quiet
if (items.length === 0) {
emitBare("No deployments found.");
return;
}
const headers = ["DEPLOYED_MODEL", "MODEL_NAME", "STATUS", "PLAN", "CAPACITY", "CREATED_AT"];
const rows = items.map((item) => [
item.deployed_model,
item.model_name,
item.status,
item.plan,
item.capacity,
item.created_at,
]);
for (const line of formatTable(headers, rows)) emitBare(line);
if (total !== undefined) emitBare(`\nTotal: ${total}`);
emitRequestId(response.request_id, settings.quiet);
emitResult({ items, total, request_id: response.request_id }, "json");
},
});
+49 -102
View File
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
listDeployableModels,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
import { defineCommand, listDeployableModels, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const MODELS_FLAGS = {
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
@@ -39,7 +34,6 @@ export default defineCommand({
],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
// Default version to v1.0 — without it, the API returns the legacy catalog
// (only old fine-tune outputs). Pass --catalog-version "" to opt out.
const version = flags.catalogVersion === "" ? undefined : (flags.catalogVersion ?? "v1.0");
@@ -54,7 +48,7 @@ export default defineCommand({
version,
model_source: modelSource,
},
format,
"json",
);
return;
}
@@ -72,102 +66,55 @@ export default defineCommand({
// Two response shapes:
// - custom (fine-tuned): top-level supported_plans: string[]
// - base (catalog): plans: [{plan, templates?, cu_specs?}]
// For json: surface the deployment-relevant fields preserved as a tree, so
// Surface the deployment-relevant fields preserved as a tree, so
// downstream tooling can drive `bl deploy <modality> create --deploy-spec <…>`
// without a second round-trip. For text: keep the compact one-line summary.
if (format === "json") {
const items = models.map((model) => {
const out: Record<string, unknown> = {
model_name: model.model_name ?? "",
};
if (model.base_model) out.base_model = model.base_model;
if (model.model_source) out.model_source = model.model_source;
if (model.supported_plans && model.supported_plans.length > 0) {
out.supported_plans = model.supported_plans;
}
if (model.plans && model.plans.length > 0) {
out.plans = model.plans.map((plan) => {
const planEntry: Record<string, unknown> = { plan: plan.plan ?? "" };
if (plan.cu_specs && plan.cu_specs.length > 0) {
planEntry.cu_specs = plan.cu_specs;
}
if (plan.templates && plan.templates.length > 0) {
// Pull the top 6 fields most useful for `bl deploy <modality> create`.
// Drop noisy/redundant: template_source, template_type,
// template_version, deploy_spec (typically == template_id).
planEntry.templates = plan.templates.map((template) => {
const tpl: Record<string, unknown> = {};
if (template.template_id) tpl.template_id = template.template_id;
if (template.template_name) tpl.template_name = template.template_name;
if (template.charge_type) tpl.charge_type = template.charge_type;
// Flatten roles.unified for the common COUPLED case.
const unified = template.roles?.unified;
if (unified?.model_unit_spec) tpl.model_unit_spec = unified.model_unit_spec;
if (unified?.capacity_unit_per_instance !== undefined)
tpl.capacity_unit_per_instance = unified.capacity_unit_per_instance;
// Preserve split-role configs (SEPERATED) as-is so callers
// can still drive prefill/decode sizing.
if (template.roles?.prefill || template.roles?.decode) {
tpl.roles = {
prefill: template.roles?.prefill,
decode: template.roles?.decode,
};
}
if (template.template_desc) tpl.template_desc = template.template_desc;
return tpl;
});
}
return planEntry;
});
}
return out;
});
emitResult({ items, total, request_id: response.request_id }, format);
return;
}
// text / quiet — keep the compact single-line summary table.
const textItems = models.map((model) => {
let plansSummary = "";
if (model.supported_plans && model.supported_plans.length > 0) {
plansSummary = model.supported_plans.join(",");
} else if (model.plans && model.plans.length > 0) {
plansSummary = model.plans
.map((plan) => {
const planName = plan.plan ?? "?";
if (plan.templates && plan.templates.length > 0) {
return `${planName}(${plan.templates.length}t)`;
}
if (plan.cu_specs && plan.cu_specs.length > 0) {
return `${planName}(${plan.cu_specs.join("/")})`;
}
return planName;
})
.join(",");
} else {
plansSummary = "-";
}
return {
// without a second round-trip.
const items = models.map((model) => {
const out: Record<string, unknown> = {
model_name: model.model_name ?? "",
base_model: model.base_model ?? "",
source: model.model_source ?? "",
plans: plansSummary,
};
if (model.base_model) out.base_model = model.base_model;
if (model.model_source) out.model_source = model.model_source;
if (model.supported_plans && model.supported_plans.length > 0) {
out.supported_plans = model.supported_plans;
}
if (model.plans && model.plans.length > 0) {
out.plans = model.plans.map((plan) => {
const planEntry: Record<string, unknown> = { plan: plan.plan ?? "" };
if (plan.cu_specs && plan.cu_specs.length > 0) {
planEntry.cu_specs = plan.cu_specs;
}
if (plan.templates && plan.templates.length > 0) {
// Pull the top 6 fields most useful for `bl deploy <modality> create`.
// Drop noisy/redundant: template_source, template_type,
// template_version, deploy_spec (typically == template_id).
planEntry.templates = plan.templates.map((template) => {
const tpl: Record<string, unknown> = {};
if (template.template_id) tpl.template_id = template.template_id;
if (template.template_name) tpl.template_name = template.template_name;
if (template.charge_type) tpl.charge_type = template.charge_type;
// Flatten roles.unified for the common COUPLED case.
const unified = template.roles?.unified;
if (unified?.model_unit_spec) tpl.model_unit_spec = unified.model_unit_spec;
if (unified?.capacity_unit_per_instance !== undefined)
tpl.capacity_unit_per_instance = unified.capacity_unit_per_instance;
// Preserve split-role configs (SEPERATED) as-is so callers
// can still drive prefill/decode sizing.
if (template.roles?.prefill || template.roles?.decode) {
tpl.roles = {
prefill: template.roles?.prefill,
decode: template.roles?.decode,
};
}
if (template.template_desc) tpl.template_desc = template.template_desc;
return tpl;
});
}
return planEntry;
});
}
return out;
});
if (textItems.length === 0) {
emitBare("No deployable models found.");
return;
}
const headers = ["MODEL_NAME", "BASE_MODEL", "SOURCE", "PLANS"];
const rows = textItems.map((item) => [
item.model_name,
item.base_model,
item.source,
item.plans,
]);
for (const line of formatTable(headers, rows)) emitBare(line);
if (total !== undefined) emitBare(`\nTotal: ${total}`);
emitRequestId(response.request_id, settings.quiet);
emitResult({ items, total, request_id: response.request_id }, "json");
},
});
@@ -0,0 +1,85 @@
import {
defineCommand,
stopModelService,
listIndependentDeployedModels,
findDeploymentEntry,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const PAUSE_FLAGS = {
deployedModel: {
type: "string",
valueHint: "<id>",
description: "Deployed model identifier (required)",
required: true,
},
skipPrecheck: {
type: "switch",
description: "Skip the local RUNNING/PENDING status precheck",
},
} satisfies FlagsDef;
/**
* `bl deploy pause` — pause a running deployment.
*
* Takes the model service offline so it no longer serves inference requests.
* For mu/ptu plans, billing stops while paused.
* Precheck: status must be RUNNING or PENDING.
*/
export default defineCommand({
description: "Pause a running model deployment (stops billing for mu/ptu)",
auth: "console",
usageArgs: "--deployed-model <id> [--skip-precheck]",
flags: PAUSE_FLAGS,
exampleArgs: [
"--deployed-model dep-...",
"--deployed-model dep-... --skip-precheck",
"--deployed-model dep-... --dry-run",
],
notes: [
"While paused, billing ceases for mu/ptu plans. Use `deploy resume` to bring it back online or `deploy delete` to remove.",
"Precheck verifies status is RUNNING/PENDING before issuing the pause; pass --skip-precheck to bypass.",
],
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
if (settings.dryRun) {
emitResult({ action: "deploy.pause", deployed_model: deployedModel }, "json");
return;
}
// Precheck: verify the deployment is in a pausable state.
if (!flags.skipPrecheck) {
try {
const entries = await listIndependentDeployedModels(ctx.client);
const entry = findDeploymentEntry(entries, deployedModel);
if (entry) {
const status = (entry.status ?? "").toUpperCase();
if (status && status !== "RUNNING" && status !== "PENDING") {
throw new BailianError(
`Deployment ${deployedModel} is ${status}. Only RUNNING / PENDING deployments can be paused. ` +
`Pass --skip-precheck to attempt the pause anyway.`,
ExitCode.USAGE,
);
}
}
// If entry not found in list, proceed — the server will surface the real error.
} catch (error) {
if (error instanceof BailianError) throw error;
// If the list call itself failed, proceed and let the API call surface the error.
}
}
const response = await stopModelService(ctx.client, deployedModel);
if (settings.quiet) {
emitBare(deployedModel);
} else {
emitResult({ deployed_model: deployedModel, action: "pause", ...response }, "json");
}
},
});
@@ -0,0 +1,84 @@
import {
defineCommand,
startModelService,
listIndependentDeployedModels,
findDeploymentEntry,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const RESUME_FLAGS = {
deployedModel: {
type: "string",
valueHint: "<id>",
description: "Deployed model identifier (required)",
required: true,
},
skipPrecheck: {
type: "switch",
description: "Skip the local STOPPED status precheck",
},
} satisfies FlagsDef;
/**
* `bl deploy resume` — resume a paused deployment.
*
* Brings the model service back online so it can serve inference requests.
* Precheck: status must be STOPPED.
*/
export default defineCommand({
description: "Resume a paused model deployment (brings service back online)",
auth: "console",
usageArgs: "--deployed-model <id> [--skip-precheck]",
flags: RESUME_FLAGS,
exampleArgs: [
"--deployed-model dep-...",
"--deployed-model dep-... --skip-precheck",
"--deployed-model dep-... --dry-run",
],
notes: [
"Precheck verifies status is STOPPED before issuing the resume; pass --skip-precheck to bypass.",
"For mu/ptu plans, billing resumes once the service is back online.",
],
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
if (settings.dryRun) {
emitResult({ action: "deploy.resume", deployed_model: deployedModel }, "json");
return;
}
// Precheck: verify the deployment is in a resumable state.
if (!flags.skipPrecheck) {
try {
const entries = await listIndependentDeployedModels(ctx.client);
const entry = findDeploymentEntry(entries, deployedModel);
if (entry) {
const status = (entry.status ?? "").toUpperCase();
if (status && status !== "STOPPED") {
throw new BailianError(
`Deployment ${deployedModel} is ${status}. Only STOPPED deployments can be resumed. ` +
`Pass --skip-precheck to attempt the resume anyway.`,
ExitCode.USAGE,
);
}
}
// If entry not found in list, proceed — the server will surface the real error.
} catch (error) {
if (error instanceof BailianError) throw error;
// If the list call itself failed, proceed and let the API call surface the error.
}
}
const response = await startModelService(ctx.client, deployedModel);
if (settings.quiet) {
emitBare(deployedModel);
} else {
emitResult({ deployed_model: deployedModel, action: "resume", ...response }, "json");
}
},
});
+4 -15
View File
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
scaleDeployment,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, scaleDeployment, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const SCALE_FLAGS = {
deployedModel: {
@@ -52,7 +47,6 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
const format = detectOutputFormat(settings.output);
const body: Record<string, unknown> = {};
if (flags.capacity !== undefined) body.capacity = flags.capacity;
@@ -60,21 +54,16 @@ export default defineCommand({
if (flags.outputTpm !== undefined) body.output_tpm = flags.outputTpm;
if (settings.dryRun) {
emitResult({ action: "deploy.scale", deployed_model: deployedModel, body }, format);
emitResult({ action: "deploy.scale", deployed_model: deployedModel, body }, "json");
return;
}
const response = await scaleDeployment(ctx.client, deployedModel, body);
const deployment = response.output ?? response.data;
if (settings.quiet) {
emitBare(deployedModel);
} else if (format === "text") {
const cap = deployment?.capacity !== undefined ? ` (capacity=${deployment.capacity})` : "";
emitBare(`Scaled ${deployedModel}${cap}.`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
updateDeployment,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, updateDeployment, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const UPDATE_FLAGS = {
deployedModel: {
@@ -48,31 +43,22 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
const format = detectOutputFormat(settings.output);
const body: Record<string, unknown> = {};
if (flags.rpmLimit !== undefined) body.rpm_limit = flags.rpmLimit;
if (flags.tpmLimit !== undefined) body.tpm_limit = flags.tpmLimit;
if (settings.dryRun) {
emitResult({ action: "deploy.update", deployed_model: deployedModel, body }, format);
emitResult({ action: "deploy.update", deployed_model: deployedModel, body }, "json");
return;
}
const response = await updateDeployment(ctx.client, deployedModel, body);
const deployment = response.output ?? response.data;
if (settings.quiet) {
emitBare(deployedModel);
} else if (format === "text") {
const parts: string[] = [];
if (deployment?.rpm_limit !== undefined) parts.push(`rpm_limit=${deployment.rpm_limit}`);
if (deployment?.tpm_limit !== undefined) parts.push(`tpm_limit=${deployment.tpm_limit}`);
const summary = parts.length ? ` (${parts.join(", ")})` : "";
emitBare(`Updated ${deployedModel}${summary}.`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, cancelFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, cancelFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const CANCEL_FLAGS = {
jobId: {
@@ -23,24 +23,18 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const jobId = flags.jobId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "finetune.cancel", job_id: jobId }, format);
emitResult({ action: "finetune.cancel", job_id: jobId }, "json");
return;
}
const response = await cancelFineTune(ctx.client, jobId);
const job = response.output ?? response.data;
if (settings.quiet) {
emitBare(jobId);
} else if (format === "text") {
const status = job?.status ? ` (status=${job.status})` : "";
emitBare(`Cancelled ${jobId}${status}.`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
@@ -1,6 +1,5 @@
import {
defineCommand,
detectOutputFormat,
fetchModelList,
fetchModelCapability,
listSupportedTrainingTypes,
@@ -43,19 +42,8 @@ async function fetchAllFoundationModels(settings: Settings): Promise<ModelCapabi
return all as ModelCapability[];
}
const VARIANT_LABEL: Record<string, string> = {
full: "full-parameter",
lora: "LoRA",
};
function describeTrainingType(value: string): string {
if (!isTrainingTypeCli(value)) return value;
const { method, variant } = trainingTypeMethodVariant(value);
return `${VARIANT_LABEL[variant] ?? variant} ${method.toUpperCase()}`;
}
const CAPABILITY_FLAGS = {
model: {
baseModel: {
type: "string",
valueHint: "<m>",
description: "List training types supported by this base model.",
@@ -71,31 +59,31 @@ export default defineCommand({
description:
"Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it)",
auth: "none",
usageArgs: "--model <m> | --training-type <t>",
usageArgs: "--base-model <m> | --training-type <t>",
flags: CAPABILITY_FLAGS,
exampleArgs: [
"--model qwen3-8b",
"--base-model qwen3-8b",
"--training-type sft-lora",
"--training-type cpt --output json",
"--training-type sft --quiet",
],
notes: [
"Exactly one of --model / --training-type is required.",
"Exactly one of --base-model / --training-type is required.",
"Training-type values use the `<method>` / `<method>-lora` convention:",
"sft | sft-lora | dpo | dpo-lora | cpt. (cpt has no -lora variant server-side.)",
"Queries listFoundationModels, a public API — no console login needed.",
],
validate: (f) => {
if (f.model && f.trainingType)
return "--model and --training-type are mutually exclusive; pass one.";
if (!f.model && !f.trainingType) return "one of --model / --training-type is required.";
if (f.baseModel && f.trainingType)
return "--base-model and --training-type are mutually exclusive; pass one.";
if (!f.baseModel && !f.trainingType)
return "one of --base-model / --training-type is required.";
return undefined;
},
async run(ctx) {
const { settings, flags } = ctx;
const model = flags.model || undefined;
const model = flags.baseModel || undefined;
const trainingType = flags.trainingType || undefined;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
@@ -104,7 +92,7 @@ export default defineCommand({
model,
training_type: trainingType,
},
format,
"json",
);
return;
}
@@ -113,7 +101,7 @@ export default defineCommand({
if (model) {
const capability = await fetchModelCapability(settings, model);
if (!capability) {
emitBare(`No foundation model found matching "${model}".`);
emitResult({ model, error: `No foundation model found matching "${model}".` }, "json");
return;
}
const supported = listSupportedTrainingTypes(capability);
@@ -121,23 +109,15 @@ export default defineCommand({
for (const value of supported) emitBare(value);
return;
}
if (format !== "text") {
emitResult(
{
model: capability.model ?? model,
supported,
supports: capability.supports,
trainingTypes: capability.trainingTypes,
},
format,
);
return;
}
emitBare(`${capability.model ?? model}`);
emitBare(supported.length ? "Supported training types:" : "No supported training types.");
for (const value of supported) {
emitBare(` ${value.padEnd(10)} ${describeTrainingType(value)}`);
}
emitResult(
{
model: capability.model ?? model,
supported,
supports: capability.supports,
trainingTypes: capability.trainingTypes,
},
"json",
);
return;
}
@@ -162,20 +142,15 @@ export default defineCommand({
for (const entry of matched) emitBare(entry.model);
return;
}
if (format !== "text") {
emitResult(
{
training_type: trainingType,
method,
variant,
count: matched.length,
models: matched,
},
format,
);
return;
}
emitBare(`Models supporting ${trainingType} (${method} / ${variant}): ${matched.length}`);
for (const entry of matched) emitBare(` ${entry.model}`);
emitResult(
{
training_type: trainingType,
method,
variant,
count: matched.length,
models: matched,
},
"json",
);
},
});
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
listCheckpoints,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
import { defineCommand, listCheckpoints, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const CHECKPOINTS_FLAGS = {
jobId: {
@@ -15,6 +10,8 @@ const CHECKPOINTS_FLAGS = {
},
} satisfies FlagsDef;
const EXPIRY_WARN_THRESHOLD_MS = 72 * 60 * 60 * 1000; // 72 hours
export default defineCommand({
description: "List checkpoints produced by a fine-tune job",
auth: "apiKey",
@@ -22,16 +19,15 @@ export default defineCommand({
flags: CHECKPOINTS_FLAGS,
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --output json"],
notes: [
"Use the returned `checkpoint` value with `finetune export` to publish",
"a deployable model.",
"`model_name` (shown for SUCCEEDED checkpoints) is the direct input for `deploy create --model-name`.",
"Checkpoints expire ~15 days after creation; `expire_time` shows the deadline. Export or deploy before expiry.",
],
async run(ctx) {
const { settings, flags } = ctx;
const jobId = flags.jobId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "finetune.checkpoints", job_id: jobId }, format);
emitResult({ action: "finetune.checkpoints", job_id: jobId }, "json");
return;
}
@@ -44,22 +40,26 @@ export default defineCommand({
checkpoint: item.checkpoint ?? item.checkpoint_id ?? "",
step: item.step !== undefined ? String(item.step) : "",
status: item.status ?? "",
model_name: item.model_name ?? "",
expire_time: item.expire_time ?? "",
}));
if (format === "json") {
emitResult({ items, total, request_id: response.request_id }, format);
return;
}
emitResult({ items, total, request_id: response.request_id }, "json");
// text / quiet
if (items.length === 0) {
emitBare("No checkpoints found.");
return;
// Near-expiry warning: check if any non-expired checkpoint is within 72h of expiry.
const now = Date.now();
const expiringSoon = items.filter((item) => {
if (!item.expire_time) return false;
const deadline = new Date(item.expire_time).getTime();
if (Number.isNaN(deadline)) return false;
const remaining = deadline - now;
return remaining > 0 && remaining < EXPIRY_WARN_THRESHOLD_MS;
});
if (expiringSoon.length > 0) {
process.stderr.write(
`\n[warning] ${expiringSoon.length} checkpoint(s) will expire within 72 hours. ` +
"Export or deploy before expiry to avoid losing the model artifact.\n",
);
}
const headers = ["CHECKPOINT", "STEP", "STATUS"];
const rows = items.map((i) => [i.checkpoint, i.step, i.status]);
for (const line of formatTable(headers, rows)) emitBare(line);
emitBare(`\nTotal: ${total}`);
emitRequestId(response.request_id, settings.quiet);
},
});
@@ -1,6 +1,5 @@
import {
defineCommand,
detectOutputFormat,
createFineTune,
getDataset,
uploadDataset,
@@ -27,7 +26,7 @@ import {
} from "bailian-cli-core";
import { existsSync, statSync } from "fs";
import { basename } from "path";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
/**
* A `--datasets` / `--validations` token is treated as a local file to upload
@@ -208,7 +207,7 @@ async function uploadResolvedLocal(
}
/** The modality a `finetune <modality> create` subcommand is bound to. */
type CommandModality = "text" | "audio" | "image";
type CommandModality = "text" | "audio" | "image" | "video";
/**
* Flags shared by every `finetune <modality> create` subcommand: what to train
@@ -216,10 +215,10 @@ type CommandModality = "text" | "audio" | "image";
* output. Every modality's model consumes these.
*/
const COMMON_FLAGS = {
model: {
baseModel: {
type: "string",
valueHint: "<model>",
description: "Base model to fine-tune",
description: "Base model to fine-tune (e.g. qwen3-8b; not the output model name)",
required: true,
},
datasets: {
@@ -317,13 +316,41 @@ const IMAGE_FLAGS = {
} satisfies FlagsDef;
const TEXT_USAGE =
"--model <model> --datasets <id|path,...> [--validations <id|path,...>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>] [--max-length <n>] [--training-type <sft|sft-lora|dpo|dpo-lora|cpt>]";
"--base-model <model> --datasets <id|path,...> [--validations <id|path,...>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>] [--max-length <n>] [--training-type <sft|sft-lora|dpo|dpo-lora|cpt>]";
const AUDIO_USAGE =
"--model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>]";
"--base-model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>]";
const IMAGE_USAGE =
"--model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>] [--generation-type <t2i|i2i>] [--learning-rate <str>]";
"--base-model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>] [--generation-type <t2i|i2i>] [--learning-rate <str>]";
/**
* Video (Wan i2v/kf2v) flags: exposes the three hyper-parameters that the
* video API supports and users may want to override. Defaults are model-specific
* (resolved by the sft-lora profile: wan2.7 batch_size 1 / max_pixels 102400,
* wan2.5 4 / 36864, wan2.2 4 / 262144).
*/
const VIDEO_FLAGS = {
...COMMON_FLAGS,
nEpochs: {
type: "number",
valueHint: "<n>",
description: "Training epochs (default: 50)",
},
batchSize: {
type: "number",
valueHint: "<n>",
description: "Batch size (default: model-specific, 1 for wan2.7, 4 for wan2.5/2.2)",
},
learningRate: {
type: "string",
valueHint: "<str>",
description: 'Learning rate as a string to preserve precision (default: "2e-5")',
},
} satisfies FlagsDef;
const VIDEO_USAGE =
"--base-model <model> --datasets <id|path> [--validations <id|path>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>]";
const COMMON_NOTES = [
"Creating a job uploads any local datasets and consumes training quota.",
@@ -383,7 +410,7 @@ async function runCreate<F extends FlagsDef>(
): Promise<void> {
const { identity, settings } = ctx;
const flags = ctx.flags as Record<string, unknown>;
const model = flags.model as string;
const model = flags.baseModel as string;
const datasetsRaw = flags.datasets as string;
// CosyVoice audio fine-tuning accepts exactly one training file
@@ -606,8 +633,6 @@ async function runCreate<F extends FlagsDef>(
if (modelName) body.model_name = modelName;
if (suffix) body.finetuned_output_suffix = suffix;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
const pending = [
...training.localPaths.map((path) => ({ field: "datasets", path })),
@@ -617,7 +642,7 @@ async function runCreate<F extends FlagsDef>(
pending.length > 0
? { action: "finetune.create", body, pending_uploads: pending }
: { action: "finetune.create", body },
format,
"json",
);
return;
}
@@ -627,16 +652,8 @@ async function runCreate<F extends FlagsDef>(
if (settings.quiet) {
if (job?.job_id) emitBare(job.job_id);
} else if (format === "text") {
if (job?.job_id) {
emitBare(`Created fine-tune job: ${job.job_id}`);
if (job.status) emitBare(`Status: ${job.status}`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
}
} else {
emitResult(response, format);
emitResult(response, "json");
}
}
@@ -647,14 +664,14 @@ export const finetuneTextCreate = defineCommand({
usageArgs: TEXT_USAGE,
flags: TEXT_FLAGS,
exampleArgs: [
"--model qwen3-8b --datasets file-xxx",
"--model qwen3-8b --datasets ./train.jsonl",
"--model qwen3-8b --datasets ./train.jsonl --validations ./eval.jsonl",
"--model qwen3-8b --datasets file-aaa,./extra.jsonl",
"--model qwen3-8b --datasets ./train.jsonl --training-type sft",
'--model qwen3-8b --datasets file-xxx --learning-rate "1.6e-5" --n-epochs 4',
"--model qwen3-8b --datasets file-xxx --output json",
"--model qwen3-8b --datasets file-xxx --dry-run",
"--base-model qwen3-8b --datasets file-xxx",
"--base-model qwen3-8b --datasets ./train.jsonl",
"--base-model qwen3-8b --datasets ./train.jsonl --validations ./eval.jsonl",
"--base-model qwen3-8b --datasets file-aaa,./extra.jsonl",
"--base-model qwen3-8b --datasets ./train.jsonl --training-type sft",
'--base-model qwen3-8b --datasets file-xxx --learning-rate "1.6e-5" --n-epochs 4',
"--base-model qwen3-8b --datasets file-xxx --output json",
"--base-model qwen3-8b --datasets file-xxx --dry-run",
],
notes: TEXT_NOTES,
run: (ctx) => runCreate("text", ctx),
@@ -667,11 +684,11 @@ export const finetuneAudioCreate = defineCommand({
usageArgs: AUDIO_USAGE,
flags: AUDIO_FLAGS,
exampleArgs: [
"--model cosyvoice-v3-flash --datasets ./audio.zip",
"--model cosyvoice-v3-flash --datasets file-xxx",
"--model cosyvoice-v3-flash --datasets ./audio.zip --model-name my-tts",
"--model cosyvoice-v3-flash --datasets file-xxx --output json",
"--model cosyvoice-v3-flash --datasets ./audio.zip --dry-run",
"--base-model cosyvoice-v3-flash --datasets ./audio.zip",
"--base-model cosyvoice-v3-flash --datasets file-xxx",
"--base-model cosyvoice-v3-flash --datasets ./audio.zip --model-name my-tts",
"--base-model cosyvoice-v3-flash --datasets file-xxx --output json",
"--base-model cosyvoice-v3-flash --datasets ./audio.zip --dry-run",
],
notes: AUDIO_NOTES,
run: (ctx) => runCreate("audio", ctx),
@@ -684,13 +701,38 @@ export const finetuneImageCreate = defineCommand({
usageArgs: IMAGE_USAGE,
flags: IMAGE_FLAGS,
exampleArgs: [
"--model wan2.7-image-pro --datasets ./images.zip",
"--model wan2.7-image-pro --datasets file-xxx",
"--model wan2.7-image-pro --datasets file-xxx --generation-type i2i",
"--model wan2.7-image-pro --datasets ./images.zip --model-name my-wan",
"--model wan2.7-image-pro --datasets file-xxx --output json",
"--model wan2.7-image-pro --datasets ./images.zip --dry-run",
"--base-model wan2.7-image-pro --datasets ./images.zip",
"--base-model wan2.7-image-pro --datasets file-xxx",
"--base-model wan2.7-image-pro --datasets file-xxx --generation-type i2i",
"--base-model wan2.7-image-pro --datasets ./images.zip --model-name my-wan",
"--base-model wan2.7-image-pro --datasets file-xxx --output json",
"--base-model wan2.7-image-pro --datasets ./images.zip --dry-run",
],
notes: IMAGE_NOTES,
run: (ctx) => runCreate("image", ctx),
});
const VIDEO_NOTES = [
...COMMON_NOTES,
"Video generation training (Wan i2v/kf2v) runs efficient_sft with model-",
"specific defaults: wan2.7 (batch_size=1, max_pixels=102400), wan2.5/2.2",
"(batch_size=4, max_pixels per model). Override with --batch-size/--n-epochs.",
"Datasets are .zip archives with data.jsonl + frame images + videos.",
"Recommended: ≥10 training samples, 20-100 for stable results.",
];
/** `bl finetune video create` — fine-tune a video generation model. Datasets are `.zip`. */
export const finetuneVideoCreate = defineCommand({
description: "Create a video generation model fine-tune job (Wan i2v/kf2v, efficient_sft)",
auth: "apiKey",
usageArgs: VIDEO_USAGE,
flags: VIDEO_FLAGS,
exampleArgs: [
"--base-model wan2.7-i2v --datasets file-xxx",
"--base-model wan2.7-i2v --datasets ./i2v-data.zip",
"--base-model wan2.2-kf2v-flash --datasets file-xxx --n-epochs 100",
"--base-model wan2.7-i2v --datasets file-xxx --dry-run",
],
notes: VIDEO_NOTES,
run: (ctx) => runCreate("video", ctx),
});
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, deleteFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const DELETE_FLAGS = {
jobId: {
@@ -23,10 +23,9 @@ export default defineCommand({
async run(ctx) {
const { settings, flags } = ctx;
const jobId = flags.jobId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "finetune.delete", job_id: jobId }, format);
emitResult({ action: "finetune.delete", job_id: jobId }, "json");
return;
}
@@ -34,11 +33,8 @@ export default defineCommand({
if (settings.quiet) {
emitBare(jobId);
} else if (format === "text") {
emitBare(`Deleted ${jobId}.`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
@@ -1,10 +1,5 @@
import {
defineCommand,
detectOutputFormat,
exportCheckpoint,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, exportCheckpoint, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const EXPORT_FLAGS = {
jobId: {
@@ -39,11 +34,10 @@ export default defineCommand({
"explicit export is the canonical path for non-best checkpoints.",
],
async run(ctx) {
const { identity, settings, flags } = ctx;
const { settings, flags } = ctx;
const jobId = flags.jobId;
const checkpoint = flags.checkpoint;
const modelName = flags.modelName;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
@@ -53,7 +47,7 @@ export default defineCommand({
checkpoint,
model_name: modelName,
},
format,
"json",
);
return;
}
@@ -64,14 +58,8 @@ export default defineCommand({
if (settings.quiet) {
emitBare(exported);
} else if (format === "text") {
emitBare(`Exported ${jobId} / ${checkpoint} → model_name=${exported}`);
emitBare(
`Next: ${identity.binName} deploy text create --model ${exported} --name <display-name>`,
);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
emitResult(response, "json");
}
},
});
@@ -0,0 +1,105 @@
/**
* Best-effort actual training fee calculation using the model catalog's
* "ft" (fine-tune) price entry. Pure API-key domain no console auth needed.
*
* The model catalog (`listFoundationModels` via public gateway) returns a
* `prices[]` array **only when `queryPrice: true` is passed** (the same flag
* `fetchModelDetail` uses). Combined with the job's `output.usage` (actual
* consumed tokens, present on SUCCEEDED / CANCELED), this gives the exact
* training cost without any console-domain login.
*/
import {
callConsoleGateway,
effectiveConsoleGatewayConfig,
unwrapResponse,
MODEL_LIST_API,
type Settings,
type ModelPriceInfo,
} from "bailian-cli-core";
export interface ActualFee {
cost: number;
unitPrice: number;
priceUnit: string;
}
/**
* Fetch the model's training price from the public catalog gateway.
* Uses the same anonymous gateway path as `fetchModelCapability` (no console
* token required), but adds `queryPrice: true` to include the prices array.
*/
async function fetchTrainingPrice(
settings: Settings,
model: string,
): Promise<ModelPriceInfo | null> {
const eff = effectiveConsoleGatewayConfig(settings);
const result = await callConsoleGateway(
{ region: eff.consoleRegion, site: eff.consoleSite, switchAgent: eff.consoleSwitchAgent },
settings.timeout,
{
api: MODEL_LIST_API,
data: {
input: {
pageNo: 1,
pageSize: 10,
group: true,
model,
queryPrice: true,
querySampleCode: false,
queryGroupByModel: true,
queryQuota: false,
queryQpmInfo: false,
queryApplyStatus: false,
queryPermissions: false,
queryActivationStatus: false,
},
},
},
);
const responseData = unwrapResponse(result as Record<string, unknown>);
const list = (responseData.list as Record<string, unknown>[]) ?? [];
// The response is grouped; find the exact model in items.
for (const group of list) {
const items = (group.items as Record<string, unknown>[]) ?? [];
for (const item of items) {
if (item.model === model) {
const prices = (item.prices as ModelPriceInfo[]) ?? [];
return prices.find((entry) => entry.type === "ft") ?? null;
}
}
// Flat response fallback (no items nesting).
if (group.model === model) {
const prices = (group.prices as ModelPriceInfo[]) ?? [];
return prices.find((entry) => entry.type === "ft") ?? null;
}
}
return null;
}
/**
* Compute the actual training fee from the model catalog's "ft" price entry.
* Returns null when the price is unavailable (network error, model not in
* catalog, or no "ft" entry). Never throws.
*
* Only uses the public model catalog (model metadata) does NOT call
* console-domain pricing APIs (modelCenter.getModelPrice). Models whose
* catalog entry lacks a "ft" price (e.g. CosyVoice) will simply omit the
* training_cost field until the platform adds it to the catalog.
*/
export async function computeActualFee(
settings: Settings,
model: string,
usageTokens: number,
): Promise<ActualFee | null> {
try {
const ftEntry = await fetchTrainingPrice(settings, model);
const unitPrice = Number(ftEntry?.price);
if (!Number.isFinite(unitPrice) || unitPrice <= 0) return null;
const priceUnit = ftEntry?.priceUnit ?? "每百万tokens";
// Catalog price is yuan per million tokens.
const cost = (usageTokens / 1_000_000) * unitPrice;
return { cost: Number(cost.toFixed(4)), unitPrice, priceUnit };
} catch {
return null;
}
}
+29 -32
View File
@@ -1,5 +1,6 @@
import { defineCommand, detectOutputFormat, getFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { defineCommand, getFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { computeActualFee } from "./fee.ts";
const GET_FLAGS = {
jobId: {
@@ -17,12 +18,11 @@ export default defineCommand({
flags: GET_FLAGS,
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --output json"],
async run(ctx) {
const { identity, settings, flags } = ctx;
const { settings, flags } = ctx;
const jobId = flags.jobId;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ action: "finetune.get", job_id: jobId }, format);
emitResult({ action: "finetune.get", job_id: jobId }, "json");
return;
}
@@ -30,18 +30,24 @@ export default defineCommand({
const job = response.output ?? response.data;
if (!job) {
emitBare(`No data returned for ${jobId}`);
emitResult({ job_id: jobId, error: "No data returned" }, "json");
return;
}
const hp = job.hyper_parameters;
const hyperParameters = job.hyper_parameters;
const hyperParts: string[] = [];
if (hp?.n_epochs !== undefined) hyperParts.push(`n_epochs=${hp.n_epochs}`);
if (hp?.batch_size !== undefined) hyperParts.push(`batch_size=${hp.batch_size}`);
if (hp?.learning_rate !== undefined) hyperParts.push(`learning_rate=${hp.learning_rate}`);
if (hp?.max_length !== undefined) hyperParts.push(`max_length=${hp.max_length}`);
if (hyperParameters?.n_epochs !== undefined)
hyperParts.push(`n_epochs=${hyperParameters.n_epochs}`);
if (hyperParameters?.batch_size !== undefined)
hyperParts.push(`batch_size=${hyperParameters.batch_size}`);
if (hyperParameters?.learning_rate !== undefined)
hyperParts.push(`learning_rate=${hyperParameters.learning_rate}`);
if (hyperParameters?.max_length !== undefined)
hyperParts.push(`max_length=${hyperParameters.max_length}`);
const item = {
const usageTokens = typeof job.usage === "number" ? job.usage : undefined;
const item: Record<string, unknown> = {
job_id: job.job_id ?? jobId,
base_model: job.model ?? "",
status: job.status ?? "",
@@ -53,29 +59,20 @@ export default defineCommand({
model_name: job.model_name ?? "",
created_at: job.create_time ?? job.gmt_create ?? "",
updated_at: job.end_time ?? job.gmt_modified ?? "",
usage_tokens: usageTokens ?? "",
charge_type: typeof job.charge_type === "string" ? job.charge_type : "",
};
if (format === "json") {
emitResult({ ...item, request_id: response.request_id }, format);
return;
// Actual fee: only when the platform reports a concrete token count
// (SUCCEEDED / CANCELED). Best-effort — silently omitted on lookup failure.
if (usageTokens !== undefined && usageTokens > 0 && job.model) {
const fee = await computeActualFee(settings, job.model, usageTokens);
if (fee) {
item.training_cost = fee.cost;
item.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`;
}
}
// text / quiet
emitBare(`job_id: ${item.job_id}`);
if (item.base_model) emitBare(`base_model: ${item.base_model}`);
if (item.status) emitBare(`status: ${item.status}`);
if (item.training_type) emitBare(`training_type: ${item.training_type}`);
if (item.training_files.length) emitBare(`training_files: ${item.training_files.join(", ")}`);
if (item.validation_files.length)
emitBare(`validation_files: ${item.validation_files.join(", ")}`);
if (item.hyper_params) emitBare(`hyper_params: ${item.hyper_params}`);
if (item.output_model)
emitBare(
`output_model: ${item.output_model} (→ ${identity.binName} deploy text create --model)`,
);
if (item.model_name) emitBare(`model_name: ${item.model_name}`);
if (item.created_at) emitBare(`created_at: ${item.created_at}`);
if (item.updated_at) emitBare(`updated_at: ${item.updated_at}`);
emitRequestId(response.request_id, settings.quiet);
emitResult({ ...item, request_id: response.request_id }, "json");
},
});
+24 -47
View File
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, listFineTunes, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime";
import { defineCommand, listFineTunes, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const LIST_FLAGS = {
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
@@ -13,71 +13,48 @@ const LIST_FLAGS = {
valueHint: "<s>",
description: "Filter by status (PENDING / RUNNING / SUCCEEDED / FAILED / CANCELED)",
},
baseModel: {
type: "string",
valueHint: "<model>",
description: "Filter by base model ID (server-side)",
},
} satisfies FlagsDef;
export default defineCommand({
description: "List fine-tune jobs",
auth: "apiKey",
usageArgs: "[--page <n>] [--page-size <n>] [--status <s>]",
usageArgs: "[--page <n>] [--page-size <n>] [--status <s>] [--base-model <model>]",
flags: LIST_FLAGS,
exampleArgs: ["", "--status RUNNING", "--page-size 20 --output json"],
exampleArgs: ["", "--status RUNNING", "--base-model qwen3-8b", "--page-size 20"],
async run(ctx) {
const { identity, settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const { settings, flags } = ctx;
const pageNo = flags.page;
const pageSize = flags.pageSize;
const status = flags.status || undefined;
const model = flags.baseModel || undefined;
if (settings.dryRun) {
emitResult({ action: "finetune.list", page: pageNo, page_size: pageSize, status }, format);
emitResult(
{ action: "finetune.list", page: pageNo, page_size: pageSize, status, model },
"json",
);
return;
}
const response = await listFineTunes(ctx.client, { pageNo, pageSize, status });
const response = await listFineTunes(ctx.client, { pageNo, pageSize, status, model });
const payload = response.output ?? response.data;
const jobs = payload?.jobs ?? [];
const total = payload?.total;
const items = jobs.map((item) => ({
job_id: item.job_id ?? "",
base_model: item.model ?? "",
status: item.status ?? "",
training_type: item.training_type ?? "",
output_model: item.finetuned_output ?? "",
created_at: item.create_time ?? item.gmt_create ?? "",
const items = jobs.map((job) => ({
job_id: job.job_id ?? "",
base_model: job.model ?? "",
status: job.status ?? "",
training_type: job.training_type ?? "",
output_model: job.finetuned_output ?? "",
created_at: job.create_time ?? job.gmt_create ?? "",
}));
if (format === "json") {
emitResult({ items, total, request_id: response.request_id }, format);
return;
}
// text / quiet
if (items.length === 0) {
emitBare("No fine-tune jobs found.");
return;
}
const headers = [
"JOB_ID",
"BASE_MODEL",
"STATUS",
"TRAINING_TYPE",
"OUTPUT_MODEL",
"CREATED_AT",
];
const rows = items.map((i) => [
i.job_id,
i.base_model,
i.status,
i.training_type,
i.output_model,
i.created_at,
]);
for (const line of formatTable(headers, rows)) emitBare(line);
if (total !== undefined) emitBare(`\nTotal: ${total}`);
emitBare(
`Tip: OUTPUT_MODEL is the input for \`${identity.binName} deploy text create --model\``,
);
emitRequestId(response.request_id, settings.quiet);
emitResult({ items, total, request_id: response.request_id }, "json");
},
});
+15 -44
View File
@@ -1,25 +1,24 @@
import {
defineCommand,
detectOutputFormat,
getFineTuneLogs,
type Client,
type FineTuneLogEntry,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult } from "bailian-cli-runtime";
/**
* Render a single log entry as a single line (mirrors the flatten logic used
* for non-search text output: prefer common fields, fall back to JSON).
* Render a single log entry as a single line (used for search matching:
* prefer common fields, fall back to JSON).
*/
function renderEntry(entry: FineTuneLogEntry | string): string {
if (typeof entry === "string") return entry;
const record = entry as Record<string, unknown>;
const ts = (record.timestamp ?? record.time ?? record.create_time ?? "") as string;
const timestamp = (record.timestamp ?? record.time ?? record.create_time ?? "") as string;
const level = (record.level ?? "") as string;
const msg = (record.message ?? record.msg ?? record.log ?? "") as string;
if (msg || ts || level) {
return [ts, level, msg].filter(Boolean).join("\t");
const message = (record.message ?? record.msg ?? record.log ?? "") as string;
if (message || timestamp || level) {
return [timestamp, level, message].filter(Boolean).join("\t");
}
return JSON.stringify(entry);
}
@@ -48,16 +47,16 @@ async function fetchAllLogs(
let total = 0;
// Hard cap to avoid an unbounded loop if the server misreports `total`.
const maxPages = 200;
for (let i = 0; i < maxPages; i++) {
for (let page = 0; page < maxPages; page++) {
const response = await getFineTuneLogs(client, jobId, { pageNo, pageSize });
const payload = response.output ?? response.data;
const page = payload?.logs ?? [];
const logs = payload?.logs ?? [];
total = payload?.total ?? total;
if (page.length === 0) break;
entries.push(...page);
if (logs.length === 0) break;
entries.push(...logs);
// Stop once we've collected everything the server claims exists.
if (total && entries.length >= total) break;
if (page.length < pageSize) break;
if (logs.length < pageSize) break;
pageNo++;
}
return { entries, total };
@@ -110,7 +109,6 @@ export default defineCommand({
const pageSize = flags.pageSize;
const search = flags.search || undefined;
const tail = flags.tail;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
@@ -122,7 +120,7 @@ export default defineCommand({
search,
tail,
},
format,
"json",
);
return;
}
@@ -147,18 +145,6 @@ export default defineCommand({
const result =
tailApplied !== undefined ? scanned.slice(scanned.length - tailApplied) : scanned;
if (settings.quiet || format === "text") {
if (result.length === 0) {
emitBare(search ? `No logs matched "${search}".` : "No logs returned.");
return;
}
for (const entry of result) emitBare(renderEntry(entry));
const parts: string[] = [`${result.length} shown`];
if (matched !== undefined) parts.push(`matched ${matched}`);
parts.push(`of ${entries.length}` + (total ? ` (total ${total})` : ""));
emitBare(`\n${parts.join(", ")}`);
return;
}
emitResult(
{
...(matched !== undefined ? { matched } : {}),
@@ -168,28 +154,13 @@ export default defineCommand({
...(tailApplied !== undefined ? { tail: tailApplied } : {}),
logs: result,
},
format,
"json",
);
return;
}
// Default: single page, verbatim response.
const response = await getFineTuneLogs(ctx.client, jobId, { pageNo, pageSize });
const payload = response.output ?? response.data;
const logs = payload?.logs ?? [];
if (settings.quiet || format === "text") {
if (logs.length === 0) {
emitBare("No logs returned.");
return;
}
for (const entry of logs) {
emitBare(renderEntry(entry));
}
if (payload?.total !== undefined) emitBare(`\nTotal: ${payload.total}`);
emitRequestId(response.request_id, settings.quiet);
} else {
emitResult(response, format);
}
emitResult(response, "json");
},
});
@@ -0,0 +1,139 @@
import {
defineCommand,
fetchTrainingModelPrice,
estimateSftDpoTokens,
estimateCptTokens,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
const PRICE_FLAGS = {
baseModel: {
type: "string",
valueHint: "<model>",
description: "Base model to fine-tune (e.g. qwen3-8b; not the output model name)",
required: true,
},
datasets: {
type: "string",
valueHint: "<ids>",
description: "Training dataset file IDs, comma-separated (required)",
required: true,
},
trainingType: {
type: "string",
valueHint: "<type>",
description: "Training type: sft | dpo | cpt (default: sft)",
},
nEpochs: {
type: "number",
valueHint: "<n>",
description: "Number of training epochs (default: 3)",
},
} satisfies FlagsDef;
const SUPPORTED_TRAINING_TYPES = ["sft", "dpo", "cpt"];
// Fixed hyper-parameters used for estimation. Only n_epochs materially affects
// the estimate; the rest are held at representative defaults (not exposed as
// flags to keep the command surface minimal).
const ESTIMATE_BATCH_SIZE = 16;
const ESTIMATE_MAX_LENGTH = 8192;
const DEFAULT_N_EPOCHS = 3;
export default defineCommand({
description: "Estimate the training cost for a fine-tune job (token billing)",
auth: "console",
usageArgs: "--base-model <model> --datasets <ids> [--training-type <type>] [--n-epochs <n>]",
flags: PRICE_FLAGS,
exampleArgs: [
"--base-model qwen3-8b --datasets file-ft-xxx",
"--base-model qwen3-8b --datasets file-ft-xxx,file-ft-yyy --n-epochs 2",
"--base-model qwen3-8b --datasets file-ft-xxx --training-type cpt",
],
notes: [
"Estimate only — the server computes token usage from the datasets; final cost is subject to the bill.",
"Covers token billing for sft / dpo / cpt. Training-unit (MTU) billing is not supported by this command.",
"Hyper-parameters other than --n-epochs are fixed at representative defaults for estimation.",
],
async run(ctx) {
const { settings, flags } = ctx;
const model = flags.baseModel;
const datasetIds = flags.datasets
.split(",")
.map((datasetId) => datasetId.trim())
.filter(Boolean);
const trainingType = (flags.trainingType ?? "sft").toLowerCase();
const nEpochs = flags.nEpochs ?? DEFAULT_N_EPOCHS;
if (!SUPPORTED_TRAINING_TYPES.includes(trainingType)) {
throw new BailianError(
`Unsupported training type "${trainingType}". Supported: ${SUPPORTED_TRAINING_TYPES.join(", ")}.`,
ExitCode.USAGE,
);
}
if (datasetIds.length === 0) {
throw new BailianError("--datasets must contain at least one file ID.", ExitCode.USAGE);
}
if (settings.dryRun) {
emitResult(
{ action: "finetune.price", model, datasets: datasetIds, trainingType, nEpochs },
"json",
);
return;
}
// Unit price (yuan per 千Token).
const priceInfo = await fetchTrainingModelPrice(ctx.client, model);
const unitPrice = Number(priceInfo.price);
if (!Number.isFinite(unitPrice)) {
throw new BailianError(
`No training price found for model "${model}".`,
ExitCode.GENERAL,
undefined,
{ rawResponse: JSON.stringify(priceInfo) },
);
}
// Per-epoch token estimate (min/max range).
const estimate =
trainingType === "cpt"
? await estimateCptTokens(ctx.client, model, datasetIds.join(","), nEpochs)
: await estimateSftDpoTokens(ctx.client, datasetIds, {
nEpochs,
batchSize: ESTIMATE_BATCH_SIZE,
maxLength: ESTIMATE_MAX_LENGTH,
});
const minPerEpoch = estimate.estimatedDatasetConsumedTokensMinPerEpoch ?? 0;
const maxPerEpoch = estimate.estimatedDatasetConsumedTokensMaxPerEpoch ?? 0;
const mixedMinPerEpoch = estimate.estimatedMixedConsumedTokensMinPerEpoch ?? 0;
const mixedMaxPerEpoch = estimate.estimatedMixedConsumedTokensMaxPerEpoch ?? 0;
const minTokens = (minPerEpoch + mixedMinPerEpoch) * nEpochs;
const maxTokens = (maxPerEpoch + mixedMaxPerEpoch) * nEpochs;
// price is yuan per 1000 tokens.
const minFee = (minTokens / 1000) * unitPrice;
const maxFee = (maxTokens / 1000) * unitPrice;
emitResult(
{
model,
training_type: trainingType,
n_epochs: nEpochs,
unit_price: unitPrice,
price_unit: priceInfo.priceUnit ?? "千Token",
estimated_tokens: { min: minTokens, max: maxTokens },
estimated_fee_yuan: {
min: Number(minFee.toFixed(4)),
max: Number(maxFee.toFixed(4)),
},
disclaimer: "Server-side estimate; final cost is subject to the bill.",
},
"json",
);
},
});
@@ -1,12 +1,12 @@
import {
defineCommand,
detectOutputFormat,
getFineTune,
BailianError,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { computeActualFee } from "./fee.ts";
const DEFAULT_INTERVAL_SEC = 10;
const MIN_INTERVAL_SEC = 1;
@@ -103,7 +103,6 @@ export default defineCommand({
const follow = flags.follow;
const intervalSec = Math.max(MIN_INTERVAL_SEC, flags.interval ?? DEFAULT_INTERVAL_SEC);
const pollTimeoutSec = flags.pollTimeout;
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult(
@@ -114,7 +113,7 @@ export default defineCommand({
interval: intervalSec,
timeout: pollTimeoutSec,
},
format,
"json",
);
return;
}
@@ -132,16 +131,24 @@ export default defineCommand({
if (settings.quiet) {
// Just the status word — ideal for `status=$(... finetune watch ... --quiet)`.
emitBare(status || "UNKNOWN");
} else if (format === "text") {
emitBare(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}`);
if (status === "SUCCEEDED") emitBare(`${jobId} ${status}`);
emitRequestId(response.request_id, settings.quiet);
} else {
// json: a compact, purpose-built status probe.
emitResult(
{ job_id: jobId, status: status || "UNKNOWN", terminal, request_id: response.request_id },
format,
);
const output: Record<string, unknown> = {
job_id: jobId,
status: status || "UNKNOWN",
terminal,
request_id: response.request_id,
};
// Enrich terminal output with actual fee when usage is reported.
const usageTokens = typeof job?.usage === "number" ? job.usage : undefined;
if (terminal && usageTokens && usageTokens > 0 && job?.model) {
output.usage_tokens = usageTokens;
const fee = await computeActualFee(settings, job.model as string, usageTokens);
if (fee) {
output.training_cost = fee.cost;
output.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`;
}
}
emitResult(output, "json");
}
if (terminal && status !== "SUCCEEDED") {
@@ -168,18 +175,28 @@ export default defineCommand({
const job = response.output ?? response.data;
const status = String(job?.status ?? "").toUpperCase();
if (format === "text" && !settings.quiet && status !== lastStatus) {
emitBare(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}`);
if (!settings.quiet && status !== lastStatus) {
process.stderr.write(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}\n`);
lastStatus = status;
}
if (TERMINAL_STATUSES.has(status)) {
const elapsed = Date.now() - startedAt;
if (format !== "text" || settings.quiet) {
emitResult(response, format);
} else if (status === "SUCCEEDED") {
emitBare(`\n✓ ${jobId} ${status} (elapsed ${formatElapsed(elapsed)})`);
emitRequestId(response.request_id, settings.quiet);
if (settings.quiet) {
emitBare(status || "UNKNOWN");
} else {
// Enrich the raw response with actual fee when usage is available.
const usageTokens = typeof job?.usage === "number" ? job.usage : undefined;
const enriched: Record<string, unknown> = { ...response };
if (usageTokens && usageTokens > 0 && job?.model) {
const fee = await computeActualFee(settings, job.model as string, usageTokens);
if (fee) {
enriched.training_cost = fee.cost;
enriched.usage_tokens = usageTokens;
enriched.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`;
}
}
emitResult(enriched, "json");
}
if (status !== "SUCCEEDED") {
throw new BailianError(
@@ -205,7 +222,7 @@ export default defineCommand({
// Any other error (including the BailianError thrown above) propagates to
// the central handler.
if (controller.signal.aborted) {
emitBare("\nInterrupted.");
process.stderr.write("\nInterrupted.\n");
return;
}
throw error;
+17 -9
View File
@@ -2,7 +2,6 @@ import {
BailianError,
ExitCode,
defineCommand,
detectOutputFormat,
detectInstalledAgents,
fetchSkillsIndex,
getSkillRegistryBaseUrl,
@@ -28,22 +27,31 @@ const INSTALL_CONCURRENCY = 3;
export default defineCommand({
description: "Install skills from the Bailian skill registry into local agents",
auth: "none",
usageArgs: "--name <all|name,...>",
usageArgs: "--all | --name <name,...>",
flags: {
all: {
type: "switch",
description: "Install all skills from the registry",
},
name: {
type: "string",
valueHint: "<all|name,...>",
description: "Skills to install: all or comma-separated skill names",
required: true,
valueHint: "<name,...>",
description: "Comma-separated skill names to install",
},
},
exampleArgs: ["--name all", "--name spark-video,bailian-model-recommend"],
validate(flags) {
if (flags.all && flags.name) return "Use either --all or --name, not both";
if (!flags.all && !flags.name)
return "Specify --all to install everything or --name <name,...> for specific skills";
return undefined;
},
exampleArgs: ["--all", "--name spark-video,bailian-model-recommend"],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const requested = parseSkillNames(ctx.flags.name, false);
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const index = await fetchSkillsIndex();
const remoteNames = Object.keys(index.skills);
const names = requested === "all" ? remoteNames : requested;
const parsed = ctx.flags.all ? "all" : parseSkillNames(ctx.flags.name, false);
const names = parsed === "all" ? remoteNames : parsed;
const lock = readSkillLock();
const agents = detectInstalledAgents();
@@ -0,0 +1,107 @@
import {
BailianError,
ExitCode,
defineCommand,
detectInstalledAgents,
fetchSkillsIndex,
getSkillRegistryBaseUrl,
installSkillWithFanout,
readSkillLock,
runWithConcurrency,
writeSkillLock,
} from "bailian-cli-core";
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
interface InitOutcome {
name: string;
status: "installed" | "failed";
publishedAt?: string;
agents?: string[];
reason?: string;
}
/** Prefix used to identify first-party Bailian skills in the registry. */
const BAILIAN_PREFIX = "bailian-";
/** Max number of skills downloading/installing at the same time. */
const INIT_CONCURRENCY = 3;
export default defineCommand({
description: "Install all bailian-* skills (one-shot bootstrap for new environments)",
auth: "none",
usageArgs: "",
exampleArgs: [""],
notes: [
"Fetches the registry index and installs every skill whose name starts with bailian-",
"Equivalent to: bl skill add --all (filtered to bailian-* skills)",
],
async run(ctx) {
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const index = await fetchSkillsIndex();
// Discover all bailian-* skills from the live registry index
const names = Object.keys(index.skills).filter((name) => name.startsWith(BAILIAN_PREFIX));
const lock = readSkillLock();
const agents = detectInstalledAgents();
const tasks = names.map((name) => async (): Promise<InitOutcome> => {
const entry = index.skills[name];
try {
const record = await installSkillWithFanout(
name,
entry,
agents,
lock.skills[name]?.links ?? [],
);
lock.skills[name] = record.lockEntry;
return {
name,
status: "installed",
publishedAt: entry.publishedAt,
agents: record.linkedAgents,
};
} catch (err) {
return {
name,
status: "failed",
reason: err instanceof Error ? err.message : String(err),
};
}
});
const results = await runWithConcurrency(tasks, INIT_CONCURRENCY);
writeSkillLock(lock);
if (format === "json") {
emitResult(
{
registry: getSkillRegistryBaseUrl(),
agents: agents.map((agent) => agent.id),
skills: results,
},
format,
);
} else if (results.length === 0) {
emitBare("No bailian-* skills found in the registry.");
} else {
const rows = results.map((result) => [
result.name,
result.status,
result.publishedAt ? result.publishedAt.slice(0, 10) : "-",
result.status === "installed" ? result.agents?.join(", ") || "-" : (result.reason ?? "-"),
]);
for (const line of formatTable(["NAME", "STATUS", "PUBLISHED", "AGENTS / REASON"], rows)) {
emitBare(line);
}
}
const failed = results.filter((result) => result.status === "failed");
if (failed.length > 0) {
throw new BailianError(
`${failed.length}/${results.length} skill(s) failed to install`,
ExitCode.GENERAL,
"Check the reason for failed skills in the output; network failures can be retried with bl skill init",
);
}
},
});
+1 -2
View File
@@ -1,6 +1,5 @@
import {
defineCommand,
detectOutputFormat,
computeSkillStatuses,
fetchSkillsIndex,
getSkillRegistryBaseUrl,
@@ -24,7 +23,7 @@ export default defineCommand({
"STATUS: installed | outdated | not-installed | missing (lock has it, dir deleted) | untracked (dir exists, not managed)",
],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
// Three-way reconciliation: live remote index × skill-lock.json (installation facts) × disk
const index = await fetchSkillsIndex();
const lock = readSkillLock();
@@ -2,7 +2,6 @@ import {
BailianError,
ExitCode,
defineCommand,
detectOutputFormat,
listSkillDirsOnDisk,
parseSkillNames,
readSkillLock,
@@ -34,7 +33,7 @@ export default defineCommand({
exampleArgs: ["--name spark-video", "--name all"],
async run(ctx) {
// Purely local operation: no remote access, works offline
const format = detectOutputFormat(ctx.settings.output);
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const requested = parseSkillNames(ctx.flags.name, false);
const lock = readSkillLock();
const names = requested === "all" ? Object.keys(lock.skills) : requested;
+15 -8
View File
@@ -2,7 +2,6 @@ import {
BailianError,
ExitCode,
defineCommand,
detectOutputFormat,
detectInstalledAgents,
fanOutSkillToAgents,
fetchSkillsIndex,
@@ -29,19 +28,27 @@ const UPDATE_CONCURRENCY = 3;
export default defineCommand({
description: "Update installed skills to the latest registry versions",
auth: "none",
usageArgs: "[--name <all|name,...>]",
usageArgs: "[--all] [--name <name,...>]",
flags: {
all: {
type: "switch",
description: "Update all installed skills (default when neither --all nor --name is given)",
},
name: {
type: "string",
valueHint: "<all|name,...>",
description:
"Skills to update: all (default, only changed ones) or comma-separated names (force update installed skills)",
valueHint: "<name,...>",
description: "Comma-separated skill names to update (must be already installed)",
},
},
exampleArgs: ["", "--name spark-video"],
validate(flags) {
if (flags.all && flags.name) return "Use either --all or --name, not both";
return undefined;
},
exampleArgs: ["", "--all", "--name spark-video"],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const requested = parseSkillNames(ctx.flags.name, true);
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const updateAll = ctx.flags.all || !ctx.flags.name;
const requested = updateAll ? "all" : parseSkillNames(ctx.flags.name, false);
const index = await fetchSkillsIndex();
const lock = readSkillLock();
const disk = new Set(listSkillDirsOnDisk());
@@ -1,6 +1,7 @@
import {
defineCommand,
videoGeneratePath,
image2videoPath,
taskPath,
detectOutputFormat,
type DashScopeVideoRequest,
@@ -43,6 +44,11 @@ export default defineCommand({
valueHint: "<url>",
description: "Input image URL for image-to-video generation",
},
lastFrame: {
type: "string",
valueHint: "<url>",
description: "Last frame image URL (with --image, enables kf2v first+last frame mode)",
},
negativePrompt: {
type: "string",
valueHint: "<text>",
@@ -110,12 +116,20 @@ export default defineCommand({
const format = detectOutputFormat(settings.output);
const imageUrl = flags.image;
const lastFrameUrl = flags.lastFrame as string | undefined;
// Auto-upload local image file for i2v
let resolvedImageUrl: string | undefined;
if (imageUrl) {
resolvedImageUrl = await ctx.client.resolveImageInput(imageUrl, model);
}
let resolvedLastFrameUrl: string | undefined;
if (lastFrameUrl) {
resolvedLastFrameUrl = await ctx.client.resolveImageInput(lastFrameUrl, model);
}
// kf2v mode: both --image and --last-frame provided.
const isKf2v = Boolean(resolvedImageUrl && resolvedLastFrameUrl);
const watermark = resolveWatermark(flags.watermark);
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
@@ -125,10 +139,16 @@ export default defineCommand({
input: {
prompt: prompt,
negative_prompt: flags.negativePrompt || undefined,
// i2v models (happyhorse-1.1-i2v) require input.media with type 'first_frame'
...(resolvedImageUrl
? { media: [{ type: "first_frame" as const, url: resolvedImageUrl }] }
: {}),
// kf2v: first+last frame flat fields via image2video endpoint.
// wan2.1~2.6 i2v: flat img_url via video-generation endpoint.
// wan2.7+ / happyhorse i2v: media[] via video-generation endpoint.
...(isKf2v
? { first_frame_url: resolvedImageUrl, last_frame_url: resolvedLastFrameUrl }
: resolvedImageUrl
? /wan[x]?2\.[1-6]/i.test(model)
? { img_url: resolvedImageUrl }
: { media: [{ type: "first_frame" as const, url: resolvedImageUrl }] }
: {}),
},
parameters: {
resolution: flags.resolution || undefined,
@@ -141,15 +161,28 @@ export default defineCommand({
};
if (settings.dryRun) {
const previewBody = resolvedImageUrl
? {
...body,
input: {
...body.input,
media: [{ type: "first_frame" as const, url: redactDataUri(resolvedImageUrl) }],
},
}
: body;
let previewBody = body;
if (isKf2v) {
previewBody = {
...body,
input: {
...body.input,
first_frame_url: redactDataUri(resolvedImageUrl ?? ""),
last_frame_url: redactDataUri(resolvedLastFrameUrl ?? ""),
},
};
} else if (resolvedImageUrl) {
const redactedUrl = redactDataUri(resolvedImageUrl);
previewBody = {
...body,
input: {
...body.input,
...(/wan[x]?2\.[1-6]/i.test(model)
? { img_url: redactedUrl }
: { media: [{ type: "first_frame" as const, url: redactedUrl }] }),
},
};
}
emitResult({ request: previewBody }, format);
return;
}
@@ -162,7 +195,7 @@ export default defineCommand({
settings,
() =>
ctx.client.requestJson<DashScopeAsyncResponse>({
path: videoGeneratePath(),
path: isKf2v ? image2videoPath() : videoGeneratePath(),
method: "POST",
body,
async: true,
+5
View File
@@ -66,6 +66,7 @@ export {
finetuneTextCreate,
finetuneAudioCreate,
finetuneImageCreate,
finetuneVideoCreate,
} from "./commands/finetune/create.ts";
export { default as finetuneList } from "./commands/finetune/list.ts";
export { default as finetuneGet } from "./commands/finetune/get.ts";
@@ -76,6 +77,7 @@ export { default as finetuneCheckpoints } from "./commands/finetune/checkpoints.
export { default as finetuneExport } from "./commands/finetune/export.ts";
export { default as finetuneWatch } from "./commands/finetune/watch.ts";
export { default as finetuneCapability } from "./commands/finetune/capability.ts";
export { default as finetunePrice } from "./commands/finetune/price.ts";
export {
deployTextCreate,
deployAudioCreate,
@@ -87,6 +89,8 @@ export { default as deployModels } from "./commands/deploy/models.ts";
export { default as deployScale } from "./commands/deploy/scale.ts";
export { default as deployUpdate } from "./commands/deploy/update.ts";
export { default as deployDelete } from "./commands/deploy/delete.ts";
export { default as deployPause } from "./commands/deploy/pause.ts";
export { default as deployResume } from "./commands/deploy/resume.ts";
export { default as tokenPlanListSeats } from "./commands/token-plan/list-seats.ts";
export { default as tokenPlanCreateKey } from "./commands/token-plan/create-key.ts";
export { default as tokenPlanAssignSeats } from "./commands/token-plan/assign-seats.ts";
@@ -117,3 +121,4 @@ export { default as skillAdd } from "./commands/skill/add.ts";
export { default as skillUpdate } from "./commands/skill/update.ts";
export { default as skillRemove } from "./commands/skill/remove.ts";
export { default as skillList } from "./commands/skill/list.ts";
export { default as skillInit } from "./commands/skill/init.ts";
@@ -30,7 +30,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--model|--name/i);
expect(stderr).toMatch(/--model-name|--display-name/i);
});
test("deploy create --dry-run 构造 lora 部署请求体", async () => {
@@ -38,9 +38,9 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
"deploy",
"text",
"create",
"--model",
"--model-name",
"qwen-plus-2025-12-01",
"--name",
"--display-name",
"my-qwen-plus",
"--dry-run",
"--output",
@@ -68,9 +68,9 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
"deploy",
"text",
"create",
"--model",
"--model-name",
"qwen3-8b",
"--name",
"--display-name",
"my-qwen3-mu",
"--plan",
"mu",
@@ -102,9 +102,9 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
"deploy",
"audio",
"create",
"--model",
"--model-name",
"my-cosyvoice-ft",
"--name",
"--display-name",
"my-tts",
"--dry-run",
"--output",
+112 -14
View File
@@ -31,7 +31,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--model|--datasets/i);
expect(stderr).toMatch(/--base-model|--datasets/i);
});
test("finetune create --dry-run 构造 SFT 默认请求体", async () => {
@@ -39,7 +39,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"finetune",
"text",
"create",
"--model",
"--base-model",
"qwen3-8b",
"--datasets",
"file-aaa,file-bbb",
@@ -73,7 +73,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"finetune",
"text",
"create",
"--model",
"--base-model",
"qwen3-8b",
"--datasets",
"file-aaa",
@@ -135,7 +135,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"finetune",
"text",
"create",
"--model",
"--base-model",
"qwen3-8b",
"--datasets",
"file-aaa",
@@ -157,7 +157,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"finetune",
"text",
"create",
"--model",
"--base-model",
"qwen3-8b",
"--datasets",
"file-aaa",
@@ -176,7 +176,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"finetune",
"text",
"create",
"--model",
"--base-model",
"qwen3-8b",
"--datasets",
`${localPath},file-bbb`,
@@ -207,7 +207,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"finetune",
"text",
"create",
"--model",
"--base-model",
"qwen3-8b",
"--datasets",
" , ",
@@ -228,7 +228,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"finetune",
"text",
"create",
"--model",
"--base-model",
"qwen3-8b",
"--datasets",
localPath,
@@ -250,7 +250,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"finetune",
"text",
"create",
"--model",
"--base-model",
"qwen3-8b",
"--datasets",
localPath,
@@ -272,7 +272,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
["cancel", ["--job-id", "ft-xxx"]],
["delete", ["--job-id", "ft-xxx"]],
["watch", ["--job-id", "ft-xxx"]],
["capability", ["--model", "qwen3-8b"]],
["capability", ["--base-model", "qwen3-8b"]],
])("finetune %s --dry-run 发出结构化动作", async (sub, extra) => {
const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
"finetune",
@@ -292,7 +292,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"finetune",
"text",
"create",
"--model",
"--base-model",
"qwen3-8b",
"--datasets",
" file-a , ,file-b ",
@@ -314,7 +314,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"finetune",
"audio",
"create",
"--model",
"--base-model",
"cosyvoice-v3-flash",
"--datasets",
"file-audio",
@@ -343,7 +343,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--model|--datasets/i);
expect(stderr).toMatch(/--base-model|--datasets/i);
expect(stderr).not.toMatch(/--training-type|--n-epochs|--batch-size|--max-length/);
});
@@ -352,7 +352,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
"finetune",
"image",
"create",
"--model",
"--base-model",
"wan2.7-image-pro",
"--datasets",
"file-image",
@@ -365,6 +365,104 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
expect(data.action).toBe("finetune.create");
expect(data.body.training_type).toBe("efficient_sft");
});
test("finetune video create --help 暴露视频超参且不含文本超参", async () => {
// Video exposes --n-epochs / --batch-size / --learning-rate; the text-only
// --training-type / --max-length surface is not offered.
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
"finetune",
"video",
"create",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--base-model/);
expect(stderr).toMatch(/--n-epochs/);
expect(stderr).toMatch(/--batch-size/);
expect(stderr).toMatch(/--learning-rate/);
expect(stderr).not.toMatch(/--training-type|--max-length/);
});
test("finetune video create --datasets 缺失时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
"finetune",
"video",
"create",
"--base-model",
"wan2.7-i2v",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--datasets|Missing required/i);
});
test.each([
// Model-family-specific defaults resolved by the sft-lora video profile.
["wan2.7-i2v", 1, 102400],
["wan2.5-i2v-preview", 4, 36864],
["wan2.2-kf2v-flash", 4, 262144],
])(
"finetune video create --dry-run %s 解析 batch_size=%i / max_pixels=%i",
async (baseModel, batchSize, maxPixels) => {
const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
"finetune",
"video",
"create",
"--base-model",
baseModel,
"--datasets",
"file-video",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
action: string;
body: {
model: string;
training_type: string;
hyper_parameters: Record<string, unknown>;
};
}>(stdout);
expect(data.action).toBe("finetune.create");
expect(data.body.model).toBe(baseModel);
expect(data.body.training_type).toBe("efficient_sft");
expect(data.body.hyper_parameters.batch_size).toBe(batchSize);
expect(data.body.hyper_parameters.max_pixels).toBe(maxPixels);
expect(data.body.hyper_parameters.learning_rate).toBe("2e-5");
expect(data.body.hyper_parameters.lora_rank).toBe(32);
},
);
test("finetune video create --dry-run 转发超参覆盖且不做 clamp", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
"finetune",
"video",
"create",
"--base-model",
"wan2.7-i2v",
"--datasets",
"file-video",
"--n-epochs",
"100",
"--batch-size",
"2",
"--learning-rate",
"1e-5",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
body: { hyper_parameters: Record<string, unknown> };
}>(stdout);
// Video overrides are forwarded verbatim (no [8, 1024] text clamp).
expect(data.body.hyper_parameters.n_epochs).toBe(100);
expect(data.body.hyper_parameters.batch_size).toBe(2);
expect(data.body.hyper_parameters.learning_rate).toBe("1e-5");
});
});
describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (DashScope)", () => {
+23 -15
View File
@@ -17,12 +17,14 @@ describe("e2e: skill", () => {
test("skill add --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "add", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--all/);
expect(stderr).toMatch(/--name/);
});
test("skill update --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "update", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--all/);
expect(stderr).toMatch(/--name/);
});
@@ -37,18 +39,37 @@ describe("e2e: skill", () => {
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/list|registry/i);
});
test("skill init --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "init", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/bailian/i);
});
});
// Local-only cases: auth "none" + validation happens before any network access, no gating needed
describe("e2e: skill (local, no credentials)", () => {
test("skill add without --name errors as usage error (2)", async () => {
test("skill add without --all or --name errors as usage error (2)", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
"skill",
"add",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(`${stdout}\n${stderr}`).toMatch(/--name|Usage:/i);
expect(`${stdout}\n${stderr}`).toMatch(/--all|--name|Usage:/i);
});
test("skill add with both --all and --name errors as usage error (2)", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
"skill",
"add",
"--all",
"--name",
"spark-video",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(`${stdout}\n${stderr}`).toMatch(/--all|--name|either/i);
});
test("skill remove without --name errors as usage error (2)", async () => {
@@ -61,19 +82,6 @@ describe("e2e: skill (local, no credentials)", () => {
expect(`${stdout}\n${stderr}`).toMatch(/--name|Usage:/i);
});
test("skill add rejects mixing all with specific names (2)", async () => {
// parseSkillNames throws UsageError before fetchSkillsIndex — offline-safe
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
"skill",
"add",
"--name",
"all,spark-video",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(`${stdout}\n${stderr}`).toMatch(/all/i);
});
test("skill remove of a not-installed skill fails with reason (1)", async () => {
const configDir = makeTempConfigDir();
const { stdout, exitCode } = await runCommandE2e(
@@ -135,6 +135,7 @@ export const FINETUNE_ROUTES: E2eRouteExports = {
"finetune text create": "finetuneTextCreate",
"finetune audio create": "finetuneAudioCreate",
"finetune image create": "finetuneImageCreate",
"finetune video create": "finetuneVideoCreate",
"finetune list": "finetuneList",
"finetune get": "finetuneGet",
"finetune cancel": "finetuneCancel",
@@ -165,6 +166,7 @@ export const SKILL_ROUTES: E2eRouteExports = {
"skill update": "skillUpdate",
"skill remove": "skillRemove",
"skill list": "skillList",
"skill init": "skillInit",
};
export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
@@ -112,6 +112,62 @@ describe("e2e: video generate (i2v)", () => {
}>(stdout);
expect(data.request?.input?.media?.[0]?.url).toBe("data:image/png;base64,<omitted>");
});
test.each([
// wan2.1~2.6 (legacy) use flat img_url; wan2.7+ and happyhorse use media[].
["wan2.5-i2v-preview", "img_url"],
["wan2.6-i2v", "img_url"],
["wan2.7-i2v", "media"],
["happyhorse-1.1-i2v", "media"],
])("video generate --dry-run %s 首帧走 %s 字段", async (model, field) => {
const configDir = makeE2eOutputDir(`video-i2v-input-shape-${model}`);
writeFileSync(
join(configDir, "config.json"),
JSON.stringify({
"token-plan": {
api_key: "sk-sp-e2e-placeholder",
base_url: "https://token-plan.cn-beijing.maas.aliyuncs.com",
},
}),
);
const { stdout, stderr, exitCode } = await runCommandE2e(
VIDEO_ROUTES,
[
"video",
"generate",
"--config",
"token-plan",
"--dry-run",
"--model",
model,
"--image",
"https://example.com/placeholder.png",
"--prompt",
"干跑校验",
"--output",
"json",
],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
},
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: {
input?: { img_url?: string; media?: Array<{ type?: string; url?: string }> };
};
}>(stdout);
if (field === "img_url") {
expect(data.request?.input?.img_url).toBe("https://example.com/placeholder.png");
expect(data.request?.input?.media).toBeUndefined();
} else {
expect(data.request?.input?.media?.[0]?.type).toBe("first_frame");
expect(data.request?.input?.img_url).toBeUndefined();
}
});
});
describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-core",
"version": "1.14.1",
"version": "1.14.2",
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
+5
View File
@@ -32,6 +32,11 @@ export function videoGeneratePath(): string {
return "/api/v1/services/aigc/video-generation/video-synthesis";
}
/** POST /api/v1/services/aigc/image2video/video-synthesis — kf2v (first+last frame). */
export function image2videoPath(): string {
return "/api/v1/services/aigc/image2video/video-synthesis";
}
// ---- Async Task Query ----
export function taskPath(taskId: string): string {
return `/api/v1/tasks/${encodeURIComponent(taskId)}`;
+1
View File
@@ -19,6 +19,7 @@ export {
taskPath,
userProfilePath,
videoGeneratePath,
image2videoPath,
} from "./endpoints.ts";
export {
isLegacyImage2ImageModel,
+1
View File
@@ -2,3 +2,4 @@ export * from "./api.ts";
export * from "./types.ts";
export * from "./constants.ts";
export * from "./plans.ts";
export * from "./lifecycle.ts";
+99
View File
@@ -0,0 +1,99 @@
/**
* Deployment lifecycle operations via the **console gateway**.
*
* Unlike the DashScope REST endpoints in `api.ts`, start/stop/list-independent
* are console-domain APIs (`zeldaEasy.broadscope-platform.modelInstance.*`).
* Commands using these must declare `auth: "console"`.
*/
import type { Client } from "../client/client.ts";
import { unwrapResponse } from "../console/models.ts";
// ---------------------------------------------------------------------------
// API names
// ---------------------------------------------------------------------------
export const DEPLOY_START_API = "zeldaEasy.broadscope-platform.modelInstance.startModelService";
export const DEPLOY_STOP_API = "zeldaEasy.broadscope-platform.modelInstance.stopModelService";
export const DEPLOY_LIST_INDEPENDENT_API =
"zeldaEasy.broadscope-platform.modelInstance.listIndependentDeployedModel";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface ModelServiceEntry {
modelServiceId?: string;
deployedModel?: string;
deployed_model?: string;
status?: string;
modelName?: string;
model_name?: string;
plan?: string;
[key: string]: unknown;
}
// ---------------------------------------------------------------------------
// API wrappers
// ---------------------------------------------------------------------------
/** Start (bring online) a stopped deployment. */
export async function startModelService(
client: Client,
modelServiceId: string,
): Promise<Record<string, unknown>> {
const raw = await client.console<Record<string, unknown>>(DEPLOY_START_API, {
input: { modelServiceId },
});
return unwrapResponse(raw);
}
/** Stop (take offline) a running deployment. Stops billing for mu/ptu plans. */
export async function stopModelService(
client: Client,
modelServiceId: string,
): Promise<Record<string, unknown>> {
const raw = await client.console<Record<string, unknown>>(DEPLOY_STOP_API, {
input: { modelServiceId },
});
return unwrapResponse(raw);
}
/**
* List independently deployed models (console domain).
* Used for precheck status verification and ID mapping.
* Paginates internally to return all entries.
*/
export async function listIndependentDeployedModels(client: Client): Promise<ModelServiceEntry[]> {
const allEntries: ModelServiceEntry[] = [];
let page = 1;
while (true) {
const raw = await client.console<Record<string, unknown>>(DEPLOY_LIST_INDEPENDENT_API, {
input: { pageNo: page, pageSize: 50 },
});
const resp = unwrapResponse(raw);
const records = (resp.records ?? []) as ModelServiceEntry[];
allEntries.push(...records);
const pageCount = (resp.pageCount as number) ?? 1;
if (page >= pageCount || records.length === 0) break;
page++;
}
return allEntries;
}
/**
* Find a deployment entry by its identifier in the console-domain list.
* Matches against `modelServiceId`, `deployedModel`, or `deployed_model`.
*/
export function findDeploymentEntry(
entries: ModelServiceEntry[],
deployedModel: string,
): ModelServiceEntry | undefined {
return entries.find(
(entry) =>
entry.modelServiceId === deployedModel ||
entry.deployedModel === deployedModel ||
entry.deployed_model === deployedModel,
);
}
+3
View File
@@ -43,6 +43,8 @@ export interface ListFineTunesParams {
pageNo?: number;
pageSize?: number;
status?: string;
/** Filter by base model ID (server-side). */
model?: string;
signal?: AbortSignal;
}
@@ -55,6 +57,7 @@ export async function listFineTunes(
if (params.pageNo !== undefined) qs.set("page_no", String(params.pageNo));
if (params.pageSize !== undefined) qs.set("page_size", String(params.pageSize));
if (params.status) qs.set("status", params.status);
if (params.model) qs.set("model", params.model);
const base = finetuneJobsPath();
const path = qs.toString() ? `${base}?${qs.toString()}` : base;
return client.requestJson<ListFineTunesResponse>({
+1
View File
@@ -3,3 +3,4 @@ export * from "./api.ts";
export * from "./capability.ts";
export * from "./preflight.ts";
export * from "./profiles/index.ts";
export * from "./price.ts";
+121
View File
@@ -0,0 +1,121 @@
/**
* Training price estimation via the **console gateway**.
*
* These are console-domain APIs (`zeldaEasy.broadscope-platform.*`); commands
* using them must declare `auth: "console"`. Two different argument wrappers
* exist: `getModelPrice` takes a top-level `query`, while the token-estimation
* APIs take a top-level `input`.
*/
import type { Client } from "../client/client.ts";
import { unwrapResponse } from "../console/models.ts";
// ---------------------------------------------------------------------------
// API names
// ---------------------------------------------------------------------------
export const TRAINING_MODEL_PRICE_API = "zeldaEasy.broadscope-platform.modelCenter.getModelPrice";
export const CALC_DATASETS_TOKENS_API =
"zeldaEasy.broadscope-platform.modelInstance.calculateDatasetsTotalTokens";
export const ESTIMATE_FINETUNE_TOKENS_API =
"zeldaEasy.broadscope-platform.modelInstance.estimateFinetuneTokens";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface TrainingModelPrice {
price?: string;
priceUnit?: string;
modelId?: string;
[key: string]: unknown;
}
export interface TokenEstimate {
estimatedDatasetConsumedTokensMinPerEpoch?: number;
estimatedDatasetConsumedTokensMaxPerEpoch?: number;
estimatedMixedConsumedTokensMinPerEpoch?: number;
estimatedMixedConsumedTokensMaxPerEpoch?: number;
[key: string]: unknown;
}
// ---------------------------------------------------------------------------
// API wrappers
// ---------------------------------------------------------------------------
/**
* Training unit price for a model. `price` is denominated in `priceUnit`
* (typically "千Token" yuan per 1000 tokens).
*/
export async function fetchTrainingModelPrice(
client: Client,
modelId: string,
): Promise<TrainingModelPrice> {
const raw = await client.console<Record<string, unknown>>(TRAINING_MODEL_PRICE_API, {
query: { type: 0, modelId },
});
return unwrapResponse(raw) as TrainingModelPrice;
}
/**
* Estimate training tokens for SFT / DPO jobs.
* Returns a per-epoch min/max range; multiply by `n_epochs` for the total.
*/
export async function estimateSftDpoTokens(
client: Client,
datasetIds: string[],
hyperParams: { nEpochs: number; batchSize: number; maxLength: number },
): Promise<TokenEstimate> {
const raw = await client.console<Record<string, unknown>>(CALC_DATASETS_TOKENS_API, {
input: { trainDatasetIds: datasetIds, hyperParams },
});
return unwrapResponse(raw) as TokenEstimate;
}
/**
* Estimate training tokens for CPT jobs.
*
* The console API requires `hyperParams` as a **JSON string** with a full
* `userDefinedObj` payload (captured from the console frontend), plus several
* top-level fields (`algorithmType`, `bizType`, `priority`, ). Only
* `n_epochs` / `max_length` materially affect the estimate; the remaining
* hyper-parameters are fixed defaults.
*/
export async function estimateCptTokens(
client: Client,
model: string,
datasetIdsCsv: string,
nEpochs: number,
): Promise<TokenEstimate> {
const userDefinedObj = {
batch_size: 16,
eval_steps: 50,
learning_rate: "7e-6",
lr_scheduler_type: "linear",
max_length: 8192,
n_epochs: nEpochs,
split: 0.9,
save_total_limit: "3",
resume_from_checkpoint: false,
save_strategy: "epoch",
};
const hyperParams = JSON.stringify({
useDefault: false,
userDefinedObj,
useQwenMixedStrategy: false,
});
const raw = await client.console<Record<string, unknown>>(ESTIMATE_FINETUNE_TOKENS_API, {
input: {
trainingType: "cpt",
instanceName: `${model}_cli_estimate`,
algorithmType: 100,
bizType: 100,
trainDatasetIds: datasetIdsCsv,
hyperParams,
bailianTrainModel: model,
validationDatasetIds: "",
jobName: `${model}_cli_estimate`,
priority: "L0",
},
});
return unwrapResponse(raw) as TokenEstimate;
}
@@ -75,20 +75,24 @@ const IMAGE_HYPER_PARAMS_I2I: Record<string, unknown> = {
*
* Shared across all video models; `batch_size` and `max_pixels` differ by model
* family (resolved per model in `resolveHyperParameters`):
* - wan2.5 (e.g. wan2.5-i2v-preview): batch_size 2, max_pixels 36864
* - wan2.2 (i2v-flash / kf2v-flash): batch_size 4, max_pixels 262144
* - wan2.7 (e.g. wan2.7-i2v): batch_size 1, max_pixels 102400
* - wan2.5 (e.g. wan2.5-i2v-preview): batch_size 4, max_pixels 36864
* - wan2.2 (i2v-flash / kf2v-flash): batch_size 4, max_pixels 262144
*
* `learning_rate` is a string to avoid JSON-number precision loss (consistent
* with the image defaults). `split` (0.9) + `max_split_val_dataset_sample`
* (5) drive the automatic train/validation split when no explicit
* validation_file_ids are provided.
*
* Values aligned with the official user guide (2026-07):
* n_epochs 50, eval_epochs 20 ( n_epochs/10).
*/
const VIDEO_HYPER_PARAMS_BASE: Record<string, unknown> = {
n_epochs: 400,
n_epochs: 50,
learning_rate: "2e-5",
split: 0.9,
split: 0.5,
max_split_val_dataset_sample: 5,
eval_epochs: 50,
eval_epochs: 20,
save_total_limit: 10,
lora_rank: 32,
lora_alpha: 32,
@@ -109,6 +113,11 @@ function isWan25(model: string | undefined): boolean {
return typeof model === "string" && /wan2\.5/i.test(model);
}
/** wan2.7 family uses batch_size 1 and max_pixels 102400. */
function isWan27(model: string | undefined): boolean {
return typeof model === "string" && /wan2\.7/i.test(model);
}
export const sftLoraProfile: TrainingProfile = {
clientTrainingType: "sft-lora",
serverTrainingType: "efficient_sft",
@@ -195,15 +204,18 @@ export const sftLoraProfile: TrainingProfile = {
}
if (isVideo(modality)) {
// Video: shared defaults + model-family-specific batch_size / max_pixels.
// wan2.5 uses batch_size 2 / max_pixels 36864; wan2.2 uses 4 / 262144.
const wan25 = isWan25(flags.model as string | undefined);
// wan2.7: batch_size 1, max_pixels 102400
// wan2.5: batch_size 4, max_pixels 36864
// wan2.2: batch_size 4, max_pixels 262144
const model = (flags.model ?? flags.baseModel) as string | undefined;
const hp: Record<string, unknown> = {
...VIDEO_HYPER_PARAMS_BASE,
batch_size: 4,
max_pixels: wan25 ? 36864 : 262144,
batch_size: isWan27(model) ? 1 : 4,
max_pixels: isWan27(model) ? 102400 : isWan25(model) ? 36864 : 262144,
};
// Optional overrides (no clamping — video batch_size is intentionally small).
if (flags.nEpochs !== undefined) hp.n_epochs = flags.nEpochs as number;
if (flags.batchSize !== undefined) hp.batch_size = flags.batchSize as number;
if (flags.learningRate !== undefined) hp.learning_rate = flags.learningRate as string;
return hp;
}
+2
View File
@@ -168,6 +168,8 @@ export interface DashScopeVideoRequest {
prompt: string;
negative_prompt?: string;
img_url?: string;
first_frame_url?: string;
last_frame_url?: string;
media?: Array<{
type: "image" | "video" | "first_frame" | "last_frame" | "driving_audio" | "first_clip";
url: string;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "knowledge-studio-cli",
"version": "1.14.1",
"version": "1.14.2",
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
"keywords": [
"alibaba-cloud",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-runtime",
"version": "1.14.1",
"version": "1.14.2",
"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 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-cli
metadata:
version: "1.14.1"
version: "1.14.2"
requires:
bins: ["bl"]
description: >-
+2 -1
View File
@@ -52,6 +52,7 @@ Use this index for the skill-scoped quick index and global flags.
| `bl quota request` | Request a temporary quota increase | [quota.md](quota.md) |
| `bl search web` | Search the web using DashScope MCP WebSearch service | [search.md](search.md) |
| `bl skill add` | Install skills from the Bailian skill registry into local agents | [skill.md](skill.md) |
| `bl skill init` | Install all bailian-\* skills (one-shot bootstrap for new environments) | [skill.md](skill.md) |
| `bl skill list` | List registry skills and diff against local installs | [skill.md](skill.md) |
| `bl skill remove` | Remove locally installed skills (registry is untouched) | [skill.md](skill.md) |
| `bl skill update` | Update installed skills to the latest registry versions | [skill.md](skill.md) |
@@ -86,7 +87,7 @@ Use this index for the skill-scoped quick index and global flags.
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
| `quota` | `check`, `history`, `list`, `request` | [quota.md](quota.md) |
| `search` | `web` | [search.md](search.md) |
| `skill` | `add`, `list`, `remove`, `update` | [skill.md](skill.md) |
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
| `text` | `chat` | [text.md](text.md) |
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) |
| `update` | `(root)` | [update.md](update.md) |
+45 -15
View File
@@ -7,12 +7,13 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| ----------------- | ---------------------------------------------------------------- |
| `bl skill add` | Install skills from the Bailian skill registry into local agents |
| `bl skill list` | List registry skills and diff against local installs |
| `bl skill remove` | Remove locally installed skills (registry is untouched) |
| `bl skill update` | Update installed skills to the latest registry versions |
| Command | Description |
| ----------------- | ----------------------------------------------------------------------- |
| `bl skill add` | Install skills from the Bailian skill registry into local agents |
| `bl skill init` | Install all bailian-\* skills (one-shot bootstrap for new environments) |
| `bl skill list` | List registry skills and diff against local installs |
| `bl skill remove` | Remove locally installed skills (registry is untouched) |
| `bl skill update` | Update installed skills to the latest registry versions |
## Command details
@@ -22,24 +23,48 @@ Index: [index.md](index.md)
| --------------- | ---------------------------------------------------------------- |
| **Name** | `skill add` |
| **Description** | Install skills from the Bailian skill registry into local agents |
| **Usage** | `bl skill add --name <all\|name,...>` |
| **Usage** | `bl skill add --all \| --name <name,...>` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------ | ------ | -------- | ----------------------------------------------------- |
| `--name <all\|name,...>` | string | yes | Skills to install: all or comma-separated skill names |
| Flag | Type | Required | Description |
| ------------------- | ------ | -------- | -------------------------------------- |
| `--all` | switch | no | Install all skills from the registry |
| `--name <name,...>` | string | no | Comma-separated skill names to install |
#### Examples
```bash
bl skill add --name all
bl skill add --all
```
```bash
bl skill add --name spark-video,bailian-model-recommend
```
### `bl skill init`
| Field | Value |
| --------------- | ----------------------------------------------------------------------- |
| **Name** | `skill init` |
| **Description** | Install all bailian-\* skills (one-shot bootstrap for new environments) |
| **Usage** | `bl skill init` |
#### Flags
_No command-specific flags._
#### Notes
- Fetches the registry index and installs every skill whose name starts with bailian-
- Equivalent to: bl skill add --all (filtered to bailian-\* skills)
#### Examples
```bash
bl skill init
```
### `bl skill list`
| Field | Value |
@@ -96,13 +121,14 @@ bl skill remove --name all
| --------------- | ------------------------------------------------------- |
| **Name** | `skill update` |
| **Description** | Update installed skills to the latest registry versions |
| **Usage** | `bl skill update [--name <all\|name,...>]` |
| **Usage** | `bl skill update [--all] [--name <name,...>]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------- |
| `--name <all\|name,...>` | string | no | Skills to update: all (default, only changed ones) or comma-separated names (force update installed skills) |
| Flag | Type | Required | Description |
| ------------------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `--all` | switch | no | Update all installed skills (default when neither --all nor --name is given) |
| `--name <name,...>` | string | no | Comma-separated skill names to update (must be already installed) |
#### Examples
@@ -110,6 +136,10 @@ bl skill remove --name all
bl skill update
```
```bash
bl skill update --all
```
```bash
bl skill update --name spark-video
```
+6 -6
View File
@@ -1,7 +1,7 @@
---
name: bailian-finetune
metadata:
version: "1.14.1"
version: "1.14.2"
requires:
bins: ["bl"]
description: >-
@@ -23,14 +23,14 @@ description: >-
```
1. Validate data bl dataset validate --file train.jsonl [--schema chatml|dpo|cpt|tts|image]
2. Upload data bl dataset upload --file train.jsonl # returns a file-id
3. Create job bl finetune text|audio|image create --model <base> --datasets <file-id|path>
3. Create job bl finetune text|audio|image create --base-model <base> --datasets <file-id|path>
4. Watch progress bl finetune watch --job-id ft-xxx # or get / logs
5. Pick artifact bl finetune checkpoints --job-id ft-xxx
6. Export model bl finetune export --job-id ft-xxx --checkpoint ckpt-N --model-name my-model
7. Deploy service bl deploy text|audio|image create --model my-model --name my-svc
7. Deploy service bl deploy text|audio|image create --model-name my-model --display-name my-svc
```
- Unsure which training methods a base model supports → `bl finetune capability --model <base>` or `--training-type sft|sft-lora|dpo|cpt`.
- Unsure which training methods a base model supports → `bl finetune capability --base-model <base>` or `--training-type sft|sft-lora|dpo|cpt`.
- Text `--training-type` values: `sft` / `sft-lora` / `dpo` / `dpo-lora` / `cpt`. Audio bases include `cosyvoice-v3-flash`; image bases include `wan2.7-image-pro`.
- Deployment plans: audio defaults to `--plan mu`; text/image default to `lora`.
- Preview write operations (create / delete / cancel / scale) with `--dry-run` first, and confirm with the user before deleting a job or dataset.
@@ -55,10 +55,10 @@ Flags, usage, and examples: see [`reference/`](reference/index.md) or `bl <comma
```bash
bl dataset validate --file train.jsonl
bl dataset upload --file train.jsonl
bl finetune text create --model qwen3-8b --training-type sft-lora --datasets file-xxx
bl finetune text create --base-model qwen3-8b --training-type sft-lora --datasets file-xxx
bl finetune watch --job-id ft-xxx
bl finetune export --job-id ft-xxx --checkpoint ckpt-3 --model-name my-qwen-sft
bl deploy text create --model my-qwen-sft --name my-svc
bl deploy text create --model-name my-qwen-sft --display-name my-svc
```
## Common hand-offs
+37 -31
View File
@@ -107,23 +107,23 @@ bl dataset list --output json
### `bl dataset upload`
| Field | Value |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `dataset upload` |
| **Description** | Upload a dataset file (.jsonl or .zip) to Bailian |
| **Usage** | `bl dataset upload --file <path> [--purpose <name>] [--schema <chatml\|dpo\|cpt\|tts\|image>] [--no-validate] [--full-validate]` |
| Field | Value |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `dataset upload` |
| **Description** | Upload a dataset file (.jsonl or .zip) to Bailian |
| **Usage** | `bl dataset upload --file <path> [--purpose <name>] [--schema <chatml\|dpo\|cpt\|tts\|image\|video>] [--no-validate] [--full-validate]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--file <path>` | string | yes | Local dataset file (.jsonl or .zip; ≤300MB text, ≤1GB image) |
| `--purpose <name>` | string | no | Dataset purpose tag (default: "fine-tune"; e.g. "evaluation") |
| `--schema <s>` | string | no | Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), or "image" (image generation). Default auto-detects per record. |
| `--no-validate` | switch | no | Skip the local JSONL pre-flight check (not recommended) |
| `--full-validate` | switch | no | JSON.parse every line instead of sampling (slower) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--file <path>` | string | yes | Local dataset file (.jsonl or .zip; ≤300MB text, ≤1GB image) |
| `--purpose <name>` | string | no | Dataset purpose tag (default: "fine-tune"; e.g. "evaluation") |
| `--schema <s>` | string | no | Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record. |
| `--no-validate` | switch | no | Skip the local JSONL pre-flight check (not recommended) |
| `--full-validate` | switch | no | JSON.parse every line instead of sampling (slower) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
@@ -170,19 +170,19 @@ bl dataset upload --file train.jsonl --no-validate
### `bl dataset validate`
| Field | Value |
| --------------- | ----------------------------------------------------------------------------------------------- |
| **Name** | `dataset validate` |
| **Description** | Locally validate a dataset file (.jsonl or .zip) without uploading |
| **Usage** | `bl dataset validate --file <path> [--full-validate] [--schema <chatml\|dpo\|cpt\|tts\|image>]` |
| Field | Value |
| --------------- | ------------------------------------------------------------------------------------------------------ |
| **Name** | `dataset validate` |
| **Description** | Locally validate a dataset file (.jsonl or .zip) without uploading |
| **Usage** | `bl dataset validate --file <path> [--full-validate] [--schema <chatml\|dpo\|cpt\|tts\|image\|video>]` |
#### Flags
| Flag | Type | Required | Description |
| ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--file <path>` | string | yes | Local dataset file (.jsonl or .zip) |
| `--full-validate` | switch | no | JSON.parse every line instead of sampling (slower) |
| `--schema <s>` | string | no | Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), or "image" (image generation). Default auto-detects per record. |
| Flag | Type | Required | Description |
| ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--file <path>` | string | yes | Local dataset file (.jsonl or .zip) |
| `--full-validate` | switch | no | JSON.parse every line instead of sampling (slower) |
| `--schema <s>` | string | no | Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record. |
#### Notes
@@ -191,13 +191,15 @@ bl dataset upload --file train.jsonl --no-validate
- 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). 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. Pass --schema to
- require a specific shape on every record. ZIP archives (.zip) are
- validated structurally (data.jsonl present, media references resolve) in
- addition to per-record content checks. Use --full-validate to JSON.parse
- every line.
- 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). 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. ZIP archives (.zip) are validated structurally
- (data.jsonl present, media references resolve) in addition to per-record
- content checks. Use --full-validate to JSON.parse every line.
#### Examples
@@ -217,6 +219,10 @@ bl dataset validate --file cpt.jsonl --schema cpt
bl dataset validate --file audio.zip --schema tts
```
```bash
bl dataset validate --file wan-i2v-training-dataset.zip --schema video
```
```bash
bl dataset validate --file eval.jsonl --full-validate
```
+168 -99
View File
@@ -7,43 +7,45 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| ------------------------ | --------------------------------------------------------- |
| `bl deploy audio create` | Create an audio (TTS) model deployment |
| `bl deploy delete` | Delete a model deployment (must be STOPPED or FAILED) |
| `bl deploy get` | Get details of a single model deployment |
| `bl deploy image create` | Create an image generation model deployment |
| `bl deploy list` | List model deployments |
| `bl deploy models` | List models available for deployment |
| `bl deploy scale` | Scale a deployment's capacity |
| `bl deploy text create` | Create a text model deployment |
| `bl deploy update` | Update a deployment's rate limits (rpm_limit / tpm_limit) |
| Command | Description |
| ------------------------ | ------------------------------------------------------------- |
| `bl deploy audio create` | Create an audio (TTS) model deployment |
| `bl deploy delete` | Delete a model deployment (must be STOPPED or FAILED) |
| `bl deploy get` | Get details of a single model deployment |
| `bl deploy image create` | Create an image generation model deployment |
| `bl deploy list` | List model deployments |
| `bl deploy models` | List models available for deployment |
| `bl deploy pause` | Pause a running model deployment (stops billing for mu/ptu) |
| `bl deploy resume` | Resume a paused model deployment (brings service back online) |
| `bl deploy scale` | Scale a deployment's capacity |
| `bl deploy text create` | Create a text model deployment |
| `bl deploy update` | Update a deployment's rate limits (rpm_limit / tpm_limit) |
## Command details
### `bl deploy audio create`
| Field | Value |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `deploy audio create` |
| **Description** | Create an audio (TTS) model deployment |
| **Usage** | `bl deploy audio create --model <model_name> --name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]` |
| Field | Value |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Name** | `deploy audio create` |
| **Description** | Create an audio (TTS) model deployment |
| **Usage** | `bl deploy audio create --model-name <model_name> --display-name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------ | -------- | ------------------------------------------------------------------------------- |
| `--model <name>` | string | yes | Model name (catalog model or fine-tuned output) (required) |
| `--name <display_name>` | string | yes | Console display name for the deployment (required) |
| `--plan <plan>` | string | no | Billing plan: lora (default, Token-billed) \| ptu (Token-billed) \| mu |
| `--deploy-spec <id>` | string | no | Deploy spec (only used by plan=mu; auto-picked if omitted) |
| `--capacity <n>` | number | no | Resource units (plan=mu only; required by API; defaults to the template's unit) |
| `--billing-method <m>` | string | no | Billing method (plan=mu only; default "POST_PAY", the only supported value) |
| `--input-tpm <n>` | number | no | PTU max input tokens/min (required for plan=ptu) |
| `--output-tpm <n>` | number | no | PTU max output tokens/min (required for plan=ptu) |
| `--thinking-output-tpm <n>` | number | no | PTU max thinking-output tokens/min (optional, some models) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| ------------------------------- | ------ | -------- | ------------------------------------------------------------------------------- |
| `--model-name <model_name>` | string | yes | Model to deploy — fine-tuned output name or catalog model (required) |
| `--display-name <display_name>` | string | yes | Console display name for the deployment (required) |
| `--plan <plan>` | string | no | Billing plan: lora (default, Token-billed) \| ptu (Token-billed) \| mu |
| `--deploy-spec <id>` | string | no | Deploy spec (only used by plan=mu; auto-picked if omitted) |
| `--capacity <n>` | number | no | Resource units (plan=mu only; required by API; defaults to the template's unit) |
| `--billing-method <m>` | string | no | Billing method (plan=mu only; default "POST_PAY", the only supported value) |
| `--input-tpm <n>` | number | no | PTU max input tokens/min (required for plan=ptu) |
| `--output-tpm <n>` | number | no | PTU max output tokens/min (required for plan=ptu) |
| `--thinking-output-tpm <n>` | number | no | PTU max thinking-output tokens/min (optional, some models) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
@@ -58,27 +60,24 @@ Index: [index.md](index.md)
- Use `bl deploy models --source base` to inspect available templates.
- After creation, status starts at PENDING and transitions to RUNNING.
- Invoke the deployed model with: bl text chat --model <deployed_model>
- WARNING: --model is overloaded across commands and refers to DIFFERENT
- values. `bl deploy <modality> create --model` takes the exported model_name
- (e.g. `qwen3-8b-ft-...`), but the create response also returns a
- `deployed_model` field (the deployment instance id, e.g.
- `qwen3-8b-5ecb5f068d79`). The inference call `bl text chat --model` must use
- the `deployed_model` from the create response — NOT the `model_name` you
- passed to `deploy <modality> create`. Do not reuse the value across the two
- commands.
- NOTE: --model-name is the model being deployed (e.g. `qwen3-8b-ft-...`).
- The create response also returns a `deployed_model` field — the deployment
- instance id (e.g. `qwen3-8b-5ecb5f068d79`). Use that id for inference
- (`bl text chat --model <deployed_model>`) and lifecycle commands
- (`deploy get/scale/pause/resume/delete --deployed-model <id>`).
#### Examples
```bash
bl deploy audio create --model my-cosyvoice-ft --name my-tts
bl deploy audio create --model-name my-cosyvoice-ft --display-name my-tts
```
```bash
bl deploy audio create --model my-cosyvoice-ft --name my-tts --deploy-spec dps-xxxx --capacity 1
bl deploy audio create --model-name my-cosyvoice-ft --display-name my-tts --deploy-spec dps-xxxx --capacity 1
```
```bash
bl deploy audio create --model my-cosyvoice-ft --name my-tts --dry-run
bl deploy audio create --model-name my-cosyvoice-ft --display-name my-tts --dry-run
```
### `bl deploy delete`
@@ -136,27 +135,27 @@ bl deploy get --deployed-model qwen-plus-2025-12-01-b6d61c71 --output json
### `bl deploy image create`
| Field | Value |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `deploy image create` |
| **Description** | Create an image generation model deployment |
| **Usage** | `bl deploy image create --model <model_name> --name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]` |
| Field | Value |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Name** | `deploy image create` |
| **Description** | Create an image generation model deployment |
| **Usage** | `bl deploy image create --model-name <model_name> --display-name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------ | -------- | ------------------------------------------------------------------------------- |
| `--model <name>` | string | yes | Model name (catalog model or fine-tuned output) (required) |
| `--name <display_name>` | string | yes | Console display name for the deployment (required) |
| `--plan <plan>` | string | no | Billing plan: lora (default, Token-billed) \| ptu (Token-billed) \| mu |
| `--deploy-spec <id>` | string | no | Deploy spec (only used by plan=mu; auto-picked if omitted) |
| `--capacity <n>` | number | no | Resource units (plan=mu only; required by API; defaults to the template's unit) |
| `--billing-method <m>` | string | no | Billing method (plan=mu only; default "POST_PAY", the only supported value) |
| `--input-tpm <n>` | number | no | PTU max input tokens/min (required for plan=ptu) |
| `--output-tpm <n>` | number | no | PTU max output tokens/min (required for plan=ptu) |
| `--thinking-output-tpm <n>` | number | no | PTU max thinking-output tokens/min (optional, some models) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| ------------------------------- | ------ | -------- | ------------------------------------------------------------------------------- |
| `--model-name <model_name>` | string | yes | Model to deploy — fine-tuned output name or catalog model (required) |
| `--display-name <display_name>` | string | yes | Console display name for the deployment (required) |
| `--plan <plan>` | string | no | Billing plan: lora (default, Token-billed) \| ptu (Token-billed) \| mu |
| `--deploy-spec <id>` | string | no | Deploy spec (only used by plan=mu; auto-picked if omitted) |
| `--capacity <n>` | number | no | Resource units (plan=mu only; required by API; defaults to the template's unit) |
| `--billing-method <m>` | string | no | Billing method (plan=mu only; default "POST_PAY", the only supported value) |
| `--input-tpm <n>` | number | no | PTU max input tokens/min (required for plan=ptu) |
| `--output-tpm <n>` | number | no | PTU max output tokens/min (required for plan=ptu) |
| `--thinking-output-tpm <n>` | number | no | PTU max thinking-output tokens/min (optional, some models) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
@@ -171,27 +170,24 @@ bl deploy get --deployed-model qwen-plus-2025-12-01-b6d61c71 --output json
- Use `bl deploy models --source base` to inspect available templates.
- After creation, status starts at PENDING and transitions to RUNNING.
- Invoke the deployed model with: bl text chat --model <deployed_model>
- WARNING: --model is overloaded across commands and refers to DIFFERENT
- values. `bl deploy <modality> create --model` takes the exported model_name
- (e.g. `qwen3-8b-ft-...`), but the create response also returns a
- `deployed_model` field (the deployment instance id, e.g.
- `qwen3-8b-5ecb5f068d79`). The inference call `bl text chat --model` must use
- the `deployed_model` from the create response — NOT the `model_name` you
- passed to `deploy <modality> create`. Do not reuse the value across the two
- commands.
- NOTE: --model-name is the model being deployed (e.g. `qwen3-8b-ft-...`).
- The create response also returns a `deployed_model` field — the deployment
- instance id (e.g. `qwen3-8b-5ecb5f068d79`). Use that id for inference
- (`bl text chat --model <deployed_model>`) and lifecycle commands
- (`deploy get/scale/pause/resume/delete --deployed-model <id>`).
#### Examples
```bash
bl deploy image create --model my-wan-ft --name my-wan
bl deploy image create --model-name my-wan-ft --display-name my-wan
```
```bash
bl deploy image create --model my-wan-ft --name my-wan-mu --plan mu
bl deploy image create --model-name my-wan-ft --display-name my-wan-mu --plan mu
```
```bash
bl deploy image create --model my-wan-ft --name my-wan --dry-run
bl deploy image create --model-name my-wan-ft --display-name my-wan --dry-run
```
### `bl deploy list`
@@ -263,6 +259,82 @@ bl deploy models --source custom --page-size 50
bl deploy models --catalog-version v1.0 --output json
```
### `bl deploy pause`
| Field | Value |
| --------------- | ----------------------------------------------------------- |
| **Name** | `deploy pause` |
| **Description** | Pause a running model deployment (stops billing for mu/ptu) |
| **Usage** | `bl deploy pause --deployed-model <id> [--skip-precheck]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
| `--deployed-model <id>` | string | yes | Deployed model identifier (required) |
| `--skip-precheck` | switch | no | Skip the local RUNNING/PENDING status precheck |
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
#### Notes
- While paused, billing ceases for mu/ptu plans. Use `deploy resume` to bring it back online or `deploy delete` to remove.
- Precheck verifies status is RUNNING/PENDING before issuing the pause; pass --skip-precheck to bypass.
#### Examples
```bash
bl deploy pause --deployed-model dep-...
```
```bash
bl deploy pause --deployed-model dep-... --skip-precheck
```
```bash
bl deploy pause --deployed-model dep-... --dry-run
```
### `bl deploy resume`
| Field | Value |
| --------------- | ------------------------------------------------------------- |
| **Name** | `deploy resume` |
| **Description** | Resume a paused model deployment (brings service back online) |
| **Usage** | `bl deploy resume --deployed-model <id> [--skip-precheck]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
| `--deployed-model <id>` | string | yes | Deployed model identifier (required) |
| `--skip-precheck` | switch | no | Skip the local STOPPED status precheck |
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
#### Notes
- Precheck verifies status is STOPPED before issuing the resume; pass --skip-precheck to bypass.
- For mu/ptu plans, billing resumes once the service is back online.
#### Examples
```bash
bl deploy resume --deployed-model dep-...
```
```bash
bl deploy resume --deployed-model dep-... --skip-precheck
```
```bash
bl deploy resume --deployed-model dep-... --dry-run
```
### `bl deploy scale`
| Field | Value |
@@ -294,27 +366,27 @@ bl deploy scale --deployed-model dep-... --capacity 2
### `bl deploy text create`
| Field | Value |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `deploy text create` |
| **Description** | Create a text model deployment |
| **Usage** | `bl deploy text create --model <model_name> --name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]` |
| Field | Value |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `deploy text create` |
| **Description** | Create a text model deployment |
| **Usage** | `bl deploy text create --model-name <model_name> --display-name <display_name> [--plan <plan>] [--deploy-spec <id>] [--capacity <n>] [--billing-method <m>] [--input-tpm <n>] [--output-tpm <n>] [--thinking-output-tpm <n>]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------ | -------- | ------------------------------------------------------------------------------- |
| `--model <name>` | string | yes | Model name (catalog model or fine-tuned output) (required) |
| `--name <display_name>` | string | yes | Console display name for the deployment (required) |
| `--plan <plan>` | string | no | Billing plan: lora (default, Token-billed) \| ptu (Token-billed) \| mu |
| `--deploy-spec <id>` | string | no | Deploy spec (only used by plan=mu; auto-picked if omitted) |
| `--capacity <n>` | number | no | Resource units (plan=mu only; required by API; defaults to the template's unit) |
| `--billing-method <m>` | string | no | Billing method (plan=mu only; default "POST_PAY", the only supported value) |
| `--input-tpm <n>` | number | no | PTU max input tokens/min (required for plan=ptu) |
| `--output-tpm <n>` | number | no | PTU max output tokens/min (required for plan=ptu) |
| `--thinking-output-tpm <n>` | number | no | PTU max thinking-output tokens/min (optional, some models) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| ------------------------------- | ------ | -------- | ------------------------------------------------------------------------------- |
| `--model-name <model_name>` | string | yes | Model to deploy — fine-tuned output name or catalog model (required) |
| `--display-name <display_name>` | string | yes | Console display name for the deployment (required) |
| `--plan <plan>` | string | no | Billing plan: lora (default, Token-billed) \| ptu (Token-billed) \| mu |
| `--deploy-spec <id>` | string | no | Deploy spec (only used by plan=mu; auto-picked if omitted) |
| `--capacity <n>` | number | no | Resource units (plan=mu only; required by API; defaults to the template's unit) |
| `--billing-method <m>` | string | no | Billing method (plan=mu only; default "POST_PAY", the only supported value) |
| `--input-tpm <n>` | number | no | PTU max input tokens/min (required for plan=ptu) |
| `--output-tpm <n>` | number | no | PTU max output tokens/min (required for plan=ptu) |
| `--thinking-output-tpm <n>` | number | no | PTU max thinking-output tokens/min (optional, some models) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
@@ -329,31 +401,28 @@ bl deploy scale --deployed-model dep-... --capacity 2
- Use `bl deploy models --source base` to inspect available templates.
- After creation, status starts at PENDING and transitions to RUNNING.
- Invoke the deployed model with: bl text chat --model <deployed_model>
- WARNING: --model is overloaded across commands and refers to DIFFERENT
- values. `bl deploy <modality> create --model` takes the exported model_name
- (e.g. `qwen3-8b-ft-...`), but the create response also returns a
- `deployed_model` field (the deployment instance id, e.g.
- `qwen3-8b-5ecb5f068d79`). The inference call `bl text chat --model` must use
- the `deployed_model` from the create response — NOT the `model_name` you
- passed to `deploy <modality> create`. Do not reuse the value across the two
- commands.
- NOTE: --model-name is the model being deployed (e.g. `qwen3-8b-ft-...`).
- The create response also returns a `deployed_model` field — the deployment
- instance id (e.g. `qwen3-8b-5ecb5f068d79`). Use that id for inference
- (`bl text chat --model <deployed_model>`) and lifecycle commands
- (`deploy get/scale/pause/resume/delete --deployed-model <id>`).
#### Examples
```bash
bl deploy text create --model my-qwen-sft --name my-sft-test
bl deploy text create --model-name my-qwen-sft --display-name my-sft-test
```
```bash
bl deploy text create --model qwen3.6-flash-2026-04-16 --name my-flash --plan ptu --input-tpm 10000 --output-tpm 1000
bl deploy text create --model-name qwen3.6-flash-2026-04-16 --display-name my-flash --plan ptu --input-tpm 10000 --output-tpm 1000
```
```bash
bl deploy text create --model qwen3-8b --name my-qwen3-mu --plan mu
bl deploy text create --model-name qwen3-8b --display-name my-qwen3-mu --plan mu
```
```bash
bl deploy text create --model qwen3-8b --name my-qwen3 --plan mu --deploy-spec MU1 --capacity 2
bl deploy text create --model-name qwen3-8b --display-name my-qwen3 --plan mu --deploy-spec MU1 --capacity 2
```
### `bl deploy update`
+158 -56
View File
@@ -19,24 +19,26 @@ Index: [index.md](index.md)
| `bl finetune image create` | Create an image generation model fine-tune job (sft-lora) |
| `bl finetune list` | List fine-tune jobs |
| `bl finetune logs` | Fetch training logs for a fine-tune job |
| `bl finetune price` | Estimate the training cost for a fine-tune job (token billing) |
| `bl finetune text create` | Create a text model fine-tune job (sft \| sft-lora \| dpo \| dpo-lora \| cpt) |
| `bl finetune video create` | Create a video generation model fine-tune job (Wan i2v/kf2v, efficient_sft) |
| `bl finetune watch` | Probe a fine-tune job's status (default: single non-blocking fetch). Pass --follow to poll until terminal. |
## Command details
### `bl finetune audio create`
| Field | Value |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `finetune audio create` |
| **Description** | Create an audio TTS model fine-tune job (sft-lora) |
| **Usage** | `bl finetune audio create --model <model> --datasets <id\|path> [--validations <id\|path>] [--model-name <name>] [--suffix <text>]` |
| Field | Value |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `finetune audio create` |
| **Description** | Create an audio TTS model fine-tune job (sft-lora) |
| **Usage** | `bl finetune audio create --base-model <model> --datasets <id\|path> [--validations <id\|path>] [--model-name <name>] [--suffix <text>]` |
#### Flags
| Flag | Type | Required | Description |
| ---------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--model <model>` | string | yes | Base model to fine-tune |
| `--base-model <model>` | string | yes | Base model to fine-tune (e.g. qwen3-8b; not the output model name) |
| `--datasets <ids\|paths>` | string | yes | Comma-separated dataset file IDs or local paths (.jsonl for text, .zip for audio/image). Local paths are uploaded (validated) first, then their file-ids are used. |
| `--validations <ids\|paths>` | string | no | Comma-separated validation dataset file IDs or local paths (auto-uploaded like --datasets). |
| `--model-name <name>` | string | no | Output model name (after training) |
@@ -58,23 +60,23 @@ Index: [index.md](index.md)
#### Examples
```bash
bl finetune audio create --model cosyvoice-v3-flash --datasets ./audio.zip
bl finetune audio create --base-model cosyvoice-v3-flash --datasets ./audio.zip
```
```bash
bl finetune audio create --model cosyvoice-v3-flash --datasets file-xxx
bl finetune audio create --base-model cosyvoice-v3-flash --datasets file-xxx
```
```bash
bl finetune audio create --model cosyvoice-v3-flash --datasets ./audio.zip --model-name my-tts
bl finetune audio create --base-model cosyvoice-v3-flash --datasets ./audio.zip --model-name my-tts
```
```bash
bl finetune audio create --model cosyvoice-v3-flash --datasets file-xxx --output json
bl finetune audio create --base-model cosyvoice-v3-flash --datasets file-xxx --output json
```
```bash
bl finetune audio create --model cosyvoice-v3-flash --datasets ./audio.zip --dry-run
bl finetune audio create --base-model cosyvoice-v3-flash --datasets ./audio.zip --dry-run
```
### `bl finetune cancel`
@@ -114,18 +116,18 @@ bl finetune cancel --job-id ft-xxx --dry-run
| --------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `finetune capability` |
| **Description** | Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it) |
| **Usage** | `bl finetune capability --model <m> \| --training-type <t>` |
| **Usage** | `bl finetune capability --base-model <m> \| --training-type <t>` |
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------------------------------------------------- |
| `--model <m>` | string | no | List training types supported by this base model. |
| `--base-model <m>` | string | no | List training types supported by this base model. |
| `--training-type <t>` | string | no | List models supporting this training type: sft \| sft-lora \| dpo \| dpo-lora \| cpt. |
#### Notes
- Exactly one of --model / --training-type is required.
- Exactly one of --base-model / --training-type is required.
- Training-type values use the `<method>` / `<method>-lora` convention:
- sft | sft-lora | dpo | dpo-lora | cpt. (cpt has no -lora variant server-side.)
- Queries listFoundationModels, a public API — no console login needed.
@@ -133,7 +135,7 @@ bl finetune cancel --job-id ft-xxx --dry-run
#### Examples
```bash
bl finetune capability --model qwen3-8b
bl finetune capability --base-model qwen3-8b
```
```bash
@@ -166,8 +168,8 @@ bl finetune capability --training-type sft --quiet
#### Notes
- Use the returned `checkpoint` value with `finetune export` to publish
- a deployable model.
- `model_name` (shown for SUCCEEDED checkpoints) is the direct input for `deploy create --model-name`.
- Checkpoints expire ~15 days after creation; `expire_time` shows the deadline. Export or deploy before expiry.
#### Examples
@@ -268,17 +270,17 @@ bl finetune get --job-id ft-xxx --output json
### `bl finetune image create`
| Field | Value |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Name** | `finetune image create` |
| **Description** | Create an image generation model fine-tune job (sft-lora) |
| **Usage** | `bl finetune image create --model <model> --datasets <id\|path> [--validations <id\|path>] [--model-name <name>] [--suffix <text>] [--generation-type <t2i\|i2i>] [--learning-rate <str>]` |
| Field | Value |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `finetune image create` |
| **Description** | Create an image generation model fine-tune job (sft-lora) |
| **Usage** | `bl finetune image create --base-model <model> --datasets <id\|path> [--validations <id\|path>] [--model-name <name>] [--suffix <text>] [--generation-type <t2i\|i2i>] [--learning-rate <str>]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--model <model>` | string | yes | Base model to fine-tune |
| `--base-model <model>` | string | yes | Base model to fine-tune (e.g. qwen3-8b; not the output model name) |
| `--datasets <ids\|paths>` | string | yes | Comma-separated dataset file IDs or local paths (.jsonl for text, .zip for audio/image). Local paths are uploaded (validated) first, then their file-ids are used. |
| `--validations <ids\|paths>` | string | no | Comma-separated validation dataset file IDs or local paths (auto-uploaded like --datasets). |
| `--model-name <name>` | string | no | Output model name (after training) |
@@ -304,46 +306,47 @@ bl finetune get --job-id ft-xxx --output json
#### Examples
```bash
bl finetune image create --model wan2.7-image-pro --datasets ./images.zip
bl finetune image create --base-model wan2.7-image-pro --datasets ./images.zip
```
```bash
bl finetune image create --model wan2.7-image-pro --datasets file-xxx
bl finetune image create --base-model wan2.7-image-pro --datasets file-xxx
```
```bash
bl finetune image create --model wan2.7-image-pro --datasets file-xxx --generation-type i2i
bl finetune image create --base-model wan2.7-image-pro --datasets file-xxx --generation-type i2i
```
```bash
bl finetune image create --model wan2.7-image-pro --datasets ./images.zip --model-name my-wan
bl finetune image create --base-model wan2.7-image-pro --datasets ./images.zip --model-name my-wan
```
```bash
bl finetune image create --model wan2.7-image-pro --datasets file-xxx --output json
bl finetune image create --base-model wan2.7-image-pro --datasets file-xxx --output json
```
```bash
bl finetune image create --model wan2.7-image-pro --datasets ./images.zip --dry-run
bl finetune image create --base-model wan2.7-image-pro --datasets ./images.zip --dry-run
```
### `bl finetune list`
| Field | Value |
| --------------- | ---------------------------------------------------------------- |
| **Name** | `finetune list` |
| **Description** | List fine-tune jobs |
| **Usage** | `bl finetune list [--page <n>] [--page-size <n>] [--status <s>]` |
| Field | Value |
| --------------- | --------------------------------------------------------------------------------------- |
| **Name** | `finetune list` |
| **Description** | List fine-tune jobs |
| **Usage** | `bl finetune list [--page <n>] [--page-size <n>] [--status <s>] [--base-model <model>]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | -------------------------------------------------------------------- |
| `--page <n>` | number | no | Page number (default: 1) |
| `--page-size <n>` | number | no | Results per page (default: 10, max 100) |
| `--status <s>` | string | no | Filter by status (PENDING / RUNNING / SUCCEEDED / FAILED / CANCELED) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| ---------------------- | ------ | -------- | -------------------------------------------------------------------- |
| `--page <n>` | number | no | Page number (default: 1) |
| `--page-size <n>` | number | no | Results per page (default: 10, max 100) |
| `--status <s>` | string | no | Filter by status (PENDING / RUNNING / SUCCEEDED / FAILED / CANCELED) |
| `--base-model <model>` | string | no | Filter by base model ID (server-side) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -356,7 +359,11 @@ bl finetune list --status RUNNING
```
```bash
bl finetune list --page-size 20 --output json
bl finetune list --base-model qwen3-8b
```
```bash
bl finetune list --page-size 20
```
### `bl finetune logs`
@@ -405,19 +412,60 @@ bl finetune logs --job-id ft-xxx --tail 20
bl finetune logs --job-id ft-xxx --search checkpoint --tail 5
```
### `bl finetune price`
| Field | Value |
| --------------- | --------------------------------------------------------------------------------------------------- |
| **Name** | `finetune price` |
| **Description** | Estimate the training cost for a fine-tune job (token billing) |
| **Usage** | `bl finetune price --base-model <model> --datasets <ids> [--training-type <type>] [--n-epochs <n>]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------------------------------------ |
| `--base-model <model>` | string | yes | Base model to fine-tune (e.g. qwen3-8b; not the output model name) |
| `--datasets <ids>` | string | yes | Training dataset file IDs, comma-separated (required) |
| `--training-type <type>` | string | no | Training type: sft \| dpo \| cpt (default: sft) |
| `--n-epochs <n>` | number | no | Number of training epochs (default: 3) |
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
#### Notes
- Estimate only — the server computes token usage from the datasets; final cost is subject to the bill.
- Covers token billing for sft / dpo / cpt. Training-unit (MTU) billing is not supported by this command.
- Hyper-parameters other than --n-epochs are fixed at representative defaults for estimation.
#### Examples
```bash
bl finetune price --base-model qwen3-8b --datasets file-ft-xxx
```
```bash
bl finetune price --base-model qwen3-8b --datasets file-ft-xxx,file-ft-yyy --n-epochs 2
```
```bash
bl finetune price --base-model qwen3-8b --datasets file-ft-xxx --training-type cpt
```
### `bl finetune text create`
| Field | Value |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `finetune text create` |
| **Description** | Create a text model fine-tune job (sft \| sft-lora \| dpo \| dpo-lora \| cpt) |
| **Usage** | `bl finetune text create --model <model> --datasets <id\|path,...> [--validations <id\|path,...>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>] [--max-length <n>] [--training-type <sft\|sft-lora\|dpo\|dpo-lora\|cpt>]` |
| Field | Value |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Name** | `finetune text create` |
| **Description** | Create a text model fine-tune job (sft \| sft-lora \| dpo \| dpo-lora \| cpt) |
| **Usage** | `bl finetune text create --base-model <model> --datasets <id\|path,...> [--validations <id\|path,...>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>] [--max-length <n>] [--training-type <sft\|sft-lora\|dpo\|dpo-lora\|cpt>]` |
#### Flags
| Flag | Type | Required | Description |
| ---------------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--model <model>` | string | yes | Base model to fine-tune |
| `--base-model <model>` | string | yes | Base model to fine-tune (e.g. qwen3-8b; not the output model name) |
| `--datasets <ids\|paths>` | string | yes | Comma-separated dataset file IDs or local paths (.jsonl for text, .zip for audio/image). Local paths are uploaded (validated) first, then their file-ids are used. |
| `--validations <ids\|paths>` | string | no | Comma-separated validation dataset file IDs or local paths (auto-uploaded like --datasets). |
| `--model-name <name>` | string | no | Output model name (after training) |
@@ -453,35 +501,89 @@ bl finetune logs --job-id ft-xxx --search checkpoint --tail 5
#### Examples
```bash
bl finetune text create --model qwen3-8b --datasets file-xxx
bl finetune text create --base-model qwen3-8b --datasets file-xxx
```
```bash
bl finetune text create --model qwen3-8b --datasets ./train.jsonl
bl finetune text create --base-model qwen3-8b --datasets ./train.jsonl
```
```bash
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --validations ./eval.jsonl
bl finetune text create --base-model qwen3-8b --datasets ./train.jsonl --validations ./eval.jsonl
```
```bash
bl finetune text create --model qwen3-8b --datasets file-aaa,./extra.jsonl
bl finetune text create --base-model qwen3-8b --datasets file-aaa,./extra.jsonl
```
```bash
bl finetune text create --model qwen3-8b --datasets ./train.jsonl --training-type sft
bl finetune text create --base-model qwen3-8b --datasets ./train.jsonl --training-type sft
```
```bash
bl finetune text create --model qwen3-8b --datasets file-xxx --learning-rate "1.6e-5" --n-epochs 4
bl finetune text create --base-model qwen3-8b --datasets file-xxx --learning-rate "1.6e-5" --n-epochs 4
```
```bash
bl finetune text create --model qwen3-8b --datasets file-xxx --output json
bl finetune text create --base-model qwen3-8b --datasets file-xxx --output json
```
```bash
bl finetune text create --model qwen3-8b --datasets file-xxx --dry-run
bl finetune text create --base-model qwen3-8b --datasets file-xxx --dry-run
```
### `bl finetune video create`
| Field | Value |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `finetune video create` |
| **Description** | Create a video generation model fine-tune job (Wan i2v/kf2v, efficient_sft) |
| **Usage** | `bl finetune video create --base-model <model> --datasets <id\|path> [--validations <id\|path>] [--model-name <name>] [--suffix <text>] [--n-epochs <n>] [--batch-size <n>] [--learning-rate <str>]` |
#### Flags
| Flag | Type | Required | Description |
| ---------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--base-model <model>` | string | yes | Base model to fine-tune (e.g. qwen3-8b; not the output model name) |
| `--datasets <ids\|paths>` | string | yes | Comma-separated dataset file IDs or local paths (.jsonl for text, .zip for audio/image). Local paths are uploaded (validated) first, then their file-ids are used. |
| `--validations <ids\|paths>` | string | no | Comma-separated validation dataset file IDs or local paths (auto-uploaded like --datasets). |
| `--model-name <name>` | string | no | Output model name (after training) |
| `--suffix <text>` | string | no | Output suffix appended by the platform (finetuned_output_suffix) |
| `--n-epochs <n>` | number | no | Training epochs (default: 50) |
| `--batch-size <n>` | number | no | Batch size (default: model-specific, 1 for wan2.7, 4 for wan2.5/2.2) |
| `--learning-rate <str>` | string | no | Learning rate as a string to preserve precision (default: "2e-5") |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Creating a job uploads any local datasets and consumes training quota.
- Use --dry-run to preview the request body without submitting.
- --datasets / --validations accept either file-ids (from `dataset upload`)
- or local paths. Local paths are validated and uploaded first, then their
- file-ids are submitted — a one-step upload-and-train.
- Video generation training (Wan i2v/kf2v) runs efficient_sft with model-
- specific defaults: wan2.7 (batch_size=1, max_pixels=102400), wan2.5/2.2
- (batch_size=4, max_pixels per model). Override with --batch-size/--n-epochs.
- Datasets are .zip archives with data.jsonl + frame images + videos.
- Recommended: ≥10 training samples, 20-100 for stable results.
#### Examples
```bash
bl finetune video create --base-model wan2.7-i2v --datasets file-xxx
```
```bash
bl finetune video create --base-model wan2.7-i2v --datasets ./i2v-data.zip
```
```bash
bl finetune video create --base-model wan2.2-kf2v-flash --datasets file-xxx --n-epochs 100
```
```bash
bl finetune video create --base-model wan2.7-i2v --datasets file-xxx --dry-run
```
### `bl finetune watch`
+9 -5
View File
@@ -22,6 +22,8 @@ Use this index for the skill-scoped quick index and global flags.
| `bl deploy image create` | Create an image generation model deployment | [deploy.md](deploy.md) |
| `bl deploy list` | List model deployments | [deploy.md](deploy.md) |
| `bl deploy models` | List models available for deployment | [deploy.md](deploy.md) |
| `bl deploy pause` | Pause a running model deployment (stops billing for mu/ptu) | [deploy.md](deploy.md) |
| `bl deploy resume` | Resume a paused model deployment (brings service back online) | [deploy.md](deploy.md) |
| `bl deploy scale` | Scale a deployment's capacity | [deploy.md](deploy.md) |
| `bl deploy text create` | Create a text model deployment | [deploy.md](deploy.md) |
| `bl deploy update` | Update a deployment's rate limits (rpm_limit / tpm_limit) | [deploy.md](deploy.md) |
@@ -35,16 +37,18 @@ Use this index for the skill-scoped quick index and global flags.
| `bl finetune image create` | Create an image generation model fine-tune job (sft-lora) | [finetune.md](finetune.md) |
| `bl finetune list` | List fine-tune jobs | [finetune.md](finetune.md) |
| `bl finetune logs` | Fetch training logs for a fine-tune job | [finetune.md](finetune.md) |
| `bl finetune price` | Estimate the training cost for a fine-tune job (token billing) | [finetune.md](finetune.md) |
| `bl finetune text create` | Create a text model fine-tune job (sft \| sft-lora \| dpo \| dpo-lora \| cpt) | [finetune.md](finetune.md) |
| `bl finetune video create` | Create a video generation model fine-tune job (Wan i2v/kf2v, efficient_sft) | [finetune.md](finetune.md) |
| `bl finetune watch` | Probe a fine-tune job's status (default: single non-blocking fetch). Pass --follow to poll until terminal. | [finetune.md](finetune.md) |
## By group
| Group | Commands | Reference |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| `dataset` | `delete`, `get`, `list`, `upload`, `validate` | [dataset.md](dataset.md) |
| `deploy` | `audio create`, `delete`, `get`, `image create`, `list`, `models`, `scale`, `text create`, `update` | [deploy.md](deploy.md) |
| `finetune` | `audio create`, `cancel`, `capability`, `checkpoints`, `delete`, `export`, `get`, `image create`, `list`, `logs`, `text create`, `watch` | [finetune.md](finetune.md) |
| Group | Commands | Reference |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| `dataset` | `delete`, `get`, `list`, `upload`, `validate` | [dataset.md](dataset.md) |
| `deploy` | `audio create`, `delete`, `get`, `image create`, `list`, `models`, `pause`, `resume`, `scale`, `text create`, `update` | [deploy.md](deploy.md) |
| `finetune` | `audio create`, `cancel`, `capability`, `checkpoints`, `delete`, `export`, `get`, `image create`, `list`, `logs`, `price`, `text create`, `video create`, `watch` | [finetune.md](finetune.md) |
## Global flags
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-gen
metadata:
version: "1.14.1"
version: "1.14.2"
requires:
bins: ["bl"]
description: >-
+1
View File
@@ -108,6 +108,7 @@ bl video edit --video https://example.com/input.mp4 --prompt "Put clothes on the
| `--model <model>` | string | no | Model ID (default: happyhorse-1.1-t2v, or happyhorse-1.1-i2v with --image) |
| `--prompt <text>` | string | yes | Video description |
| `--image <url>` | string | no | Input image URL for image-to-video generation |
| `--last-frame <url>` | string | no | Last frame image URL (with --image, enables kf2v first+last frame mode) |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--resolution <res>` | string | no | Resolution: 720P or 1080P (default: 1080P) |
| `--ratio <ratio>` | string | no | Aspect ratio (e.g. 16:9, 9:16, 1:1) |
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-managed-agent
metadata:
version: "1.14.1"
version: "1.14.2"
requires:
bins: ["bl"]
description: >-
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-protocol
metadata:
version: "1.14.1"
version: "1.14.2"
requires:
bins: ["bl"]
description: >-