@yandy0725/mediagen-cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,3306 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import fs6 from "fs";
5
+ import readline from "readline";
6
+ import { Writable } from "stream";
7
+ import { fileURLToPath, pathToFileURL } from "url";
8
+ import { Command, CommanderError } from "commander";
9
+ import { parse as parseToml } from "smol-toml";
10
+
11
+ // src/auth.ts
12
+ import { randomUUID as randomUUID2 } from "crypto";
13
+ import fs3 from "fs";
14
+ import path3 from "path";
15
+
16
+ // src/config.ts
17
+ import { randomUUID } from "crypto";
18
+ import fs2 from "fs";
19
+ import os from "os";
20
+ import path2 from "path";
21
+ import { parse } from "smol-toml";
22
+
23
+ // src/errors.ts
24
+ var ConfigError = class extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = "ConfigError";
28
+ }
29
+ };
30
+ var UsageError = class extends Error {
31
+ constructor(message) {
32
+ super(message);
33
+ this.name = "UsageError";
34
+ }
35
+ };
36
+ var NO_RESPONSE_STATUS = 0;
37
+ var ApiError = class extends Error {
38
+ code;
39
+ /** The task this failure is about, when one exists: what `video status` resumes. */
40
+ taskId;
41
+ /** The HTTP status, or `NO_RESPONSE_STATUS` when the request got no response at all. */
42
+ status;
43
+ constructor(message, code, taskId, status) {
44
+ super(message);
45
+ this.name = "ApiError";
46
+ this.code = code;
47
+ this.taskId = taskId;
48
+ this.status = status;
49
+ }
50
+ };
51
+ function textOf(error) {
52
+ return error instanceof Error ? error.message : String(error);
53
+ }
54
+ function fieldsOf(error) {
55
+ return typeof error === "object" && error !== null ? error : void 0;
56
+ }
57
+ function parseLocator(error) {
58
+ const fields = fieldsOf(error);
59
+ const line = fields?.line;
60
+ const column = fields?.column;
61
+ if (typeof line === "number" && typeof column === "number") {
62
+ return ` (line ${line}, column ${column})`;
63
+ }
64
+ const position = /position (\d+)/.exec(textOf(error))?.[1];
65
+ return position === void 0 ? "" : ` (position ${position})`;
66
+ }
67
+ function malformedDocumentMessage(file, format, error) {
68
+ const remedy = format === "JSON" ? 'Repair it, or delete it and run "mediagen login --provider <name> --api-key ..." to write a fresh one.' : "Repair it by hand; mediagen reads these settings from no other file.";
69
+ return `invalid ${format} in ${file}${parseLocator(error)}: the parser message is suppressed because it quotes the document, which may hold an API key. ${remedy}`;
70
+ }
71
+ function unreadableFileMessage(file, error) {
72
+ const code = fieldsOf(error)?.code;
73
+ return `cannot read ${file} (${typeof code === "string" ? code : "unreadable"}): check that it is a readable file`;
74
+ }
75
+ function unwritableFileMessage(file, error) {
76
+ const code = fieldsOf(error)?.code;
77
+ return `cannot create ${file} (${typeof code === "string" ? code : "unwritable"}): point MEDIAGEN_CLI_CONFIG_DIR at a writable directory, or fix the permissions on the directory that holds this file`;
78
+ }
79
+
80
+ // src/providers.ts
81
+ var SEED_CONFIG = `# mediagen configuration. Edit and save; the CLI re-reads it on every run.
82
+ #
83
+ # A provider is one account: one host (base_url) and one key. Each task it can
84
+ # serve hangs off it by name \u2014 [providers.<name>.image], [providers.<name>.video]
85
+ # \u2014 and declares its own protocol (api_type) plus the models you allow there. A
86
+ # second model of the same vendor is one more entry in \`models\`, not another
87
+ # provider. api_type may also sit on the provider table as the default for every
88
+ # task under it.
89
+ #
90
+ # The protocols are "openai" (the OpenAI-compatible wire format), "dashscope"
91
+ # (Alibaba's native input.messages/parameters form), "ark" (Volcengine's own API,
92
+ # OpenAI-shaped for images but with its own parameters and per-item errors) and
93
+ # "minimax" (MiniMax's video API). "qwen" still works as a deprecated alias of
94
+ # "openai". dashscope serves images only: it has no video task.
95
+ #
96
+ # base_url is the origin. The version segment belongs to the endpoint, which the
97
+ # dialect appends per task (/api/v3/images/generations, /compatible-mode/v1/...).
98
+
99
+ default_provider = "volcengine"
100
+ # output_dir = "./media-output" # where generated files land; defaults to the cwd
101
+
102
+ [providers.volcengine]
103
+ base_url = "https://ark.cn-beijing.volces.com"
104
+ api_key_env = "ARK_API_KEY" # which environment variable holds this provider's key
105
+
106
+ [providers.volcengine.image]
107
+ api_type = "ark"
108
+ default = "doubao-seedream-5-0-flash-260915"
109
+ models = ["doubao-seedream-5-0-flash-260915", "doubao-seedream-5-0-pro"]
110
+
111
+ [providers.volcengine.video]
112
+ api_type = "ark"
113
+ default = "doubao-seedance-1-0-pro-250528"
114
+ models = ["doubao-seedance-1-0-pro-250528"] # ids are dated snapshots; add more here
115
+
116
+ [providers.aliyun]
117
+ # Bailian's native protocol is served on the public host, so this entry works as
118
+ # written. For the OpenAI-compatible protocol use your workspace-scoped host
119
+ # instead \u2014 https://{workspace-id}.cn-beijing.maas.aliyuncs.com \u2014 which is also
120
+ # where Alibaba recommends sending native calls.
121
+ base_url = "https://dashscope.aliyuncs.com"
122
+ api_key_env = "DASHSCOPE_API_KEY"
123
+
124
+ [providers.aliyun.image]
125
+ api_type = "dashscope" # input.messages + parameters, output.choices[].image
126
+ default = "qwen-image-3.0"
127
+ models = ["qwen-image-3.0"]
128
+
129
+ [providers.minimax]
130
+ base_url = "https://api.minimax.io"
131
+ api_key_env = "MINIMAX_API_KEY"
132
+
133
+ [providers.minimax.video]
134
+ api_type = "minimax"
135
+ default = "MiniMax-H3"
136
+ models = ["MiniMax-H3", "MiniMax-H3-Max"] # -Max: 480P/768P only, 5s minimum
137
+
138
+ # A gateway that serves DashScope's native protocol on its own path:
139
+ # [providers.my-gateway]
140
+ # base_url = "https://gateway.example.com"
141
+ # api_key_env = "MY_GATEWAY_KEY"
142
+ #
143
+ # [providers.my-gateway.image]
144
+ # api_type = "dashscope" # input.messages + parameters, output.choices[].image
145
+ # path = "/v1/images/generations"
146
+ # default = "qwen-image-2.0-pro"
147
+ # models = ["qwen-image-2.0-pro"]
148
+
149
+ # Your own entry, e.g. a proxy of the openai dialect:
150
+ # [providers.my-proxy]
151
+ # base_url = "https://proxy.example.com/my-gateway" # a gateway prefix is fine
152
+ # api_key_env = "MY_PROXY_KEY"
153
+ #
154
+ # [providers.my-proxy.image]
155
+ # api_type = "openai"
156
+ # default = "qwen-image-3.0"
157
+ # models = ["qwen-image-3.0"]
158
+ `;
159
+
160
+ // src/toml.ts
161
+ function isBareTomlKey(value) {
162
+ return /^[A-Za-z0-9_-]+$/.test(value);
163
+ }
164
+ function tomlKey(name) {
165
+ return isBareTomlKey(name) ? name : tomlBasicString(name);
166
+ }
167
+ function tomlBasicString(value) {
168
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
169
+ return `"${escaped}"`;
170
+ }
171
+ function shellArg(value) {
172
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
173
+ return `'${value.replace(/'/g, `'\\''`)}'`;
174
+ }
175
+
176
+ // src/util.ts
177
+ import fs from "fs";
178
+ import path from "path";
179
+ var IMAGE_MIME_TYPES = {
180
+ ".png": "image/png",
181
+ ".jpg": "image/jpeg",
182
+ ".jpeg": "image/jpeg",
183
+ ".webp": "image/webp",
184
+ ".bmp": "image/bmp",
185
+ ".tif": "image/tiff",
186
+ ".tiff": "image/tiff",
187
+ ".gif": "image/gif",
188
+ ".heic": "image/heic",
189
+ ".heif": "image/heif"
190
+ };
191
+ function errorMessage(error) {
192
+ return error instanceof Error ? error.message : String(error);
193
+ }
194
+ function isUrl(value) {
195
+ return /^https?:\/\//i.test(value);
196
+ }
197
+ function normalizeHttpBaseUrl(value) {
198
+ const trimmed = value.trim();
199
+ if (/[?#]/.test(trimmed)) return void 0;
200
+ let url;
201
+ try {
202
+ url = new URL(trimmed);
203
+ } catch {
204
+ return void 0;
205
+ }
206
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
207
+ if (url.username !== "" || url.password !== "") return void 0;
208
+ return trimmed.replace(/\/+$/, "");
209
+ }
210
+ var SCHEME_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
211
+ function toImageInput(value) {
212
+ if (value.startsWith("data:")) return value;
213
+ if (isUrl(value)) return value;
214
+ if (SCHEME_URL.test(value)) return value;
215
+ const extension = path.extname(value).toLowerCase();
216
+ const mime = IMAGE_MIME_TYPES[extension];
217
+ if (mime === void 0) {
218
+ const supported = Object.keys(IMAGE_MIME_TYPES).join(", ");
219
+ throw new UsageError(
220
+ `unsupported image extension "${extension || "(none)"}" for ${value}; supported extensions: ${supported}`
221
+ );
222
+ }
223
+ let bytes;
224
+ try {
225
+ bytes = fs.readFileSync(value);
226
+ } catch (error) {
227
+ throw new UsageError(`cannot read image ${value}: ${errorMessage(error)}`);
228
+ }
229
+ return `data:${mime};base64,${bytes.toString("base64")}`;
230
+ }
231
+ function nowStamp(date = /* @__PURE__ */ new Date()) {
232
+ const pad = (value) => String(value).padStart(2, "0");
233
+ const year = date.getFullYear();
234
+ const month = pad(date.getMonth() + 1);
235
+ const day = pad(date.getDate());
236
+ const hours = pad(date.getHours());
237
+ const minutes = pad(date.getMinutes());
238
+ const seconds = pad(date.getSeconds());
239
+ return `${year}${month}${day}-${hours}${minutes}${seconds}`;
240
+ }
241
+ function slugModel(model) {
242
+ return model.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
243
+ }
244
+
245
+ // src/api/ark.ts
246
+ var QWEN_ONLY_FIELDS = [
247
+ "negative_prompt",
248
+ "prompt_extend",
249
+ "prompt_extend_mode",
250
+ "enable_thinking"
251
+ ];
252
+ function isRecord(value) {
253
+ return typeof value === "object" && value !== null && !Array.isArray(value);
254
+ }
255
+ function numberArray(value) {
256
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "number")) {
257
+ return void 0;
258
+ }
259
+ return [...value];
260
+ }
261
+ function parseBoundingBox(value) {
262
+ if (!isRecord(value)) return void 0;
263
+ const absolute = numberArray(value.absolute);
264
+ const normalized = numberArray(value.normalized);
265
+ if (absolute === void 0 || normalized === void 0) return void 0;
266
+ return { absolute, normalized };
267
+ }
268
+ function buildArkRequest(req) {
269
+ for (const field of QWEN_ONLY_FIELDS) {
270
+ if (req[field] !== void 0) {
271
+ throw new UsageError(
272
+ `option "${field}" is openai-dialect only and is not supported by the ark dialect; remove it or set api_type = "openai" on this task`
273
+ );
274
+ }
275
+ }
276
+ const body = {
277
+ model: req.model,
278
+ prompt: req.prompt
279
+ };
280
+ if (req.images.length === 1) {
281
+ body.image = req.images[0];
282
+ } else if (req.images.length > 1) {
283
+ body.image = [...req.images];
284
+ }
285
+ if (req.size !== void 0) body.size = req.size;
286
+ if (req.seed !== void 0) body.seed = req.seed;
287
+ if (req.watermark !== void 0) body.watermark = req.watermark;
288
+ if (req.output_format !== void 0) body.output_format = req.output_format;
289
+ if (req.optimize_prompt_mode !== void 0) {
290
+ body.optimize_prompt_options = { mode: req.optimize_prompt_mode };
291
+ }
292
+ if (req.layer_decomposition !== void 0) body.layer_decomposition = req.layer_decomposition;
293
+ if (req.background !== void 0) body.background = req.background;
294
+ body.response_format = "url";
295
+ return body;
296
+ }
297
+ function parseArkFailure(value) {
298
+ const code = typeof value.code === "string" && value.code !== "" ? value.code : "UnknownError";
299
+ const message = typeof value.message === "string" && value.message.trim() !== "" ? value.message : "ark API image request failed";
300
+ return { code, message };
301
+ }
302
+ function parseArkResponse(json, requestId) {
303
+ if (!isRecord(json)) {
304
+ throw new ApiError("ark API returned a response that is not a JSON object");
305
+ }
306
+ if ("error" in json && json.error !== void 0 && json.error !== null) {
307
+ if (!isRecord(json.error)) {
308
+ throw new ApiError("ark API returned an unrecognized error body");
309
+ }
310
+ const code = typeof json.error.code === "string" ? json.error.code : void 0;
311
+ const message = typeof json.error.message === "string" && json.error.message.trim() !== "" ? json.error.message : "ark API request failed";
312
+ throw new ApiError(message, code);
313
+ }
314
+ if (!("data" in json) || json.data === void 0) {
315
+ throw new ApiError("ark API response is missing the data array");
316
+ }
317
+ if (!Array.isArray(json.data)) {
318
+ throw new ApiError("ark API response data is not an array");
319
+ }
320
+ const images = [];
321
+ const failures = [];
322
+ json.data.forEach((entry, index) => {
323
+ if (!isRecord(entry)) {
324
+ throw new ApiError(`ark API response item ${index + 1} is not a JSON object`);
325
+ }
326
+ if ("error" in entry && entry.error !== void 0 && entry.error !== null) {
327
+ if (!isRecord(entry.error)) {
328
+ throw new ApiError(`ark API response item ${index + 1} has an unrecognized error body`);
329
+ }
330
+ failures.push(parseArkFailure(entry.error));
331
+ return;
332
+ }
333
+ if (typeof entry.url !== "string" || entry.url === "") {
334
+ throw new ApiError(
335
+ `ark API response item ${index + 1} has neither a url nor an error`
336
+ );
337
+ }
338
+ const image = { url: entry.url };
339
+ if (typeof entry.size === "string") image.size = entry.size;
340
+ if (typeof entry.output_format === "string") image.outputFormat = entry.output_format;
341
+ if (typeof entry.z_index === "number") image.zIndex = entry.z_index;
342
+ const boundingBox = parseBoundingBox(entry.bounding_box);
343
+ if (boundingBox !== void 0) image.boundingBox = boundingBox;
344
+ if (typeof entry.name === "string") image.name = entry.name;
345
+ if (typeof entry.description === "string") image.description = entry.description;
346
+ images.push(image);
347
+ });
348
+ if (images.length === 0) {
349
+ if (failures.length > 0) {
350
+ const [first] = failures;
351
+ throw new ApiError(first.message, first.code);
352
+ }
353
+ throw new ApiError("ark API returned no images");
354
+ }
355
+ const parsed = { images, failures };
356
+ if (requestId !== void 0) parsed.requestId = requestId;
357
+ if (isRecord(json.usage)) parsed.usage = json.usage;
358
+ return parsed;
359
+ }
360
+ var ArkDialect = {
361
+ createPath: "/api/v3/images/generations",
362
+ buildRequest: buildArkRequest,
363
+ parseResponse: parseArkResponse
364
+ };
365
+ var ARK_VIDEO_TASKS_PATH = "/api/v3/contents/generations/tasks";
366
+ function buildArkVideoRequest(req) {
367
+ const content = [{ type: "text", text: req.prompt }];
368
+ if (req.first_frame !== void 0) {
369
+ content.push({
370
+ type: "image_url",
371
+ image_url: { url: req.first_frame },
372
+ role: "first_frame"
373
+ });
374
+ }
375
+ const body = { model: req.model, content };
376
+ if (req.resolution !== void 0) body.resolution = req.resolution;
377
+ if (req.ratio !== void 0) body.ratio = req.ratio;
378
+ if (req.duration !== void 0) body.duration = req.duration;
379
+ if (req.seed !== void 0) body.seed = req.seed;
380
+ if (req.watermark !== void 0) body.watermark = req.watermark;
381
+ if (req.generate_audio !== void 0) body.generate_audio = req.generate_audio;
382
+ if (req.output_format !== void 0) body.output_format = req.output_format;
383
+ return body;
384
+ }
385
+ function taskIdFromArk(json) {
386
+ if (!isRecord(json) || typeof json.id !== "string" || json.id === "") {
387
+ throw new ApiError("ark create response is missing the task id");
388
+ }
389
+ return json.id;
390
+ }
391
+ function arkPollPath(id) {
392
+ return `${ARK_VIDEO_TASKS_PATH}/${encodeURIComponent(id)}`;
393
+ }
394
+ function arkVideoResult(json) {
395
+ const content = json.content;
396
+ if (!isRecord(content) || typeof content.video_url !== "string" || content.video_url === "") {
397
+ throw new ApiError("ark task succeeded without content.video_url");
398
+ }
399
+ const result = { url: content.video_url };
400
+ if (typeof json.model === "string") result.model = json.model;
401
+ if (typeof json.duration === "number") result.duration = json.duration;
402
+ if (typeof json.resolution === "string") result.resolution = json.resolution;
403
+ if (typeof json.ratio === "string") result.ratio = json.ratio;
404
+ if (typeof json.output_format === "string") result.format = json.output_format;
405
+ if (isRecord(json.usage)) result.usage = json.usage;
406
+ return result;
407
+ }
408
+ function arkVideoFailure(json, status) {
409
+ const error = json.error;
410
+ if (!isRecord(error)) {
411
+ return { message: `ark task ${status}` };
412
+ }
413
+ const failure = {
414
+ message: typeof error.message === "string" && error.message.trim() !== "" ? error.message : `ark task ${status}`
415
+ };
416
+ if (typeof error.code === "string" && error.code !== "") failure.code = error.code;
417
+ return failure;
418
+ }
419
+ function arkPollOutcome(json) {
420
+ if (!isRecord(json)) {
421
+ throw new ApiError("ark task response is not a JSON object");
422
+ }
423
+ const status = json.status;
424
+ if (status === "queued") return { state: "pending" };
425
+ if (status === "running") return { state: "running" };
426
+ if (status === "succeeded") return { state: "succeeded", result: arkVideoResult(json) };
427
+ if (status === "failed" || status === "cancelled" || status === "expired") {
428
+ return { state: status, failure: arkVideoFailure(json, status) };
429
+ }
430
+ throw new ApiError(`ark task reported the unknown status ${JSON.stringify(status)}`);
431
+ }
432
+ var ArkVideoDialect = {
433
+ dialect: "ark",
434
+ createPath: ARK_VIDEO_TASKS_PATH,
435
+ buildRequest: buildArkVideoRequest,
436
+ taskIdFrom: taskIdFromArk,
437
+ pollPath: arkPollPath,
438
+ pollOutcome: arkPollOutcome,
439
+ resolutions: ["480p", "720p", "1080p", "4k"],
440
+ exclusiveFields: ["generate_audio", "output_format", "watermark", "seed"]
441
+ };
442
+
443
+ // src/api/dashscope.ts
444
+ var ARK_ONLY_FIELDS = [
445
+ "output_format",
446
+ "optimize_prompt_mode",
447
+ "layer_decomposition",
448
+ "background"
449
+ ];
450
+ var OPENAI_ONLY_FIELDS = ["prompt_extend_mode", "enable_thinking"];
451
+ var PIXEL_PAIR = /^(\d+)x(\d+)$/;
452
+ function isRecord2(value) {
453
+ return typeof value === "object" && value !== null && !Array.isArray(value);
454
+ }
455
+ function toDashscopeSize(size) {
456
+ const value = size.trim();
457
+ const pair = PIXEL_PAIR.exec(value);
458
+ if (pair === null || Number(pair[1]) <= 0 || Number(pair[2]) <= 0) {
459
+ throw new UsageError(
460
+ `invalid --size ${JSON.stringify(size)}: the dashscope dialect sends a WxH pixel pair as \u5BBD*\u9AD8, e.g. 2048x2048`
461
+ );
462
+ }
463
+ return `${pair[1]}*${pair[2]}`;
464
+ }
465
+ function buildDashscopeRequest(req) {
466
+ for (const field of ARK_ONLY_FIELDS) {
467
+ if (req[field] !== void 0) {
468
+ throw new UsageError(
469
+ `option "${field}" is ark-only and is not supported by the dashscope dialect; remove it or use an api_type = "ark" provider`
470
+ );
471
+ }
472
+ }
473
+ for (const field of OPENAI_ONLY_FIELDS) {
474
+ if (req[field] !== void 0) {
475
+ throw new UsageError(
476
+ `option "${field}" is openai dialect only and is not supported by the dashscope dialect; remove it or use an api_type = "openai" provider`
477
+ );
478
+ }
479
+ }
480
+ const content = req.images.map((image) => ({ image }));
481
+ content.push({ text: req.prompt });
482
+ const parameters = { n: req.n };
483
+ if (req.size !== void 0) parameters.size = toDashscopeSize(req.size);
484
+ if (req.negative_prompt !== void 0) parameters.negative_prompt = req.negative_prompt;
485
+ if (req.prompt_extend !== void 0) parameters.prompt_extend = req.prompt_extend;
486
+ if (req.watermark !== void 0) parameters.watermark = req.watermark;
487
+ if (req.seed !== void 0) parameters.seed = req.seed;
488
+ return {
489
+ model: req.model,
490
+ input: { messages: [{ role: "user", content }] },
491
+ parameters
492
+ };
493
+ }
494
+ var EXPECTED_SHAPE = "output.choices[].message.content[].image";
495
+ function parseDashscopeResponse(json, requestId) {
496
+ if (!isRecord2(json)) {
497
+ throw new ApiError("dashscope API returned a response that is not a JSON object");
498
+ }
499
+ if (typeof json.code === "string" && json.code !== "") {
500
+ const message = typeof json.message === "string" && json.message.trim() !== "" ? json.message : "dashscope API request failed";
501
+ throw new ApiError(message, json.code);
502
+ }
503
+ if (!isRecord2(json.output) || !Array.isArray(json.output.choices)) {
504
+ throw new ApiError(`dashscope API response is missing ${EXPECTED_SHAPE}`);
505
+ }
506
+ const images = [];
507
+ for (const choice of json.output.choices) {
508
+ if (!isRecord2(choice) || !isRecord2(choice.message)) continue;
509
+ const content = choice.message.content;
510
+ if (!Array.isArray(content)) continue;
511
+ for (const item of content) {
512
+ if (isRecord2(item) && typeof item.image === "string" && item.image !== "") {
513
+ images.push({ url: item.image });
514
+ }
515
+ }
516
+ }
517
+ if (images.length === 0) {
518
+ throw new ApiError("dashscope API returned no images");
519
+ }
520
+ const parsed = { images, failures: [] };
521
+ const bodyRequestId = json.request_id;
522
+ if (requestId !== void 0) {
523
+ parsed.requestId = requestId;
524
+ } else if (typeof bodyRequestId === "string" && bodyRequestId !== "") {
525
+ parsed.requestId = bodyRequestId;
526
+ }
527
+ if (isRecord2(json.usage)) parsed.usage = json.usage;
528
+ return parsed;
529
+ }
530
+ function parseDashscopeError(json) {
531
+ if (!isRecord2(json)) return void 0;
532
+ const message = typeof json.message === "string" ? json.message.trim() : "";
533
+ if (message === "") return void 0;
534
+ const code = typeof json.code === "string" && json.code !== "" ? json.code : void 0;
535
+ return code === void 0 ? { message } : { code, message };
536
+ }
537
+ var DashscopeDialect = {
538
+ createPath: "/api/v1/services/aigc/multimodal-generation/generation",
539
+ buildRequest: buildDashscopeRequest,
540
+ parseResponse: parseDashscopeResponse,
541
+ parseErrorBody: parseDashscopeError
542
+ };
543
+
544
+ // src/api/minimax.ts
545
+ var UNSUPPORTED_FIELDS = ["watermark", "generate_audio", "output_format", "seed"];
546
+ function isRecord3(value) {
547
+ return typeof value === "object" && value !== null && !Array.isArray(value);
548
+ }
549
+ var DEFAULT_RESOLUTION = "768p";
550
+ var DEFAULT_DURATION_SECONDS = 5;
551
+ function wireResolution(value) {
552
+ return value.toUpperCase();
553
+ }
554
+ function buildMinimaxVideoRequest(req) {
555
+ for (const field of UNSUPPORTED_FIELDS) {
556
+ if (req[field] !== void 0) {
557
+ throw new UsageError(
558
+ `option "${field}" is not supported by the minimax dialect; remove it`
559
+ );
560
+ }
561
+ }
562
+ const content = [{ type: "text", text: req.prompt }];
563
+ if (req.first_frame !== void 0) {
564
+ content.push({
565
+ type: "image_url",
566
+ image_url: { url: req.first_frame },
567
+ role: "first_frame"
568
+ });
569
+ }
570
+ return {
571
+ model: req.model,
572
+ content,
573
+ resolution: wireResolution(req.resolution ?? DEFAULT_RESOLUTION),
574
+ duration: req.duration ?? DEFAULT_DURATION_SECONDS,
575
+ ...req.ratio === void 0 ? {} : { ratio: req.ratio }
576
+ };
577
+ }
578
+ function taskIdFromMinimax(json) {
579
+ if (!isRecord3(json) || typeof json.task_id !== "string" || json.task_id === "") {
580
+ throw new ApiError("minimax create response is missing the task id");
581
+ }
582
+ return json.task_id;
583
+ }
584
+ function minimaxPollPath(id) {
585
+ return `/v2/query/video_generation/${encodeURIComponent(id)}`;
586
+ }
587
+ function minimaxFailure(error) {
588
+ const fallback = "minimax reported no message";
589
+ if (isRecord3(error)) {
590
+ const message = typeof error.message === "string" && error.message.trim() !== "" ? error.message : fallback;
591
+ const code = typeof error.type === "string" && error.type !== "" ? error.type : void 0;
592
+ return code === void 0 ? { message } : { code, message };
593
+ }
594
+ return { message: fallback };
595
+ }
596
+ function minimaxPollOutcome(json) {
597
+ if (!isRecord3(json) || !isRecord3(json.task)) {
598
+ throw new ApiError("minimax query response is missing the task object");
599
+ }
600
+ const task = json.task;
601
+ const status = task.status;
602
+ if (status === "queued") return { state: "pending" };
603
+ if (status === "running") return { state: "running" };
604
+ if (status === "succeeded") {
605
+ if (!isRecord3(task.content) || typeof task.content.url !== "string" || task.content.url === "") {
606
+ throw new ApiError("minimax task succeeded without content.url");
607
+ }
608
+ const result = { url: task.content.url };
609
+ if (typeof task.model === "string" && task.model !== "") result.model = task.model;
610
+ if (typeof task.duration === "number") result.duration = task.duration;
611
+ if (typeof task.resolution === "string") result.resolution = task.resolution;
612
+ if (typeof task.ratio === "string") result.ratio = task.ratio;
613
+ if (isRecord3(task.usage)) result.usage = task.usage;
614
+ return { state: "succeeded", result };
615
+ }
616
+ if (status === "failed") return { state: "failed", failure: minimaxFailure(task.error) };
617
+ if (status === "cancelled") return { state: "cancelled", failure: minimaxFailure(task.error) };
618
+ throw new ApiError(
619
+ `minimax task reported the unknown status ${JSON.stringify(status)}`
620
+ );
621
+ }
622
+ function parseMinimaxError(json) {
623
+ if (!isRecord3(json) || json.type !== "error" || !isRecord3(json.error)) return void 0;
624
+ const message = typeof json.error.message === "string" ? json.error.message.trim() : "";
625
+ if (message === "") return void 0;
626
+ const code = typeof json.error.type === "string" && json.error.type !== "" ? json.error.type : void 0;
627
+ return code === void 0 ? { message } : { code, message };
628
+ }
629
+ var MinimaxVideoDialect = {
630
+ dialect: "minimax",
631
+ createPath: "/v2/video_generation",
632
+ buildRequest: buildMinimaxVideoRequest,
633
+ taskIdFrom: taskIdFromMinimax,
634
+ pollPath: minimaxPollPath,
635
+ pollOutcome: minimaxPollOutcome,
636
+ parseErrorBody: parseMinimaxError,
637
+ resolutions: ["480p", "768p", "2k"],
638
+ // None of the dialect-specific optional fields: this API takes model, content,
639
+ // resolution, duration and ratio, and nothing else.
640
+ exclusiveFields: []
641
+ };
642
+
643
+ // src/api/qwen.ts
644
+ var ARK_ONLY_FIELDS2 = [
645
+ "output_format",
646
+ "optimize_prompt_mode",
647
+ "layer_decomposition",
648
+ "background"
649
+ ];
650
+ function isRecord4(value) {
651
+ return typeof value === "object" && value !== null && !Array.isArray(value);
652
+ }
653
+ function buildQwenRequest(req) {
654
+ for (const field of ARK_ONLY_FIELDS2) {
655
+ if (req[field] !== void 0) {
656
+ throw new UsageError(
657
+ `option "${field}" is ark-only and is not supported by the openai dialect; remove it or set api_type = "ark" on this task`
658
+ );
659
+ }
660
+ }
661
+ if (req.prompt_extend_mode === "agent" && req.images.length > 0) {
662
+ throw new UsageError(
663
+ 'prompt_extend_mode "agent" is only supported for text-to-image; image-to-image (\u56FE\u751F\u56FE) does not support it'
664
+ );
665
+ }
666
+ const body = {
667
+ model: req.model,
668
+ prompt: req.prompt
669
+ };
670
+ if (req.images.length === 1) {
671
+ body.image = req.images[0];
672
+ } else if (req.images.length > 1) {
673
+ body.image = [...req.images];
674
+ }
675
+ body.n = req.n;
676
+ if (req.size !== void 0) body.size = req.size;
677
+ if (req.negative_prompt !== void 0) body.negative_prompt = req.negative_prompt;
678
+ if (req.seed !== void 0) body.seed = req.seed;
679
+ if (req.prompt_extend !== void 0) body.prompt_extend = req.prompt_extend;
680
+ if (req.prompt_extend_mode !== void 0) body.prompt_extend_mode = req.prompt_extend_mode;
681
+ if (req.enable_thinking !== void 0) body.enable_thinking = req.enable_thinking;
682
+ if (req.watermark !== void 0) body.watermark = req.watermark;
683
+ return body;
684
+ }
685
+ function parseQwenResponse(json, requestId) {
686
+ if (!isRecord4(json)) {
687
+ throw new ApiError("openai API returned a response that is not a JSON object");
688
+ }
689
+ if ("error" in json && json.error !== void 0 && json.error !== null) {
690
+ if (!isRecord4(json.error)) {
691
+ throw new ApiError("openai API returned an unrecognized error body");
692
+ }
693
+ const code = typeof json.error.code === "string" ? json.error.code : void 0;
694
+ const message = typeof json.error.message === "string" && json.error.message.trim() !== "" ? json.error.message : "openai API request failed";
695
+ throw new ApiError(message, code);
696
+ }
697
+ if (!("data" in json) || json.data === void 0) {
698
+ throw new ApiError("openai API response is missing the data array");
699
+ }
700
+ if (!Array.isArray(json.data)) {
701
+ throw new ApiError("openai API response data is not an array");
702
+ }
703
+ if (json.data.length === 0) {
704
+ throw new ApiError("openai API returned no images");
705
+ }
706
+ const images = json.data.map((entry, index) => {
707
+ if (!isRecord4(entry) || typeof entry.url !== "string" || entry.url === "") {
708
+ throw new ApiError(`openai API response image ${index + 1} is missing a url`);
709
+ }
710
+ return { url: entry.url };
711
+ });
712
+ const parsed = { images, failures: [] };
713
+ if (requestId !== void 0) parsed.requestId = requestId;
714
+ if (isRecord4(json.usage)) parsed.usage = json.usage;
715
+ return parsed;
716
+ }
717
+ var QwenDialect = {
718
+ createPath: "/compatible-mode/v1/images/generations",
719
+ buildRequest: buildQwenRequest,
720
+ parseResponse: parseQwenResponse
721
+ };
722
+
723
+ // src/api/index.ts
724
+ var DIALECTS = {
725
+ openai: { image: QwenDialect },
726
+ // The native DashScope protocol serves images only: its video task was
727
+ // dropped, because its wire shape shares nothing with the async video dialects.
728
+ dashscope: { image: DashscopeDialect },
729
+ ark: { image: ArkDialect, video: ArkVideoDialect },
730
+ // A video-only service: nothing in this dialect serves an image today, and the
731
+ // registry says so rather than pretending.
732
+ minimax: { video: MinimaxVideoDialect }
733
+ };
734
+ function adapterPath(apiType, task) {
735
+ if (!Object.hasOwn(DIALECTS, apiType)) return void 0;
736
+ return DIALECTS[apiType][task]?.createPath;
737
+ }
738
+ function getAdapter(apiType, task) {
739
+ if (!Object.hasOwn(DIALECTS, apiType)) {
740
+ const allowed = Object.keys(DIALECTS).join(", ");
741
+ throw new ConfigError(
742
+ `unknown api_type "${String(apiType)}"; expected one of: ${allowed}`
743
+ );
744
+ }
745
+ const adapters = DIALECTS[apiType];
746
+ const adapter = adapters[task];
747
+ if (adapter === void 0) {
748
+ throw new ConfigError(
749
+ `the "${apiType}" dialect has no "${task}" adapter; implemented tasks: ${Object.keys(adapters).join(", ")}`
750
+ );
751
+ }
752
+ return adapter;
753
+ }
754
+
755
+ // src/config.ts
756
+ function configDir() {
757
+ const override = process.env.MEDIAGEN_CLI_CONFIG_DIR;
758
+ if (override) return override;
759
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME;
760
+ if (xdgConfigHome) return path2.join(xdgConfigHome, "mediagen-cli");
761
+ return path2.join(os.homedir(), ".config", "mediagen-cli");
762
+ }
763
+ function stateDir() {
764
+ const override = process.env.MEDIAGEN_CLI_STATE_DIR;
765
+ if (override) return override;
766
+ const xdgStateHome = process.env.XDG_STATE_HOME;
767
+ if (xdgStateHome) return path2.join(xdgStateHome, "mediagen-cli");
768
+ return path2.join(os.homedir(), ".local", "state", "mediagen-cli");
769
+ }
770
+ function configPath() {
771
+ return path2.join(configDir(), "config.toml");
772
+ }
773
+ function noProviderSelectedMessage(file = configPath()) {
774
+ return `no provider selected: set "default_provider" in ${file} or pass --provider <name>`;
775
+ }
776
+ function writeConfigFile(text) {
777
+ const file = configPath();
778
+ const dir = path2.dirname(file);
779
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
780
+ const tmp = path2.join(dir, `.config.toml.${process.pid}.${randomUUID()}.tmp`);
781
+ try {
782
+ fs2.writeFileSync(tmp, text);
783
+ fs2.renameSync(tmp, file);
784
+ } catch (error) {
785
+ try {
786
+ fs2.unlinkSync(tmp);
787
+ } catch {
788
+ }
789
+ throw error;
790
+ }
791
+ return file;
792
+ }
793
+ function ensureConfigFile() {
794
+ const file = configPath();
795
+ if (fs2.existsSync(file)) return;
796
+ try {
797
+ fs2.mkdirSync(configDir(), { recursive: true, mode: 448 });
798
+ fs2.writeFileSync(file, SEED_CONFIG, { flag: "wx" });
799
+ } catch (error) {
800
+ const code = error?.code;
801
+ if (code === "EEXIST" && fs2.existsSync(file)) return;
802
+ try {
803
+ fs2.unlinkSync(file);
804
+ } catch {
805
+ }
806
+ throw new ConfigError(unwritableFileMessage(file, error));
807
+ }
808
+ }
809
+ function prepareConfigFile() {
810
+ ensureConfigFile();
811
+ loadConfig();
812
+ }
813
+ function resolveBaseUrl(name, apiType, file, value) {
814
+ const normalized = normalizeHttpBaseUrl(value);
815
+ if (normalized !== void 0) return normalized;
816
+ const table = `[providers.${tomlKey(name)}] in ${file}`;
817
+ if (value.includes("<")) {
818
+ throw new ConfigError(
819
+ `base_url under ${table} still contains the placeholder ${JSON.stringify(value)}: replace <workspace-id> with your Bailian workspace id, e.g. "https://abc123.cn-beijing.maas.aliyuncs.com"`
820
+ );
821
+ }
822
+ const example = apiType === "ark" ? "https://ark.cn-beijing.volces.com" : apiType === "openai" || apiType === "dashscope" ? "https://abc123.cn-beijing.maas.aliyuncs.com" : "https://gateway.example.com";
823
+ throw new ConfigError(
824
+ `provider "${name}" has invalid base_url ${JSON.stringify(value)} under ${table}: expected an absolute http(s) origin \u2014 a gateway prefix is allowed, a version segment is not \u2014 without credentials, query or fragment, e.g. ${example}`
825
+ );
826
+ }
827
+ function readOptionalString(table, key, file) {
828
+ const value = table[key];
829
+ if (value === void 0) return void 0;
830
+ if (typeof value !== "string") {
831
+ throw new ConfigError(`"${key}" in ${file} must be a string`);
832
+ }
833
+ return value;
834
+ }
835
+ function vendorPathIssue(value) {
836
+ let url;
837
+ try {
838
+ url = new URL(value);
839
+ } catch {
840
+ return void 0;
841
+ }
842
+ if (!isVendorHost(url.hostname)) return void 0;
843
+ if (url.pathname === "" || url.pathname === "/") return void 0;
844
+ return { prefix: url.pathname, origin: `${url.protocol}//${url.host}` };
845
+ }
846
+ function pathAfterVendorRoot(createPath) {
847
+ if (createPath === void 0) return void 0;
848
+ const segments = createPath.split("/");
849
+ const rootSegments = segments.length >= 5 ? 3 : segments.length >= 3 ? 2 : 0;
850
+ if (rootSegments === 0) return void 0;
851
+ return `/${segments.slice(rootSegments).join("/")}`;
852
+ }
853
+ function isVendorHost(hostname) {
854
+ const host = hostname.toLowerCase();
855
+ return host === "ark.cn-beijing.volces.com" || host === "dashscope.aliyuncs.com" || host === "dashscope-intl.aliyuncs.com" || host === "api.minimax.io" || host.endsWith(".maas.aliyuncs.com");
856
+ }
857
+ function readDialect(value) {
858
+ if (value === "openai" || value === "dashscope" || value === "ark" || value === "minimax") {
859
+ return { dialect: value, deprecated: false };
860
+ }
861
+ if (value === "qwen") return { dialect: "openai", deprecated: true };
862
+ return void 0;
863
+ }
864
+ var DIALECT_CHOICES = 'expected "openai", "dashscope", "ark" or "minimax" ("qwen" is accepted as a deprecated alias of "openai")';
865
+ function isTable(value) {
866
+ return typeof value === "object" && value !== null && !Array.isArray(value);
867
+ }
868
+ var KNOWN_TASKS = ["image", "video"];
869
+ var DEFAULT_TIMEOUT_SECONDS = { image: 300, video: 1800 };
870
+ var MAX_TIMEOUT_SECONDS = 86400;
871
+ var PROVIDER_ENTRY_FIELDS = ["api_type", "base_url", "api_key_env"];
872
+ var TASK_BLOCK_FIELDS = ["default", "models", "timeout_seconds", "path", "api_type"];
873
+ function loadConfig() {
874
+ const file = configPath();
875
+ if (!fs2.existsSync(file)) {
876
+ return { default_provider: "", output_dir: process.cwd(), providers: {} };
877
+ }
878
+ let source;
879
+ try {
880
+ source = fs2.readFileSync(file, "utf8");
881
+ } catch (error) {
882
+ throw new ConfigError(unreadableFileMessage(file, error));
883
+ }
884
+ let parsed;
885
+ try {
886
+ parsed = parse(source);
887
+ } catch (error) {
888
+ throw new ConfigError(malformedDocumentMessage(file, "TOML", error));
889
+ }
890
+ const providers = /* @__PURE__ */ Object.create(null);
891
+ const rawProviders = parsed.providers;
892
+ if (rawProviders !== void 0) {
893
+ if (!isTable(rawProviders)) {
894
+ throw new ConfigError(`"providers" in ${file} must be a table`);
895
+ }
896
+ for (const [name, rawEntry] of Object.entries(rawProviders)) {
897
+ if (!isTable(rawEntry)) {
898
+ throw new ConfigError(`provider "${name}" in ${file} must be a table`);
899
+ }
900
+ providers[name] = readProviderEntry(name, file, rawEntry);
901
+ }
902
+ }
903
+ return {
904
+ default_provider: readOptionalString(parsed, "default_provider", file) ?? "",
905
+ output_dir: readOptionalString(parsed, "output_dir", file) ?? process.cwd(),
906
+ providers
907
+ };
908
+ }
909
+ function readProviderEntry(name, file, table) {
910
+ const entry = { tasks: {} };
911
+ for (const [key, value] of Object.entries(table)) {
912
+ if (key === "api_type") {
913
+ const parsed = readDialect(value);
914
+ if (parsed === void 0) {
915
+ throw new ConfigError(
916
+ `provider "${name}" in ${file} has invalid api_type ${JSON.stringify(value)}; ${DIALECT_CHOICES}`
917
+ );
918
+ }
919
+ entry.api_type = parsed.dialect;
920
+ if (parsed.deprecated) entry.api_type_alias = true;
921
+ continue;
922
+ }
923
+ if (key === "base_url" || key === "api_key_env") {
924
+ if (typeof value !== "string") {
925
+ throw new ConfigError(
926
+ `provider "${name}" in ${file} has invalid ${key}: expected a string`
927
+ );
928
+ }
929
+ entry[key] = value;
930
+ continue;
931
+ }
932
+ if (isTask(key)) {
933
+ if (!isTable(value)) {
934
+ throw new ConfigError(
935
+ `provider "${name}" in ${file} has invalid [providers.${tomlKey(name)}.${key}]: expected a table`
936
+ );
937
+ }
938
+ entry.tasks[key] = readTaskBlock(name, key, file, value);
939
+ continue;
940
+ }
941
+ const singular = key.endsWith("s") ? key.slice(0, -1) : void 0;
942
+ if (singular !== void 0 && isTask(singular)) {
943
+ throw new ConfigError(
944
+ `unknown task "${key}" under [providers.${tomlKey(name)}]; expected ${KNOWN_TASKS.join(", ")}`
945
+ );
946
+ }
947
+ throw new ConfigError(
948
+ `unknown key "${key}" under [providers.${tomlKey(name)}]; expected ${[...PROVIDER_ENTRY_FIELDS, ...KNOWN_TASKS].join(", ")}`
949
+ );
950
+ }
951
+ return entry;
952
+ }
953
+ function readTaskBlock(name, task, file, table) {
954
+ const block = {};
955
+ const where = `[providers.${tomlKey(name)}.${task}]`;
956
+ for (const [key, value] of Object.entries(table)) {
957
+ if (key === "api_type") {
958
+ const parsed = readDialect(value);
959
+ if (parsed === void 0) {
960
+ throw new ConfigError(
961
+ `api_type under ${where} is invalid: ${JSON.stringify(value)}; ${DIALECT_CHOICES}`
962
+ );
963
+ }
964
+ block.api_type = parsed.dialect;
965
+ if (parsed.deprecated) block.api_type_alias = true;
966
+ continue;
967
+ }
968
+ if (key === "default") {
969
+ if (typeof value !== "string") {
970
+ throw new ConfigError(`default under ${where} must be a string (${file})`);
971
+ }
972
+ block.default = value;
973
+ continue;
974
+ }
975
+ if (key === "models") {
976
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
977
+ throw new ConfigError(
978
+ `models under ${where} must be an array of strings (${file})`
979
+ );
980
+ }
981
+ block.models = value;
982
+ continue;
983
+ }
984
+ if (key === "path") {
985
+ if (typeof value !== "string" || !value.startsWith("/") || /[?#]/.test(value)) {
986
+ throw new ConfigError(
987
+ `path under ${where} must start with "/" and carry no query or fragment (${file})`
988
+ );
989
+ }
990
+ block.path = value.replace(/\/+$/, "") || "/";
991
+ continue;
992
+ }
993
+ if (key === "timeout_seconds") {
994
+ if (typeof value !== "number") {
995
+ throw new ConfigError(`timeout_seconds under ${where} must be a number (${file})`);
996
+ }
997
+ block.timeout_seconds = value;
998
+ continue;
999
+ }
1000
+ throw new ConfigError(
1001
+ `unknown key "${key}" under ${where}; expected ${TASK_BLOCK_FIELDS.join(", ")}`
1002
+ );
1003
+ }
1004
+ return block;
1005
+ }
1006
+ function validateProviderShape(name, config) {
1007
+ const file = configPath();
1008
+ if (name === "") throw new ConfigError(noProviderSelectedMessage(file));
1009
+ const entry = config.providers[name];
1010
+ if (entry === void 0) {
1011
+ throw new ConfigError(
1012
+ `unknown provider "${name}": no [providers.${tomlKey(name)}] table in ${file}; add one by hand, or run "mediagen config add ${shellArg(name)} --api-type <openai|dashscope|ark|minimax> --base-url <origin> --model <id>"`
1013
+ );
1014
+ }
1015
+ const declared = taskNames(entry);
1016
+ const missing = [];
1017
+ const hasDialect = entry.api_type !== void 0 || KNOWN_TASKS.some((task) => entry.tasks[task]?.api_type !== void 0);
1018
+ if (!hasDialect) missing.push("api_type");
1019
+ if (entry.base_url === void 0) missing.push("base_url");
1020
+ if (missing.length > 0) {
1021
+ const where = missing.includes("api_type") && declared.length > 0 ? `; api_type may live on the provider or on a task table, e.g. ${declared.map((task) => `[providers.${tomlKey(name)}.${task}]`).join(" or ")}` : "";
1022
+ throw new ConfigError(
1023
+ `provider "${name}" is missing required field(s): ${missing.join(", ")}; fix [providers.${tomlKey(name)}] in ${file}${where}`
1024
+ );
1025
+ }
1026
+ if (declared.length === 0) {
1027
+ throw new ConfigError(
1028
+ `provider "${name}" declares no task tables; expected ${KNOWN_TASKS.join(", ")}`
1029
+ );
1030
+ }
1031
+ const baseUrl = entry.base_url;
1032
+ const provider = {
1033
+ name,
1034
+ base_url: resolveBaseUrl(name, entry.api_type, file, baseUrl)
1035
+ };
1036
+ if (entry.api_type !== void 0) provider.api_type = entry.api_type;
1037
+ if (entry.api_key_env !== void 0) provider.api_key_env = entry.api_key_env;
1038
+ return provider;
1039
+ }
1040
+ function resolveSelection(request, config) {
1041
+ const name = selectProviderName(request, config);
1042
+ const entry = config.providers[name];
1043
+ const where = `[providers.${tomlKey(name)}.${request.task}]`;
1044
+ if (entry === void 0) {
1045
+ validateProviderShape(name, config);
1046
+ }
1047
+ const block = entry?.tasks[request.task];
1048
+ if (block === void 0) {
1049
+ const declared = entry === void 0 ? [] : taskNames(entry);
1050
+ throw new ConfigError(
1051
+ `provider "${name}" declares no "${request.task}" models; declared tasks: ${declared.length === 0 ? "(none)" : declared.join(", ")}`
1052
+ );
1053
+ }
1054
+ const provider = validateProviderShape(name, config);
1055
+ const dialect = block.api_type ?? entry?.api_type;
1056
+ if (dialect === void 0) {
1057
+ throw new ConfigError(
1058
+ `${where} declares no api_type and [providers.${tomlKey(name)}] sets no default; add api_type = "openai" | "dashscope" | "ark" under ${where} in ${configPath()}`
1059
+ );
1060
+ }
1061
+ if (block.path === void 0) {
1062
+ const issue = vendorPathIssue(provider.base_url);
1063
+ if (issue !== void 0) {
1064
+ const defaultPath = adapterPath(dialect, request.task);
1065
+ throw new ConfigError(
1066
+ `provider "${name}" has base_url ${JSON.stringify(provider.base_url)} in ${configPath()}: it points at the sub-path "${issue.prefix}", and the ${dialect} dialect appends its own path to base_url. Set base_url to the origin (e.g. "${issue.origin}"), or keep the prefix and declare path = ${JSON.stringify(pathAfterVendorRoot(defaultPath) ?? "/<path from the prefix>")} under ${where}`
1067
+ );
1068
+ }
1069
+ }
1070
+ const models = block.models;
1071
+ if (models === void 0) {
1072
+ throw new ConfigError(`${where} is missing required field(s): models`);
1073
+ }
1074
+ if (models.length === 0) {
1075
+ throw new ConfigError(`models must not be empty under ${where}`);
1076
+ }
1077
+ const model = request.model ?? block.default;
1078
+ if (model === void 0) {
1079
+ throw new ConfigError(
1080
+ `${where} sets no default model; pass --model <id> or add default = "<id>"`
1081
+ );
1082
+ }
1083
+ if (!models.includes(model)) {
1084
+ const otherTask = KNOWN_TASKS.find(
1085
+ (task) => task !== request.task && entry?.tasks[task]?.models?.includes(model) === true
1086
+ );
1087
+ if (otherTask !== void 0) {
1088
+ throw new ConfigError(
1089
+ `model "${model}" is declared for task "${otherTask}", not "${request.task}"; run the command that matches its task, or add it under ${where}`
1090
+ );
1091
+ }
1092
+ if (block.default === model) {
1093
+ throw new ConfigError(`default "${model}" is not listed in models under ${where}`);
1094
+ }
1095
+ throw new ConfigError(
1096
+ `unknown model "${model}" for provider "${name}" (task ${request.task}) in ${configPath()}; add it to models = [...] under ${where}`
1097
+ );
1098
+ }
1099
+ getAdapter(dialect, request.task);
1100
+ const seconds = block.timeout_seconds ?? DEFAULT_TIMEOUT_SECONDS[request.task];
1101
+ if (!Number.isInteger(seconds) || seconds < 1 || seconds > MAX_TIMEOUT_SECONDS) {
1102
+ throw new ConfigError(
1103
+ `timeout_seconds must be an integer in 1..${MAX_TIMEOUT_SECONDS} under ${where}, got ${Number.isNaN(seconds) ? "NaN" : String(seconds)}`
1104
+ );
1105
+ }
1106
+ const binding = {
1107
+ provider,
1108
+ task: request.task,
1109
+ model,
1110
+ timeout_ms: seconds * 1e3,
1111
+ dialect
1112
+ };
1113
+ if (block.path !== void 0) binding.path = block.path;
1114
+ return binding;
1115
+ }
1116
+ function selectProviderName(request, config) {
1117
+ if (request.provider !== void 0) return request.provider;
1118
+ if (request.model !== void 0) {
1119
+ const hits = Object.entries(config.providers).filter(([, entry]) => entry.tasks[request.task]?.models?.includes(request.model)).map(([name]) => name);
1120
+ if (hits.length === 1) return hits[0];
1121
+ if (hits.length > 1) {
1122
+ throw new ConfigError(
1123
+ `model "${request.model}" is declared by providers ${hits.map((name) => `"${name}"`).join(", ")}; pass --provider`
1124
+ );
1125
+ }
1126
+ const otherTask = KNOWN_TASKS.find(
1127
+ (task) => task !== request.task && knownModels(task, config).includes(request.model)
1128
+ );
1129
+ if (otherTask !== void 0) {
1130
+ throw new ConfigError(
1131
+ `model "${request.model}" is declared for task "${otherTask}", not "${request.task}"; run the command that matches its task`
1132
+ );
1133
+ }
1134
+ const known = knownModels(request.task, config);
1135
+ throw new ConfigError(
1136
+ `unknown model "${request.model}": no provider declares it for task "${request.task}"; known models for task "${request.task}": ${known.length === 0 ? "(none)" : known.join(", ")}`
1137
+ );
1138
+ }
1139
+ return config.default_provider;
1140
+ }
1141
+ function knownModels(task, config) {
1142
+ const seen = [];
1143
+ for (const entry of Object.values(config.providers)) {
1144
+ for (const model of entry.tasks[task]?.models ?? []) {
1145
+ if (!seen.includes(model)) seen.push(model);
1146
+ }
1147
+ }
1148
+ return seen;
1149
+ }
1150
+ function taskNames(entry) {
1151
+ return KNOWN_TASKS.filter((task) => entry.tasks[task] !== void 0);
1152
+ }
1153
+ function isTask(value) {
1154
+ return KNOWN_TASKS.includes(value);
1155
+ }
1156
+
1157
+ // src/auth.ts
1158
+ function authPath() {
1159
+ return path3.join(stateDir(), "auth.json");
1160
+ }
1161
+ function loadAuth() {
1162
+ const file = authPath();
1163
+ let source;
1164
+ try {
1165
+ source = fs3.readFileSync(file, "utf8");
1166
+ } catch (error) {
1167
+ if (isNotFound(error)) return { providers: {} };
1168
+ throw new ConfigError(unreadableFileMessage(file, error));
1169
+ }
1170
+ let parsed;
1171
+ try {
1172
+ parsed = JSON.parse(source);
1173
+ } catch (error) {
1174
+ throw new ConfigError(malformedDocumentMessage(file, "JSON", error));
1175
+ }
1176
+ return normalizeAuth(parsed, file);
1177
+ }
1178
+ function saveApiKey(providerName, apiKey) {
1179
+ const auth = loadAuth();
1180
+ auth.providers[providerName] = { api_key: apiKey };
1181
+ writeAuth(auth);
1182
+ }
1183
+ function removeApiKey(providerName) {
1184
+ const auth = loadAuth();
1185
+ if (!Object.prototype.hasOwnProperty.call(auth.providers, providerName)) return false;
1186
+ delete auth.providers[providerName];
1187
+ writeAuth(auth);
1188
+ return true;
1189
+ }
1190
+ function maskKey(key) {
1191
+ if (key.length < 12) return "***";
1192
+ return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
1193
+ }
1194
+ function resolveApiKey(provider, flagKey, env = process.env) {
1195
+ if (flagKey) return { key: flagKey, source: "flag" };
1196
+ const configuredEnv = provider.api_key_env;
1197
+ if (configuredEnv) {
1198
+ const value = env[configuredEnv];
1199
+ if (value) return { key: value, source: "env" };
1200
+ }
1201
+ const stored = loadAuth().providers[provider.name]?.api_key;
1202
+ if (stored) return { key: stored, source: "auth" };
1203
+ return null;
1204
+ }
1205
+ function writeAuth(auth) {
1206
+ const file = authPath();
1207
+ const dir = path3.dirname(file);
1208
+ fs3.mkdirSync(dir, { recursive: true, mode: 448 });
1209
+ const tmp = path3.join(dir, `.auth.json.${process.pid}.${randomUUID2()}.tmp`);
1210
+ try {
1211
+ fs3.writeFileSync(tmp, `${JSON.stringify(auth, null, 2)}
1212
+ `, { mode: 384 });
1213
+ fs3.renameSync(tmp, file);
1214
+ } catch (error) {
1215
+ try {
1216
+ fs3.unlinkSync(tmp);
1217
+ } catch {
1218
+ }
1219
+ throw error;
1220
+ }
1221
+ fs3.chmodSync(file, 384);
1222
+ }
1223
+ function normalizeAuth(parsed, file) {
1224
+ if (!isRecord5(parsed)) {
1225
+ throw new ConfigError(`auth file ${file} must contain a JSON object`);
1226
+ }
1227
+ const rawProviders = parsed.providers;
1228
+ if (rawProviders === void 0) return { providers: {} };
1229
+ if (!isRecord5(rawProviders)) {
1230
+ throw new ConfigError(`"providers" in ${file} must be an object`);
1231
+ }
1232
+ const providers = {};
1233
+ for (const [name, entry] of Object.entries(rawProviders)) {
1234
+ if (!isRecord5(entry) || typeof entry.api_key !== "string") {
1235
+ throw new ConfigError(`provider "${name}" in ${file} must have a string "api_key"`);
1236
+ }
1237
+ providers[name] = { api_key: entry.api_key };
1238
+ }
1239
+ return { providers };
1240
+ }
1241
+ function isRecord5(value) {
1242
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1243
+ }
1244
+ function isNotFound(error) {
1245
+ return isRecord5(error) && error.code === "ENOENT";
1246
+ }
1247
+
1248
+ // src/generate.ts
1249
+ import fs5 from "fs";
1250
+ import os2 from "os";
1251
+ import path5 from "path";
1252
+
1253
+ // src/download.ts
1254
+ import fs4 from "fs";
1255
+ import path4 from "path";
1256
+ var DOWNLOAD_TIMEOUT_MS = 12e4;
1257
+ var VIDEO_DOWNLOAD_TIMEOUT_MS = 6e5;
1258
+ var SAFE_EXTENSION = /^[a-z0-9]+$/;
1259
+ var KNOWN_IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
1260
+ "png",
1261
+ "jpg",
1262
+ "jpeg",
1263
+ "webp",
1264
+ "bmp",
1265
+ "tif",
1266
+ "tiff",
1267
+ "gif",
1268
+ "heic",
1269
+ "heif"
1270
+ ]);
1271
+ var KNOWN_VIDEO_EXTENSIONS = /* @__PURE__ */ new Set(["mp4", "mov", "webm", "mkv"]);
1272
+ var IMAGE_CONTENT_TYPE_EXTENSIONS = {
1273
+ "image/png": "png",
1274
+ "image/jpeg": "jpg",
1275
+ "image/webp": "webp",
1276
+ "image/bmp": "bmp",
1277
+ "image/tiff": "tiff",
1278
+ "image/gif": "gif",
1279
+ "image/heic": "heic",
1280
+ "image/heif": "heif"
1281
+ };
1282
+ var VIDEO_CONTENT_TYPE_EXTENSIONS = {
1283
+ "video/mp4": "mp4",
1284
+ "video/quicktime": "mov",
1285
+ "video/webm": "webm",
1286
+ "video/x-matroska": "mkv"
1287
+ };
1288
+ function buildFilename(params) {
1289
+ return `${params.provider}-${slugModel(params.model)}-${params.stamp}-${params.index}.${params.ext}`;
1290
+ }
1291
+ function errorMessage2(error) {
1292
+ return error instanceof Error ? error.message : String(error);
1293
+ }
1294
+ function isTimeoutError(error) {
1295
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
1296
+ }
1297
+ function downloadFailure(url, error, timeoutMs) {
1298
+ if (isTimeoutError(error)) {
1299
+ return new ApiError(`download timeout after ${timeoutMs / 1e3}s for ${url}`);
1300
+ }
1301
+ return new ApiError(`failed to download ${url}: ${errorMessage2(error)}`);
1302
+ }
1303
+ function extensionFromUrl(url, knownExtensions) {
1304
+ const pathname = url.split(/[?#]/)[0] ?? "";
1305
+ const extension = path4.extname(pathname).toLowerCase().replace(/^\./, "");
1306
+ return knownExtensions.has(extension) ? extension : void 0;
1307
+ }
1308
+ function extensionFromContentType(contentType, knownExtensions) {
1309
+ const mime = contentType.split(";")[0]?.trim().toLowerCase();
1310
+ return mime === void 0 ? void 0 : knownExtensions[mime];
1311
+ }
1312
+ function inferExtension(result, contentType, kind = "image") {
1313
+ if (kind === "video") {
1314
+ const declared = "format" in result ? result.format?.trim().toLowerCase() : void 0;
1315
+ if (declared !== void 0 && SAFE_EXTENSION.test(declared) && KNOWN_VIDEO_EXTENSIONS.has(declared)) {
1316
+ return declared;
1317
+ }
1318
+ const fromVideoUrl = extensionFromUrl(result.url, KNOWN_VIDEO_EXTENSIONS);
1319
+ if (fromVideoUrl !== void 0) return fromVideoUrl;
1320
+ if (contentType !== void 0) {
1321
+ const fromContentType = extensionFromContentType(contentType, VIDEO_CONTENT_TYPE_EXTENSIONS);
1322
+ if (fromContentType !== void 0) return fromContentType;
1323
+ }
1324
+ return "mp4";
1325
+ }
1326
+ const outputFormat = "outputFormat" in result ? result.outputFormat?.trim().toLowerCase() : void 0;
1327
+ if (outputFormat !== void 0 && SAFE_EXTENSION.test(outputFormat)) {
1328
+ return outputFormat === "jpeg" ? "jpg" : outputFormat;
1329
+ }
1330
+ const fromUrl = extensionFromUrl(result.url, KNOWN_IMAGE_EXTENSIONS);
1331
+ if (fromUrl !== void 0) return fromUrl;
1332
+ if (contentType !== void 0) {
1333
+ const fromContentType = extensionFromContentType(contentType, IMAGE_CONTENT_TYPE_EXTENSIONS);
1334
+ if (fromContentType !== void 0) return fromContentType;
1335
+ }
1336
+ return "png";
1337
+ }
1338
+ function uniquePath(filePath) {
1339
+ if (!fs4.existsSync(filePath)) return filePath;
1340
+ const dir = path4.dirname(filePath);
1341
+ const ext = path4.extname(filePath);
1342
+ const base = path4.basename(filePath, ext);
1343
+ for (let suffix = 2; ; suffix += 1) {
1344
+ const candidate = path4.join(dir, `${base}-${suffix}${ext}`);
1345
+ if (!fs4.existsSync(candidate)) return candidate;
1346
+ }
1347
+ }
1348
+ async function downloadMedia(result, opts) {
1349
+ const timeoutMs = opts.kind === "video" ? VIDEO_DOWNLOAD_TIMEOUT_MS : DOWNLOAD_TIMEOUT_MS;
1350
+ fs4.mkdirSync(opts.outDir, { recursive: true });
1351
+ let response;
1352
+ try {
1353
+ response = await fetch(result.url, { signal: AbortSignal.timeout(timeoutMs) });
1354
+ } catch (error) {
1355
+ throw downloadFailure(result.url, error, timeoutMs);
1356
+ }
1357
+ if (!response.ok) {
1358
+ throw new ApiError(`download failed with status ${response.status} for ${result.url}`);
1359
+ }
1360
+ let bytes;
1361
+ try {
1362
+ bytes = Buffer.from(await response.arrayBuffer());
1363
+ } catch (error) {
1364
+ throw downloadFailure(result.url, error, timeoutMs);
1365
+ }
1366
+ const ext = inferExtension(result, response.headers.get("content-type") ?? void 0, opts.kind);
1367
+ const name = buildFilename({
1368
+ provider: opts.provider,
1369
+ model: opts.model,
1370
+ stamp: opts.stamp,
1371
+ index: opts.index,
1372
+ ext
1373
+ });
1374
+ const target = uniquePath(path4.resolve(opts.outDir, name));
1375
+ fs4.writeFileSync(target, bytes);
1376
+ return { path: target, url: result.url, index: opts.index, meta: result };
1377
+ }
1378
+ async function downloadImage(result, opts) {
1379
+ return downloadMedia(result, { ...opts, kind: "image" });
1380
+ }
1381
+
1382
+ // src/http.ts
1383
+ var BODY_EXCERPT_LIMIT = 200;
1384
+ var QWEN_WORKSPACE_BASE_URL = "https://<workspace-id>.cn-beijing.maas.aliyuncs.com";
1385
+ var REQUEST_TIMEOUT_MS = 6e4;
1386
+ function errorMessage3(error) {
1387
+ return error instanceof Error ? error.message : String(error);
1388
+ }
1389
+ function isTimeoutError2(error) {
1390
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
1391
+ }
1392
+ function isRecord6(value) {
1393
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1394
+ }
1395
+ function redactorFor(apiKey) {
1396
+ if (apiKey === "") return (text) => text;
1397
+ return (text) => text.split(apiKey).join("[redacted]");
1398
+ }
1399
+ var ANSI_ESCAPE_SEQUENCE = /\u001b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\))/g;
1400
+ var CONTROL_BYTE = /[\u0000-\u001f\u007f-\u009f]/g;
1401
+ function oneLine(text) {
1402
+ return text.replace(ANSI_ESCAPE_SEQUENCE, "").replace(CONTROL_BYTE, " ").replace(/\s+/g, " ").trim();
1403
+ }
1404
+ function capLength(line) {
1405
+ return line.length > BODY_EXCERPT_LIMIT ? `${line.slice(0, BODY_EXCERPT_LIMIT)}...` : line;
1406
+ }
1407
+ function bodyExcerpt(text) {
1408
+ const line = oneLine(text);
1409
+ return line === "" ? "empty body" : capLength(line);
1410
+ }
1411
+ function safeLine(text) {
1412
+ return capLength(oneLine(text));
1413
+ }
1414
+ function httpError(message, status, code) {
1415
+ return new ApiError(message, code, void 0, status);
1416
+ }
1417
+ function hostOf(url) {
1418
+ try {
1419
+ return new URL(url).host;
1420
+ } catch {
1421
+ return url;
1422
+ }
1423
+ }
1424
+ function vendorError(json) {
1425
+ if (!isRecord6(json) || !isRecord6(json.error)) return void 0;
1426
+ const message = typeof json.error.message === "string" ? json.error.message.trim() : "";
1427
+ if (message === "") return void 0;
1428
+ const code = vendorCode(json.error.code);
1429
+ return code === void 0 ? { message } : { code, message };
1430
+ }
1431
+ function vendorCode(code) {
1432
+ if (typeof code === "string") return code === "" ? void 0 : code;
1433
+ if (typeof code === "number" && Number.isFinite(code)) return String(code);
1434
+ return void 0;
1435
+ }
1436
+ function parseJson(text, status, redact) {
1437
+ if (text.trim() === "") {
1438
+ throw httpError(`API response with status ${status} has an empty body`, status);
1439
+ }
1440
+ try {
1441
+ return JSON.parse(text);
1442
+ } catch {
1443
+ throw httpError(
1444
+ `API response with status ${status} is not valid JSON: ${redact(bodyExcerpt(text))}`,
1445
+ status
1446
+ );
1447
+ }
1448
+ }
1449
+ function missingQwenEndpointMessage(provider, status, redact) {
1450
+ const host = redact(hostOf(provider.base_url));
1451
+ return [
1452
+ `${provider.name} API request failed with status ${status}: this endpoint does not exist on ${host}.`,
1453
+ `The OpenAI-compatible images API is served only on your workspace-scoped Bailian host, ${QWEN_WORKSPACE_BASE_URL}.`,
1454
+ `Fix it by setting base_url = "${QWEN_WORKSPACE_BASE_URL}" under [providers.${tomlKey(provider.name)}] in config.toml \u2014 base_url is the origin, and the dialect appends the endpoint path, version segment included \u2014 then run the command again.`
1455
+ ].join("\n");
1456
+ }
1457
+ function errorForStatus(text, status, provider, dialect, adapter, redact) {
1458
+ if (status === 404 && text.trim() === "" && dialect === "openai") {
1459
+ return httpError(missingQwenEndpointMessage(provider, status, redact), status);
1460
+ }
1461
+ let json;
1462
+ try {
1463
+ json = text.trim() === "" ? void 0 : JSON.parse(text);
1464
+ } catch {
1465
+ json = void 0;
1466
+ }
1467
+ const vendor = adapter.parseErrorBody?.(json) ?? vendorError(json);
1468
+ if (vendor !== void 0) {
1469
+ const code = vendor.code === void 0 ? void 0 : redact(vendor.code);
1470
+ const codeSuffix = code === void 0 ? "" : ` (code: ${code})`;
1471
+ return httpError(
1472
+ `${provider.name} API request failed with status ${status}: ${redact(vendor.message)}${codeSuffix}`,
1473
+ status,
1474
+ code
1475
+ );
1476
+ }
1477
+ return httpError(
1478
+ `${provider.name} API request failed with status ${status}: ${redact(bodyExcerpt(text))}`,
1479
+ status
1480
+ );
1481
+ }
1482
+ async function readResponseText(response, url, redact, timeoutSeconds, timeoutMessage) {
1483
+ try {
1484
+ return await response.text();
1485
+ } catch (error) {
1486
+ if (isTimeoutError2(error)) {
1487
+ throw httpError(
1488
+ redact(
1489
+ timeoutMessage ?? `API request timeout after ${timeoutSeconds}s while reading the response from ${redact(hostOf(url))}`
1490
+ ),
1491
+ NO_RESPONSE_STATUS
1492
+ );
1493
+ }
1494
+ throw httpError(
1495
+ `failed to read the API response from ${redact(hostOf(url))}: ${redact(errorMessage3(error))}`,
1496
+ NO_RESPONSE_STATUS
1497
+ );
1498
+ }
1499
+ }
1500
+ function isImageAdapter(adapter) {
1501
+ return "parseResponse" in adapter;
1502
+ }
1503
+ async function callApi(params) {
1504
+ const { binding, apiKey, request } = params;
1505
+ const provider = binding.provider;
1506
+ const timeoutSeconds = binding.timeout_ms / 1e3;
1507
+ const adapter = getAdapter(binding.dialect, binding.task);
1508
+ if (!isImageAdapter(adapter)) {
1509
+ throw new ApiError(
1510
+ `this request is for the image task; the binding is for the "${binding.task}" task`
1511
+ );
1512
+ }
1513
+ const body = adapter.buildRequest(request);
1514
+ const payload = JSON.stringify(body);
1515
+ const redact = redactorFor(apiKey);
1516
+ const url = `${provider.base_url}${binding.path ?? adapter.createPath}`;
1517
+ const signal = AbortSignal.timeout(binding.timeout_ms);
1518
+ let response;
1519
+ try {
1520
+ response = await fetch(url, {
1521
+ method: "POST",
1522
+ headers: {
1523
+ Authorization: `Bearer ${apiKey}`,
1524
+ "Content-Type": "application/json"
1525
+ },
1526
+ body: payload,
1527
+ signal
1528
+ });
1529
+ } catch (error) {
1530
+ if (isTimeoutError2(error)) {
1531
+ throw httpError(
1532
+ `${provider.name} API request timeout after ${timeoutSeconds}s (${redact(url)})`,
1533
+ NO_RESPONSE_STATUS
1534
+ );
1535
+ }
1536
+ throw httpError(
1537
+ `${provider.name} API request to ${redact(hostOf(url))} failed: ${redact(errorMessage3(error))}`,
1538
+ NO_RESPONSE_STATUS
1539
+ );
1540
+ }
1541
+ const text = await readResponseText(response, url, redact, timeoutSeconds);
1542
+ if (!response.ok) {
1543
+ throw errorForStatus(text, response.status, provider, binding.dialect, adapter, redact);
1544
+ }
1545
+ const json = parseJson(text, response.status, redact);
1546
+ const requestId = requestIdOf(response);
1547
+ const parsed = adapter.parseResponse(json, requestId);
1548
+ if (parsed.requestId === void 0 && requestId !== void 0) {
1549
+ parsed.requestId = requestId;
1550
+ }
1551
+ return parsed;
1552
+ }
1553
+ var UNRESOLVED_CREATE_NOTE = "the task may already have been created and paid for. No task id came back, so check your provider's console or usage page for a recent task before submitting again.";
1554
+ function createTimeoutMessage(provider) {
1555
+ return `${provider.name} create request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s: ` + UNRESOLVED_CREATE_NOTE;
1556
+ }
1557
+ function unresolvedSuffix(note) {
1558
+ return note === void 0 ? "" : ` ${note}`;
1559
+ }
1560
+ function withNote(error, note) {
1561
+ if (note === void 0) return error;
1562
+ const message = error instanceof Error ? error.message : String(error);
1563
+ if (error instanceof ApiError) {
1564
+ return new ApiError(`${message} ${note}`, error.code, error.taskId, error.status);
1565
+ }
1566
+ return new ApiError(`${message} ${note}`);
1567
+ }
1568
+ function unknownTaskMessage(provider, taskId, host) {
1569
+ return (bodyText) => {
1570
+ const lines = [
1571
+ // The id is echoed back, so it is treated like any other untrusted text and
1572
+ // bounded like a body: pasting tens of KB must not flood the terminal.
1573
+ `${provider.name} API request failed with status 404: no task "${safeLine(taskId)}" exists on ${host}.`,
1574
+ "That is not a failed task: a task id is readable only on the provider that created it, so check --provider and that the id is complete."
1575
+ ];
1576
+ if (oneLine(bodyText) !== "") lines.push(`The response said: ${bodyExcerpt(bodyText)}`);
1577
+ return lines.join("\n");
1578
+ };
1579
+ }
1580
+ function requestIdOf(response) {
1581
+ const header = response.headers.get("x-request-id");
1582
+ return header === null || header === "" ? void 0 : header;
1583
+ }
1584
+ async function sendVideoRequest(params) {
1585
+ const { binding, adapter, apiKey, method, url, body, timeoutMessage, notFoundMessage, unresolvedNote } = params;
1586
+ const provider = binding.provider;
1587
+ const redact = redactorFor(apiKey);
1588
+ const timeoutSeconds = REQUEST_TIMEOUT_MS / 1e3;
1589
+ const headers = { Authorization: `Bearer ${apiKey}` };
1590
+ if (body !== void 0) headers["Content-Type"] = "application/json";
1591
+ let response;
1592
+ try {
1593
+ response = await fetch(url, {
1594
+ method,
1595
+ headers,
1596
+ body,
1597
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
1598
+ });
1599
+ } catch (error) {
1600
+ if (isTimeoutError2(error)) {
1601
+ throw httpError(
1602
+ redact(
1603
+ timeoutMessage ?? `${provider.name} API request timeout after ${timeoutSeconds}s (${url})`
1604
+ ),
1605
+ NO_RESPONSE_STATUS
1606
+ );
1607
+ }
1608
+ throw httpError(
1609
+ `${provider.name} API request to ${redact(hostOf(url))} failed: ${redact(errorMessage3(error))}${unresolvedSuffix(unresolvedNote)}`,
1610
+ NO_RESPONSE_STATUS
1611
+ );
1612
+ }
1613
+ let text;
1614
+ try {
1615
+ text = await readResponseText(
1616
+ response,
1617
+ url,
1618
+ redact,
1619
+ timeoutSeconds,
1620
+ // A create whose body could not be read may already be accepted and paid
1621
+ // for, so the dangerous wording has to survive a read timeout too.
1622
+ timeoutMessage
1623
+ );
1624
+ } catch (error) {
1625
+ throw withNote(error, timeoutMessage === void 0 ? unresolvedNote : void 0);
1626
+ }
1627
+ if (!response.ok) {
1628
+ if (response.status === 404 && notFoundMessage !== void 0) {
1629
+ throw httpError(redact(notFoundMessage(text)), response.status);
1630
+ }
1631
+ const note = response.status >= 500 ? unresolvedNote : void 0;
1632
+ throw withNote(
1633
+ errorForStatus(text, response.status, provider, binding.dialect, adapter, redact),
1634
+ note
1635
+ );
1636
+ }
1637
+ let json;
1638
+ try {
1639
+ json = parseJson(text, response.status, redact);
1640
+ } catch (error) {
1641
+ throw withNote(error, unresolvedNote);
1642
+ }
1643
+ return {
1644
+ json,
1645
+ requestId: requestIdOf(response)
1646
+ };
1647
+ }
1648
+ function redactedApiError(error, redact) {
1649
+ if (!(error instanceof ApiError)) return error;
1650
+ const message = redact(error.message);
1651
+ const code = error.code === void 0 ? void 0 : redact(error.code);
1652
+ if (message === error.message && code === error.code) return error;
1653
+ return new ApiError(message, code, error.taskId, error.status);
1654
+ }
1655
+ function redacting(action, redact) {
1656
+ try {
1657
+ return action();
1658
+ } catch (error) {
1659
+ throw redactedApiError(error, redact);
1660
+ }
1661
+ }
1662
+ async function createTask(params) {
1663
+ const { binding, apiKey, request } = params;
1664
+ const redact = redactorFor(apiKey);
1665
+ const adapter = getAdapter(binding.dialect, "video");
1666
+ const payload = redacting(() => JSON.stringify(adapter.buildRequest(request)), redact);
1667
+ const url = `${binding.provider.base_url}${binding.path ?? adapter.createPath}`;
1668
+ const { json, requestId } = await sendVideoRequest({
1669
+ binding,
1670
+ adapter,
1671
+ apiKey,
1672
+ method: "POST",
1673
+ url,
1674
+ body: payload,
1675
+ timeoutMessage: createTimeoutMessage(binding.provider),
1676
+ unresolvedNote: UNRESOLVED_CREATE_NOTE
1677
+ });
1678
+ let taskId;
1679
+ try {
1680
+ taskId = safeLine(redacting(() => adapter.taskIdFrom(json), redact));
1681
+ } catch (error) {
1682
+ throw withNote(error, UNRESOLVED_CREATE_NOTE);
1683
+ }
1684
+ return requestId === void 0 ? { taskId } : { taskId, requestId };
1685
+ }
1686
+ function redactedOutcome(outcome, redact) {
1687
+ switch (outcome.state) {
1688
+ case "failed":
1689
+ case "cancelled":
1690
+ case "expired": {
1691
+ const failure = {
1692
+ message: redact(outcome.failure.message)
1693
+ };
1694
+ if (outcome.failure.code !== void 0) failure.code = redact(outcome.failure.code);
1695
+ return { state: outcome.state, failure };
1696
+ }
1697
+ default:
1698
+ return outcome;
1699
+ }
1700
+ }
1701
+ async function pollTask(params) {
1702
+ const { binding, apiKey, taskId } = params;
1703
+ const provider = binding.provider;
1704
+ const adapter = getAdapter(binding.dialect, "video");
1705
+ const url = binding.path === void 0 ? `${provider.base_url}${adapter.pollPath(taskId)}` : `${provider.base_url}${binding.path}/${encodeURIComponent(taskId)}`;
1706
+ const { json, requestId } = await sendVideoRequest({
1707
+ binding,
1708
+ adapter,
1709
+ apiKey,
1710
+ method: "GET",
1711
+ url,
1712
+ notFoundMessage: unknownTaskMessage(provider, taskId, hostOf(url))
1713
+ });
1714
+ const redact = redactorFor(apiKey);
1715
+ const outcome = redactedOutcome(redacting(() => adapter.pollOutcome(json), redact), redact);
1716
+ return requestId === void 0 ? outcome : { ...outcome, requestId };
1717
+ }
1718
+
1719
+ // src/generate.ts
1720
+ var SIZE_TIERS = ["1K", "1.5K", "2K", "3K", "4K"];
1721
+ var PIXEL_PAIR2 = /^(\d+)x(\d+)$/;
1722
+ var N_LIMIT = 6;
1723
+ function validateSize(size, dialect) {
1724
+ const value = size.trim();
1725
+ if (dialect === "dashscope") {
1726
+ const native = PIXEL_PAIR2.exec(value);
1727
+ if (native !== null && Number(native[1]) > 0 && Number(native[2]) > 0) return;
1728
+ throw new UsageError(
1729
+ `invalid --size ${JSON.stringify(size)}: the dashscope dialect accepts only a WxH pixel pair like --size 2048x2048; size tiers and "auto" belong to the openai dialect`
1730
+ );
1731
+ }
1732
+ if (SIZE_TIERS.includes(value.toUpperCase())) return;
1733
+ const pair = PIXEL_PAIR2.exec(value);
1734
+ if (pair !== null && Number(pair[1]) > 0 && Number(pair[2]) > 0) return;
1735
+ if (dialect === "openai" && value.toLowerCase() === "auto") return;
1736
+ const accepted = dialect === "openai" ? `a tier (${SIZE_TIERS.join(", ")}), a WxH pixel pair of positive integers using a lowercase "x" (e.g. 1024x1024), or "auto"` : `a tier (${SIZE_TIERS.join(", ")}) or a WxH pixel pair of positive integers using a lowercase "x" (e.g. 1024x1024); "auto" is openai-only`;
1737
+ throw new UsageError(`invalid --size ${JSON.stringify(size)}: expected ${accepted}`);
1738
+ }
1739
+ function resolveOutDir(outDir, configOutDir, cwd = process.cwd(), home = os2.homedir()) {
1740
+ return path5.resolve(cwd, expandHome(outDir ?? configOutDir ?? cwd, home));
1741
+ }
1742
+ function expandHome(value, home) {
1743
+ if (value === "~") return home;
1744
+ if (value.startsWith("~/") || value.startsWith("~\\")) return path5.join(home, value.slice(2));
1745
+ return value;
1746
+ }
1747
+ function normalizeBaseUrl(value) {
1748
+ const normalized = normalizeHttpBaseUrl(value);
1749
+ if (normalized === void 0) {
1750
+ throw new UsageError(
1751
+ `invalid --base-url ${JSON.stringify(value)}: expected an absolute http(s) origin without credentials, query or fragment`
1752
+ );
1753
+ }
1754
+ const issue = vendorPathIssue(normalized);
1755
+ if (issue !== void 0) {
1756
+ throw new UsageError(
1757
+ `invalid --base-url ${JSON.stringify(value)}: it points at the sub-path "${issue.prefix}", and the dialect appends its own path; set base_url to the origin, e.g. "${issue.origin}"`
1758
+ );
1759
+ }
1760
+ return normalized;
1761
+ }
1762
+ function validateN(value) {
1763
+ const n = value ?? 1;
1764
+ if (!Number.isInteger(n) || n < 1) {
1765
+ throw new UsageError(
1766
+ `invalid --n ${JSON.stringify(value)}: expected an integer in the accepted range 1-${N_LIMIT}`
1767
+ );
1768
+ }
1769
+ if (n > N_LIMIT) {
1770
+ throw new UsageError(
1771
+ `invalid --n ${n}: the limit is ${N_LIMIT} images per run; the accepted range is 1-${N_LIMIT}`
1772
+ );
1773
+ }
1774
+ return n;
1775
+ }
1776
+ function validateSeed(value) {
1777
+ if (value !== void 0 && !Number.isInteger(value)) {
1778
+ throw new UsageError(`invalid --seed ${JSON.stringify(value)}: expected an integer`);
1779
+ }
1780
+ return value;
1781
+ }
1782
+ function ensureOutDir(dir) {
1783
+ try {
1784
+ fs5.mkdirSync(dir, { recursive: true });
1785
+ } catch (error) {
1786
+ const cause = error instanceof Error ? error.message : String(error);
1787
+ throw new UsageError(
1788
+ `cannot create the output directory ${dir}: ${cause}; check --out-dir (or output_dir in config.toml)`
1789
+ );
1790
+ }
1791
+ }
1792
+ function missingKeyError(provider) {
1793
+ const hints = [`run "mediagen login --provider ${shellArg(provider.name)}"`];
1794
+ if (provider.api_key_env) hints.push(`set ${provider.api_key_env}`);
1795
+ hints.push("pass --api-key");
1796
+ return new ConfigError(
1797
+ `no API key found for provider "${provider.name}": ${hints.join(", ")}`
1798
+ );
1799
+ }
1800
+ function buildImageRequest(prompt, opts, model, images, n, seed) {
1801
+ const request = { model, prompt, images, n };
1802
+ if (seed !== void 0) request.seed = seed;
1803
+ if (opts.size !== void 0) request.size = opts.size;
1804
+ if (opts.watermark !== void 0) request.watermark = opts.watermark;
1805
+ if (opts.negativePrompt !== void 0) request.negative_prompt = opts.negativePrompt;
1806
+ if (opts.promptExtend !== void 0) request.prompt_extend = opts.promptExtend;
1807
+ if (opts.promptExtendMode !== void 0) request.prompt_extend_mode = opts.promptExtendMode;
1808
+ if (opts.enableThinking !== void 0) request.enable_thinking = opts.enableThinking;
1809
+ if (opts.outputFormat !== void 0) request.output_format = opts.outputFormat;
1810
+ if (opts.optimizePrompt !== void 0) request.optimize_prompt_mode = opts.optimizePrompt;
1811
+ if (opts.layerDecomposition !== void 0) request.layer_decomposition = opts.layerDecomposition;
1812
+ if (opts.background !== void 0) request.background = opts.background;
1813
+ return request;
1814
+ }
1815
+ function fanOut(request, count, seed) {
1816
+ return Array.from({ length: count }, (_unused, index) => {
1817
+ const sub = { ...request, n: 1 };
1818
+ if (seed !== void 0) sub.seed = seed + index;
1819
+ return sub;
1820
+ });
1821
+ }
1822
+ function failedResponse(error) {
1823
+ const message = error instanceof Error ? error.message : String(error);
1824
+ const code = error instanceof ApiError && error.code !== void 0 ? error.code : "RequestFailed";
1825
+ return { images: [], failures: [{ code, message }] };
1826
+ }
1827
+ function mergeResponses(responses) {
1828
+ const merged = { images: [], failures: [] };
1829
+ for (const response of responses) {
1830
+ merged.images.push(...response.images);
1831
+ merged.failures.push(...response.failures);
1832
+ if (merged.usage === void 0) merged.usage = response.usage;
1833
+ if (merged.requestId === void 0) merged.requestId = response.requestId;
1834
+ }
1835
+ return merged;
1836
+ }
1837
+ function noImagesError(providerName, failures) {
1838
+ const [first] = failures;
1839
+ if (first === void 0) {
1840
+ return new ApiError(`${providerName} API returned no images`);
1841
+ }
1842
+ const detail = failures.map((failure) => `${failure.code}: ${failure.message}`).join("; ");
1843
+ return new ApiError(
1844
+ `${providerName} API returned no images; ${failures.length} request(s) failed: ${detail}`,
1845
+ first.code
1846
+ );
1847
+ }
1848
+ function downloadFailure2(index, image, error) {
1849
+ const cause = error instanceof Error ? error.message : String(error);
1850
+ return { code: "DownloadError", message: `image ${index} (${image.url}): ${cause}` };
1851
+ }
1852
+ function nothingSavedError(providerName, failures) {
1853
+ const detail = failures.map((failure) => `${failure.code}: ${failure.message}`).join("; ");
1854
+ const code = failures[0]?.code;
1855
+ return new ApiError(
1856
+ `${providerName} API returned images but none could be saved; ${failures.length} failure(s): ${detail}`,
1857
+ code === void 0 || code === "" ? "DownloadError" : code
1858
+ );
1859
+ }
1860
+ async function runGenerate(prompt, opts, env = process.env) {
1861
+ if (opts.edit && opts.images.length === 0) {
1862
+ throw new UsageError("edit requires at least one --image");
1863
+ }
1864
+ const n = validateN(opts.n);
1865
+ const seed = validateSeed(opts.seed);
1866
+ const baseUrl = opts.baseUrl === void 0 ? void 0 : normalizeBaseUrl(opts.baseUrl);
1867
+ const config = loadConfig();
1868
+ const binding = resolveSelection(
1869
+ { provider: opts.provider, model: opts.model, task: "image" },
1870
+ config
1871
+ );
1872
+ const provider = baseUrl === void 0 ? binding.provider : { ...binding.provider, base_url: baseUrl };
1873
+ const resolved = { ...binding, provider };
1874
+ const adapter = getAdapter(binding.dialect, "image");
1875
+ if (opts.size !== void 0) validateSize(opts.size, binding.dialect);
1876
+ const model = resolved.model;
1877
+ const draft = buildImageRequest(prompt, opts, model, opts.images, n, seed);
1878
+ adapter.buildRequest(draft);
1879
+ const request = { ...draft, images: draft.images.map(toImageInput) };
1880
+ const auth = resolveApiKey(provider, opts.apiKey, env);
1881
+ if (auth === null) throw missingKeyError(provider);
1882
+ const outDir = resolveOutDir(opts.outDir, config.output_dir);
1883
+ ensureOutDir(outDir);
1884
+ const fanOutRequests = binding.dialect === "ark" && n > 1 ? fanOut(request, n, seed) : [request];
1885
+ const responses = fanOutRequests.length === 1 ? [await callApi({ binding: resolved, apiKey: auth.key, request: fanOutRequests[0] })] : await Promise.all(
1886
+ fanOutRequests.map(
1887
+ (sub) => callApi({ binding: resolved, apiKey: auth.key, request: sub }).catch(failedResponse)
1888
+ )
1889
+ );
1890
+ const response = mergeResponses(responses);
1891
+ if (response.images.length === 0) throw noImagesError(provider.name, response.failures);
1892
+ const stamp = nowStamp();
1893
+ const outcomes = await Promise.allSettled(
1894
+ response.images.map(
1895
+ (image, index) => downloadImage(image, { outDir, provider: provider.name, model, stamp, index })
1896
+ )
1897
+ );
1898
+ const saved = [];
1899
+ response.images.forEach((image, index) => {
1900
+ const outcome = outcomes[index];
1901
+ if (outcome.status === "fulfilled") {
1902
+ saved.push(outcome.value);
1903
+ } else {
1904
+ response.failures.push(downloadFailure2(index, image, outcome.reason));
1905
+ }
1906
+ });
1907
+ if (saved.length === 0) throw nothingSavedError(provider.name, response.failures);
1908
+ return { saved, response, provider };
1909
+ }
1910
+
1911
+ // src/video.ts
1912
+ var RATIOS = ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"];
1913
+ function validateResolution(value, adapter) {
1914
+ const tier = value.trim().toLowerCase();
1915
+ if (!adapter.resolutions.includes(tier)) {
1916
+ throw new UsageError(
1917
+ `invalid --resolution ${JSON.stringify(value)}: the ${adapter.dialect} dialect accepts ${adapter.resolutions.join(", ")}; whether a model supports a tier is checked by the vendor`
1918
+ );
1919
+ }
1920
+ return tier;
1921
+ }
1922
+ function validateDuration(value) {
1923
+ if (value === void 0) return void 0;
1924
+ if (!Number.isInteger(value) || value < 1) {
1925
+ throw new UsageError(
1926
+ `invalid --duration ${JSON.stringify(value)}: expected a positive integer of seconds, e.g. --duration 5; the range a model allows is checked by the vendor`
1927
+ );
1928
+ }
1929
+ return value;
1930
+ }
1931
+ function validateSeed2(value) {
1932
+ if (value === void 0) return void 0;
1933
+ if (!Number.isInteger(value) || value < 0) {
1934
+ throw new UsageError(`invalid --seed ${JSON.stringify(value)}: expected a non-negative integer`);
1935
+ }
1936
+ return value;
1937
+ }
1938
+ function validateRatio(value) {
1939
+ if (value === void 0) return void 0;
1940
+ const ratio = value.trim();
1941
+ if (!RATIOS.includes(ratio)) {
1942
+ throw new UsageError(
1943
+ `invalid --ratio ${JSON.stringify(value)}: expected one of ${RATIOS.join(", ")}`
1944
+ );
1945
+ }
1946
+ return ratio;
1947
+ }
1948
+ function validatePrompt(prompt) {
1949
+ const text = prompt.trim();
1950
+ if (text === "") {
1951
+ throw new UsageError("a video prompt is required: pass the clip's description as the first argument");
1952
+ }
1953
+ return text;
1954
+ }
1955
+ function firstFrame(images) {
1956
+ if (images.length > 1) {
1957
+ throw new UsageError(
1958
+ `only one --image is supported, the clip's first frame; the second --image ${JSON.stringify(images[1])} would be a last frame (last_frame), and first/last-frame mode is not implemented yet`
1959
+ );
1960
+ }
1961
+ return images[0];
1962
+ }
1963
+ var DIALECT_SPECIFIC_FIELDS = [
1964
+ { option: "generateAudio", field: "generate_audio", flag: "--generate-audio" },
1965
+ { option: "outputFormat", field: "output_format", flag: "--output-format" },
1966
+ { option: "watermark", field: "watermark", flag: "--watermark" },
1967
+ { option: "seed", field: "seed", flag: "--seed" }
1968
+ ];
1969
+ function rejectUndeclaredFlags(opts, adapter) {
1970
+ const declared = adapter.exclusiveFields ?? [];
1971
+ for (const { option, field, flag } of DIALECT_SPECIFIC_FIELDS) {
1972
+ if (opts[option] !== void 0 && !declared.includes(field)) {
1973
+ throw undeclaredFlag(flag, field, adapter);
1974
+ }
1975
+ }
1976
+ }
1977
+ function undeclaredFlag(flag, field, adapter) {
1978
+ return new UsageError(
1979
+ `option "${field}" (${flag}) is not supported by the ${adapter.dialect} dialect; remove ${flag}, or select a provider whose dialect accepts it`
1980
+ );
1981
+ }
1982
+ function buildVideoRequest(prompt, opts, model, adapter) {
1983
+ const text = validatePrompt(prompt);
1984
+ const duration = validateDuration(opts.duration);
1985
+ const seed = validateSeed2(opts.seed);
1986
+ const ratio = validateRatio(opts.ratio);
1987
+ const frame = firstFrame(opts.image ?? []);
1988
+ rejectUndeclaredFlags(opts, adapter);
1989
+ const resolution = opts.resolution === void 0 ? void 0 : validateResolution(opts.resolution, adapter);
1990
+ const request = { model, prompt: text };
1991
+ if (resolution !== void 0) request.resolution = resolution;
1992
+ if (ratio !== void 0) request.ratio = ratio;
1993
+ if (duration !== void 0) request.duration = duration;
1994
+ if (seed !== void 0) request.seed = seed;
1995
+ if (opts.watermark !== void 0) request.watermark = opts.watermark;
1996
+ if (opts.generateAudio !== void 0) request.generate_audio = opts.generateAudio;
1997
+ if (opts.outputFormat !== void 0) request.output_format = opts.outputFormat;
1998
+ if (frame !== void 0) request.first_frame = readFrame(frame);
1999
+ return request;
2000
+ }
2001
+ function readFrame(value) {
2002
+ try {
2003
+ return toImageInput(value);
2004
+ } catch (error) {
2005
+ if (!(error instanceof UsageError)) throw error;
2006
+ throw new UsageError(`invalid --image ${JSON.stringify(value)}: ${error.message}`);
2007
+ }
2008
+ }
2009
+ var POLL_INITIAL_MS = 2e3;
2010
+ var POLL_FACTOR = 1.5;
2011
+ var POLL_MAX_MS = 1e4;
2012
+ var POLL_MAX_CONSECUTIVE_FAILURES = 5;
2013
+ var REAL_DEPS = {
2014
+ sleep: (ms) => new Promise((resolve) => {
2015
+ setTimeout(resolve, ms);
2016
+ }),
2017
+ now: () => Date.now()
2018
+ };
2019
+ function nextDelay(delay) {
2020
+ return Math.min(Math.round(delay * POLL_FACTOR), POLL_MAX_MS);
2021
+ }
2022
+ async function runVideoGenerate(prompt, opts, deps = REAL_DEPS, env = process.env) {
2023
+ const baseUrl = opts.baseUrl === void 0 ? void 0 : normalizeBaseUrl(opts.baseUrl);
2024
+ const config = loadConfig();
2025
+ const selection = resolveSelection(
2026
+ { provider: opts.provider, model: opts.model, task: "video" },
2027
+ config
2028
+ );
2029
+ const binding = baseUrl === void 0 ? selection : { ...selection, provider: { ...selection.provider, base_url: baseUrl } };
2030
+ const adapter = getAdapter(binding.dialect, "video");
2031
+ const request = buildVideoRequest(prompt, opts, binding.model, adapter);
2032
+ const auth = resolveApiKey(binding.provider, opts.apiKey, env);
2033
+ if (auth === null) throw missingKeyError(binding.provider);
2034
+ const outDir = resolveOutDir(opts.outDir, config.output_dir);
2035
+ ensureOutDir(outDir);
2036
+ const created = await createTask({ binding, apiKey: auth.key, request });
2037
+ noteTaskCreated(created.taskId, opts.noWait);
2038
+ if (opts.noWait) {
2039
+ const pending = {
2040
+ saved: [],
2041
+ taskId: created.taskId,
2042
+ status: "pending",
2043
+ response: void 0
2044
+ };
2045
+ if (created.requestId !== void 0) pending.requestId = created.requestId;
2046
+ return pending;
2047
+ }
2048
+ const waited = await waitForTask(binding, auth.key, created.taskId, deps);
2049
+ const media = waited.result;
2050
+ const saved = await saveVideo(media, {
2051
+ outDir,
2052
+ provider: binding.provider.name,
2053
+ // The model the server echoed, not the one we asked for: `video status` builds
2054
+ // a binding from the default model, and naming a 2.5 clip after 1.0 would be a
2055
+ // false statement about its contents.
2056
+ model: media.model ?? binding.model,
2057
+ taskId: created.taskId
2058
+ });
2059
+ const result = {
2060
+ saved: [saved],
2061
+ taskId: created.taskId,
2062
+ status: "succeeded",
2063
+ response: media
2064
+ };
2065
+ if (media.usage !== void 0) result.usage = media.usage;
2066
+ const requestId = created.requestId ?? waited.requestId;
2067
+ if (requestId !== void 0) result.requestId = requestId;
2068
+ return result;
2069
+ }
2070
+ function withTaskId(cause, taskId) {
2071
+ if (cause instanceof ApiError && cause.taskId !== void 0) return cause;
2072
+ const reason = cause instanceof Error ? cause.message : String(cause);
2073
+ const code = cause instanceof ApiError ? cause.code : void 0;
2074
+ const alreadyNamed = reason.includes(safeLine(taskId));
2075
+ return new ApiError(
2076
+ alreadyNamed ? reason : `${reason} (task ${safeLine(taskId)})`,
2077
+ code,
2078
+ taskId,
2079
+ causeStatus(cause)
2080
+ );
2081
+ }
2082
+ async function runVideoStatus(taskId, opts, env = process.env) {
2083
+ const id = taskId.trim();
2084
+ if (id === "") {
2085
+ throw new UsageError("a task id is required: pass the id a previous run printed");
2086
+ }
2087
+ const baseUrl = opts.baseUrl === void 0 ? void 0 : normalizeBaseUrl(opts.baseUrl);
2088
+ const config = loadConfig();
2089
+ const selection = resolveSelection(
2090
+ { provider: opts.provider, model: opts.model, task: "video" },
2091
+ config
2092
+ );
2093
+ const binding = baseUrl === void 0 ? selection : { ...selection, provider: { ...selection.provider, base_url: baseUrl } };
2094
+ const auth = resolveApiKey(binding.provider, opts.apiKey, env);
2095
+ if (auth === null) throw missingKeyError(binding.provider);
2096
+ let outcome;
2097
+ try {
2098
+ outcome = await pollTask({ binding, apiKey: auth.key, taskId: id });
2099
+ } catch (error) {
2100
+ throw withTaskId(error, id);
2101
+ }
2102
+ if (outcome.state === "failed" || outcome.state === "cancelled" || outcome.state === "expired") {
2103
+ throw taskEndedError(binding, id, outcome);
2104
+ }
2105
+ if (outcome.state !== "succeeded") {
2106
+ const unfinished = {
2107
+ saved: [],
2108
+ taskId: id,
2109
+ status: outcome.state,
2110
+ response: void 0
2111
+ };
2112
+ if (outcome.requestId !== void 0) unfinished.requestId = outcome.requestId;
2113
+ return unfinished;
2114
+ }
2115
+ const outDir = resolveOutDir(opts.outDir, config.output_dir);
2116
+ ensureOutDir(outDir);
2117
+ const media = outcome.result;
2118
+ const saved = await saveVideo(media, {
2119
+ outDir,
2120
+ provider: binding.provider.name,
2121
+ model: media.model ?? binding.model,
2122
+ taskId: id
2123
+ });
2124
+ const result = {
2125
+ saved: [saved],
2126
+ taskId: id,
2127
+ status: "succeeded",
2128
+ response: media
2129
+ };
2130
+ if (media.usage !== void 0) result.usage = media.usage;
2131
+ if (outcome.requestId !== void 0) result.requestId = outcome.requestId;
2132
+ return result;
2133
+ }
2134
+ function noteTaskCreated(taskId, noWait) {
2135
+ const id = safeLine(taskId);
2136
+ const remedy = noWait ? `fetch it later with "mediagen video status ${id}"` : `if this process is interrupted, resume with "mediagen video status ${id}"`;
2137
+ process.stderr.write(`note: task ${id} was created and paid for; ${remedy}
2138
+ `);
2139
+ }
2140
+ async function waitForTask(binding, apiKey, taskId, deps) {
2141
+ const deadline = deps.now() + binding.timeout_ms;
2142
+ let delay = POLL_INITIAL_MS;
2143
+ let failures = 0;
2144
+ for (; ; ) {
2145
+ await deps.sleep(delay);
2146
+ let polled;
2147
+ try {
2148
+ polled = await pollTask({ binding, apiKey, taskId });
2149
+ } catch (error) {
2150
+ if (!isTransientPollFailure(error)) throw stoppedPollingError(taskId, error);
2151
+ failures += 1;
2152
+ if (failures >= POLL_MAX_CONSECUTIVE_FAILURES) throw gaveUpPollingError(binding, taskId, error);
2153
+ if (deps.now() >= deadline) throw notFinishedError(binding, taskId);
2154
+ delay = nextDelay(delay);
2155
+ continue;
2156
+ }
2157
+ failures = 0;
2158
+ const outcome = polled;
2159
+ switch (outcome.state) {
2160
+ case "succeeded":
2161
+ return polled.requestId === void 0 ? { result: outcome.result } : { result: outcome.result, requestId: polled.requestId };
2162
+ case "pending":
2163
+ case "running":
2164
+ break;
2165
+ default:
2166
+ throw taskEndedError(binding, taskId, outcome);
2167
+ }
2168
+ if (deps.now() >= deadline) throw notFinishedError(binding, taskId);
2169
+ delay = nextDelay(delay);
2170
+ }
2171
+ }
2172
+ function isTransientPollFailure(error) {
2173
+ if (!(error instanceof ApiError)) return false;
2174
+ const status = error.status;
2175
+ if (status === void 0) return false;
2176
+ return status === NO_RESPONSE_STATUS || status === 429 || status >= 500;
2177
+ }
2178
+ function resumeHint(id) {
2179
+ return `The task was created and paid for, so do not resubmit it: run "mediagen video status ${id}", which checks it and downloads the clip once it is ready.`;
2180
+ }
2181
+ function causeStatus(cause) {
2182
+ return cause instanceof ApiError ? cause.status : void 0;
2183
+ }
2184
+ function notFinishedError(binding, taskId) {
2185
+ const id = safeLine(taskId);
2186
+ return new ApiError(
2187
+ `task ${id} has not finished after the ${binding.timeout_ms / 1e3}s poll budget (raise timeout_seconds on this provider's video task to wait longer). ` + resumeHint(id),
2188
+ "TaskNotFinished",
2189
+ taskId
2190
+ );
2191
+ }
2192
+ function gaveUpPollingError(binding, taskId, cause) {
2193
+ const id = safeLine(taskId);
2194
+ const reason = safeLine(cause instanceof Error ? cause.message : String(cause));
2195
+ const code = cause instanceof ApiError && cause.code !== void 0 ? cause.code : "PollFailed";
2196
+ return new ApiError(
2197
+ `polling task ${id} gave up after ${POLL_MAX_CONSECUTIVE_FAILURES} failures in a row: ${reason}. ${resumeHint(id)}`,
2198
+ code,
2199
+ taskId,
2200
+ causeStatus(cause)
2201
+ );
2202
+ }
2203
+ function stoppedPollingError(taskId, cause) {
2204
+ const id = safeLine(taskId);
2205
+ const reason = safeLine(cause instanceof Error ? cause.message : String(cause));
2206
+ const code = cause instanceof ApiError && cause.code !== void 0 ? cause.code : "PollFailed";
2207
+ return new ApiError(
2208
+ `polling task ${id} stopped before its state was known: ${reason}. ${resumeHint(id)}`,
2209
+ code,
2210
+ taskId,
2211
+ causeStatus(cause)
2212
+ );
2213
+ }
2214
+ function taskEndedError(binding, taskId, outcome) {
2215
+ const id = safeLine(taskId);
2216
+ return new ApiError(
2217
+ `${binding.provider.name} video task ${id} ${outcome.state}: ${safeLine(outcome.failure.message)}`,
2218
+ outcome.failure.code,
2219
+ taskId
2220
+ );
2221
+ }
2222
+ async function saveVideo(media, params) {
2223
+ try {
2224
+ return await downloadMedia(media, {
2225
+ kind: "video",
2226
+ outDir: params.outDir,
2227
+ provider: params.provider,
2228
+ model: params.model,
2229
+ stamp: nowStamp(),
2230
+ index: 0
2231
+ });
2232
+ } catch (error) {
2233
+ const cause = safeLine(error instanceof Error ? error.message : String(error));
2234
+ const id = safeLine(params.taskId);
2235
+ const code = error instanceof ApiError && error.code !== void 0 ? error.code : "DownloadError";
2236
+ throw new ApiError(
2237
+ `task ${id} succeeded but its video could not be saved: ${cause}. The task is paid for, so do not generate it again: "mediagen video status ${id}" downloads the clip without paying twice.`,
2238
+ code,
2239
+ params.taskId
2240
+ );
2241
+ }
2242
+ }
2243
+
2244
+ // src/output.ts
2245
+ function renderHuman(saved, res) {
2246
+ const lines = res.failures.map((failure) => `warning: ${failure.code}: ${failure.message}`);
2247
+ if (saved.length > 0 && res.failures.length > 0) {
2248
+ lines.push(
2249
+ `note: ${saved.length} file(s) written, ${res.failures.length} failed; files are already on disk, do not retry the whole request.`
2250
+ );
2251
+ }
2252
+ return {
2253
+ stdout: saved.map((file) => file.path).join("\n"),
2254
+ stderr: lines.join("\n")
2255
+ };
2256
+ }
2257
+ function imageSize(meta) {
2258
+ return meta !== void 0 && "size" in meta ? meta.size : void 0;
2259
+ }
2260
+ function renderJson(saved, res) {
2261
+ const payload = {
2262
+ ok: saved.length > 0,
2263
+ partial: saved.length > 0 && res.failures.length > 0,
2264
+ files: saved.map((file) => {
2265
+ const size = imageSize(file.meta);
2266
+ return {
2267
+ path: file.path,
2268
+ index: file.index,
2269
+ ...size !== void 0 ? { size } : {},
2270
+ url: file.url
2271
+ };
2272
+ }),
2273
+ errors: res.failures.map((failure) => ({
2274
+ code: failure.code,
2275
+ message: failure.message
2276
+ }))
2277
+ };
2278
+ if (res.usage !== void 0) {
2279
+ payload.usage = res.usage;
2280
+ }
2281
+ if (res.requestId !== void 0) {
2282
+ payload.request_id = res.requestId;
2283
+ }
2284
+ return JSON.stringify(payload);
2285
+ }
2286
+ function videoExtras(meta) {
2287
+ const extras = {};
2288
+ if (meta === void 0) return extras;
2289
+ if ("duration" in meta && typeof meta.duration === "number") extras.duration = meta.duration;
2290
+ if ("resolution" in meta && typeof meta.resolution === "string") {
2291
+ extras.resolution = meta.resolution;
2292
+ }
2293
+ return extras;
2294
+ }
2295
+ function renderVideoJson(result) {
2296
+ const payload = {
2297
+ ok: true,
2298
+ task_id: result.taskId,
2299
+ status: result.status,
2300
+ files: result.saved.map((file) => {
2301
+ const extras = videoExtras(file.meta);
2302
+ return {
2303
+ path: file.path,
2304
+ index: file.index,
2305
+ url: file.url,
2306
+ ...extras.duration === void 0 ? {} : { duration: extras.duration },
2307
+ ...extras.resolution === void 0 ? {} : { resolution: extras.resolution }
2308
+ };
2309
+ }),
2310
+ errors: []
2311
+ };
2312
+ if (result.usage !== void 0) payload.usage = result.usage;
2313
+ if (result.requestId !== void 0) payload.request_id = result.requestId;
2314
+ return JSON.stringify(payload);
2315
+ }
2316
+ function renderVideoHuman(result) {
2317
+ const lines = result.saved.length > 0 ? result.saved.map((file) => file.path) : [result.taskId];
2318
+ return { stdout: lines.join("\n"), stderr: "" };
2319
+ }
2320
+ function renderError(err, json) {
2321
+ const message = describeError(err);
2322
+ const exitCode = err instanceof UsageError ? 2 : 1;
2323
+ if (json) {
2324
+ const error = { message };
2325
+ if (err instanceof ApiError && err.code !== void 0) {
2326
+ error.code = err.code;
2327
+ }
2328
+ if (err instanceof ApiError && err.taskId !== void 0) {
2329
+ error.task_id = err.taskId;
2330
+ }
2331
+ return { stdout: JSON.stringify({ error }), stderr: "", exitCode };
2332
+ }
2333
+ return { stdout: "", stderr: message, exitCode };
2334
+ }
2335
+ function describeError(err) {
2336
+ if (err instanceof Error) {
2337
+ return err.message.trim() ? err.message : err.name || "unknown error";
2338
+ }
2339
+ if (typeof err === "string") {
2340
+ return err.trim() ? err : "unknown error";
2341
+ }
2342
+ if (typeof err === "object" && err !== null) {
2343
+ const message = err.message;
2344
+ if (typeof message === "string" && message.trim()) {
2345
+ return message;
2346
+ }
2347
+ return "unknown error";
2348
+ }
2349
+ if (typeof err === "number" || typeof err === "bigint" || typeof err === "boolean") {
2350
+ return String(err);
2351
+ }
2352
+ return "unknown error";
2353
+ }
2354
+
2355
+ // src/cli.ts
2356
+ var ROOT_HELP = `
2357
+ Built for AI agents: explicit flags, no interactive prompts, machine-readable output.
2358
+
2359
+ Quickstart:
2360
+ mediagen config show # any command creates config.toml on first use
2361
+ mediagen config add my-proxy --api-type openai --base-url https://proxy.example.com --model qwen-image-3.0
2362
+ mediagen login --provider my-proxy --api-key sk-... # store a key in auth.json
2363
+ mediagen image generate "a red fox in fresh snow" --provider my-proxy --out-dir ./out
2364
+ mediagen image edit "make the sky sunset orange" --image ./photo.jpg
2365
+ mediagen image generate "..." --json # {"ok":..,"files":[..],"errors":[..]}
2366
+ mediagen video generate "a fox running through deep snow" --provider volcengine
2367
+ mediagen video generate "..." --no-wait --json # returns the task id immediately
2368
+ mediagen video status <task-id> # check a task you already paid for
2369
+
2370
+ Subcommands:
2371
+ login / logout store or remove a provider's API key in auth.json; provider
2372
+ definitions live in config.toml
2373
+ auth status which key each provider would use and where it came from (masked)
2374
+ config show the resolved configuration, with a source note for each value
2375
+ config path where config.toml lives
2376
+ config add create a provider in config.toml (edit the file for anything else)
2377
+ video generate create and pay for a video task, then save the result
2378
+ video status look at one task once, and save its video if it finished
2379
+
2380
+ Exit codes: 0 success \u2014 including a partial image success that saved at least one
2381
+ file, a video task that was created and is still running, or a video task that was
2382
+ merely read; 1 an API/config/runtime failure, 2 a usage error.
2383
+
2384
+ Run "mediagen image generate --help" or "mediagen video generate --help" for the flag
2385
+ list, the model table and examples.`;
2386
+ var LOGIN_HELP = `
2387
+ A key belongs to a provider entry in config.toml, and every task of that entry uses it:
2388
+ --api-key on any command, else the entry's api_key_env variable, else auth.json. There is no
2389
+ protocol-level default variable, so two entries that speak the same protocol can hold
2390
+ different keys. Stored keys are only ever printed masked.`;
2391
+ function imageHelpText(command) {
2392
+ const intro = command === "edit" ? 'Edit images: identical to "mediagen image generate" except that at least one --image is required.' : "Generate an image from a prompt. Add --image to switch to image-to-image editing.";
2393
+ const examples = command === "edit" ? ` # edit a local file (local paths are read and sent as base64 data URLs)
2394
+ mediagen image edit "make the sky sunset orange, keep the composition" --image ./photo.jpg --provider aliyun
2395
+
2396
+ # combine several references
2397
+ mediagen image edit "put the character from the first image into the scene from the second" --image ./character.png --image ./scene.jpg --provider aliyun
2398
+
2399
+ # ark layer decomposition: one paid request, several files back (base + layers)
2400
+ mediagen image edit "separate the sticker from its background" --image ./sticker.png --provider volcengine --layer-decomposition true --json
2401
+
2402
+ # a third-party proxy declared as [providers.my-proxy] in config.toml
2403
+ mediagen image edit "remove the timestamp" --image ./shot.png --provider my-proxy --out-dir ./out` : ` # text to image
2404
+ mediagen image generate "a red fox in fresh snow, soft morning light" --provider aliyun --size 1024x1024 --out-dir ./out
2405
+
2406
+ # image to image; "mediagen image edit" is the same command with --image required
2407
+ mediagen image generate "make the sky sunset orange, keep the composition" --image ./photo.jpg --provider aliyun
2408
+
2409
+ # several references at once
2410
+ mediagen image generate "the character from the first image wearing the jacket from the second" --image ./character.png --image ./jacket.png --provider aliyun
2411
+
2412
+ # four ark variants as JSON, for an agent to parse
2413
+ mediagen image generate "minimalist logo for a coffee roastery" --provider volcengine --model doubao-seedream-5-0-pro --n 4 --seed 42 --json
2414
+
2415
+ # a third-party proxy declared as [providers.my-proxy] in config.toml
2416
+ mediagen image generate "product photo on a white background" --provider my-proxy --out-dir ./shots
2417
+
2418
+ # a one-off test against a proxy, without touching config.toml
2419
+ mediagen image generate "colour test card" --provider aliyun --base-url https://proxy.example.com --api-key sk-...`;
2420
+ return `
2421
+ ${intro}
2422
+
2423
+ Models (--provider picks the account, --model the exact id; config.toml decides what is allowed):
2424
+ qwen-image-3.0 aliyun best text rendering (CJK + Latin) and image editing
2425
+ doubao-seedream-5-0-flash volcengine fastest; use it for drafts and iteration
2426
+ doubao-seedream-5-0-pro volcengine highest quality; slower and more expensive
2427
+ A model must be listed in models = [...] under [providers.<name>.image]; the provider
2428
+ names above are what the first run writes into config.toml, and a second model of the
2429
+ same vendor is one more entry there, not another provider.
2430
+
2431
+ Protocols (api_type, declared per task in config.toml):
2432
+ openai the OpenAI-compatible wire format; "qwen" still works as an alias
2433
+ dashscope Alibaba's native form: input.messages + parameters, output.choices[].image
2434
+ ark Volcengine's own API (image + video)
2435
+
2436
+ Sizes (--size, passed through to the vendor; omit it for the vendor default):
2437
+ tiers 1K | 1.5K | 2K | 3K | 4K ark and openai take these (ark defaults to 2K)
2438
+ pixels WxH with a lowercase x, e.g. 2048x2048, 1536x2048, 1024x1024
2439
+ auto openai only; omitting --size lets the model choose from the prompt
2440
+ dashscope takes pixels only; it sends \u5BBD*\u9AD8 and rejects tiers and auto.
2441
+
2442
+ Images and count:
2443
+ --image <path-or-url> repeatable; a local file becomes a data:<mime>;base64 URL
2444
+ --n <count> 1-6 images. ark fans out into one paid request per image
2445
+ (seeds seed, seed+1, ...); openai and dashscope send a single request.
2446
+
2447
+ Output:
2448
+ Files are written to --out-dir (default: config output_dir, else the current
2449
+ directory) as <provider>-<model>-<yyyyMMdd-HHmmss>-<index>.<ext>. Human mode prints
2450
+ one absolute path per line on stdout and warnings on stderr; --json prints a single
2451
+ object with ok, partial, files[], errors[], usage and request_id. Partial success
2452
+ exits 0 and lists each failure: the images on disk were already paid for, so do not
2453
+ retry the whole request.
2454
+
2455
+ Protocol-specific flags:
2456
+ openai only --prompt-extend-mode --enable-thinking
2457
+ openai and dashscope --negative-prompt --prompt-extend
2458
+ ark only --output-format --optimize-prompt --layer-decomposition --background
2459
+ A flag the selected dialect does not accept fails before any paid request is sent.
2460
+ Boolean flags take an explicit value: --watermark false, not --no-watermark.
2461
+
2462
+ Examples:
2463
+ ${examples}`;
2464
+ }
2465
+ var IMAGE_OPTION_HELP = {
2466
+ provider: "provider name declared in config.toml (default: config default_provider)",
2467
+ model: "model id, must be listed in the task's models (default: the task's default)",
2468
+ image: "reference image, repeatable; local files are sent as data URLs",
2469
+ size: "1K|1.5K|2K|3K|4K, WxH pixels, or auto (openai only; dashscope takes pixels)",
2470
+ n: "how many images, 1-6 (ark sends one paid request per image)",
2471
+ seed: "random seed; ark fan-out uses seed, seed+1, ...",
2472
+ watermark: "true|false (default: the vendor default, false on openai and dashscope)",
2473
+ negativePrompt: "openai and dashscope: what the image must not contain",
2474
+ promptExtend: "openai and dashscope: true|false prompt rewriting",
2475
+ promptExtendMode: "openai only: agent mode is text-to-image only",
2476
+ enableThinking: "openai only: true|false",
2477
+ outputFormat: "ark only (default: the vendor default, jpeg)",
2478
+ optimizePrompt: "ark only; flash models accept standard only",
2479
+ layerDecomposition: "ark only: true|false, needs --image, returns several files",
2480
+ background: "ark only: transparent needs one input with an alpha channel",
2481
+ outDir: "where to write files (default: config output_dir, else cwd)",
2482
+ json: "print one JSON object instead of file paths",
2483
+ apiKey: "one-off API key; overrides env and auth.json",
2484
+ baseUrl: "one-off provider origin, e.g. https://ark.cn-beijing.volces.com (a proxy prefix is allowed)"
2485
+ };
2486
+ function addImageOptions(command) {
2487
+ return command.option("--provider <name>", IMAGE_OPTION_HELP.provider).option("--model <id>", IMAGE_OPTION_HELP.model).option("--image <path-or-url>", IMAGE_OPTION_HELP.image, collectImage).option("--size <size>", IMAGE_OPTION_HELP.size).option("--n <count>", IMAGE_OPTION_HELP.n).option("--seed <seed>", IMAGE_OPTION_HELP.seed).option("--watermark <bool>", IMAGE_OPTION_HELP.watermark).option("--negative-prompt <text>", IMAGE_OPTION_HELP.negativePrompt).option("--prompt-extend <bool>", IMAGE_OPTION_HELP.promptExtend).option("--prompt-extend-mode <direct|agent>", IMAGE_OPTION_HELP.promptExtendMode).option("--enable-thinking <bool>", IMAGE_OPTION_HELP.enableThinking).option("--output-format <png|jpeg>", IMAGE_OPTION_HELP.outputFormat).option("--optimize-prompt <standard|fast>", IMAGE_OPTION_HELP.optimizePrompt).option("--layer-decomposition <bool>", IMAGE_OPTION_HELP.layerDecomposition).option("--background <transparent|opaque>", IMAGE_OPTION_HELP.background).option("--out-dir <dir>", IMAGE_OPTION_HELP.outDir).option("--json", IMAGE_OPTION_HELP.json).option("--api-key <key>", IMAGE_OPTION_HELP.apiKey).option("--base-url <url>", IMAGE_OPTION_HELP.baseUrl);
2488
+ }
2489
+ function collectImage(value, previous) {
2490
+ return previous === void 0 ? [value] : [...previous, value];
2491
+ }
2492
+ var VIDEO_OPTION_HELP = {
2493
+ provider: "provider name declared in config.toml (default: config default_provider)",
2494
+ model: "model id, must be listed in the task's models (default: the task's default)",
2495
+ image: "first frame, one per request; local files are sent as data URLs",
2496
+ resolution: "a resolution tier the dialect accepts, case-insensitive (e.g. 480p, 720p; which tiers a model supports is the vendor's call)",
2497
+ ratio: `one of ${RATIOS.join(", ")}`,
2498
+ duration: "seconds, a positive integer (the range is the model's)",
2499
+ seed: "non-negative integer",
2500
+ watermark: "true|false (default: the vendor default)",
2501
+ generateAudio: "true|false, where the model supports it (2.0/2.5)",
2502
+ outputFormat: "mp4|mov, where the model supports it (2.5)",
2503
+ outDir: "where to write files (default: config output_dir, else cwd)",
2504
+ json: "print one JSON object instead of the file path",
2505
+ apiKey: "one-off API key; overrides env and auth.json",
2506
+ baseUrl: "one-off provider origin, e.g. https://ark.cn-beijing.volces.com",
2507
+ noWait: "create the task and print its id instead of waiting for the video"
2508
+ };
2509
+ function videoHelpText() {
2510
+ return `
2511
+ Generate a video from a prompt. Unlike an image, a video is an asynchronous task:
2512
+ creating it is what gets billed, and the file appears one to five minutes later.
2513
+
2514
+ Models (--provider picks the account, --model the exact id; config.toml decides what is allowed):
2515
+ doubao-seedance-1-0-pro-250528 volcengine text to video, or from one first frame
2516
+ MiniMax-H3 minimax text to video, or from one first frame
2517
+ A model must be listed in models = [...] under [providers.<name>.video].
2518
+
2519
+ Cost and interruption:
2520
+ The task is paid for when it is created. If this process is interrupted, do not
2521
+ submit again: the task keeps running for a while, and "mediagen video status <id>"
2522
+ fetches it later. --no-wait prints that id immediately for exactly this reason, and
2523
+ the id is also written to stderr right after the task is created.
2524
+
2525
+ One video per request:
2526
+ --n belongs to image; here it is not an option at all. For a second variant, run the
2527
+ command again (add a different --seed on the ark dialect if you want it to differ).
2528
+
2529
+ Flags the vendor checks, not this CLI:
2530
+ --resolution --duration are passed through; whether a given model supports a tier or
2531
+ a range is validated by the vendor when the task is created, before anything is billed.
2532
+
2533
+ Ark-only flags, rejected locally by any other dialect:
2534
+ --seed --generate-audio --output-format --watermark. The minimax API takes only the
2535
+ model, the content, the resolution, the duration and the ratio.
2536
+
2537
+ Output:
2538
+ Files are written to --out-dir (default: config output_dir, else the current
2539
+ directory) as <provider>-<model>-<yyyyMMdd-HHmmss>-<index>.<ext>. With --json the
2540
+ envelope carries task_id and status: "pending" or "running" with files: [] means the
2541
+ task exists and was paid for \u2014 not that the command failed.
2542
+
2543
+ Examples:
2544
+ # text to video, then find the file on stdout
2545
+ mediagen video generate "a red fox running through deep snow, golden hour" --provider volcengine
2546
+
2547
+ # animate the first frame of a still image
2548
+ mediagen video generate "the same fox, now shaking the snow off" --image ./fox.png --provider volcengine
2549
+
2550
+ # do not wait: hand the task id to a later step (an agent usually wants this)
2551
+ mediagen video generate "a slow pan over a frozen lake" --provider volcengine --no-wait --json
2552
+
2553
+ # collect it afterwards, without paying again
2554
+ mediagen video status cgt-20261028-abcd --provider volcengine --out-dir ./clips`;
2555
+ }
2556
+ function videoStatusHelpText() {
2557
+ return `
2558
+ Look at one video task once, by the id a previous run printed. Nothing is created and
2559
+ nothing is billed: this is the way back into a task whose run was interrupted.
2560
+
2561
+ It asks once. A task that is still pending or running is a success (exit 0) with its id
2562
+ and no files, so a caller can poll this command on its own schedule. Of the commands that
2563
+ talk to a provider, this is the only one that cannot spend money. A task that has
2564
+ finished is downloaded to --out-dir. A task that failed, was cancelled or expired exits 1
2565
+ with the vendor's own message.
2566
+
2567
+ Flags not passed here are taken from config.toml: the provider from default_provider, the
2568
+ model from that provider's video table. The provider picks the account and the endpoint;
2569
+ --model is checked against config.toml and is not sent, because the task id decides
2570
+ everything else (it is only a fallback for the file name if the server reports no model).
2571
+
2572
+ Examples:
2573
+ mediagen video status cgt-20261028-abcd --provider volcengine
2574
+ mediagen video status cgt-20261028-abcd --provider volcengine --json
2575
+ mediagen video status cgt-20261028-abcd --provider volcengine --out-dir ./clips`;
2576
+ }
2577
+ function addVideoOptions(command) {
2578
+ return command.option("--provider <name>", VIDEO_OPTION_HELP.provider).option("--model <id>", VIDEO_OPTION_HELP.model).option("--image <path-or-url>", VIDEO_OPTION_HELP.image, collectImage).option("--resolution <tier>", VIDEO_OPTION_HELP.resolution).option("--ratio <ratio>", VIDEO_OPTION_HELP.ratio).option("--duration <seconds>", VIDEO_OPTION_HELP.duration).option("--seed <seed>", VIDEO_OPTION_HELP.seed).option("--watermark <bool>", VIDEO_OPTION_HELP.watermark).option("--generate-audio <bool>", VIDEO_OPTION_HELP.generateAudio).option("--output-format <format>", VIDEO_OPTION_HELP.outputFormat).option("--out-dir <dir>", VIDEO_OPTION_HELP.outDir).option("--no-wait", VIDEO_OPTION_HELP.noWait).option("--json", VIDEO_OPTION_HELP.json).option("--api-key <key>", VIDEO_OPTION_HELP.apiKey).option("--base-url <url>", VIDEO_OPTION_HELP.baseUrl);
2579
+ }
2580
+ function packageVersion() {
2581
+ try {
2582
+ const raw = fs6.readFileSync(new URL("../package.json", import.meta.url), "utf8");
2583
+ const parsed = JSON.parse(raw);
2584
+ return typeof parsed.version === "string" ? parsed.version : void 0;
2585
+ } catch {
2586
+ return void 0;
2587
+ }
2588
+ }
2589
+ function createProgram(options = {}) {
2590
+ const deps = options.deps ?? {};
2591
+ const program = new Command("mediagen");
2592
+ const version = packageVersion();
2593
+ if (version !== void 0) {
2594
+ program.version(version, "-V, --version", "print the packaged version and exit");
2595
+ }
2596
+ program.description(
2597
+ "Generate and edit images (Bailian qwen, Volcengine Ark doubao/seedream) and generate video (Ark seedance, MiniMax H3)."
2598
+ ).exitOverride().configureOutput({
2599
+ writeOut: options.writeOut ?? ((text) => process.stdout.write(text)),
2600
+ writeErr: options.writeErr ?? ((text) => process.stderr.write(text))
2601
+ }).addHelpText("after", ROOT_HELP);
2602
+ program.command("login").description("Store an API key for a provider in auth.json (mode 600)").option("--provider <name>", "provider name (default: config default_provider)").option("--api-key <key>", "the key; omit to be prompted (needs a TTY)").option("--json", "print a JSON result").addHelpText("after", LOGIN_HELP).action(async (flags) => {
2603
+ await runLogin(flags, deps);
2604
+ });
2605
+ program.command("logout").description("Remove the stored API key for a provider").option("--provider <name>", "provider name (default: config default_provider)").option("--json", "print a JSON result").action(async (flags) => {
2606
+ runLogout(flags);
2607
+ });
2608
+ const auth = program.command("auth").description("Inspect stored credentials");
2609
+ auth.command("status").description("Show which API key each provider would use, and where it came from").option("--json", "print a JSON result").action(async (flags) => {
2610
+ runAuthStatus(flags);
2611
+ });
2612
+ const config = program.command("config").description("Inspect the resolved configuration");
2613
+ config.command("show").description("Print the effective configuration with a source note for each value").option("--json", "print a JSON result").action(async (flags) => {
2614
+ runConfigShow(flags);
2615
+ });
2616
+ config.command("path").description("Print the path of config.toml").option("--json", "print a JSON result").action(async (flags) => {
2617
+ runConfigPath(flags);
2618
+ });
2619
+ config.command("add <name>").description("Create a [providers.<name>] table in config.toml").option("--api-type <dialect>", "protocol dialect: openai, dashscope, ark or minimax").option("--base-url <url>", "the provider's origin, e.g. https://ark.cn-beijing.volces.com").option("--task <image|video>", "which task table to create (default: image)").option("--model <id>", "the model id this task defaults to").option("--api-key-env <VAR>", "environment variable holding this provider's key").option("--json", "print a JSON result").action(async (name, flags) => {
2620
+ runConfigAdd(name, flags);
2621
+ });
2622
+ const image = program.command("image").description("Generate and edit images");
2623
+ const generate = image.command("generate").description("Generate an image from a prompt; add --image to edit").argument("<prompt>", "what to draw; quote it and describe the subject, style and text");
2624
+ addImageOptions(generate).addHelpText("after", imageHelpText("generate"));
2625
+ generate.action(async (prompt, flags) => {
2626
+ await runImage(prompt, flags, false);
2627
+ });
2628
+ const edit = image.command("edit").description("Edit images: like generate, but --image is required").argument("<prompt>", "the edit to perform; quote it and name what must stay unchanged");
2629
+ addImageOptions(edit).addHelpText("after", imageHelpText("edit"));
2630
+ edit.action(async (prompt, flags) => {
2631
+ await runImage(prompt, flags, true);
2632
+ });
2633
+ const video = program.command("video").description("Generate video from a prompt; the task is asynchronous and billed on create");
2634
+ const videoGenerate = video.command("generate").description("Create a video task and save the result; add --image for a first frame").argument("<prompt>", "what to film; quote it and describe the subject, motion and camera");
2635
+ addVideoOptions(videoGenerate).addHelpText("after", videoHelpText());
2636
+ videoGenerate.action(async (prompt, flags) => {
2637
+ await runVideo(prompt, flags);
2638
+ });
2639
+ const videoStatus = video.command("status").description("Look at one video task once, and save its video if it finished").argument("<task-id>", "the id a previous run printed");
2640
+ videoStatus.option("--provider <name>", VIDEO_OPTION_HELP.provider).option("--model <id>", VIDEO_OPTION_HELP.model).option("--out-dir <dir>", VIDEO_OPTION_HELP.outDir).option("--json", VIDEO_OPTION_HELP.json).option("--api-key <key>", VIDEO_OPTION_HELP.apiKey).option("--base-url <url>", VIDEO_OPTION_HELP.baseUrl).addHelpText("after", videoStatusHelpText());
2641
+ videoStatus.action(async (taskId, flags) => {
2642
+ await runVideoStatusCommand(taskId, flags);
2643
+ });
2644
+ return program;
2645
+ }
2646
+ function requestsHelpOrVersion(argv) {
2647
+ if (argv[0] === "help") return true;
2648
+ for (const arg of argv) {
2649
+ if (arg === "--") return false;
2650
+ if (arg === "-h" || arg === "--help" || arg === "-V" || arg === "--version") return true;
2651
+ }
2652
+ return false;
2653
+ }
2654
+ async function main(argv, deps = {}) {
2655
+ const buffer = [];
2656
+ try {
2657
+ if (!requestsHelpOrVersion(argv)) prepareConfigFile();
2658
+ const program = createProgram({
2659
+ deps,
2660
+ writeOut: (text) => buffer.push(text),
2661
+ writeErr: (text) => buffer.push(text)
2662
+ });
2663
+ await program.parseAsync(argv, { from: "user" });
2664
+ return 0;
2665
+ } catch (error) {
2666
+ if (error instanceof CommanderError) {
2667
+ return reportCommanderError(error, buffer.join(""), argv.includes("--json"));
2668
+ }
2669
+ return reportError(error, argv.includes("--json"));
2670
+ }
2671
+ }
2672
+ function emit(rendered) {
2673
+ if (rendered.stdout !== "") process.stdout.write(`${rendered.stdout}
2674
+ `);
2675
+ if (rendered.stderr !== "") process.stderr.write(`${rendered.stderr}
2676
+ `);
2677
+ }
2678
+ function writeLine(text) {
2679
+ process.stdout.write(`${text}
2680
+ `);
2681
+ }
2682
+ function writeErrLine(text) {
2683
+ process.stderr.write(`${text}
2684
+ `);
2685
+ }
2686
+ function reportError(error, jsonMode) {
2687
+ const rendered = renderError(error, jsonMode);
2688
+ emit(rendered);
2689
+ return rendered.exitCode;
2690
+ }
2691
+ function reportCommanderError(error, text, jsonMode) {
2692
+ if (error.exitCode === 0) {
2693
+ if (text !== "") process.stdout.write(text);
2694
+ return 0;
2695
+ }
2696
+ if (error.code === "commander.help") {
2697
+ if (!jsonMode) {
2698
+ if (text !== "") process.stdout.write(text);
2699
+ return 2;
2700
+ }
2701
+ return reportError(
2702
+ new UsageError('missing command; run "mediagen --help" to list the commands'),
2703
+ true
2704
+ );
2705
+ }
2706
+ const message = error.message.replace(/^error:\s*/, "").trim();
2707
+ if (jsonMode) return reportError(new UsageError(message), true);
2708
+ process.stderr.write(text.trim() === "" ? `${message}
2709
+ ` : text);
2710
+ return 2;
2711
+ }
2712
+ async function runImage(prompt, flags, edit) {
2713
+ const options = toGenerateOptions(flags, edit);
2714
+ const result = await runGenerate(prompt, options);
2715
+ if (flags.json === true) {
2716
+ writeLine(renderJson(result.saved, result.response));
2717
+ return;
2718
+ }
2719
+ emit(renderHuman(result.saved, result.response));
2720
+ }
2721
+ async function runVideo(prompt, flags) {
2722
+ const result = await runVideoGenerate(prompt, toVideoOptions(flags));
2723
+ if (flags.json === true) {
2724
+ writeLine(renderVideoJson(result));
2725
+ return;
2726
+ }
2727
+ emit(renderVideoHuman(result));
2728
+ }
2729
+ async function runVideoStatusCommand(taskId, flags) {
2730
+ const options = {
2731
+ provider: flags.provider,
2732
+ model: flags.model,
2733
+ outDir: flags.outDir,
2734
+ apiKey: flags.apiKey,
2735
+ baseUrl: flags.baseUrl
2736
+ };
2737
+ const result = await runVideoStatus(taskId, options);
2738
+ if (flags.json === true) {
2739
+ writeLine(renderVideoJson(result));
2740
+ return;
2741
+ }
2742
+ emit(renderVideoHuman(result));
2743
+ }
2744
+ function toVideoOptions(flags) {
2745
+ return {
2746
+ provider: flags.provider,
2747
+ model: flags.model,
2748
+ image: flags.image ?? [],
2749
+ resolution: flags.resolution,
2750
+ ratio: parseChoice(flags.ratio, RATIOS, "--ratio"),
2751
+ duration: parseInteger(flags.duration, "--duration"),
2752
+ seed: parseInteger(flags.seed, "--seed"),
2753
+ watermark: parseBool(flags.watermark, "--watermark"),
2754
+ generateAudio: parseBool(flags.generateAudio, "--generate-audio"),
2755
+ outputFormat: parseChoice(flags.outputFormat, ["mp4", "mov"], "--output-format"),
2756
+ outDir: flags.outDir,
2757
+ apiKey: flags.apiKey,
2758
+ baseUrl: flags.baseUrl,
2759
+ noWait: flags.wait === false
2760
+ };
2761
+ }
2762
+ function toGenerateOptions(flags, edit) {
2763
+ return {
2764
+ provider: flags.provider,
2765
+ model: flags.model,
2766
+ images: flags.image ?? [],
2767
+ size: flags.size,
2768
+ n: parseInteger(flags.n, "--n"),
2769
+ seed: parseInteger(flags.seed, "--seed"),
2770
+ watermark: parseBool(flags.watermark, "--watermark"),
2771
+ negativePrompt: flags.negativePrompt,
2772
+ promptExtend: parseBool(flags.promptExtend, "--prompt-extend"),
2773
+ promptExtendMode: parseChoice(flags.promptExtendMode, ["direct", "agent"], "--prompt-extend-mode"),
2774
+ enableThinking: parseBool(flags.enableThinking, "--enable-thinking"),
2775
+ outputFormat: parseChoice(flags.outputFormat, ["png", "jpeg"], "--output-format"),
2776
+ optimizePrompt: parseChoice(flags.optimizePrompt, ["standard", "fast"], "--optimize-prompt"),
2777
+ layerDecomposition: parseBool(flags.layerDecomposition, "--layer-decomposition"),
2778
+ background: parseChoice(flags.background, ["transparent", "opaque"], "--background"),
2779
+ outDir: flags.outDir,
2780
+ apiKey: flags.apiKey,
2781
+ baseUrl: flags.baseUrl,
2782
+ edit
2783
+ };
2784
+ }
2785
+ function parseBool(value, flag) {
2786
+ if (value === void 0) return void 0;
2787
+ const normalized = value.trim().toLowerCase();
2788
+ if (normalized === "true") return true;
2789
+ if (normalized === "false") return false;
2790
+ throw new UsageError(`invalid ${flag} ${JSON.stringify(value)}: expected true or false`);
2791
+ }
2792
+ var INTEGER_PATTERN = /^[+-]?\d+$/;
2793
+ function parseInteger(value, flag) {
2794
+ if (value === void 0) return void 0;
2795
+ const trimmed = value.trim();
2796
+ if (!INTEGER_PATTERN.test(trimmed)) {
2797
+ throw new UsageError(`invalid ${flag} ${JSON.stringify(value)}: expected an integer`);
2798
+ }
2799
+ return Number(trimmed);
2800
+ }
2801
+ function parseChoice(value, allowed, flag) {
2802
+ if (value === void 0) return void 0;
2803
+ const normalized = value.trim().toLowerCase();
2804
+ const match = allowed.find((entry) => entry === normalized);
2805
+ if (match === void 0) {
2806
+ throw new UsageError(
2807
+ `invalid ${flag} ${JSON.stringify(value)}: expected one of ${allowed.join(", ")}`
2808
+ );
2809
+ }
2810
+ return match;
2811
+ }
2812
+ async function runLogin(flags, deps) {
2813
+ const config = loadConfig();
2814
+ const name = flags.provider ?? config.default_provider;
2815
+ assertKnownProvider(name, config);
2816
+ const key = await resolveLoginKey(name, flags.apiKey, deps);
2817
+ saveApiKey(name, key);
2818
+ const masked = maskKey(key);
2819
+ if (flags.json === true) {
2820
+ writeLine(
2821
+ JSON.stringify({
2822
+ ok: true,
2823
+ provider: name,
2824
+ masked_key: masked,
2825
+ auth_file: authPath()
2826
+ })
2827
+ );
2828
+ return;
2829
+ }
2830
+ writeLine(`saved API key ${masked} for provider "${name}" to ${authPath()}`);
2831
+ }
2832
+ function assertKnownProvider(name, config) {
2833
+ if (name === "") throw new UsageError(noProviderSelectedMessage());
2834
+ if (!Object.hasOwn(config.providers, name)) {
2835
+ throw new UsageError(
2836
+ `unknown provider "${name}": login stores API keys but never creates providers; add a [providers.${tomlKey(name)}] table to ${configPath()} first (or run "mediagen config add ${shellArg(name)} ...")`
2837
+ );
2838
+ }
2839
+ try {
2840
+ validateProviderShape(name, config);
2841
+ } catch (error) {
2842
+ throw new UsageError(error instanceof Error ? error.message : String(error));
2843
+ }
2844
+ }
2845
+ async function resolveLoginKey(provider, flagKey, deps) {
2846
+ const fromFlag = flagKey?.trim();
2847
+ if (fromFlag !== void 0 && fromFlag !== "") return fromFlag;
2848
+ const isTTY = deps.isTTY ?? (() => Boolean(process.stdin.isTTY));
2849
+ if (!isTTY()) {
2850
+ throw new UsageError(
2851
+ `no API key for provider "${provider}": pass --api-key <key>; reading the key interactively needs a TTY on stdin`
2852
+ );
2853
+ }
2854
+ const readSecret = deps.readSecret ?? readSecretFromTerminal;
2855
+ const key = (await readSecret(`API key for provider "${provider}" (input is hidden): `)).trim();
2856
+ if (key === "") throw new UsageError("the API key was empty; nothing was saved");
2857
+ return key;
2858
+ }
2859
+ function readSecretFromTerminal(promptText) {
2860
+ return new Promise((resolve, reject) => {
2861
+ process.stderr.write(promptText);
2862
+ const muted = new Writable({
2863
+ write(_chunk, _encoding, callback) {
2864
+ callback();
2865
+ }
2866
+ });
2867
+ const rl = readline.createInterface({
2868
+ input: process.stdin,
2869
+ output: muted,
2870
+ terminal: true
2871
+ });
2872
+ const finish = (action) => {
2873
+ rl.close();
2874
+ process.stderr.write("\n");
2875
+ action();
2876
+ };
2877
+ rl.on("SIGINT", () => {
2878
+ finish(() => reject(new UsageError("login cancelled")));
2879
+ });
2880
+ rl.question("", (answer) => {
2881
+ finish(() => resolve(answer));
2882
+ });
2883
+ });
2884
+ }
2885
+ function runLogout(flags) {
2886
+ const config = loadConfig();
2887
+ const name = flags.provider ?? config.default_provider;
2888
+ if (name === "") throw new UsageError(noProviderSelectedMessage());
2889
+ const removed = removeApiKey(name);
2890
+ if (flags.json === true) {
2891
+ writeLine(JSON.stringify({ ok: true, provider: name, removed }));
2892
+ return;
2893
+ }
2894
+ writeLine(
2895
+ removed ? `removed the API key for provider "${name}" from ${authPath()}` : `no API key stored for provider "${name}"; nothing to do`
2896
+ );
2897
+ }
2898
+ function runAuthStatus(flags) {
2899
+ const config = loadConfig();
2900
+ const entries = [];
2901
+ const details = [];
2902
+ for (const [name, override] of Object.entries(config.providers)) {
2903
+ const apiKeyEnv = override.api_key_env;
2904
+ const resolved = resolveApiKey({ name, api_key_env: apiKeyEnv }, void 0, process.env);
2905
+ const entry = {
2906
+ name,
2907
+ configured: resolved !== null,
2908
+ source: resolved?.source ?? "none"
2909
+ };
2910
+ if (resolved !== null) {
2911
+ entry.masked_key = maskKey(resolved.key);
2912
+ details.push(`${entry.masked_key} (${describeKeySource(apiKeyEnv, resolved.source)})`);
2913
+ } else {
2914
+ details.push(`not configured (${missingKeyHint(name, apiKeyEnv)})`);
2915
+ }
2916
+ entries.push(entry);
2917
+ }
2918
+ if (flags.json === true) {
2919
+ writeLine(JSON.stringify({ providers: entries }));
2920
+ return;
2921
+ }
2922
+ const width = entries.reduce((max, entry) => Math.max(max, entry.name.length), 0);
2923
+ const lines = entries.map((entry, index) => `${entry.name.padEnd(width)} ${details[index]}`);
2924
+ lines.push(`auth file: ${authPath()}`);
2925
+ writeLine(lines.join("\n"));
2926
+ }
2927
+ function describeKeySource(apiKeyEnv, source) {
2928
+ if (source === "env") return `env ${apiKeyEnv ?? "environment variable"}`;
2929
+ return `auth.json: ${authPath()}`;
2930
+ }
2931
+ function missingKeyHint(name, apiKeyEnv) {
2932
+ const hints = [`run "mediagen login --provider ${shellArg(name)}"`];
2933
+ if (apiKeyEnv !== void 0) hints.push(`set ${apiKeyEnv}`);
2934
+ hints.push("pass --api-key");
2935
+ return hints.join(", ");
2936
+ }
2937
+ function runConfigPath(flags) {
2938
+ if (flags.json === true) {
2939
+ writeLine(JSON.stringify({ config_path: configPath(), auth_path: authPath() }));
2940
+ return;
2941
+ }
2942
+ writeLine(configPath());
2943
+ }
2944
+ function runConfigAdd(name, flags) {
2945
+ const provider = name.trim();
2946
+ if (provider === "") throw new UsageError("a provider name is required");
2947
+ const apiType = parseChoice(
2948
+ flags.apiType,
2949
+ ["openai", "dashscope", "ark", "minimax", "qwen"],
2950
+ "--api-type"
2951
+ );
2952
+ if (apiType === void 0) {
2953
+ throw new UsageError(
2954
+ '--api-type is required: "openai", "dashscope", "ark" or "minimax" ("qwen" still works as a deprecated alias of "openai")'
2955
+ );
2956
+ }
2957
+ if (flags.baseUrl === void 0) throw new UsageError("--base-url is required");
2958
+ const task = parseChoice(flags.task, ["image", "video"], "--task") ?? "image";
2959
+ const model = (flags.model ?? "").trim();
2960
+ if (model === "") throw new UsageError("--model is required");
2961
+ const apiKeyEnv = flags.apiKeyEnv?.trim();
2962
+ if (flags.apiKeyEnv !== void 0 && apiKeyEnv === "") {
2963
+ throw new UsageError("--api-key-env must not be empty");
2964
+ }
2965
+ const file = configPath();
2966
+ const config = loadConfig();
2967
+ if (Object.hasOwn(config.providers, provider)) {
2968
+ throw new UsageError(
2969
+ `a [providers.${tomlKey(provider)}] section already exists in ${file}; add a model with one more entry in models = [...] under [providers.${tomlKey(provider)}.${task}], or edit the table by hand`
2970
+ );
2971
+ }
2972
+ const dialect = apiType === "qwen" ? "openai" : apiType;
2973
+ const fields = {
2974
+ base_url: originBaseUrl(flags.baseUrl)
2975
+ };
2976
+ if (apiKeyEnv !== void 0) fields.api_key_env = apiKeyEnv;
2977
+ const before = fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : "";
2978
+ const beforeTable = before.trim() === "" ? void 0 : parseToml(before);
2979
+ if (apiType === "qwen") {
2980
+ writeErrLine('note: api_type "qwen" is a deprecated alias; writing api_type = "openai"');
2981
+ }
2982
+ const after = appendProviderTable(before, provider, fields, task, model, dialect);
2983
+ assertFaithfulRewrite(beforeTable, after, provider, fields, task, model, dialect, file);
2984
+ try {
2985
+ writeConfigFile(after);
2986
+ } catch (error) {
2987
+ const reason = error instanceof Error ? error.message : String(error);
2988
+ throw new ConfigError(`cannot write ${file}: ${reason}`);
2989
+ }
2990
+ if (flags.json === true) {
2991
+ writeLine(
2992
+ JSON.stringify({
2993
+ ok: true,
2994
+ provider,
2995
+ task,
2996
+ model,
2997
+ config_file: file,
2998
+ fields: { ...fields, api_type: dialect }
2999
+ })
3000
+ );
3001
+ return;
3002
+ }
3003
+ const loginHint = `mediagen login --provider ${shellArg(provider)} --api-key ...`;
3004
+ const keyRoute = apiKeyEnv === void 0 ? `run "${loginHint}" to store its key` : `set ${shellArg(apiKeyEnv)}, or run "${loginHint}"`;
3005
+ if (adapterPath(dialect, task) === void 0) {
3006
+ writeErrLine(
3007
+ `note: the "${dialect}" dialect has no "${task}" task; the table is stored, and "config show" will say the same`
3008
+ );
3009
+ }
3010
+ writeLine(
3011
+ [
3012
+ `added provider "${provider}" to ${file} with an ${task} table defaulting to "${model}"`,
3013
+ `another model of the same provider is one more entry in models = [...] under [providers.${tomlKey(provider)}.${task}]`,
3014
+ keyRoute
3015
+ ].join("\n")
3016
+ );
3017
+ }
3018
+ function runConfigShow(flags) {
3019
+ const config = loadConfig();
3020
+ const file = configPath();
3021
+ const exists = fs6.existsSync(file);
3022
+ const declared = exists ? parseToml(fs6.readFileSync(file, "utf8")) : {};
3023
+ const defaultProvider = configuredValue(
3024
+ config.default_provider,
3025
+ declared.default_provider
3026
+ );
3027
+ const outputDir = configuredValue(config.output_dir, declared.output_dir);
3028
+ const views = Object.entries(config.providers).map(([name, entry]) => ({
3029
+ name,
3030
+ apiKeyEnv: entry.api_key_env,
3031
+ view: buildProviderView(name, entry)
3032
+ }));
3033
+ if (flags.json === true) {
3034
+ writeLine(
3035
+ JSON.stringify({
3036
+ config_file: { path: file, exists },
3037
+ default_provider: defaultProvider,
3038
+ output_dir: outputDir,
3039
+ providers: views.map(({ view }) => view)
3040
+ })
3041
+ );
3042
+ return;
3043
+ }
3044
+ const lines = [
3045
+ `config file: ${file} (loaded)`,
3046
+ `default_provider: ${defaultProvider.value === "" ? `(not set) (${defaultProvider.source})` : show(defaultProvider)}`,
3047
+ `output_dir: ${outputDir.value} (${outputDir.source === "default" ? "default: current directory" : outputDir.source})`,
3048
+ "",
3049
+ "providers:"
3050
+ ];
3051
+ if (views.length === 0) {
3052
+ lines.push(
3053
+ ' (none; run "mediagen config add <name> --api-type <openai|dashscope|ark|minimax> --base-url <origin> --model <id>")'
3054
+ );
3055
+ }
3056
+ for (const { name, apiKeyEnv, view } of views) {
3057
+ lines.push(
3058
+ ` ${view.name} (dialect ${view.api_type?.value ?? (view.tasks?.some((task) => task.api_type !== void 0) === true ? "per task" : "unknown")})`
3059
+ );
3060
+ for (const field of ["api_type", "base_url", "api_key_env"]) {
3061
+ const value = view[field];
3062
+ if (value !== void 0) lines.push(` ${field}: ${show(value)}${noteSuffix(value)}`);
3063
+ }
3064
+ if (view.missing !== void 0 && view.missing.length > 0) {
3065
+ lines.push(` missing: ${view.missing.join(", ")} (required in ${file})`);
3066
+ }
3067
+ lines.push(
3068
+ view.api_key.configured && view.api_key.source !== "none" ? ` api_key: ${view.api_key.masked_key} (${describeKeySource(apiKeyEnv, view.api_key.source)})` : ` api_key: not set (${missingKeyHint(name, apiKeyEnv)})`
3069
+ );
3070
+ for (const taskView of view.tasks ?? []) {
3071
+ lines.push(` ${taskView.task}:`);
3072
+ if (taskView.api_type !== void 0) {
3073
+ lines.push(` api_type: ${show(taskView.api_type)}${noteSuffix(taskView.api_type)}`);
3074
+ }
3075
+ if (taskView.default !== void 0) lines.push(` default: ${show(taskView.default)}`);
3076
+ if (taskView.models !== void 0) {
3077
+ lines.push(` models: ${taskView.models.join(", ")}`);
3078
+ }
3079
+ if (taskView.path !== void 0) {
3080
+ lines.push(` path: ${show(taskView.path)}`);
3081
+ }
3082
+ if (taskView.timeout_seconds !== void 0) {
3083
+ lines.push(
3084
+ ` timeout_seconds: ${taskView.timeout_seconds}` + (taskView.timeout_seconds_default === true ? " (default)" : "")
3085
+ );
3086
+ }
3087
+ if (taskView.endpoint !== void 0) {
3088
+ lines.push(` endpoint: ${taskView.endpoint} (computed)`);
3089
+ } else {
3090
+ lines.push(` endpoint: (${taskView.endpoint_note ?? "no endpoint"})`);
3091
+ }
3092
+ }
3093
+ }
3094
+ writeLine(lines.join("\n"));
3095
+ }
3096
+ function show(value) {
3097
+ return `${value.value} (${value.source})`;
3098
+ }
3099
+ function noteSuffix(value) {
3100
+ return value.note === void 0 ? "" : `; ${value.note}`;
3101
+ }
3102
+ function configuredValue(value, declared) {
3103
+ return declared === void 0 ? { value, source: "default" } : { value, source: "config.toml" };
3104
+ }
3105
+ function buildProviderView(name, entry) {
3106
+ const missing = [];
3107
+ const fields = {};
3108
+ for (const key of ["api_type", "base_url", "api_key_env"]) {
3109
+ const value = entry[key];
3110
+ if (value === void 0) {
3111
+ if (key === "base_url") missing.push(key);
3112
+ continue;
3113
+ }
3114
+ fields[key] = { value, source: "config.toml" };
3115
+ if (key === "api_type" && entry.api_type_alias === true) {
3116
+ fields[key] = {
3117
+ value,
3118
+ source: "config.toml",
3119
+ note: 'declared as the deprecated alias "qwen"'
3120
+ };
3121
+ }
3122
+ }
3123
+ const declaresProtocol = entry.api_type !== void 0 || KNOWN_TASKS.some((task) => entry.tasks[task]?.api_type !== void 0);
3124
+ if (!declaresProtocol) missing.push("api_type");
3125
+ const view = {
3126
+ name,
3127
+ ...fields,
3128
+ api_key: keyStatus(name, entry.api_key_env)
3129
+ };
3130
+ const base = fields.base_url === void 0 ? void 0 : normalizeHttpBaseUrl(fields.base_url.value);
3131
+ const tasks = KNOWN_TASKS.filter((task) => entry.tasks[task] !== void 0).map(
3132
+ (task) => {
3133
+ const block = entry.tasks[task] ?? {};
3134
+ const taskView = { task };
3135
+ const ownDialect = block.api_type;
3136
+ const effectiveDialect = ownDialect ?? entry.api_type;
3137
+ if (ownDialect !== void 0) {
3138
+ taskView.api_type = {
3139
+ value: ownDialect,
3140
+ source: "config.toml",
3141
+ ...block.api_type_alias === true ? { note: 'declared as the deprecated alias "qwen"' } : {}
3142
+ };
3143
+ } else if (effectiveDialect !== void 0) {
3144
+ taskView.api_type = {
3145
+ value: effectiveDialect,
3146
+ source: "default",
3147
+ note: "provider default"
3148
+ };
3149
+ }
3150
+ if (block.default !== void 0) {
3151
+ taskView.default = { value: block.default, source: "config.toml" };
3152
+ }
3153
+ if (block.models !== void 0) taskView.models = block.models;
3154
+ taskView.timeout_seconds = block.timeout_seconds ?? DEFAULT_TIMEOUT_SECONDS[task];
3155
+ taskView.timeout_seconds_default = block.timeout_seconds === void 0;
3156
+ if (block.path !== void 0) {
3157
+ taskView.path = { value: block.path, source: "config.toml" };
3158
+ }
3159
+ const path6 = block.path ?? (effectiveDialect === void 0 ? void 0 : adapterPath(effectiveDialect, task));
3160
+ if (effectiveDialect === void 0) {
3161
+ taskView.endpoint_note = "api_type is not set";
3162
+ } else if (path6 === void 0) {
3163
+ taskView.endpoint_note = `the ${effectiveDialect} dialect has no ${task} task`;
3164
+ } else if (base === void 0) {
3165
+ taskView.endpoint_note = "base_url is not a URL yet";
3166
+ } else if (block.path === void 0 && vendorPathIssue(base) !== void 0) {
3167
+ taskView.endpoint_note = "base_url carries a path; declare path = ... or use the origin";
3168
+ } else {
3169
+ taskView.endpoint = `${base}${path6}`;
3170
+ }
3171
+ return taskView;
3172
+ }
3173
+ );
3174
+ if (tasks.length > 0) view.tasks = tasks;
3175
+ if (viewsTaskCount(entry) === 0) missing.push("image or video task table");
3176
+ if (missing.length > 0) view.missing = missing;
3177
+ return view;
3178
+ }
3179
+ function viewsTaskCount(entry) {
3180
+ return KNOWN_TASKS.filter((task) => entry.tasks[task] !== void 0).length;
3181
+ }
3182
+ function keyStatus(name, apiKeyEnv) {
3183
+ const resolved = resolveApiKey({ name, api_key_env: apiKeyEnv }, void 0, process.env);
3184
+ if (resolved === null) return { configured: false, source: "none" };
3185
+ return { configured: true, source: resolved.source, masked_key: maskKey(resolved.key) };
3186
+ }
3187
+ function originBaseUrl(value) {
3188
+ const normalized = normalizeBaseUrl2(value);
3189
+ const issue = vendorPathIssue(normalized);
3190
+ if (issue !== void 0) {
3191
+ throw new UsageError(
3192
+ `invalid --base-url ${JSON.stringify(value)}: it points at the sub-path "${issue.prefix}", and the dialect appends its own path; set base_url to the origin, e.g. "${issue.origin}", or declare path = "<path from there>" on the task table by hand`
3193
+ );
3194
+ }
3195
+ return normalized;
3196
+ }
3197
+ function normalizeBaseUrl2(value) {
3198
+ const normalized = normalizeHttpBaseUrl(value);
3199
+ if (normalized === void 0) {
3200
+ throw new UsageError(
3201
+ `invalid --base-url ${JSON.stringify(value)}: expected an absolute http(s) URL without credentials, query or fragment`
3202
+ );
3203
+ }
3204
+ return normalized;
3205
+ }
3206
+ function appendProviderTable(text, name, fields, task, model, dialect) {
3207
+ const lines = [`[providers.${tomlKey(name)}]`];
3208
+ for (const [key, value] of Object.entries(fields)) {
3209
+ lines.push(`${key} = ${tomlBasicString(value)}`);
3210
+ }
3211
+ lines.push("", `[providers.${tomlKey(name)}.${task}]`);
3212
+ lines.push(`api_type = ${tomlBasicString(dialect)}`);
3213
+ lines.push(`default = ${tomlBasicString(model)}`);
3214
+ lines.push(`models = [${tomlBasicString(model)}]`);
3215
+ const base = text.trim() === "" ? "" : text.endsWith("\n") ? `${text}
3216
+ ` : `${text}
3217
+
3218
+ `;
3219
+ return `${base}${lines.join("\n")}
3220
+ `;
3221
+ }
3222
+ function assertFaithfulRewrite(before, after, name, expected, task, model, apiType, file) {
3223
+ function fail() {
3224
+ throw new UsageError(
3225
+ `cannot safely rewrite ${file} for provider "${name}"; edit ${file} by hand instead`
3226
+ );
3227
+ }
3228
+ let parsed;
3229
+ try {
3230
+ parsed = parseToml(after);
3231
+ } catch {
3232
+ fail();
3233
+ }
3234
+ const table = tableAt(parsed, ["providers", name]);
3235
+ for (const [key, value] of Object.entries(expected)) {
3236
+ if (table?.[key] !== value) fail();
3237
+ }
3238
+ const taskTable = tableAt(parsed, ["providers", name, task]);
3239
+ if (taskTable?.api_type !== apiType) fail();
3240
+ if (taskTable?.default !== model) fail();
3241
+ const models = taskTable?.models;
3242
+ if (!Array.isArray(models) || models.length !== 1 || models[0] !== model) fail();
3243
+ if (before !== void 0 && !isPreserved(before, parsed, [])) fail();
3244
+ }
3245
+ function tableAt(table, keys) {
3246
+ let current = table;
3247
+ for (const key of keys) {
3248
+ if (!isRecord7(current)) return void 0;
3249
+ current = current[key];
3250
+ }
3251
+ return isRecord7(current) ? current : void 0;
3252
+ }
3253
+ function isPreserved(before, after, exempt) {
3254
+ if (before instanceof Date || after instanceof Date) {
3255
+ return sameInstant(before, after);
3256
+ }
3257
+ if (isRecord7(before)) {
3258
+ if (!isRecord7(after)) return false;
3259
+ for (const [key, value] of Object.entries(before)) {
3260
+ if (exempt.length === 1 && exempt[0] === key) continue;
3261
+ if (!(key in after)) return false;
3262
+ const nested = exempt.length > 1 && exempt[0] === key ? exempt.slice(1) : [];
3263
+ if (!isPreserved(value, after[key], nested)) return false;
3264
+ }
3265
+ return true;
3266
+ }
3267
+ if (Array.isArray(before)) {
3268
+ if (!Array.isArray(after) || after.length !== before.length) return false;
3269
+ return before.every((entry, index) => isPreserved(entry, after[index], []));
3270
+ }
3271
+ return before === after;
3272
+ }
3273
+ function isRecord7(value) {
3274
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3275
+ }
3276
+ function sameInstant(before, after) {
3277
+ if (!(before instanceof Date) || !(after instanceof Date)) return false;
3278
+ const left = before.getTime();
3279
+ const right = after.getTime();
3280
+ return Number.isNaN(left) ? Number.isNaN(right) : left === right;
3281
+ }
3282
+ function realHref(modulePath) {
3283
+ if (modulePath === void 0) return void 0;
3284
+ try {
3285
+ return pathToFileURL(fs6.realpathSync(modulePath)).href;
3286
+ } catch {
3287
+ return void 0;
3288
+ }
3289
+ }
3290
+ function selfHref(moduleUrl) {
3291
+ try {
3292
+ return realHref(fileURLToPath(moduleUrl));
3293
+ } catch {
3294
+ return void 0;
3295
+ }
3296
+ }
3297
+ var thisModule = selfHref(import.meta.url);
3298
+ var invokedAsScript = thisModule !== void 0 && thisModule === realHref(process.argv[1]);
3299
+ if (invokedAsScript) {
3300
+ process.exit(await main(process.argv.slice(2)));
3301
+ }
3302
+ export {
3303
+ createProgram,
3304
+ main,
3305
+ requestsHelpOrVersion
3306
+ };