@wavespeed/cli 0.4.22 → 0.4.23

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.23",
4
4
  "description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
5
5
  "publishConfig": {
6
6
  "access": "public"