Skip to content

Commit 440cbfe

Browse files
Merge pull request #114 from modelstudioai/feat/token-plan-default-models
feat: update Token Plan defaults and support local image inputs
2 parents d04012b + 8153900 commit 440cbfe

29 files changed

Lines changed: 711 additions & 82 deletions

packages/commands/src/commands/auth/login-api-key.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ interface ApiKeyLoginProfile {
2020
baseUrl: string;
2121
persistBaseUrl?: string;
2222
defaultTextModel?: string;
23+
defaultVideoModel?: string;
24+
defaultImageToVideoModel?: string;
25+
defaultReferenceToVideoModel?: string;
2326
defaultImageModel?: string;
2427
persistPatch?: AuthPersistPatch;
2528
}
@@ -54,17 +57,18 @@ export async function validateAndPersistApiKey(
5457
const persistBaseUrl = profile.persistBaseUrl
5558
? normalizeModelBaseUrl(profile.persistBaseUrl)
5659
: undefined;
60+
const validationModel = profile.defaultTextModel || "qwen3.7-max";
5761
const requestOpts = {
5862
url: baseUrl + chatPath(),
5963
method: "POST",
6064
headers: { Authorization: `Bearer ${key}` },
6165
timeout: Math.min(deps.settings.timeout, 30),
6266
body: {
63-
model: profile.defaultTextModel || "qwen3.7-max",
67+
model: validationModel,
6468
messages: [{ role: "user", content: "hi" }],
6569
max_tokens: 1,
6670
stream: false,
67-
enable_thinking: false,
71+
enable_thinking: validationModel === "qwen3.8-max-preview",
6872
},
6973
};
7074

@@ -88,6 +92,9 @@ export async function validateAndPersistApiKey(
8892
api_key: key,
8993
base_url: persistBaseUrl,
9094
default_text_model: profile.defaultTextModel,
95+
default_video_model: profile.defaultVideoModel,
96+
default_image_to_video_model: profile.defaultImageToVideoModel,
97+
default_reference_to_video_model: profile.defaultReferenceToVideoModel,
9198
default_image_model: profile.defaultImageModel,
9299
});
93100
}

packages/commands/src/commands/auth/login.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ export default defineCommand({
150150
baseUrl: resolvedBaseUrl,
151151
persistBaseUrl,
152152
defaultTextModel: profilePreset?.defaultTextModel,
153+
defaultVideoModel: profilePreset?.defaultVideoModel,
154+
defaultImageToVideoModel: profilePreset?.defaultImageToVideoModel,
155+
defaultReferenceToVideoModel: profilePreset?.defaultReferenceToVideoModel,
153156
defaultImageModel: profilePreset?.defaultImageModel,
154157
});
155158
},

packages/commands/src/commands/config/shared.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const VALID_KEYS = [
1313
"security_token",
1414
"default_text_model",
1515
"default_video_model",
16+
"default_image_to_video_model",
17+
"default_reference_to_video_model",
1618
"default_image_model",
1719
"default_speech_model",
1820
"default_omni_model",
@@ -41,6 +43,8 @@ export const KEY_ALIASES: Record<string, string> = {
4143
"security-token": "security_token",
4244
"default-text-model": "default_text_model",
4345
"default-video-model": "default_video_model",
46+
"default-image-to-video-model": "default_image_to_video_model",
47+
"default-reference-to-video-model": "default_reference_to_video_model",
4448
"default-image-model": "default_image_model",
4549
"default-speech-model": "default_speech_model",
4650
"default-omni-model": "default_omni_model",

packages/commands/src/commands/image/edit.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
resolveWatermark,
2323
ASYNC_FLAG,
2424
CONCURRENT_FLAG,
25+
redactDataUri,
2526
} from "bailian-cli-core";
2627
import { poll } from "bailian-cli-runtime";
2728
import { downloadFile } from "bailian-cli-runtime";
@@ -31,10 +32,15 @@ import { resolveImageSize } from "bailian-cli-runtime";
3132
import { join } from "path";
3233
import { BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";
3334

34-
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
35+
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max", "wan2.7-image"];
36+
const PROMPT_EXTEND_DEFAULT_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
3537

3638
function isSyncModel(model: string): boolean {
37-
return SYNC_MODEL_PREFIXES.some((p) => model.startsWith(p));
39+
return SYNC_MODEL_PREFIXES.some((prefix) => model.startsWith(prefix));
40+
}
41+
42+
function enablesPromptExtendByDefault(model: string): boolean {
43+
return PROMPT_EXTEND_DEFAULT_PREFIXES.some((prefix) => model.startsWith(prefix));
3844
}
3945

4046
const EDIT_FLAGS = {
@@ -98,7 +104,7 @@ const EDIT_FLAGS = {
98104
type EditFlags = ParsedFlags<typeof EDIT_FLAGS>;
99105

100106
export default defineCommand({
101-
description: "Edit an existing image with text instructions (Qwen-Image)",
107+
description: "Edit an existing image with text instructions (Qwen-Image / Wan 2.7)",
102108
auth: "apiKey",
103109
usageArgs: "--image <url> --prompt <text> [flags]",
104110
flags: EDIT_FLAGS,
@@ -107,6 +113,7 @@ export default defineCommand({
107113
'--image https://example.com/logo.png --prompt "Change color to blue" --n 3',
108114
'--image ./a.png --image ./b.png --prompt "Merge two images into one collage"',
109115
'--image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro',
116+
'--image ./photo.png --prompt "Change the style" --model wan2.7-image',
110117
'--image ./photo.png --prompt "Replace the background with a beach" --watermark false',
111118
],
112119
async run(ctx) {
@@ -125,13 +132,13 @@ export default defineCommand({
125132

126133
// Auto-upload local files (resolve all images in parallel)
127134
const resolvedImages = await Promise.all(
128-
rawImages.map((img) => ctx.client.uploadFile(img, model)),
135+
rawImages.map((image) => ctx.client.resolveImageInput(image, model)),
129136
);
130137
const n = flags.n ?? 1;
131138

132139
const promptExtend = resolveBooleanFlag(
133140
flags.promptExtend,
134-
useSync ? true : undefined,
141+
enablesPromptExtendByDefault(model) ? true : undefined,
135142
"prompt-extend",
136143
);
137144

@@ -169,7 +176,18 @@ export default defineCommand({
169176
const format = detectOutputFormat(settings.output);
170177

171178
if (settings.dryRun) {
172-
emitResult({ request: body, mode: useSync ? "sync" : "async" }, format);
179+
const previewBody = {
180+
...body,
181+
input: {
182+
messages: body.input.messages.map((message) => ({
183+
...message,
184+
content: message.content.map((item) =>
185+
item.image ? { ...item, image: redactDataUri(item.image) } : item,
186+
),
187+
})),
188+
},
189+
};
190+
emitResult({ request: previewBody, mode: useSync ? "sync" : "async" }, format);
173191
return;
174192
}
175193

packages/commands/src/commands/image/generate.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,16 @@ import { BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, BOOL_FLAG_WATERMARK } from "bai
3131

3232
import { join } from "path";
3333

34-
// qwen-image-2.0 series uses the sync multimodal-generation endpoint
35-
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
34+
// Qwen-Image 2.0 and Wan 2.7 use the sync multimodal-generation endpoint.
35+
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max", "wan2.7-image"];
36+
const PROMPT_EXTEND_DEFAULT_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
3637

3738
function isSyncModel(model: string): boolean {
38-
return SYNC_MODEL_PREFIXES.some((p) => model.startsWith(p));
39+
return SYNC_MODEL_PREFIXES.some((prefix) => model.startsWith(prefix));
40+
}
41+
42+
function enablesPromptExtendByDefault(model: string): boolean {
43+
return PROMPT_EXTEND_DEFAULT_PREFIXES.some((prefix) => model.startsWith(prefix));
3944
}
4045

4146
const GENERATE_FLAGS = {
@@ -121,7 +126,7 @@ export default defineCommand({
121126

122127
const promptExtend = resolveBooleanFlag(
123128
flags.promptExtend,
124-
useSync ? true : undefined,
129+
enablesPromptExtendByDefault(model) ? true : undefined,
125130
"prompt-extend",
126131
);
127132

packages/commands/src/commands/video/generate.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
resolveWatermark,
1414
ASYNC_FLAG,
1515
CONCURRENT_FLAG,
16+
redactDataUri,
1617
} from "bailian-cli-core";
1718
import { poll } from "bailian-cli-runtime";
1819
import { downloadFile, formatBytes } from "bailian-cli-runtime";
@@ -103,16 +104,17 @@ export default defineCommand({
103104

104105
const model =
105106
flags.model ||
106-
settings.defaultVideoModel ||
107-
(flags.image ? "happyhorse-1.1-i2v" : "happyhorse-1.1-t2v");
107+
(flags.image
108+
? settings.defaultImageToVideoModel || "happyhorse-1.1-i2v"
109+
: settings.defaultVideoModel || "happyhorse-1.1-t2v");
108110
const format = detectOutputFormat(settings.output);
109111

110112
const imageUrl = flags.image;
111113

112114
// Auto-upload local image file for i2v
113115
let resolvedImageUrl: string | undefined;
114116
if (imageUrl) {
115-
resolvedImageUrl = await ctx.client.uploadFile(imageUrl, model);
117+
resolvedImageUrl = await ctx.client.resolveImageInput(imageUrl, model);
116118
}
117119

118120
const watermark = resolveWatermark(flags.watermark);
@@ -139,7 +141,16 @@ export default defineCommand({
139141
};
140142

141143
if (settings.dryRun) {
142-
emitResult({ request: body }, format);
144+
const previewBody = resolvedImageUrl
145+
? {
146+
...body,
147+
input: {
148+
...body.input,
149+
media: [{ type: "first_frame" as const, url: redactDataUri(resolvedImageUrl) }],
150+
},
151+
}
152+
: body;
153+
emitResult({ request: previewBody }, format);
143154
return;
144155
}
145156

packages/commands/src/commands/video/ref.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
resolveWatermark,
1414
ASYNC_FLAG,
1515
CONCURRENT_FLAG,
16+
redactDataUri,
1617
} from "bailian-cli-core";
1718
import { poll } from "bailian-cli-runtime";
1819
import { downloadFile, formatBytes } from "bailian-cli-runtime";
@@ -117,40 +118,40 @@ export default defineCommand({
117118
const imageVoices = flags.imageVoice || [];
118119
const videoVoices = flags.videoVoice || [];
119120

120-
const model = flags.model || "happyhorse-1.1-r2v";
121+
const model = flags.model || settings.defaultReferenceToVideoModel || "happyhorse-1.1-r2v";
121122
const format = detectOutputFormat(settings.output);
122123

123124
// --- Resolve file URLs (auto-upload local files) ---
124125
const media: DashScopeVideoRefRequest["input"]["media"] = [];
125126

126127
// Add reference images
127-
for (let i = 0; i < images.length; i++) {
128-
const resolved = await ctx.client.uploadFile(images[i]!, model);
128+
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
129+
const resolved = await ctx.client.resolveImageInput(images[imageIndex]!, model);
129130
const entry: DashScopeVideoRefRequest["input"]["media"][number] = {
130131
type: "reference_image",
131132
url: resolved,
132133
};
133134

134135
// Pair voice by position
135-
if (imageVoices[i]) {
136-
const resolvedVoice = await ctx.client.uploadFile(imageVoices[i]!, model);
136+
if (imageVoices[imageIndex]) {
137+
const resolvedVoice = await ctx.client.uploadFile(imageVoices[imageIndex]!, model);
137138
entry.reference_voice = resolvedVoice;
138139
}
139140

140141
media.push(entry);
141142
}
142143

143144
// Add reference videos
144-
for (let i = 0; i < refVideos.length; i++) {
145-
const resolved = await ctx.client.uploadFile(refVideos[i]!, model);
145+
for (let videoIndex = 0; videoIndex < refVideos.length; videoIndex++) {
146+
const resolved = await ctx.client.uploadFile(refVideos[videoIndex]!, model);
146147
const entry: DashScopeVideoRefRequest["input"]["media"][number] = {
147148
type: "reference_video",
148149
url: resolved,
149150
};
150151

151152
// Pair voice by position
152-
if (videoVoices[i]) {
153-
const resolvedVoice = await ctx.client.uploadFile(videoVoices[i]!, model);
153+
if (videoVoices[videoIndex]) {
154+
const resolvedVoice = await ctx.client.uploadFile(videoVoices[videoIndex]!, model);
154155
entry.reference_voice = resolvedVoice;
155156
}
156157

@@ -178,7 +179,18 @@ export default defineCommand({
178179
};
179180

180181
if (settings.dryRun) {
181-
emitResult({ request: body }, format);
182+
const previewBody = {
183+
...body,
184+
input: {
185+
...body.input,
186+
media: body.input.media.map((item) => ({
187+
...item,
188+
url: redactDataUri(item.url),
189+
reference_voice: item.reference_voice ? redactDataUri(item.reference_voice) : undefined,
190+
})),
191+
},
192+
};
193+
emitResult({ request: previewBody }, format);
182194
return;
183195
}
184196

@@ -233,11 +245,11 @@ export default defineCommand({
233245
);
234246

235247
const videos: Array<{ taskId: string; videoUrl: string }> = [];
236-
for (let i = 0; i < results.length; i++) {
237-
const result = results[i]!;
248+
for (let resultIndex = 0; resultIndex < results.length; resultIndex++) {
249+
const result = results[resultIndex]!;
238250
const videoUrl =
239251
result.output.video_url || (result.output.results && result.output.results[0]?.url);
240-
if (videoUrl) videos.push({ taskId: taskIds[i]!, videoUrl });
252+
if (videoUrl) videos.push({ taskId: taskIds[resultIndex]!, videoUrl });
241253
}
242254

243255
if (videos.length === 0) {

packages/commands/src/commands/vision/describe.ts

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,13 @@ import {
88
BailianError,
99
ExitCode,
1010
isLocalFile,
11+
imageFileToDataUri,
12+
redactDataUri,
1113
} from "bailian-cli-core";
1214
import { emitResult, emitBare } from "bailian-cli-runtime";
13-
import { readFileSync, existsSync } from "fs";
15+
import { existsSync, statSync } from "fs";
1416
import { extname } from "path";
1517

16-
const IMAGE_MIME_TYPES: Record<string, string> = {
17-
".jpg": "image/jpeg",
18-
".jpeg": "image/jpeg",
19-
".png": "image/png",
20-
".webp": "image/webp",
21-
};
22-
2318
const VIDEO_EXTENSIONS = new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv"]);
2419

2520
function isVideoInput(input: string): boolean {
@@ -35,18 +30,7 @@ async function toImageUrl(image: string): Promise<string> {
3530
if (image.startsWith("data:")) return image;
3631
if (image.startsWith("http://") || image.startsWith("https://")) return image;
3732
if (image.startsWith("oss://")) return image;
38-
39-
// Local file → data URI (for small files < 10MB, fallback)
40-
if (!existsSync(image)) throw new BailianError(`File not found: ${image}`, ExitCode.USAGE);
41-
const ext = extname(image).toLowerCase();
42-
const mime = IMAGE_MIME_TYPES[ext];
43-
if (!mime)
44-
throw new BailianError(
45-
`Unsupported image format "${ext}". Supported: jpg, jpeg, png, webp`,
46-
ExitCode.USAGE,
47-
);
48-
const buf = readFileSync(image);
49-
return `data:${mime};base64,${buf.toString("base64")}`;
33+
return imageFileToDataUri(image);
5034
}
5135

5236
export default defineCommand({
@@ -86,7 +70,10 @@ export default defineCommand({
8670
const { settings, flags } = ctx;
8771
let image = flags.image;
8872
const videoInputs = flags.video ?? [];
89-
const model = flags.model || "qwen3-vl-plus";
73+
const model =
74+
flags.model ||
75+
(ctx.client.usesTokenPlanEndpoint() ? settings.defaultTextModel : undefined) ||
76+
"qwen3-vl-plus";
9077

9178
// Auto-detect: if --image was given a video file, treat it as --video
9279
if (image && isVideoInput(image)) {
@@ -102,7 +89,14 @@ export default defineCommand({
10289

10390
if (settings.dryRun) {
10491
emitResult(
105-
{ request: { prompt, image, video: videoInputs.length ? videoInputs : undefined, model } },
92+
{
93+
request: {
94+
prompt,
95+
image: image ? redactDataUri(image) : undefined,
96+
video: videoInputs.length ? videoInputs.map(redactDataUri) : undefined,
97+
model,
98+
},
99+
},
106100
format,
107101
);
108102
return;
@@ -132,10 +126,9 @@ export default defineCommand({
132126

133127
let finalImageUrl = imageUrl;
134128
if (isLocalFile(image) && imageUrl.startsWith("data:")) {
135-
const { statSync } = await import("fs");
136129
const fileSize = statSync(image).size;
137130
if (fileSize > 5 * 1024 * 1024) {
138-
finalImageUrl = await ctx.client.uploadFile(image, model);
131+
finalImageUrl = await ctx.client.resolveImageInput(image, model);
139132
}
140133
}
141134

0 commit comments

Comments
 (0)