@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.
package/dist/commands/media.js
CHANGED
|
@@ -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
|
});
|
package/dist/commands/price.js
CHANGED
|
@@ -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
|
|
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;
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
package/dist/lib/local-files.js
CHANGED
|
@@ -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,
|
|
45
|
+
export function collectLocalFiles(input, opts = {}) {
|
|
25
46
|
const found = [];
|
|
26
|
-
|
|
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
|
-
|
|
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)
|