@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.
@@ -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: [...]}')
@@ -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",
4
- "description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
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
  },