@wavespeed/cli 0.4.3 → 0.4.5
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/README.md +5 -0
- package/dist/commands/run.js +49 -3
- package/dist/lib/inputs.js +16 -2
- package/dist/lib/validate-inputs.js +62 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,3 +195,8 @@ Stack: TypeScript, Commander, Inquirer, Chalk, Ora, the official `wavespeed` SDK
|
|
|
195
195
|
## License
|
|
196
196
|
|
|
197
197
|
MIT — see [LICENSE](LICENSE).
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
**[WaveSpeed AI](https://wavespeed.ai/)** — AI image & video generation platform.
|
|
202
|
+
Try it in the browser: **[Image generator](https://wavespeed.ai/image-generator)** · **[Video generator](https://wavespeed.ai/video-generator)**
|
package/dist/commands/run.js
CHANGED
|
@@ -3,9 +3,10 @@ import ora from 'ora';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { requireClient } from '../lib/client.js';
|
|
6
|
-
import { parseInputs } from '../lib/inputs.js';
|
|
6
|
+
import { parseInputs, withInputSyntaxHint } from '../lib/inputs.js';
|
|
7
7
|
import { resolveLocalFiles } from '../lib/local-files.js';
|
|
8
|
-
import { submitPrediction, waitForPrediction } from '../lib/api.js';
|
|
8
|
+
import { fetchModels, submitPrediction, waitForPrediction } from '../lib/api.js';
|
|
9
|
+
import { findUnknownInputs } from '../lib/validate-inputs.js';
|
|
9
10
|
import { uploadWithCache } from '../lib/upload-cache.js';
|
|
10
11
|
import { downloadOutputs, printOutputs } from '../lib/output.js';
|
|
11
12
|
import { emitJson, emitJsonError, isJsonMode, log, setJsonMode } from '../lib/log.js';
|
|
@@ -74,6 +75,7 @@ export function registerRun(program) {
|
|
|
74
75
|
.option('--input-file <path>', 'JSON file with full input object')
|
|
75
76
|
.option('--download [path]', 'Save outputs locally (optional path template, e.g. "./out/{index}.{ext}")')
|
|
76
77
|
.option('--sync', 'Attempt sync mode; timed-out tasks can still be queried later')
|
|
78
|
+
.option('--no-validate', 'Skip pre-submit input validation against the model schema')
|
|
77
79
|
.option('--json', 'Emit a single JSON object on stdout (progress goes to stderr)')
|
|
78
80
|
.option('--output-dir <dir>', 'Directory for --download when no path is given')
|
|
79
81
|
.action(async (modelArg, opts) => {
|
|
@@ -97,6 +99,50 @@ export function registerRun(program) {
|
|
|
97
99
|
Object.assign(input, parseInputs(opts.input ?? []));
|
|
98
100
|
if (opts.prompt)
|
|
99
101
|
input.prompt = opts.prompt;
|
|
102
|
+
// Reject inputs the model's schema does not declare BEFORE anything
|
|
103
|
+
// costs money. The API's entry whitelist silently drops unknown keys,
|
|
104
|
+
// so a typo doesn't fail — it bills a generation that ignored the
|
|
105
|
+
// parameter. Validation fails open (no schema → no check) and, to
|
|
106
|
+
// avoid stale-cache false positives, re-fetches the catalog once
|
|
107
|
+
// before rejecting.
|
|
108
|
+
if (opts.validate !== false) {
|
|
109
|
+
try {
|
|
110
|
+
let { models } = await fetchModels();
|
|
111
|
+
let found = models.find((m) => m.model_id === model);
|
|
112
|
+
let report = findUnknownInputs(input, found);
|
|
113
|
+
if (report) {
|
|
114
|
+
({ models } = await fetchModels({ refresh: true }));
|
|
115
|
+
found = models.find((m) => m.model_id === model);
|
|
116
|
+
report = findUnknownInputs(input, found);
|
|
117
|
+
}
|
|
118
|
+
if (report) {
|
|
119
|
+
const lines = report.unknown.map((k) => {
|
|
120
|
+
const hint = report.suggestions.get(k);
|
|
121
|
+
return ` ${k}` + (hint ? ` (did you mean \`${hint}\`?)` : '');
|
|
122
|
+
});
|
|
123
|
+
const msg = `Model ${model} does not accept: ${report.unknown.join(', ')}. ` +
|
|
124
|
+
`The API silently drops unknown inputs, so this would bill a run that ignores them.`;
|
|
125
|
+
if (isJsonMode()) {
|
|
126
|
+
emitJsonError(msg, { unknown_inputs: report.unknown, accepted_inputs: report.known });
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.error(chalk.red('Error: ') + `unknown input${report.unknown.length > 1 ? 's' : ''} for ` + chalk.cyan(model) + ':');
|
|
130
|
+
for (const l of lines)
|
|
131
|
+
console.error(chalk.yellow(l));
|
|
132
|
+
console.error(chalk.gray(' The API silently drops unknown inputs — the run would be billed with them ignored.'));
|
|
133
|
+
console.error(chalk.gray(' Accepted inputs: ') + report.known.join(', '));
|
|
134
|
+
console.error(chalk.gray(' Full schema: ') + chalk.cyan(`wavespeed schema ${model}`));
|
|
135
|
+
console.error(chalk.gray(' Bypass check: ') + chalk.cyan('--no-validate'));
|
|
136
|
+
}
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Catalog unreachable — validation is best-effort, never a blocker.
|
|
142
|
+
// (The unknown-input rejection above exits the process directly and
|
|
143
|
+
// is not routed through here.)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
100
146
|
// `@path` inputs become hosted URLs before submission. Only the
|
|
101
147
|
// explicit @ marker uploads — the CLI never guesses that a bare value
|
|
102
148
|
// was meant to be a file.
|
|
@@ -137,7 +183,7 @@ export function registerRun(program) {
|
|
|
137
183
|
submitted = await submitPrediction(model, input, { sync: !!opts.sync });
|
|
138
184
|
}
|
|
139
185
|
catch (err) {
|
|
140
|
-
const message = err.message ?? String(err);
|
|
186
|
+
const message = withInputSyntaxHint(err.message ?? String(err), opts.input);
|
|
141
187
|
spinner.fail(message);
|
|
142
188
|
if (isJsonMode())
|
|
143
189
|
emitJsonError(message);
|
package/dist/lib/inputs.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// - bare strings stay strings
|
|
3
3
|
// - "123" / "1.5" parsed as number
|
|
4
4
|
// - "true" / "false" parsed as boolean
|
|
5
|
-
// -
|
|
6
|
-
//
|
|
5
|
+
// - values starting with { or [ are parsed as JSON, and stay strings if that
|
|
6
|
+
// fails — so an array has to be written as valid JSON: '["a","b"]'.
|
|
7
|
+
// A bare "[a,b,c]" is not JSON and reaches the API as a string.
|
|
7
8
|
// - keys with dots like "size.width" become nested
|
|
8
9
|
function coerce(raw) {
|
|
9
10
|
if (raw === 'true')
|
|
@@ -50,3 +51,16 @@ function setPath(obj, dotted, value) {
|
|
|
50
51
|
}
|
|
51
52
|
cur[parts[parts.length - 1]] = value;
|
|
52
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The API rejects a scalar sent to an array field. The message says what is
|
|
56
|
+
* wrong but not how to write it, and `-i key=value` gives no hint that an
|
|
57
|
+
* array needs JSON — so point at the fix rather than mutating the input.
|
|
58
|
+
*/
|
|
59
|
+
export function withInputSyntaxHint(message, pairs = []) {
|
|
60
|
+
const field = /field "([^"]+)" must be an array/.exec(message)?.[1];
|
|
61
|
+
if (!field)
|
|
62
|
+
return message;
|
|
63
|
+
const given = pairs.find((p) => p.slice(0, p.indexOf('=')).trim() === field);
|
|
64
|
+
const sample = given ? given.slice(given.indexOf('=') + 1) : 'value';
|
|
65
|
+
return `${message}\nWrite arrays as JSON: -i '${field}=["${sample}"]'`;
|
|
66
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Pre-submit input validation against a model's published request schema.
|
|
2
|
+
//
|
|
3
|
+
// The API's entry whitelist silently DROPS any input key that is not in the
|
|
4
|
+
// model's schema — a typo like `size=2560*1440` against a model whose real
|
|
5
|
+
// knobs are `aspect_ratio`/`resolution` doesn't fail, it just quietly bills a
|
|
6
|
+
// generation with defaults. Catching that before submission is the only
|
|
7
|
+
// moment the mistake is still free.
|
|
8
|
+
//
|
|
9
|
+
// Validation fails OPEN: no schema (brand-new model, catalog lag, offline
|
|
10
|
+
// cache miss) means no check. It must never block a submission the API would
|
|
11
|
+
// have accepted.
|
|
12
|
+
export function requestSchemaProps(model) {
|
|
13
|
+
const props = model?.api_schema?.api_schemas?.[0]?.request_schema?.properties;
|
|
14
|
+
if (!props)
|
|
15
|
+
return null;
|
|
16
|
+
const keys = Object.keys(props);
|
|
17
|
+
return keys.length > 0 ? keys : null;
|
|
18
|
+
}
|
|
19
|
+
// Small edit distance for typo hints — inputs are short snake_case tokens, so
|
|
20
|
+
// a full Levenshtein over ≤40 keys is nothing.
|
|
21
|
+
function editDistance(a, b) {
|
|
22
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
23
|
+
for (let j = 0; j <= b.length; j++)
|
|
24
|
+
dp[0][j] = j;
|
|
25
|
+
for (let i = 1; i <= a.length; i++)
|
|
26
|
+
for (let j = 1; j <= b.length; j++)
|
|
27
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
28
|
+
return dp[a.length][b.length];
|
|
29
|
+
}
|
|
30
|
+
function closest(key, known) {
|
|
31
|
+
let best;
|
|
32
|
+
let bestD = Infinity;
|
|
33
|
+
for (const k of known) {
|
|
34
|
+
const d = editDistance(key.toLowerCase(), k.toLowerCase());
|
|
35
|
+
if (d < bestD) {
|
|
36
|
+
bestD = d;
|
|
37
|
+
best = k;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// A hint is only a hint when it's plausibly the same word.
|
|
41
|
+
return bestD <= Math.max(2, Math.floor(key.length / 3)) ? best : undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compare top-level input keys with the schema's properties.
|
|
45
|
+
* Returns null when everything checks out or no schema is available.
|
|
46
|
+
*/
|
|
47
|
+
export function findUnknownInputs(input, model) {
|
|
48
|
+
const known = requestSchemaProps(model);
|
|
49
|
+
if (!known)
|
|
50
|
+
return null;
|
|
51
|
+
const knownSet = new Set(known);
|
|
52
|
+
const unknown = Object.keys(input).filter((k) => !knownSet.has(k));
|
|
53
|
+
if (unknown.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
const suggestions = new Map();
|
|
56
|
+
for (const k of unknown) {
|
|
57
|
+
const hit = closest(k, known);
|
|
58
|
+
if (hit)
|
|
59
|
+
suggestions.set(k, hit);
|
|
60
|
+
}
|
|
61
|
+
return { unknown, known, suggestions };
|
|
62
|
+
}
|