@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.
@@ -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
  });
@@ -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 localRefs = collectLocalFiles(inputs);
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;
@@ -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
@@ -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, _opts = {}) {
45
+ export function collectLocalFiles(input, opts = {}) {
25
46
  const found = [];
26
- const walk = (node, at) => {
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
- walk(v, at ? `${at}.${k}` : k);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavespeed/cli",
3
- "version": "0.4.22",
3
+ "version": "0.4.24",
4
4
  "description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
5
5
  "publishConfig": {
6
6
  "access": "public"
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. There are no `image` / `video` shortcuts; the model id is always explicit.
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