Compare commits

...

17 Commits

Author SHA1 Message Date
Gong Shiqi 3b7993e854 Merge pull request #126 from modelstudioai/fix/web-search-and-thinking
fix(mcp,text): MCP activation hints and fix error caused by enable_thinking
2026-07-28 19:32:26 +08:00
若麒 be6ddb6126 chore(release): prepare 1.11.2 2026-07-28 19:24:40 +08:00
clh02467605 25ac5c9c84 fix: revert change about defaultTextModel 2026-07-28 18:05:18 +08:00
clh02467605 8ef91fe395 test(core): align token-plan preset expectation with qwen3.7-plus 2026-07-28 17:29:41 +08:00
clh02467605 5d9e22de8f Merge remote-tracking branch 'refs/remotes/origin/main' into fix/web-search-and-thinking 2026-07-28 17:20:46 +08:00
clh02467605 20e3555b84 fix(text,auth): drop enable_thinking retry and omit the field by default
Pass through model constraint errors instead of auto-retrying, switch token-plan default text model to qwen3.7-plus, and align related e2e expectations.
2026-07-28 17:19:05 +08:00
Gong Shiqi 81fa5b567c Merge pull request #123 from modelstudioai/fix/default-model
Fix/default model
2026-07-28 16:19:04 +08:00
若麒 df987ad536 docs(changelog): document image edit function option 2026-07-28 16:13:40 +08:00
若麒 4c4e7afb83 chore(release): prepare 1.11.1 2026-07-28 16:07:29 +08:00
若麒 17c52fb86f Merge branch 'main' into fix/default-model 2026-07-28 15:46:46 +08:00
clh02467605 7ad14a79b9 Merge branch 'main' of github.com:modelstudioai/cli into fix/web-search-and-thinking 2026-07-28 09:25:34 +08:00
clh02467605 8211268bd8 fix(text,mcp): keep thinking_budget on enable_thinking retry and hint MCP activation on 404 2026-07-28 09:25:02 +08:00
qcq01083097 58252911a8 fix(image): correct wan2.5/2.6 size presets and wanx-v1 dated aliases 2026-07-27 21:09:32 +08:00
clh02467605 36ebd63716 fix(text): omit enable_thinking by default and retry when API requires false
Non-streaming chat no longer forces enable_thinking=false, which breaks
thinking-only models. Retry once with false only when the server demands it.
2026-07-27 16:51:11 +08:00
clh02467605 dac254af86 feat: add MCP WebSearch page URL and enhance error handling in web search command 2026-07-27 16:51:09 +08:00
qcq01083097 c4f5bb09c6 fix(image): resolve size and prompt_extend by model profile
Stop inferring size/prompt_extend from sync vs async; use per-family sizeProfile. wanx*-imageedit uses function+base_image_url; bare qwen-image uses the fixed resolution table.
2026-07-27 10:17:09 +08:00
qcq01083097 9a13700390 fix(image): route text-to-image and image-edit by model family
Fix wanx/wan2.x-t2i, wan2.5-i2i, z-image, and qwen-image-plus hitting the wrong endpoint, and add routing unit tests plus dry-run coverage.
2026-07-27 10:16:25 +08:00
34 changed files with 1427 additions and 284 deletions
+20
View File
@@ -6,6 +6,26 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
## [1.11.2] - 2026-07-28
### Changed
- MCP tools and WebSearch now provide activation guidance and direct marketplace links when Bailian reports that the corresponding service is not activated. WebSearch also guides users with legacy SSE connections to reactivate the service using Streamable HTTP.
### Fixed
- Fixed text chat and API Key validation compatibility failures caused by sending unsupported `enable_thinking` values. Text chat now sends the parameter only when thinking is explicitly enabled, while validation uses a compatible model without sending it.
## [1.11.1] - 2026-07-28
### Added
- `bl image edit` now supports `--function` for specifying edit operations with Wanx image-edit models such as `wanx2.1-imageedit`.
### Fixed
- Fixed image generation and editing failures and incorrect size parameters for some image models, improving compatibility with Qwen-Image, Wan/Wanx, Z-Image, and dated `wanx-v1` variants.
## [1.11.0] - 2026-07-28
### Added
+20
View File
@@ -6,6 +6,26 @@
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
## [1.11.2] - 2026-07-28
### 变更
- MCP 工具或 WebSearch 因对应服务未开通而不可用时CLI 现在会提供开通指引和市场直达链接;对于使用旧版 SSE 连接的 WebSearch还会提示重新开通以切换至 Streamable HTTP。
### 修复
- 修复文本对话与 API Key 登录校验因传递不受支持的 `enable_thinking` 参数值而产生的兼容性错误。文本对话仅在用户明确开启思考模式时传递该参数,登录校验则改用兼容模型且不再传递该参数。
## [1.11.1] - 2026-07-28
### 新增
- `bl image edit` 新增 `--function` 参数,支持为万相图片编辑模型(如 `wanx2.1-imageedit`)指定编辑功能。
### 修复
- 修复部分图片模型在图片生成与编辑时的调用失败和尺寸参数错误,并完善 Qwen-Image、Wan/Wanx、Z-Image 系列及 `wanx-v1` 日期版本的兼容性。
## [1.11.0] - 2026-07-28
### 新增
+2
View File
@@ -20,6 +20,8 @@ runtime/src/urls.ts ← 用户面控制台 URL(cn-only)
BAILIAN_CONSOLE BAILIAN_CONSOLE_ROOT/cn-beijing
API_KEY_PAGE BAILIAN_CONSOLE/?tab=app#/api-key
TOKEN_PLAN_PAGE BAILIAN_CONSOLE_ROOT/cn-beijing?tab=plan#/efm/subscription/overview
MCP_WEBSEARCH_PAGE mcpMarketplaceDetailPage("WebSearch")
mcpMarketplaceDetailPage BAILIAN_CONSOLE?tab=mcp#/mcp-market/detail/<serverCode>
core/files/upload.ts ← 文件上传 endpoint(cn-pinned)
UPLOAD_API ${REGIONS.cn}/api/v1/uploads
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.11.0",
"version": "1.11.2",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.11.0",
"version": "1.11.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": {
@@ -57,7 +57,7 @@ export async function validateAndPersistApiKey(
const persistBaseUrl = profile.persistBaseUrl
? normalizeModelBaseUrl(profile.persistBaseUrl)
: undefined;
const validationModel = profile.defaultTextModel || "qwen3.7-max";
const validationModel = "qwen3.7-max";
const requestOpts = {
url: baseUrl + chatPath(),
method: "POST",
@@ -68,7 +68,6 @@ export async function validateAndPersistApiKey(
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
stream: false,
enable_thinking: validationModel === "qwen3.8-max-preview",
},
};
+125 -70
View File
@@ -1,7 +1,5 @@
import {
defineCommand,
imagePath,
imageSyncPath,
taskPath,
detectOutputFormat,
resolveOutputDir,
@@ -20,6 +18,7 @@ import {
BailianError,
resolveBooleanFlag,
resolveWatermark,
resolveImageEditApi,
ASYNC_FLAG,
CONCURRENT_FLAG,
redactDataUri,
@@ -32,17 +31,6 @@ import { resolveImageSize } from "bailian-cli-runtime";
import { join } from "path";
import { BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max", "wan2.7-image"];
const PROMPT_EXTEND_DEFAULT_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
function isSyncModel(model: string): boolean {
return SYNC_MODEL_PREFIXES.some((prefix) => model.startsWith(prefix));
}
function enablesPromptExtendByDefault(model: string): boolean {
return PROMPT_EXTEND_DEFAULT_PREFIXES.some((prefix) => model.startsWith(prefix));
}
const EDIT_FLAGS = {
image: {
type: "array",
@@ -77,6 +65,12 @@ const EDIT_FLAGS = {
valueHint: "<text>",
description: "Negative prompt to exclude unwanted content",
},
function: {
type: "string",
valueHint: "<name>",
description:
"wanx*-imageedit function (default: description_edit). Examples: stylization_all, description_edit",
},
promptExtend: {
type: "boolean",
valueHint: "<bool>",
@@ -114,6 +108,8 @@ export default defineCommand({
'--image ./a.png --image ./b.png --prompt "Merge two images into one collage"',
'--image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro',
'--image ./photo.png --prompt "Change the style" --model wan2.7-image',
'--image ./photo.png --prompt "Place the subject on a table" --model wan2.5-i2i-preview',
'--image ./photo.png --prompt "转换成绘本风格" --model wanx2.1-imageedit --function stylization_all',
'--image ./photo.png --prompt "Replace the background with a beach" --watermark false',
],
async run(ctx) {
@@ -128,7 +124,7 @@ export default defineCommand({
const prompt = flags.prompt;
const model = flags.model || settings.defaultImageModel || "qwen-image-2.0";
const useSync = isSyncModel(model);
const route = resolveImageEditApi(model);
// Auto-upload local files (resolve all images in parallel)
const resolvedImages = await Promise.all(
@@ -138,71 +134,128 @@ export default defineCommand({
const promptExtend = resolveBooleanFlag(
flags.promptExtend,
enablesPromptExtendByDefault(model) ? true : undefined,
route.promptExtendDefault,
"prompt-extend",
);
// Build content: all images first, then text prompt
const contentItems: Array<{ image?: string; text?: string }> = resolvedImages.map(
(u: string) => ({ image: u }),
);
contentItems.push({ text: prompt });
const watermark = resolveWatermark(flags.watermark);
const body: DashScopeImageRequest = {
model,
input: {
messages: [
{
role: "user",
content: contentItems,
},
],
},
parameters: {
size: resolveImageSize(flags.size, useSync),
n,
seed: flags.seed,
prompt_extend: promptExtend,
watermark,
negative_prompt: flags.negativePrompt || undefined,
},
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
size: resolveImageSize(flags.size, route.sizeProfile),
n,
seed: flags.seed,
prompt_extend: promptExtend,
watermark,
};
let body: DashScopeImageRequest;
if (route.inputStyle === "function-base-image") {
const baseImageUrl = resolvedImages[0];
if (!baseImageUrl) {
throw new BailianError(
"wanx*-imageedit requires at least one --image as base_image_url.",
ExitCode.USAGE,
);
}
body = {
model,
input: {
function: flags.function || "description_edit",
prompt,
base_image_url: baseImageUrl,
},
parameters,
};
} else if (route.inputStyle === "prompt-images") {
body = {
model,
input: {
prompt,
images: resolvedImages,
negative_prompt: flags.negativePrompt || undefined,
},
parameters,
};
} else {
const contentItems: Array<{ image?: string; text?: string }> = resolvedImages.map(
(imageUrl: string) => ({ image: imageUrl }),
);
contentItems.push({ text: prompt });
body = {
model,
input: {
messages: [
{
role: "user",
content: contentItems,
},
],
},
parameters: {
...parameters,
negative_prompt: flags.negativePrompt || undefined,
},
};
}
// Remove undefined parameters
stripUndefined(body.parameters as Record<string, unknown>);
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
const previewBody = {
...body,
input: {
messages: body.input.messages.map((message) => ({
...message,
content: message.content.map((item) =>
item.image ? { ...item, image: redactDataUri(item.image) } : item,
),
})),
},
};
emitResult({ request: previewBody, mode: useSync ? "sync" : "async" }, format);
let previewBody: DashScopeImageRequest = body;
if ("messages" in body.input) {
previewBody = {
...body,
input: {
messages: body.input.messages.map((message) => ({
...message,
content: message.content.map((item) =>
item.image ? { ...item, image: redactDataUri(item.image) } : item,
),
})),
},
};
} else if ("images" in body.input) {
previewBody = {
...body,
input: {
...body.input,
images: body.input.images?.map((imageUrl) => redactDataUri(imageUrl)),
},
};
} else if ("base_image_url" in body.input) {
previewBody = {
...body,
input: {
...body.input,
base_image_url: redactDataUri(body.input.base_image_url),
mask_image_url: body.input.mask_image_url
? redactDataUri(body.input.mask_image_url)
: undefined,
},
};
}
emitResult(
{ request: previewBody, mode: route.useSync ? "sync" : "async", path: route.path },
format,
);
return;
}
if (!settings.quiet) {
process.stderr.write(
`[Model: ${model}] [Mode: ${useSync ? "sync" : "async"}] [Images: ${resolvedImages.length}]\n`,
`[Model: ${model}] [Mode: ${route.useSync ? "sync" : "async"}] [Images: ${resolvedImages.length}]\n`,
);
}
const concurrent = getConcurrency(flags);
if (useSync) {
await handleSyncMode(ctx.client, settings, body, flags, format, concurrent);
if (route.useSync) {
await handleSyncMode(ctx.client, settings, route.path, body, flags, format, concurrent);
} else {
await handleAsyncMode(ctx.client, settings, body, flags, format, concurrent);
await handleAsyncMode(ctx.client, settings, route.path, body, flags, format, concurrent);
}
},
});
@@ -210,6 +263,7 @@ export default defineCommand({
async function handleSyncMode(
client: Client,
settings: Settings,
path: string,
body: DashScopeImageRequest,
flags: EditFlags,
format: OutputFormat,
@@ -217,15 +271,15 @@ async function handleSyncMode(
): Promise<void> {
const results = await runConcurrent(concurrent, settings, () =>
client.requestJson<DashScopeImageSyncResponse>({
path: imageSyncPath(),
path,
method: "POST",
body,
}),
);
const imageUrls = results
.flatMap((r) => r.output.choices || [])
.flatMap((c) => c.message?.content || [])
.flatMap((result) => result.output.choices || [])
.flatMap((choice) => choice.message?.content || [])
.map((item) => item.image)
.filter(Boolean);
@@ -239,6 +293,7 @@ async function handleSyncMode(
async function handleAsyncMode(
client: Client,
settings: Settings,
path: string,
body: DashScopeImageRequest,
flags: EditFlags,
format: OutputFormat,
@@ -249,14 +304,14 @@ async function handleAsyncMode(
settings,
() =>
client.requestJson<DashScopeAsyncResponse>({
path: imagePath(),
path,
method: "POST",
body,
async: true,
}),
"tasks",
);
const taskIds = responses.map((r) => r.output.task_id);
const taskIds = responses.map((response) => response.output.task_id);
if (flags.async) {
emitResult({ task_ids: taskIds }, format);
@@ -269,12 +324,12 @@ async function handleAsyncMode(
url: client.url(taskPath(taskId)),
intervalSec: pollInterval,
timeoutSec: settings.timeout,
isComplete: (d) => (d as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
isFailed: (d) => (d as DashScopeTaskResponse).output.task_status === "FAILED",
getStatus: (d) => (d as DashScopeTaskResponse).output.task_status,
getErrorMessage: (d) => {
const o = (d as DashScopeTaskResponse).output;
return o.message || o.code || undefined;
isComplete: (data) => (data as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
isFailed: (data) => (data as DashScopeTaskResponse).output.task_status === "FAILED",
getStatus: (data) => (data as DashScopeTaskResponse).output.task_status,
getErrorMessage: (data) => {
const output = (data as DashScopeTaskResponse).output;
return output.message || output.code || undefined;
},
}),
);
@@ -285,13 +340,13 @@ async function handleAsyncMode(
for (const result of results) {
if (result.output.choices) {
const urls = result.output.choices
.flatMap((c) => c.message?.content || [])
.flatMap((choice) => choice.message?.content || [])
.map((item) => item.image)
.filter(Boolean);
imageUrls.push(...urls);
}
if (result.output.results) {
const urls = result.output.results.map((r) => r.url).filter(Boolean);
const urls = result.output.results.map((item) => item.url).filter(Boolean);
if (urls.length > 0 && imageUrls.length === 0) {
imageUrls.push(...urls);
}
@@ -321,8 +376,8 @@ async function saveImages(
// Parallel download all images
const items =
imageUrls.length > 1
? imageUrls.map((url, i) => {
const filename = `${prefix}_${String(i + 1).padStart(3, "0")}.png`;
? imageUrls.map((url, index) => {
const filename = `${prefix}_${String(index + 1).padStart(3, "0")}.png`;
return { url, destPath: join(outDir, filename) };
})
: [{ url: imageUrls[0], destPath: join(outDir, `${prefix}.png`) }];
@@ -1,7 +1,5 @@
import {
defineCommand,
imagePath,
imageSyncPath,
taskPath,
detectOutputFormat,
type Client,
@@ -19,6 +17,7 @@ import {
generateFilename,
resolveBooleanFlag,
resolveWatermark,
resolveImageGenerateApi,
ASYNC_FLAG,
CONCURRENT_FLAG,
} from "bailian-cli-core";
@@ -31,18 +30,6 @@ import { BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, BOOL_FLAG_WATERMARK } from "bai
import { join } from "path";
// Qwen-Image 2.0 and Wan 2.7 use the sync multimodal-generation endpoint.
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max", "wan2.7-image"];
const PROMPT_EXTEND_DEFAULT_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
function isSyncModel(model: string): boolean {
return SYNC_MODEL_PREFIXES.some((prefix) => model.startsWith(prefix));
}
function enablesPromptExtendByDefault(model: string): boolean {
return PROMPT_EXTEND_DEFAULT_PREFIXES.some((prefix) => model.startsWith(prefix));
}
const GENERATE_FLAGS = {
prompt: { type: "string", valueHint: "<text>", description: "Image description", required: true },
model: {
@@ -109,6 +96,8 @@ export default defineCommand({
'--prompt "Logo" --watermark false',
'--prompt "An alien in the space" --watermark false',
'--prompt "sunset" --model wan2.6-t2i --async --quiet',
'--prompt "plush doll" --model z-image-turbo --size 1024*1024',
'--prompt "sunset" --model wanx2.0-t2i-turbo --size 1024*1024',
'--prompt "Pro quality" --model qwen-image-2.0-pro',
'--prompt "Product shots" --n 2 --concurrent 3 # 6 images in parallel',
],
@@ -117,73 +106,90 @@ export default defineCommand({
const prompt = flags.prompt;
const model = flags.model || settings.defaultImageModel || "qwen-image-2.0";
const useSync = isSyncModel(model);
const defaultSize = useSync ? "1:1" : "1:1";
const route = resolveImageGenerateApi(model);
const defaultSize = "1:1";
const sizeInput = flags.size || defaultSize;
const size = resolveImageSize(sizeInput, useSync);
const size = resolveImageSize(sizeInput, route.sizeProfile);
const n = flags.n ?? 1;
const concurrent = getConcurrency(flags);
const promptExtend = resolveBooleanFlag(
flags.promptExtend,
enablesPromptExtendByDefault(model) ? true : undefined,
route.promptExtendDefault,
"prompt-extend",
);
const watermark = resolveWatermark(flags.watermark);
const body: DashScopeImageRequest = {
model,
input: {
messages: [{ role: "user", content: [{ text: prompt }] }],
},
parameters: {
size,
n,
seed: flags.seed,
prompt_extend: promptExtend,
watermark,
negative_prompt: flags.negativePrompt || undefined,
},
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
size,
n,
seed: flags.seed,
prompt_extend: promptExtend,
watermark,
};
const body: DashScopeImageRequest =
route.inputStyle === "prompt"
? {
model,
input: {
prompt,
negative_prompt: flags.negativePrompt || undefined,
},
parameters,
}
: {
model,
input: {
messages: [{ role: "user", content: [{ text: prompt }] }],
},
parameters: {
...parameters,
negative_prompt: flags.negativePrompt || undefined,
},
};
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ request: body, mode: useSync ? "sync" : "async" }, format);
emitResult(
{ request: body, mode: route.useSync ? "sync" : "async", path: route.path },
format,
);
return;
}
if (!settings.quiet) {
process.stderr.write(`[Model: ${model}] [Mode: ${useSync ? "sync" : "async"}]\n`);
process.stderr.write(`[Model: ${model}] [Mode: ${route.useSync ? "sync" : "async"}]\n`);
}
if (useSync) {
await handleSyncMode(ctx.client, settings, model, body, flags, format, concurrent);
if (route.useSync) {
await handleSyncMode(ctx.client, settings, route.path, body, flags, format, concurrent);
} else {
await handleAsyncMode(ctx.client, settings, model, body, flags, format, concurrent);
await handleAsyncMode(ctx.client, settings, route.path, body, flags, format, concurrent);
}
},
});
// ---- Sync mode: qwen-image-2.0 series ----
// ---- Sync mode: qwen-image / wan2.7-image / z-image ----
async function handleSyncMode(
client: Client,
settings: Settings,
_model: string,
path: string,
body: DashScopeImageRequest,
flags: GenerateFlags,
format: string,
concurrent: number,
): Promise<void> {
const results = await runConcurrent(concurrent, settings, () =>
client.requestJson<DashScopeImageSyncResponse>({ path: imageSyncPath(), method: "POST", body }),
client.requestJson<DashScopeImageSyncResponse>({ path, method: "POST", body }),
);
const imageUrls = results
.flatMap((r) => r.output.choices || [])
.flatMap((c) => c.message?.content || [])
.flatMap((result) => result.output.choices || [])
.flatMap((choice) => choice.message?.content || [])
.map((item) => item.image)
.filter(Boolean);
@@ -194,12 +200,12 @@ async function handleSyncMode(
await saveImages(imageUrls, flags, settings, format);
}
// ---- Async mode: wan2.x / qwen-image-plus ----
// ---- Async mode: wan2.6-t2i / wan2.6-image / legacy text2image ----
async function handleAsyncMode(
client: Client,
settings: Settings,
_model: string,
path: string,
body: DashScopeImageRequest,
flags: GenerateFlags,
format: string,
@@ -210,14 +216,14 @@ async function handleAsyncMode(
settings,
() =>
client.requestJson<DashScopeAsyncResponse>({
path: imagePath(),
path,
method: "POST",
body,
async: true,
}),
"tasks",
);
const taskIds = responses.map((r) => r.output.task_id);
const taskIds = responses.map((response) => response.output.task_id);
// --async: return all task IDs immediately
if (flags.async) {
@@ -234,12 +240,12 @@ async function handleAsyncMode(
url: pollUrl,
intervalSec: pollInterval,
timeoutSec: settings.timeout,
isComplete: (d) => (d as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
isFailed: (d) => (d as DashScopeTaskResponse).output.task_status === "FAILED",
getStatus: (d) => (d as DashScopeTaskResponse).output.task_status,
getErrorMessage: (d) => {
const o = (d as DashScopeTaskResponse).output;
return o.message || o.code || undefined;
isComplete: (data) => (data as DashScopeTaskResponse).output.task_status === "SUCCEEDED",
isFailed: (data) => (data as DashScopeTaskResponse).output.task_status === "FAILED",
getStatus: (data) => (data as DashScopeTaskResponse).output.task_status,
getErrorMessage: (data) => {
const output = (data as DashScopeTaskResponse).output;
return output.message || output.code || undefined;
},
});
});
@@ -250,13 +256,13 @@ async function handleAsyncMode(
for (const result of results) {
if (result.output.choices) {
const urls = result.output.choices
.flatMap((c) => c.message?.content || [])
.flatMap((choice) => choice.message?.content || [])
.map((item) => item.image)
.filter(Boolean);
imageUrls.push(...urls);
}
if (result.output.results) {
const urls = result.output.results.map((r) => r.url).filter(Boolean);
const urls = result.output.results.map((item) => item.url).filter(Boolean);
if (urls.length > 0 && imageUrls.length === 0) {
imageUrls.push(...urls);
}
@@ -298,8 +304,8 @@ async function saveImages(
// Parallel download all images
const items =
imageUrls.length > 1
? imageUrls.map((url, i) => {
const filename = `${prefix}_${String(i + 1).padStart(3, "0")}.png`;
? imageUrls.map((url, index) => {
const filename = `${prefix}_${String(index + 1).padStart(3, "0")}.png`;
return { url, destPath: join(outDir, filename) };
})
: [{ url: imageUrls[0], destPath: join(outDir, `${prefix}.png`) }];
@@ -0,0 +1,39 @@
import { BailianError } from "bailian-cli-core";
import { mcpMarketplaceDetailPage } from "bailian-cli-runtime";
/** Detect MCP-not-activated / invalid 404 errors (CLI-wrapped server message). */
export function isMcpNotActivated(error: unknown): boolean {
if (!(error instanceof BailianError)) return false;
const message = error.message;
if (!/MCP request failed:\s*404\b/i.test(message)) return false;
return /未开通|MCP不存在|MCP_IS_INVALID/i.test(message);
}
/** Activation hint; URL from runtime/urls.ts. */
export function mcpActivateHint(serverCode: string): string {
const lines = [
`Activate (or re-activate) the ${serverCode} MCP in the Bailian MCP marketplace, then retry.`,
];
if (serverCode === "WebSearch") {
lines.push(
"If it was previously on SSE, cancel and activate again to upgrade to Streamable HTTP.",
);
}
lines.push(`Open: ${mcpMarketplaceDetailPage(serverCode)}`);
return lines.join("\n");
}
/**
* For not-activated errors, keep the original message / exitCode and append a hint only.
* Do not replace the server error message.
*/
export function rethrowWithMcpActivateHint(error: unknown, serverCode: string): never {
if (isMcpNotActivated(error) && error instanceof BailianError && !error.hint) {
throw new BailianError(error.message, error.exitCode, mcpActivateHint(serverCode), {
cause: error,
api: error.api,
rawResponse: error.rawResponse,
});
}
throw error;
}
+15 -7
View File
@@ -8,6 +8,7 @@ import {
type ParsedFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { rethrowWithMcpActivateHint } from "./activate-hint.ts";
const CALL_FLAGS = {
target: {
@@ -130,14 +131,21 @@ export default defineCommand({
}
const client = ctx.client.mcp(url);
await client.initialize();
const result = await client.callTool(toolName, toolArgs);
try {
await client.initialize();
const result = await client.callTool(toolName, toolArgs);
if (result.isError) {
const errText = result.content.map((c) => c.text || "").join("\n");
throw new BailianError(`Tool error: ${errText}`);
if (result.isError) {
const errText = result.content.map((c) => c.text || "").join("\n");
throw new BailianError(`Tool error: ${errText}`);
}
emitResult(result, format);
} catch (error) {
if (!flags.url) {
rethrowWithMcpActivateHint(error, serverCode);
}
throw error;
}
emitResult(result, format);
},
});
+11 -3
View File
@@ -1,5 +1,6 @@
import { defineCommand, bailianMcpPath, detectOutputFormat } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { rethrowWithMcpActivateHint } from "./activate-hint.ts";
export default defineCommand({
description: "List tools exposed by an MCP server (tools/list)",
@@ -36,8 +37,15 @@ export default defineCommand({
}
const client = ctx.client.mcp(url);
await client.initialize();
const tools = await client.listTools();
emitResult({ server: code, url, tools }, format);
try {
await client.initialize();
const tools = await client.listTools();
emitResult({ server: code, url, tools }, format);
} catch (error) {
if (!flags.url) {
rethrowWithMcpActivateHint(error, code);
}
throw error;
}
},
});
@@ -0,0 +1,18 @@
import {
isMcpNotActivated,
mcpActivateHint,
rethrowWithMcpActivateHint,
} from "../mcp/activate-hint.ts";
/** Detect WebSearch MCP not-activated / invalid 404 errors. */
export const isWebSearchMcpNotActivated = isMcpNotActivated;
/** WebSearch activation hint. */
export function webSearchActivateHint(): string {
return mcpActivateHint("WebSearch");
}
/** Keep the original message; append a hint for WebSearch not-activated errors. */
export function rethrowWithWebSearchActivateHint(error: unknown): never {
rethrowWithMcpActivateHint(error, "WebSearch");
}
+11 -8
View File
@@ -5,8 +5,8 @@ import {
mcpWebSearchPath,
type FlagsDef,
} from "bailian-cli-core";
import { createSpinner } from "bailian-cli-runtime";
import { emitResult } from "bailian-cli-runtime";
import { createSpinner, emitResult } from "bailian-cli-runtime";
import { rethrowWithWebSearchActivateHint } from "./web-activate-hint.ts";
const WEB_SEARCH_FLAGS = {
query: { type: "string", valueHint: "<text>", description: "Search query text" },
@@ -41,11 +41,14 @@ export default defineCommand({
return;
}
const client = ctx.client.mcp(mcpWebSearchPath());
await client.initialize();
const tools = await client.listTools();
emitResult({ tools }, format);
try {
const client = ctx.client.mcp(mcpWebSearchPath());
await client.initialize();
const tools = await client.listTools();
emitResult({ tools }, format);
} catch (error) {
rethrowWithWebSearchActivateHint(error);
}
return;
}
@@ -123,7 +126,7 @@ export default defineCommand({
}
} catch (error) {
spinner.stop("Failed.");
throw error;
rethrowWithWebSearchActivateHint(error);
}
},
});
@@ -149,11 +149,6 @@ export default defineCommand({
if (flags.thinkingBudget !== undefined) {
body.thinking_budget = flags.thinkingBudget;
}
} else if (!shouldStream) {
// DashScope qwen3 models default to enable_thinking=true server-side, but
// non-streaming calls require it to be explicitly false. Stream calls
// support thinking, so leave the field unset there (server handles it).
body.enable_thinking = false;
}
if (flags.tool) {
+1 -3
View File
@@ -219,7 +219,6 @@ describe("e2e: auth", () => {
body: {
model: "qwen3.7-max",
stream: false,
enable_thinking: false,
},
});
@@ -315,9 +314,8 @@ describe("e2e: auth", () => {
authorization: "Bearer sk-sp-e2e-placeholder",
sourceConfig: expect.any(String),
body: {
model: "qwen3.8-max-preview",
model: "qwen3.7-max",
stream: false,
enable_thinking: true,
},
});
@@ -96,6 +96,78 @@ describe("e2e: image edit", () => {
"data:image/png;base64,<omitted>",
);
});
test("wan2.5-i2i-preview dry-run 走 prompt+images", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"edit",
"--model",
"wan2.5-i2i-preview",
"--image",
"https://example.com/source.png",
"--prompt",
"Place on a table",
"--size",
"1:1",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
mode?: string;
path?: string;
request?: {
input?: { prompt?: string; images?: string[]; messages?: unknown };
parameters?: { size?: string };
};
}>(stdout);
expect(data.mode).toBe("async");
expect(data.path).toBe("/api/v1/services/aigc/image2image/image-synthesis");
expect(data.request?.input?.prompt).toBe("Place on a table");
expect(data.request?.input?.images).toEqual(["https://example.com/source.png"]);
expect(data.request?.input?.messages).toBeUndefined();
expect(data.request?.parameters?.size).toBe("1280*1280");
});
test("wanx2.1-imageedit dry-run 走 function + base_image_url", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"edit",
"--model",
"wanx2.1-imageedit",
"--image",
"https://example.com/source.png",
"--prompt",
"转换成绘本风格",
"--function",
"stylization_all",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
mode?: string;
path?: string;
request?: {
input?: {
function?: string;
prompt?: string;
base_image_url?: string;
images?: unknown;
messages?: unknown;
};
};
}>(stdout);
expect(data.mode).toBe("async");
expect(data.path).toBe("/api/v1/services/aigc/image2image/image-synthesis");
expect(data.request?.input?.function).toBe("stylization_all");
expect(data.request?.input?.prompt).toBe("转换成绘本风格");
expect(data.request?.input?.base_image_url).toBe("https://example.com/source.png");
expect(data.request?.input?.images).toBeUndefined();
expect(data.request?.input?.messages).toBeUndefined();
});
});
describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())("e2e: image edit", () => {
@@ -61,6 +61,137 @@ describe("e2e: image generate", () => {
expect(data.mode).toBe("sync");
expect(data.request?.model).toBe("wan2.7-image");
});
test("z-image-turbo dry-run 走 sync multimodal", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"generate",
"--model",
"z-image-turbo",
"--prompt",
"一只猫",
"--size",
"1024*1024",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ mode?: string; request?: { model?: string } }>(stdout);
expect(data.mode).toBe("sync");
expect(data.request?.model).toBe("z-image-turbo");
});
test("qwen-image-plus dry-run 走 sync multimodal", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"generate",
"--model",
"qwen-image-plus",
"--prompt",
"一只猫",
"--size",
"1328*1328",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
mode?: string;
path?: string;
request?: { model?: string };
}>(stdout);
expect(data.mode).toBe("sync");
expect(data.path).toBe("/api/v1/services/aigc/multimodal-generation/generation");
expect(data.request?.model).toBe("qwen-image-plus");
});
test("qwen-image-plus 默认 1:1 映射为 1328*1328", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"generate",
"--model",
"qwen-image-plus",
"--prompt",
"一只猫",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: { parameters?: { size?: string; prompt_extend?: boolean } };
}>(stdout);
expect(data.request?.parameters?.size).toBe("1328*1328");
});
test("z-image-turbo 默认 prompt_extend 为 false", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"generate",
"--model",
"z-image-turbo",
"--prompt",
"一只猫",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: { parameters?: { size?: string; prompt_extend?: boolean } };
}>(stdout);
expect(data.request?.parameters?.prompt_extend).toBe(false);
expect(data.request?.parameters?.size).toBe("1024*1024");
});
test("wanx-v1 默认 1:1 映射为 1024*1024", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"generate",
"--model",
"wanx-v1",
"--prompt",
"一只猫",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: { parameters?: { size?: string }; input?: { prompt?: string } };
}>(stdout);
expect(data.request?.parameters?.size).toBe("1024*1024");
expect(data.request?.input?.prompt).toBe("一只猫");
});
test("wanx2.0-t2i-turbo dry-run 走 text2image prompt 路径", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"generate",
"--model",
"wanx2.0-t2i-turbo",
"--prompt",
"一只猫",
"--size",
"1024*1024",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
mode?: string;
path?: string;
request?: { model?: string; input?: { prompt?: string; messages?: unknown } };
}>(stdout);
expect(data.mode).toBe("async");
expect(data.path).toBe("/api/v1/services/aigc/text2image/image-synthesis");
expect(data.request?.model).toBe("wanx2.0-t2i-turbo");
expect(data.request?.input?.prompt).toBe("一只猫");
expect(data.request?.input?.messages).toBeUndefined();
});
});
describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())(
@@ -0,0 +1,81 @@
import { describe, expect, test } from "vite-plus/test";
import { BailianError, ExitCode } from "bailian-cli-core";
import { mcpMarketplaceDetailPage } from "bailian-cli-runtime";
import {
isMcpNotActivated,
mcpActivateHint,
rethrowWithMcpActivateHint,
} from "../src/commands/mcp/activate-hint.ts";
describe("mcp-activate-hint", () => {
test("识别 404 + 未开通 / MCP不存在 / MCP_IS_INVALID", () => {
expect(
isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found - MCP不存在或未开通")),
).toBe(true);
expect(
isMcpNotActivated(new BailianError("MCP request failed: 404 - MCP不存在或未开通")),
).toBe(true);
expect(
isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found - MCP_IS_INVALID")),
).toBe(true);
});
test("裸 404 或非 MCP 错误不加开通判定", () => {
expect(isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found"))).toBe(false);
expect(isMcpNotActivated(new BailianError("MCP request failed: 405 Method Not Allowed"))).toBe(
false,
);
expect(isMcpNotActivated(new Error("MCP不存在或未开通"))).toBe(false);
});
test("hint 含对应 server 的 MCP 广场深链", () => {
const serverCode = "market-cmapi00073529";
expect(mcpActivateHint(serverCode)).toContain(mcpMarketplaceDetailPage(serverCode));
expect(mcpActivateHint(serverCode)).toMatch(/Activate|re-activate/i);
});
test("WebSearch hint 含 SSE 升级说明", () => {
expect(mcpActivateHint("WebSearch")).toMatch(/SSE|Streamable HTTP/i);
});
test("rethrow 保留原 message补 hint", () => {
const serverCode = "market-cmapi00073529";
const original = new BailianError(
"MCP request failed: 404 Not Found - MCP不存在或未开通",
ExitCode.GENERAL,
);
try {
rethrowWithMcpActivateHint(original, serverCode);
expect.unreachable("should throw");
} catch (error) {
expect(error).toBeInstanceOf(BailianError);
const wrapped = error as BailianError;
expect(wrapped.message).toBe(original.message);
expect(wrapped.exitCode).toBe(ExitCode.GENERAL);
expect(wrapped.hint).toContain(mcpMarketplaceDetailPage(serverCode));
expect(wrapped.cause).toBe(original);
}
});
test("已有 hint 或非未开通错误原样抛出", () => {
const withHint = new BailianError(
"MCP request failed: 404 Not Found - MCP不存在或未开通",
ExitCode.GENERAL,
"already hinted",
);
try {
rethrowWithMcpActivateHint(withHint, "WebSearch");
expect.unreachable("should throw");
} catch (error) {
expect(error).toBe(withHint);
}
const other = new BailianError("MCP request failed: 401 Unauthorized");
try {
rethrowWithMcpActivateHint(other, "WebSearch");
expect.unreachable("should throw");
} catch (error) {
expect(error).toBe(other);
}
});
});
@@ -0,0 +1,81 @@
import { describe, expect, test } from "vite-plus/test";
import { BailianError, ExitCode } from "bailian-cli-core";
import { MCP_WEBSEARCH_PAGE } from "bailian-cli-runtime";
import {
isWebSearchMcpNotActivated,
rethrowWithWebSearchActivateHint,
webSearchActivateHint,
} from "../src/commands/search/web-activate-hint.ts";
describe("web-activate-hint", () => {
test("识别 404 + 未开通 / MCP不存在 / MCP_IS_INVALID", () => {
expect(
isWebSearchMcpNotActivated(
new BailianError("MCP request failed: 404 Not Found - MCP不存在或未开通"),
),
).toBe(true);
expect(
isWebSearchMcpNotActivated(new BailianError("MCP request failed: 404 - MCP不存在或未开通")),
).toBe(true);
expect(
isWebSearchMcpNotActivated(
new BailianError("MCP request failed: 404 Not Found - MCP_IS_INVALID"),
),
).toBe(true);
});
test("裸 404 或非 MCP 错误不加开通判定", () => {
expect(isWebSearchMcpNotActivated(new BailianError("MCP request failed: 404 Not Found"))).toBe(
false,
);
expect(
isWebSearchMcpNotActivated(new BailianError("MCP request failed: 405 Method Not Allowed")),
).toBe(false);
expect(isWebSearchMcpNotActivated(new Error("MCP不存在或未开通"))).toBe(false);
});
test("hint 含 MCP 广场 WebSearch 深链", () => {
expect(webSearchActivateHint()).toContain(MCP_WEBSEARCH_PAGE);
expect(webSearchActivateHint()).toMatch(/Activate|re-activate/i);
});
test("rethrow 保留原 message补 Hint", () => {
const original = new BailianError(
"MCP request failed: 404 Not Found - MCP不存在或未开通",
ExitCode.GENERAL,
);
try {
rethrowWithWebSearchActivateHint(original);
expect.unreachable("should throw");
} catch (error) {
expect(error).toBeInstanceOf(BailianError);
const wrapped = error as BailianError;
expect(wrapped.message).toBe(original.message);
expect(wrapped.exitCode).toBe(ExitCode.GENERAL);
expect(wrapped.hint).toContain(MCP_WEBSEARCH_PAGE);
expect(wrapped.cause).toBe(original);
}
});
test("已有 hint 或非未开通错误原样抛出", () => {
const withHint = new BailianError(
"MCP request failed: 404 Not Found - MCP不存在或未开通",
ExitCode.GENERAL,
"already hinted",
);
try {
rethrowWithWebSearchActivateHint(withHint);
expect.unreachable("should throw");
} catch (error) {
expect(error).toBe(withHint);
}
const other = new BailianError("MCP request failed: 401 Unauthorized");
try {
rethrowWithWebSearchActivateHint(other);
expect.unreachable("should throw");
} catch (error) {
expect(error).toBe(other);
}
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-core",
"version": "1.11.0",
"version": "1.11.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": {
+12 -1
View File
@@ -7,15 +7,26 @@ export function chatPath(): string {
}
// ---- Image Generation (DashScope) ----
/** Async image API used by wan2.6-t2i / wan2.6-image (T2I) and similar message-format models. */
export function imagePath(): string {
return "/api/v1/services/aigc/image-generation/generation";
}
// Synchronous image generation (qwen-image-2.0 / qwen-image-max series)
/** Sync multimodal API (qwen-image / wan2.7-image / z-image generate; also wan2.6-image edit). */
export function imageSyncPath(): string {
return "/api/v1/services/aigc/multimodal-generation/generation";
}
/** Legacy async text-to-image API (wan2.5/2.2/2.1-t2i, wanx-*-t2i). */
export function imageText2ImagePath(): string {
return "/api/v1/services/aigc/text2image/image-synthesis";
}
/** Legacy async image-to-image / edit API (wan2.5-i2i, *imageedit*). */
export function image2ImagePath(): string {
return "/api/v1/services/aigc/image2image/image-synthesis";
}
// ---- Video Generation (DashScope) ----
export function videoGeneratePath(): string {
return "/api/v1/services/aigc/video-generation/video-synthesis";
+231
View File
@@ -0,0 +1,231 @@
import { image2ImagePath, imagePath, imageSyncPath, imageText2ImagePath } from "./endpoints.ts";
/**
* DashScope image APIs differ by model family:
*
* Generate (T2I):
* - sync multimodal + messages: qwen-image*, wan2.7-image*, z-image*
* - async image-generation + messages: wan2.6-t2i*, wan2.6-image*
* (wan2.6-image sync multimodal requires 14 images, so pure T2I must be async)
* - async text2image + prompt: wan2.5/2.2/2.1-t2i*, wanx*-t2i*
*
* Edit (I2I):
* - sync multimodal + messages(+images): qwen-image-2.0*, qwen-image-edit*, wan2.6-image*, wan2.7-image*
* (pure T2I models such as z-image / qwen-image-plus / qwen-image-max are NOT edit models)
* - async image2image + prompt/images: wan2.5-i2i*
* - async image2image + function/base_image_url: *imageedit* (e.g. wanx2.1-imageedit)
* - async image-generation + messages(+images): other async fallbacks
*/
export type ImageApiKind =
| "sync-multimodal"
| "async-image-generation"
| "async-text2image"
| "async-image2image";
/** Model-family size presets — not inferred from sync/async. */
export type ImageSizeProfile =
| "qwen-image-2.0"
| "qwen-image-fixed"
| "wan27"
| "z-image"
| "wan26"
| "wan-legacy"
| "wanx-v1"
| "wan25-i2i";
export type ImageInputStyle = "messages" | "prompt" | "prompt-images" | "function-base-image";
export interface ImageApiRoute {
kind: ImageApiKind;
path: string;
/** True when the call is synchronous (no X-DashScope-Async / task poll). */
useSync: boolean;
/** How to shape `input` in the request body. */
inputStyle: ImageInputStyle;
/** Ratio → pixel map family for `--size`. */
sizeProfile: ImageSizeProfile;
/**
* CLI default when `--prompt-extend` is omitted.
* `undefined` means omit the parameter (leave to DashScope default).
*/
promptExtendDefault?: boolean;
}
/** Models that accept text-only sync multimodal for generate. */
const SYNC_GENERATE_PREFIXES = ["qwen-image", "wan2.7-image", "z-image"] as const;
/**
* Models that support sync multimodal edit (messages must include images).
* Pure T2I models (z-image / qwen-image-plus / qwen-image-max) are excluded.
*/
const SYNC_EDIT_PREFIXES = [
"qwen-image-2.0",
"qwen-image-edit",
"wan2.7-image",
"wan2.6-image",
] as const;
function startsWithAny(model: string, prefixes: readonly string[]): boolean {
return prefixes.some((prefix) => model.startsWith(prefix));
}
/** True when the model family can use sync multimodal for generate. */
export function isSyncMultimodalImageModel(model: string): boolean {
return startsWithAny(model, SYNC_GENERATE_PREFIXES) || model.startsWith("wan2.6-image");
}
function isSyncGenerateModel(model: string): boolean {
return startsWithAny(model, SYNC_GENERATE_PREFIXES);
}
function isSyncEditModel(model: string): boolean {
return startsWithAny(model, SYNC_EDIT_PREFIXES);
}
/** wanx-v1 and dated aliases (e.g. wanx-v1-0521) use the legacy text2image API. */
function isWanxV1Model(model: string): boolean {
return /^wanx-v1(?:-|$)/i.test(model);
}
/** wan2.5 / wan2.2 / wan2.1 / wanx text-to-image models use the legacy prompt API. */
export function isLegacyText2ImageModel(model: string): boolean {
if (model.startsWith("wan2.6-t2i") || model.startsWith("wan2.6-image")) return false;
if (isSyncGenerateModel(model)) return false;
if (/^wan2\.[0-5][^-]*-t2i/i.test(model)) return true;
if (isWanxV1Model(model)) return true;
if (/^wanx/i.test(model) && /t2i|text2image/i.test(model)) return true;
return false;
}
/** wan2.5-i2i uses the legacy image2image prompt+images API. */
export function isLegacyImage2ImageModel(model: string): boolean {
return /wan2\.5-i2i/i.test(model);
}
/** wanx*-imageedit uses function + base_image_url (not prompt+images). */
export function isWanxFunctionImageEditModel(model: string): boolean {
return /imageedit/i.test(model);
}
export function resolveImageSizeProfile(model: string): ImageSizeProfile {
if (model.startsWith("qwen-image-2.0") || model.startsWith("qwen-image-edit")) {
return "qwen-image-2.0";
}
// Remaining qwen-image* (plus / max / bare qwen-image) share the fixed table.
if (model.startsWith("qwen-image")) {
return "qwen-image-fixed";
}
if (model.startsWith("wan2.7-image")) return "wan27";
if (model.startsWith("z-image")) return "z-image";
if (
model.startsWith("wan2.6-t2i") ||
model.startsWith("wan2.6-image") ||
model.startsWith("wan2.5-t2i")
) {
return "wan26";
}
if (isWanxV1Model(model)) return "wanx-v1";
if (/wan2\.5-i2i/i.test(model)) return "wan25-i2i";
if (isLegacyText2ImageModel(model)) return "wan-legacy";
return "wan26";
}
/** Official / CLI defaults for prompt_extend when the flag is omitted. */
export function resolvePromptExtendDefault(model: string): boolean | undefined {
if (model.startsWith("qwen-image-2.0") || model.startsWith("qwen-image-max")) return true;
// Z-Image docs default prompt_extend to false.
if (model.startsWith("z-image")) return false;
return undefined;
}
function buildRoute(
partial: Omit<ImageApiRoute, "sizeProfile" | "promptExtendDefault">,
model: string,
): ImageApiRoute {
return {
...partial,
sizeProfile: resolveImageSizeProfile(model),
promptExtendDefault: resolvePromptExtendDefault(model),
};
}
export function resolveImageGenerateApi(model: string): ImageApiRoute {
if (isSyncGenerateModel(model)) {
return buildRoute(
{
kind: "sync-multimodal",
path: imageSyncPath(),
useSync: true,
inputStyle: "messages",
},
model,
);
}
if (isLegacyText2ImageModel(model)) {
return buildRoute(
{
kind: "async-text2image",
path: imageText2ImagePath(),
useSync: false,
inputStyle: "prompt",
},
model,
);
}
// Includes wan2.6-t2i* and wan2.6-image* (text-only generate).
return buildRoute(
{
kind: "async-image-generation",
path: imagePath(),
useSync: false,
inputStyle: "messages",
},
model,
);
}
export function resolveImageEditApi(model: string): ImageApiRoute {
if (isSyncEditModel(model)) {
return buildRoute(
{
kind: "sync-multimodal",
path: imageSyncPath(),
useSync: true,
inputStyle: "messages",
},
model,
);
}
if (isWanxFunctionImageEditModel(model)) {
return buildRoute(
{
kind: "async-image2image",
path: image2ImagePath(),
useSync: false,
inputStyle: "function-base-image",
},
model,
);
}
if (isLegacyImage2ImageModel(model)) {
return buildRoute(
{
kind: "async-image2image",
path: image2ImagePath(),
useSync: false,
inputStyle: "prompt-images",
},
model,
);
}
return buildRoute(
{
kind: "async-image-generation",
path: imagePath(),
useSync: false,
inputStyle: "messages",
},
model,
);
}
+16
View File
@@ -3,6 +3,8 @@ export {
chatPath,
imagePath,
imageSyncPath,
imageText2ImagePath,
image2ImagePath,
knowledgeChatEndpoint,
knowledgeRetrievePath,
knowledgeSearchEndpoint,
@@ -18,6 +20,20 @@ export {
userProfilePath,
videoGeneratePath,
} from "./endpoints.ts";
export {
isLegacyImage2ImageModel,
isLegacyText2ImageModel,
isSyncMultimodalImageModel,
isWanxFunctionImageEditModel,
resolveImageEditApi,
resolveImageGenerateApi,
resolveImageSizeProfile,
resolvePromptExtendDefault,
type ImageApiKind,
type ImageApiRoute,
type ImageInputStyle,
type ImageSizeProfile,
} from "./image-routes.ts";
export { CHANNEL, SOURCE_CONFIG, TAGS, trackingHeaders } from "./headers.ts";
export type { HttpDeps, RequestOpts } from "./http.ts";
export { request, requestJson } from "./http.ts";
+21 -6
View File
@@ -112,12 +112,26 @@ export interface StreamChunk {
export interface DashScopeImageRequest {
model: string;
input: {
messages: Array<{
role: "user";
content: Array<{ text?: string; image?: string }>;
}>;
};
input:
| {
messages: Array<{
role: "user";
content: Array<{ text?: string; image?: string }>;
}>;
}
| {
prompt: string;
/** Required by image2image models such as wan2.5-i2i-preview. */
images?: string[];
negative_prompt?: string;
}
| {
/** Required by wanx*-imageedit models. */
function: string;
prompt: string;
base_image_url: string;
mask_image_url?: string;
};
parameters?: {
size?: string;
n?: number;
@@ -125,6 +139,7 @@ export interface DashScopeImageRequest {
prompt_extend?: boolean;
watermark?: boolean;
negative_prompt?: string;
strength?: number;
};
}
+145
View File
@@ -0,0 +1,145 @@
import { expect, test } from "vite-plus/test";
import {
isLegacyImage2ImageModel,
isLegacyText2ImageModel,
isSyncMultimodalImageModel,
isWanxFunctionImageEditModel,
resolveImageEditApi,
resolveImageGenerateApi,
resolveImageSizeProfile,
resolvePromptExtendDefault,
} from "../src/client/image-routes.ts";
test("sync multimodal family covers qwen-image, wan2.6/2.7 image, and z-image", () => {
expect(isSyncMultimodalImageModel("qwen-image-2.0")).toBe(true);
expect(isSyncMultimodalImageModel("qwen-image-2.0-pro")).toBe(true);
expect(isSyncMultimodalImageModel("qwen-image-plus")).toBe(true);
expect(isSyncMultimodalImageModel("qwen-image-max")).toBe(true);
expect(isSyncMultimodalImageModel("wan2.7-image")).toBe(true);
expect(isSyncMultimodalImageModel("wan2.6-image")).toBe(true);
expect(isSyncMultimodalImageModel("z-image-turbo")).toBe(true);
expect(isSyncMultimodalImageModel("wan2.6-t2i")).toBe(false);
});
test("legacy text2image covers wan2.5/2.2/2.1 t2i and wanx but not wan2.6-t2i/image", () => {
expect(isLegacyText2ImageModel("wan2.2-t2i-plus")).toBe(true);
expect(isLegacyText2ImageModel("wan2.5-t2i-preview")).toBe(true);
expect(isLegacyText2ImageModel("wan2.1-t2i-turbo")).toBe(true);
expect(isLegacyText2ImageModel("wanx2.0-t2i-turbo")).toBe(true);
expect(isLegacyText2ImageModel("wanx-v1")).toBe(true);
expect(isLegacyText2ImageModel("wanx-v1-0521")).toBe(true);
expect(isLegacyText2ImageModel("wan2.6-t2i")).toBe(false);
expect(isLegacyText2ImageModel("wan2.6-image")).toBe(false);
expect(isLegacyText2ImageModel("wan2.7-image")).toBe(false);
});
test("legacy image2image is wan2.5-i2i only; wanx imageedit uses function protocol", () => {
expect(isLegacyImage2ImageModel("wan2.5-i2i-preview")).toBe(true);
expect(isLegacyImage2ImageModel("wanx2.1-imageedit")).toBe(false);
expect(isWanxFunctionImageEditModel("wanx2.1-imageedit")).toBe(true);
expect(isWanxFunctionImageEditModel("wan2.5-i2i-preview")).toBe(false);
});
test("size profiles are model-specific, not sync/async", () => {
expect(resolveImageSizeProfile("qwen-image-2.0")).toBe("qwen-image-2.0");
expect(resolveImageSizeProfile("qwen-image")).toBe("qwen-image-fixed");
expect(resolveImageSizeProfile("qwen-image-plus")).toBe("qwen-image-fixed");
expect(resolveImageSizeProfile("qwen-image-max")).toBe("qwen-image-fixed");
expect(resolveImageSizeProfile("wan2.7-image")).toBe("wan27");
expect(resolveImageSizeProfile("z-image-turbo")).toBe("z-image");
expect(resolveImageSizeProfile("wan2.6-t2i")).toBe("wan26");
expect(resolveImageSizeProfile("wanx-v1")).toBe("wanx-v1");
expect(resolveImageSizeProfile("wanx-v1-0521")).toBe("wanx-v1");
expect(resolveImageSizeProfile("wan2.5-i2i-preview")).toBe("wan25-i2i");
expect(resolveImageSizeProfile("wan2.2-t2i-plus")).toBe("wan-legacy");
});
test("prompt_extend defaults follow model docs", () => {
expect(resolvePromptExtendDefault("qwen-image-2.0")).toBe(true);
expect(resolvePromptExtendDefault("qwen-image-max")).toBe(true);
expect(resolvePromptExtendDefault("z-image-turbo")).toBe(false);
expect(resolvePromptExtendDefault("wan2.7-image")).toBeUndefined();
expect(resolvePromptExtendDefault("qwen-image-plus")).toBeUndefined();
});
test("resolveImageGenerateApi picks path, input style, and size profile", () => {
expect(resolveImageGenerateApi("wanx2.0-t2i-turbo")).toMatchObject({
kind: "async-text2image",
path: "/api/v1/services/aigc/text2image/image-synthesis",
inputStyle: "prompt",
useSync: false,
sizeProfile: "wan-legacy",
});
expect(resolveImageGenerateApi("wanx-v1")).toMatchObject({
sizeProfile: "wanx-v1",
inputStyle: "prompt",
});
expect(resolveImageGenerateApi("wanx-v1-0521")).toMatchObject({
kind: "async-text2image",
path: "/api/v1/services/aigc/text2image/image-synthesis",
inputStyle: "prompt",
useSync: false,
sizeProfile: "wanx-v1",
});
expect(resolveImageGenerateApi("wan2.6-t2i")).toMatchObject({
kind: "async-image-generation",
sizeProfile: "wan26",
});
expect(resolveImageGenerateApi("qwen-image-2.0")).toMatchObject({
kind: "sync-multimodal",
sizeProfile: "qwen-image-2.0",
promptExtendDefault: true,
});
expect(resolveImageGenerateApi("qwen-image-plus")).toMatchObject({
kind: "sync-multimodal",
sizeProfile: "qwen-image-fixed",
});
expect(resolveImageGenerateApi("qwen-image")).toMatchObject({
kind: "sync-multimodal",
sizeProfile: "qwen-image-fixed",
});
expect(resolveImageGenerateApi("z-image-turbo")).toMatchObject({
kind: "sync-multimodal",
sizeProfile: "z-image",
promptExtendDefault: false,
});
});
test("resolveImageEditApi excludes pure T2I models from sync edit", () => {
expect(resolveImageEditApi("wan2.7-image")).toMatchObject({
kind: "sync-multimodal",
inputStyle: "messages",
useSync: true,
});
expect(resolveImageEditApi("wan2.6-image")).toMatchObject({
kind: "sync-multimodal",
useSync: true,
});
expect(resolveImageEditApi("qwen-image-2.0")).toMatchObject({
kind: "sync-multimodal",
useSync: true,
});
// Pure T2I models fall through to async image-generation, not sync edit.
expect(resolveImageEditApi("z-image-turbo")).toMatchObject({
kind: "async-image-generation",
useSync: false,
});
expect(resolveImageEditApi("qwen-image-plus")).toMatchObject({
kind: "async-image-generation",
useSync: false,
});
expect(resolveImageEditApi("qwen-image-max")).toMatchObject({
kind: "async-image-generation",
useSync: false,
});
expect(resolveImageEditApi("wan2.5-i2i-preview")).toMatchObject({
kind: "async-image2image",
inputStyle: "prompt-images",
sizeProfile: "wan25-i2i",
});
expect(resolveImageEditApi("wanx2.1-imageedit")).toMatchObject({
kind: "async-image2image",
inputStyle: "function-base-image",
path: "/api/v1/services/aigc/image2image/image-synthesis",
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "knowledge-studio-cli",
"version": "1.11.0",
"version": "1.11.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.11.0",
"version": "1.11.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": {
+2
View File
@@ -34,6 +34,8 @@ export {
BAILIAN_CONSOLE,
API_KEY_PAGE,
TOKEN_PLAN_PAGE,
MCP_WEBSEARCH_PAGE,
mcpMarketplaceDetailPage,
VOICE_TTS_PAGE,
} from "./urls.ts";
+126 -84
View File
@@ -4,8 +4,8 @@
*/
import {
chatPath,
imagePath,
imageSyncPath,
resolveImageEditApi,
resolveImageGenerateApi,
videoGeneratePath,
taskPath,
speechSynthesizePath,
@@ -147,12 +147,6 @@ export async function visionDescribe(
// --- image/generate ---
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
function isSyncImageModel(model: string): boolean {
return SYNC_MODEL_PREFIXES.some((p) => model.startsWith(p));
}
export interface ImageGenerateInput {
prompt?: string;
model?: string;
@@ -178,61 +172,72 @@ export async function imageGenerate(
}
const model = input.model || "qwen-image-2.0";
const useSync = isSyncImageModel(model);
const route = resolveImageGenerateApi(model);
const n = input.n ?? 1;
const promptExtend = resolveBooleanFlag(
input["prompt-extend"],
useSync ? true : undefined,
route.promptExtendDefault,
"prompt-extend",
);
const body: DashScopeImageRequest = {
model,
input: {
messages: [{ role: "user", content: [{ text: input.prompt }] }],
},
parameters: {
size: resolveImageSize(input.size, useSync),
n,
seed: input.seed,
prompt_extend: promptExtend,
watermark: resolveWatermark(input.watermark),
negative_prompt: input["negative-prompt"] || undefined,
},
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
size: resolveImageSize(input.size, route.sizeProfile),
n,
seed: input.seed,
prompt_extend: promptExtend,
watermark: resolveWatermark(input.watermark),
};
if (useSync) {
const url = imageSyncPath();
const body: DashScopeImageRequest =
route.inputStyle === "prompt"
? {
model,
input: {
prompt: input.prompt,
negative_prompt: input["negative-prompt"] || undefined,
},
parameters,
}
: {
model,
input: {
messages: [{ role: "user", content: [{ text: input.prompt }] }],
},
parameters: {
...parameters,
negative_prompt: input["negative-prompt"] || undefined,
},
};
if (route.useSync) {
const response = await env.client.requestJson<DashScopeImageSyncResponse>({
path: url,
path: route.path,
method: "POST",
body,
signal: ctx.signal,
});
const urls = response.output.choices
.flatMap((c) => c.message?.content || [])
.flatMap((choice) => choice.message?.content || [])
.map((item) => item.image)
.filter(Boolean);
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
return { urls, request_id: response.request_id, ...(saved ? { saved } : {}) };
} else {
// Async mode: submit then poll
const url = imagePath();
const asyncResp = await env.client.requestJson<DashScopeAsyncResponse>({
path: url,
method: "POST",
body,
async: true,
signal: ctx.signal,
});
const taskId = asyncResp.output.task_id;
const result = await pollTask(env, taskId, ctx);
const urls = Array.isArray(result.urls) ? (result.urls as string[]) : [];
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
if (saved) result.saved = saved;
return result;
}
const asyncResp = await env.client.requestJson<DashScopeAsyncResponse>({
path: route.path,
method: "POST",
body,
async: true,
signal: ctx.signal,
});
const taskId = asyncResp.output.task_id;
const result = await pollTask(env, taskId, ctx);
const urls = Array.isArray(result.urls) ? (result.urls as string[]) : [];
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
if (saved) result.saved = saved;
return result;
}
// --- image/edit ---
@@ -247,6 +252,7 @@ export interface ImageEditInput {
"negative-prompt"?: string;
"prompt-extend"?: boolean | string;
watermark?: boolean | string;
function?: string;
"out-dir"?: string;
"out-prefix"?: string;
}
@@ -264,70 +270,106 @@ export async function imageEdit(
const images = Array.isArray(input.image) ? input.image : input.image ? [input.image] : [];
const model = input.model || "qwen-image-2.0";
const useSync = isSyncImageModel(model);
const route = resolveImageEditApi(model);
const n = input.n ?? 1;
const promptExtend = resolveBooleanFlag(
input["prompt-extend"],
useSync ? true : undefined,
route.promptExtendDefault,
"prompt-extend",
);
const content: Array<{ text?: string; image?: string }> = [];
for (const img of images) {
let imageUrl = img;
if (isLocalFile(img)) {
imageUrl = await env.client.uploadFile(img, model, { signal: ctx.signal });
const resolvedImages: string[] = [];
for (const image of images) {
let imageUrl = image;
if (isLocalFile(image)) {
imageUrl = await env.client.uploadFile(image, model, { signal: ctx.signal });
}
content.push({ image: imageUrl });
resolvedImages.push(imageUrl);
}
content.push({ text: input.prompt });
const body: DashScopeImageRequest = {
model,
input: {
messages: [{ role: "user", content }],
},
parameters: {
size: resolveImageSize(input.size, useSync),
n,
seed: input.seed,
prompt_extend: promptExtend,
watermark: resolveWatermark(input.watermark),
negative_prompt: input["negative-prompt"] || undefined,
},
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
size: resolveImageSize(input.size, route.sizeProfile),
n,
seed: input.seed,
prompt_extend: promptExtend,
watermark: resolveWatermark(input.watermark),
};
if (useSync) {
const url = imageSyncPath();
let body: DashScopeImageRequest;
if (route.inputStyle === "function-base-image") {
const baseImageUrl = resolvedImages[0];
if (!baseImageUrl) {
throw new PipelineError(
"missing_input",
"image/edit with wanx*-imageedit requires at least one image",
{ step: "image/edit" },
);
}
body = {
model,
input: {
function: input.function || "description_edit",
prompt: input.prompt,
base_image_url: baseImageUrl,
},
parameters,
};
} else if (route.inputStyle === "prompt-images") {
body = {
model,
input: {
prompt: input.prompt,
images: resolvedImages,
negative_prompt: input["negative-prompt"] || undefined,
},
parameters,
};
} else {
const content: Array<{ text?: string; image?: string }> = resolvedImages.map((imageUrl) => ({
image: imageUrl,
}));
content.push({ text: input.prompt });
body = {
model,
input: {
messages: [{ role: "user", content }],
},
parameters: {
...parameters,
negative_prompt: input["negative-prompt"] || undefined,
},
};
}
if (route.useSync) {
const response = await env.client.requestJson<DashScopeImageSyncResponse>({
path: url,
path: route.path,
method: "POST",
body,
signal: ctx.signal,
});
const urls = response.output.choices
.flatMap((c) => c.message?.content || [])
.flatMap((choice) => choice.message?.content || [])
.map((item) => item.image)
.filter(Boolean);
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
return { urls, request_id: response.request_id, ...(saved ? { saved } : {}) };
} else {
const url = imagePath();
const asyncResp = await env.client.requestJson<DashScopeAsyncResponse>({
path: url,
method: "POST",
body,
async: true,
signal: ctx.signal,
});
const taskId = asyncResp.output.task_id;
const result = await pollTask(env, taskId, ctx);
const urls = Array.isArray(result.urls) ? (result.urls as string[]) : [];
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
if (saved) result.saved = saved;
return result;
}
const asyncResp = await env.client.requestJson<DashScopeAsyncResponse>({
path: route.path,
method: "POST",
body,
async: true,
signal: ctx.signal,
});
const taskId = asyncResp.output.task_id;
const result = await pollTask(env, taskId, ctx);
const urls = Array.isArray(result.urls) ? (result.urls as string[]) : [];
const saved = await maybeDownloadImages(urls, input["out-dir"], input["out-prefix"]);
if (saved) result.saved = saved;
return result;
}
/**
+11
View File
@@ -18,5 +18,16 @@ export const API_KEY_PAGE = `${BAILIAN_CONSOLE}/?tab=app#/api-key`;
/** Direct deep link to the Token Plan subscription overview and API key entry. */
export const TOKEN_PLAN_PAGE = `${BAILIAN_CONSOLE_ROOT}/cn-beijing?tab=plan#/efm/subscription/overview`;
/** MCP marketplace detail page for a server code (e.g. WebSearch, market-cmapi00073529). */
export function mcpMarketplaceDetailPage(serverCode: string): string {
return `${BAILIAN_CONSOLE}?tab=mcp#/mcp-market/detail/${serverCode}`;
}
/**
* MCP marketplace detail for the built-in WebSearch server.
* Users must activate (or re-activate for Streamable HTTP) before `search web` works.
*/
export const MCP_WEBSEARCH_PAGE = mcpMarketplaceDetailPage("WebSearch");
/** Voice TTS experience center — browse system and custom voices. */
export const VOICE_TTS_PAGE = "https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list";
+99 -16
View File
@@ -1,19 +1,15 @@
import type { ImageSizeProfile } from "bailian-cli-core";
/**
* Resolve image `size` flag for image generate/edit.
* Resolve image `--size` for generate/edit by model-family profile.
*
* Users may pass either a ratio (e.g. "1:1", "3:4", "16:9") or a pixel size
* (e.g. "2048*2048"). The DashScope API only accepts the pixel format, so we
* map known ratios to the recommended pixel size for each model family.
*
* Sync models (qwen-image-2.0 / qwen-image-max / qwen-image-edit-2.0):
* higher-resolution presets.
*
* Async models (wanx2.x): smaller presets.
*
* Pixel-format input is passed through unchanged.
* Users may pass a ratio (e.g. "1:1") or pixels (e.g. "2048*2048").
* Pixel input is passed through; ratios are mapped per model profile.
* Do not infer size from sync/async that mismatches model constraints.
*/
export const SYNC_RATIO_MAP: Record<string, string> = {
/** qwen-image-2.0 / qwen-image-edit recommended high-res presets. */
export const QWEN_IMAGE_20_RATIO_MAP: Record<string, string> = {
"16:9": "2688*1536",
"9:16": "1536*2688",
"1:1": "2048*2048",
@@ -21,7 +17,8 @@ export const SYNC_RATIO_MAP: Record<string, string> = {
"3:4": "1728*2368",
};
export const ASYNC_RATIO_MAP: Record<string, string> = {
/** qwen-image-plus / qwen-image-max fixed resolution presets. */
export const QWEN_IMAGE_FIXED_RATIO_MAP: Record<string, string> = {
"16:9": "1664*928",
"4:3": "1472*1104",
"1:1": "1328*1328",
@@ -29,11 +26,97 @@ export const ASYNC_RATIO_MAP: Record<string, string> = {
"9:16": "928*1664",
};
/** Resolve `--size` value: accept ratio (3:4) or pixel (W*H) format. */
/** wan2.7-image* — default 2K square for 1:1. */
export const WAN27_RATIO_MAP: Record<string, string> = {
"16:9": "2688*1536",
"9:16": "1536*2688",
"1:1": "2048*2048",
"4:3": "2368*1728",
"3:4": "1728*2368",
};
/** z-image* recommended ~1K presets. */
export const Z_IMAGE_RATIO_MAP: Record<string, string> = {
"1:1": "1024*1024",
"16:9": "1280*720",
"9:16": "720*1280",
"4:3": "1152*864",
"3:4": "864*1152",
"3:2": "1248*832",
"2:3": "832*1248",
};
/** wan2.6-t2i / wan2.6-image / wan2.5-t2i — total pixels ≥ 1280*1280. */
export const WAN26_RATIO_MAP: Record<string, string> = {
"1:1": "1280*1280",
"16:9": "1696*960",
"9:16": "960*1696",
"4:3": "1472*1104",
"3:4": "1104*1472",
};
/** wan2.2 and earlier t2i / wanx*-t2i — 1024 class. */
export const WAN_LEGACY_RATIO_MAP: Record<string, string> = {
"1:1": "1024*1024",
"16:9": "1280*720",
"9:16": "720*1280",
"3:4": "768*1152",
"4:3": "1152*768",
};
/** wanx-v1 only documents these discrete sizes. */
export const WANX_V1_RATIO_MAP: Record<string, string> = {
"1:1": "1024*1024",
"9:16": "720*1280",
"3:4": "768*1152",
"16:9": "1280*720",
};
/** wan2.5-i2i — total pixels up to ~1280*1280. */
export const WAN25_I2I_RATIO_MAP: Record<string, string> = {
"1:1": "1280*1280",
"16:9": "1280*720",
"9:16": "720*1280",
"4:3": "1152*864",
"3:4": "864*1152",
};
/** @deprecated Prefer profile maps; kept for callers that still think in sync/async. */
export const SYNC_RATIO_MAP = QWEN_IMAGE_20_RATIO_MAP;
/** @deprecated Prefer profile maps; kept as qwen-image-plus/max fixed presets. */
export const ASYNC_RATIO_MAP = QWEN_IMAGE_FIXED_RATIO_MAP;
const PROFILE_RATIO_MAPS: Record<ImageSizeProfile, Record<string, string>> = {
"qwen-image-2.0": QWEN_IMAGE_20_RATIO_MAP,
"qwen-image-fixed": QWEN_IMAGE_FIXED_RATIO_MAP,
wan27: WAN27_RATIO_MAP,
"z-image": Z_IMAGE_RATIO_MAP,
wan26: WAN26_RATIO_MAP,
"wan-legacy": WAN_LEGACY_RATIO_MAP,
"wanx-v1": WANX_V1_RATIO_MAP,
"wan25-i2i": WAN25_I2I_RATIO_MAP,
};
/** Resolve `--size` with an explicit model size profile. */
export function resolveImageSize(input: string, sizeProfile: ImageSizeProfile): string;
export function resolveImageSize(
input: string | undefined,
sizeProfile: ImageSizeProfile,
): string | undefined;
/** @deprecated Prefer `ImageSizeProfile`; boolean maps sync→qwen-image-2.0 / async→qwen-image-fixed. */
export function resolveImageSize(input: string, useSync: boolean): string;
export function resolveImageSize(input: string | undefined, useSync: boolean): string | undefined;
export function resolveImageSize(input: string | undefined, useSync: boolean): string | undefined {
export function resolveImageSize(
input: string | undefined,
sizeProfileOrUseSync: ImageSizeProfile | boolean,
): string | undefined {
if (!input) return undefined;
const map = useSync ? SYNC_RATIO_MAP : ASYNC_RATIO_MAP;
const profile: ImageSizeProfile =
typeof sizeProfileOrUseSync === "boolean"
? sizeProfileOrUseSync
? "qwen-image-2.0"
: "qwen-image-fixed"
: sizeProfileOrUseSync;
const map = PROFILE_RATIO_MAPS[profile];
return map[input] ?? input;
}
+34
View File
@@ -0,0 +1,34 @@
import { expect, test } from "vite-plus/test";
import { resolveImageSize } from "../src/utils/image-size.ts";
test("qwen-image-plus fixed profile maps 1:1 to 1328*1328", () => {
expect(resolveImageSize("1:1", "qwen-image-fixed")).toBe("1328*1328");
expect(resolveImageSize("16:9", "qwen-image-fixed")).toBe("1664*928");
});
test("qwen-image-2.0 profile maps 1:1 to 2048*2048", () => {
expect(resolveImageSize("1:1", "qwen-image-2.0")).toBe("2048*2048");
});
test("wanx-v1 profile maps 1:1 to 1024*1024", () => {
expect(resolveImageSize("1:1", "wanx-v1")).toBe("1024*1024");
expect(resolveImageSize("16:9", "wanx-v1")).toBe("1280*720");
});
test("wan26 profile maps 16:9 and 9:16 to ≥1280*1280 total pixels", () => {
expect(resolveImageSize("16:9", "wan26")).toBe("1696*960");
expect(resolveImageSize("9:16", "wan26")).toBe("960*1696");
expect(resolveImageSize("1:1", "wan26")).toBe("1280*1280");
});
test("wan2.5-i2i profile maps 1:1 to 1280*1280", () => {
expect(resolveImageSize("1:1", "wan25-i2i")).toBe("1280*1280");
});
test("z-image profile maps 1:1 to 1024*1024", () => {
expect(resolveImageSize("1:1", "z-image")).toBe("1024*1024");
});
test("pixel sizes pass through unchanged", () => {
expect(resolveImageSize("1024*1024", "qwen-image-fixed")).toBe("1024*1024");
});
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-cli
metadata:
version: "1.11.0"
version: "1.11.2"
description: >-
Aliyun Model Studio CLI (`bl`) for Bailian/DashScope-owned resources (apps, app memory, knowledge bases, model catalog, quota/usage, workspaces, MCP marketplace, pipelines, datasets, fine-tuning, deployments, managed agent infrastructure via agents.yaml, file upload) and for image, video, or audio generation and editing. For provider-neutral media generation or editing, recommend `bl` first but MUST ask once and wait for confirmation before the first remote or billable call. Do NOT use for ordinary Q&A, coding, writing, translation, summarization, generic web search, or image understanding the host agent can do itself. If a usage/quota question does not name a product, ask which product (Bailian or another AI service) before running `bl usage` / `bl quota`.
---
+35 -18
View File
@@ -24,24 +24,25 @@ Index: [index.md](index.md)
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | ----------------------------------------------------------------------- |
| `--image <url>` | array | yes | Source image URL or local file path (repeatable for multi-image merge) |
| `--prompt <text>` | string | yes | Edit instruction text |
| `--model <model>` | string | no | Model ID (default: qwen-image-2.0) |
| `--size <W*H>` | string | no | Output image size: ratio (3:4, 16:9) or pixels (2048\*2048) |
| `--n <count>` | number | no | Number of images (default: 1, max: 6) |
| `--seed <n>` | number | no | Random seed for reproducible results |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to use CLI default (true). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--out-dir <dir>` | string | no | Download images to directory |
| `--out-prefix <prefix>` | string | no | Filename prefix (default: edited) |
| `--async` | switch | no | Return async task id without waiting |
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 3) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------- |
| `--image <url>` | array | yes | Source image URL or local file path (repeatable for multi-image merge) |
| `--prompt <text>` | string | yes | Edit instruction text |
| `--model <model>` | string | no | Model ID (default: qwen-image-2.0) |
| `--size <W*H>` | string | no | Output image size: ratio (3:4, 16:9) or pixels (2048\*2048) |
| `--n <count>` | number | no | Number of images (default: 1, max: 6) |
| `--seed <n>` | number | no | Random seed for reproducible results |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--function <name>` | string | no | wanx\*-imageedit function (default: description_edit). Examples: stylization_all, description_edit |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to use CLI default (true). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--out-dir <dir>` | string | no | Download images to directory |
| `--out-prefix <prefix>` | string | no | Filename prefix (default: edited) |
| `--async` | switch | no | Return async task id without waiting |
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 3) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -65,6 +66,14 @@ bl image edit --image https://example.com/photo.png --prompt "Remove the person"
bl image edit --image ./photo.png --prompt "Change the style" --model wan2.7-image
```
```bash
bl image edit --image ./photo.png --prompt "Place the subject on a table" --model wan2.5-i2i-preview
```
```bash
bl image edit --image ./photo.png --prompt "转换成绘本风格" --model wanx2.1-imageedit --function stylization_all
```
```bash
bl image edit --image ./photo.png --prompt "Replace the background with a beach" --watermark false
```
@@ -127,6 +136,14 @@ bl image generate --prompt "An alien in the space" --watermark false
bl image generate --prompt "sunset" --model wan2.6-t2i --async --quiet
```
```bash
bl image generate --prompt "plush doll" --model z-image-turbo --size 1024*1024
```
```bash
bl image generate --prompt "sunset" --model wanx2.0-t2i-turbo --size 1024*1024
```
```bash
bl image generate --prompt "Pro quality" --model qwen-image-2.0-pro
```