@wavespeed/cli 0.4.4 → 0.4.6
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/history.js +1 -1
- package/dist/commands/run.js +47 -1
- package/dist/lib/api.js +2 -1
- package/dist/lib/validate-inputs.js +62 -0
- package/package.json +2 -2
package/dist/commands/history.js
CHANGED
|
@@ -35,7 +35,7 @@ export function registerHistory(program) {
|
|
|
35
35
|
.option('--page <n>', 'Page number', '1')
|
|
36
36
|
.option('--limit <n>', 'Page size (1-100)', '20')
|
|
37
37
|
.option('--model <id>', 'Filter by model ID')
|
|
38
|
-
.option('--status <s>', 'Filter by status (created, processing, completed, failed, cancelled, timeout)')
|
|
38
|
+
.option('--status <s>', 'Filter by status (created, processing, completed, failed, cancelled, timeout, deleted)')
|
|
39
39
|
.option('--since <time>', 'Predictions created after this time (ISO or YYYY-MM-DD)')
|
|
40
40
|
.option('--until <time>', 'Predictions created before this time (ISO or YYYY-MM-DD)')
|
|
41
41
|
.option('--json', 'Emit JSON: {page, items: [...]}')
|
package/dist/commands/run.js
CHANGED
|
@@ -5,7 +5,8 @@ import fs from 'node:fs';
|
|
|
5
5
|
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
|
-
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.
|
package/dist/lib/api.js
CHANGED
|
@@ -153,6 +153,7 @@ export const PREDICTION_STATUSES = [
|
|
|
153
153
|
'failed',
|
|
154
154
|
'cancelled',
|
|
155
155
|
'timeout',
|
|
156
|
+
'deleted',
|
|
156
157
|
];
|
|
157
158
|
/**
|
|
158
159
|
* Submit a prediction WITHOUT waiting for it. Split from polling so the
|
|
@@ -176,7 +177,7 @@ export async function waitForPrediction(id, opts = {}) {
|
|
|
176
177
|
opts.onTick?.(item.status);
|
|
177
178
|
if (item.status === 'completed')
|
|
178
179
|
return item;
|
|
179
|
-
if (item.status === 'failed' || item.status === 'cancelled' || item.status === 'timeout') {
|
|
180
|
+
if (item.status === 'failed' || item.status === 'cancelled' || item.status === 'timeout' || item.status === 'deleted') {
|
|
180
181
|
throw new Error(`Prediction ${item.status}${item.error ? `: ${item.error}` : ''} (task_id: ${id})`);
|
|
181
182
|
}
|
|
182
183
|
if (opts.signal?.cancelled)
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavespeed/cli",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "WaveSpeed CLI
|
|
3
|
+
"version": "0.4.6",
|
|
4
|
+
"description": "WaveSpeed CLI \u2014 one command to run any WaveSpeed AI model from your terminal.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|