@wavespeed/cli 0.4.22 → 0.4.24
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/commands/media.js +1 -0
- package/dist/commands/price.js +4 -1
- package/dist/commands/run.js +2 -1
- package/dist/lib/local-files.js +31 -7
- package/dist/lib/validate-inputs.js +11 -0
- package/package.json +1 -1
- package/skills/SKILL.md +2 -1
package/dist/commands/media.js
CHANGED
|
@@ -45,6 +45,7 @@ async function execute(prompt, opts, build, media, rest) {
|
|
|
45
45
|
try {
|
|
46
46
|
const spinner = !isJsonMode() ? ora({ color: 'magenta' }) : null;
|
|
47
47
|
const resolved = await resolveLocalFiles(media, {
|
|
48
|
+
allFields: true,
|
|
48
49
|
upload: async (file) => (await uploadWithCache(file, (f) => client.upload(f))).url,
|
|
49
50
|
onUpload: (file, i, total) => spinner?.start(total > 1 ? `Uploading ${path.basename(file)} (${i + 1}/${total})…` : `Uploading ${path.basename(file)}…`),
|
|
50
51
|
});
|
package/dist/commands/price.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { fetchPricing, modelPlaygroundUrl } from '../lib/api.js';
|
|
4
4
|
import { parseInputs } from '../lib/inputs.js';
|
|
5
|
+
import { modelInputProps } from '../lib/validate-inputs.js';
|
|
5
6
|
import { resolveModelToken } from '../lib/config.js';
|
|
6
7
|
import { collectLocalFiles, resolveLocalFiles } from '../lib/local-files.js';
|
|
7
8
|
import { uploadWithCache } from '../lib/upload-cache.js';
|
|
@@ -34,7 +35,8 @@ export function registerPrice(program) {
|
|
|
34
35
|
// pricing engine. Uploading is slow and quota-limited, so it is opt-in:
|
|
35
36
|
// without --upload we stop and say why, rather than silently sending a
|
|
36
37
|
// filesystem path and quoting from garbage.
|
|
37
|
-
const
|
|
38
|
+
const props = await modelInputProps(resolved.model);
|
|
39
|
+
const localRefs = collectLocalFiles(inputs, { props });
|
|
38
40
|
let uploadedUrls = {};
|
|
39
41
|
if (localRefs.length > 0 && !opts.upload) {
|
|
40
42
|
const files = [...new Set(localRefs.map((r) => r.path))].join(', ');
|
|
@@ -52,6 +54,7 @@ export function registerPrice(program) {
|
|
|
52
54
|
const upSpinner = !opts.json ? ora('Uploading for an exact quote…').start() : null;
|
|
53
55
|
try {
|
|
54
56
|
const resolvedFiles = await resolveLocalFiles(inputs, {
|
|
57
|
+
props,
|
|
55
58
|
upload: async (file) => {
|
|
56
59
|
const { url, cached } = await uploadWithCache(file, (f) => client.upload(f));
|
|
57
60
|
uploadedUrls[file] = url;
|
package/dist/commands/run.js
CHANGED
|
@@ -6,7 +6,7 @@ import { requireClient } from '../lib/client.js';
|
|
|
6
6
|
import { parseInputs, withInputSyntaxHint } from '../lib/inputs.js';
|
|
7
7
|
import { resolveLocalFiles } from '../lib/local-files.js';
|
|
8
8
|
import { fetchModels, submitPrediction, waitForPrediction } from '../lib/api.js';
|
|
9
|
-
import { findUnknownInputs } from '../lib/validate-inputs.js';
|
|
9
|
+
import { findUnknownInputs, modelInputProps } from '../lib/validate-inputs.js';
|
|
10
10
|
import { uploadWithCache } from '../lib/upload-cache.js';
|
|
11
11
|
import { downloadOutputs, printOutputs } from '../lib/output.js';
|
|
12
12
|
import { emitJson, emitJsonError, isJsonMode, log, setJsonMode } from '../lib/log.js';
|
|
@@ -149,6 +149,7 @@ export function registerRun(program) {
|
|
|
149
149
|
try {
|
|
150
150
|
const uploadSpinner = !isJsonMode() ? ora({ color: 'magenta' }) : null;
|
|
151
151
|
const resolved = await resolveLocalFiles(input, {
|
|
152
|
+
props: await modelInputProps(model),
|
|
152
153
|
upload: async (file) => (await uploadWithCache(file, (f) => client.upload(f))).url,
|
|
153
154
|
onUpload: (file, i, total) => {
|
|
154
155
|
uploadSpinner?.start(total > 1
|
package/dist/lib/local-files.js
CHANGED
|
@@ -6,9 +6,30 @@
|
|
|
6
6
|
// asked to share based on a heuristic; a value that merely looks like a
|
|
7
7
|
// filename must never leave the machine. No `@`, no upload: bare paths pass
|
|
8
8
|
// through to the API untouched and fail model validation there if they were
|
|
9
|
-
// meant to be files.
|
|
9
|
+
// meant to be files. The `@` is only read in media fields (isMediaField):
|
|
10
|
+
// prompts legitimately start with "@Image 1".
|
|
10
11
|
import fs from 'node:fs';
|
|
11
12
|
import path from 'node:path';
|
|
13
|
+
// The same rule api-server (pkg/openapi IsMediaField) and the dashboard use to
|
|
14
|
+
// tell which inputs carry media, so a text field such as `prompt` that merely
|
|
15
|
+
// starts with "@" ("@Image 1 waves") is never read as a file path:
|
|
16
|
+
// - an enum is never media;
|
|
17
|
+
// - an explicit x-ui-component decides: uploader, uploaders and mask are
|
|
18
|
+
// media, any other component is not;
|
|
19
|
+
// - otherwise the name decides: image, *_image, *_image_url, video…, audio…
|
|
20
|
+
// (and their plurals), plus mask (api-server needs the uploader component
|
|
21
|
+
// for masks; the CLI also takes the bare name, which no text field uses).
|
|
22
|
+
const MEDIA_NAME = /(^|_)(image|image_url|video|video_url|audio|audio_url|images|image_urls|videos|video_urls|audios|audio_urls|mask)$/;
|
|
23
|
+
export function isMediaField(name, prop) {
|
|
24
|
+
const def = (prop && typeof prop === 'object' ? prop : {});
|
|
25
|
+
if (Array.isArray(def.enum) && def.enum.length > 0)
|
|
26
|
+
return false;
|
|
27
|
+
const component = def['x-ui-component'];
|
|
28
|
+
if (typeof component === 'string' && component !== '') {
|
|
29
|
+
return ['uploader', 'uploaders', 'mask'].includes(component.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
return MEDIA_NAME.test(name.toLowerCase());
|
|
32
|
+
}
|
|
12
33
|
function isRegularFile(p) {
|
|
13
34
|
try {
|
|
14
35
|
return fs.statSync(p).isFile();
|
|
@@ -21,24 +42,27 @@ function isRegularFile(p) {
|
|
|
21
42
|
* Walk the input and list every `@path` value. Pure over `exists`, so the
|
|
22
43
|
* rules can be tested without touching the disk.
|
|
23
44
|
*/
|
|
24
|
-
export function collectLocalFiles(input,
|
|
45
|
+
export function collectLocalFiles(input, opts = {}) {
|
|
25
46
|
const found = [];
|
|
26
|
-
|
|
47
|
+
// `media` is whether the nearest named field (an array's elements take the
|
|
48
|
+
// array's name) carries media.
|
|
49
|
+
const walk = (node, at, media, depth) => {
|
|
27
50
|
if (Array.isArray(node)) {
|
|
28
|
-
node.forEach((v, i) => walk(v, at ? `${at}.${i}` : String(i)));
|
|
51
|
+
node.forEach((v, i) => walk(v, at ? `${at}.${i}` : String(i), media, depth));
|
|
29
52
|
return;
|
|
30
53
|
}
|
|
31
54
|
if (node && typeof node === 'object') {
|
|
32
55
|
for (const [k, v] of Object.entries(node)) {
|
|
33
|
-
|
|
56
|
+
const prop = depth === 0 ? opts.props?.[k] : undefined;
|
|
57
|
+
walk(v, at ? `${at}.${k}` : k, opts.allFields || isMediaField(k, prop), depth + 1);
|
|
34
58
|
}
|
|
35
59
|
return;
|
|
36
60
|
}
|
|
37
|
-
if (typeof node !== 'string' || !node.startsWith('@'))
|
|
61
|
+
if (!media || typeof node !== 'string' || !node.startsWith('@'))
|
|
38
62
|
return;
|
|
39
63
|
found.push({ path: node.slice(1), at });
|
|
40
64
|
};
|
|
41
|
-
walk(input, '');
|
|
65
|
+
walk(input, '', !!opts.allFields, 0);
|
|
42
66
|
return found;
|
|
43
67
|
}
|
|
44
68
|
function setAt(root, dotted, value) {
|
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
// Validation fails OPEN: no schema (brand-new model, catalog lag, offline
|
|
10
10
|
// cache miss) means no check. It must never block a submission the API would
|
|
11
11
|
// have accepted.
|
|
12
|
+
import { fetchModels } from './api.js';
|
|
13
|
+
/** The model's input properties with their definitions, or undefined when the catalog has no schema (fails open). */
|
|
14
|
+
export async function modelInputProps(modelId) {
|
|
15
|
+
try {
|
|
16
|
+
const { models } = await fetchModels();
|
|
17
|
+
return models.find((m) => m.model_id === modelId)?.api_schema?.api_schemas?.[0]?.request_schema?.properties;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
12
23
|
export function requestSchemaProps(model) {
|
|
13
24
|
const props = model?.api_schema?.api_schemas?.[0]?.request_schema?.properties;
|
|
14
25
|
if (!props)
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Generate or edit AI media (image, video, audio, 3D) by calling the
|
|
|
5
5
|
|
|
6
6
|
# WaveSpeed
|
|
7
7
|
|
|
8
|
-
You have access to the `wavespeed` CLI. Every generation flows through one verb
|
|
8
|
+
You have access to the `wavespeed` CLI. Every generation flows through one generic verb, `wavespeed run`, and the model id is always explicit. For plain image and video work there are also shortcut commands (see the end).
|
|
9
9
|
|
|
10
10
|
## The three-step pattern
|
|
11
11
|
|
|
@@ -78,6 +78,7 @@ The CLI never modifies the user's prompt or inputs. The single exception is expl
|
|
|
78
78
|
- Use `--json` on every `run` so you can read `outputs[0]` programmatically.
|
|
79
79
|
- `wavespeed delete` requires `--yes` when run non-interactively (that includes you).
|
|
80
80
|
- Cost and spend: `wavespeed price <id> -i k=v --json` estimates a run without generating or charging; `wavespeed balance` shows remaining credit; `wavespeed usage` (totals, per-model) and `wavespeed billings` (per-charge records) show what was spent.
|
|
81
|
+
- If a run fails with `Insufficient credits` (or says a top-up is required), the balance is too low: do not retry. Tell the user, and give them https://wavespeed.ai/top-up to add credits (`wavespeed balance` shows the balance and the link). You cannot pay for them.
|
|
81
82
|
- A `run` that fails with a gateway or network error (502/503/504, connection reset, timeout) does NOT mean the task failed, and the submit request is never retried automatically, so re-running can create a duplicate paid task. Do not re-run immediately. If the error contains a task id (`task_id` / prediction id), keep checking it with `wavespeed show <id> --json` until its status is terminal (`completed`, `failed`, `cancelled`, `timeout`, `deleted`). If no id was returned, check recent predictions with `wavespeed history --json` (add `--model <id>`; default window is the last 24h) and re-run once only if none matches. A client-side wait timeout or an interrupted `run` does not mean the task failed: tasks can run for a long time, so do not give up after a fixed time.
|
|
82
83
|
|
|
83
84
|
## Shortcuts for common image/video work
|