@wavespeed/cli 0.4.0 → 0.4.2
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/README.md +13 -1
- package/dist/commands/billings.js +3 -2
- package/dist/commands/delete.js +1 -1
- package/dist/commands/history.js +5 -4
- package/dist/commands/price.js +44 -3
- package/dist/commands/upload.js +1 -1
- package/dist/commands/usage.js +1 -10
- package/dist/lib/pricing-vars.js +75 -0
- package/dist/lib/time.js +16 -0
- package/package.json +1 -1
- package/skills/SKILL.md +12 -9
package/README.md
CHANGED
|
@@ -133,11 +133,13 @@ wavespeed models [query] Browse the live catalog (cached 1h)
|
|
|
133
133
|
wavespeed aliases List aliases from wavespeed.json + user config
|
|
134
134
|
|
|
135
135
|
# files
|
|
136
|
-
wavespeed upload <file...> Upload local file(s) → CDN URLs
|
|
136
|
+
wavespeed upload <file...> Upload local file(s) → CDN URLs (24h dedup cache)
|
|
137
137
|
wavespeed download <url...> Save URLs to disk
|
|
138
138
|
|
|
139
139
|
# account & history
|
|
140
140
|
wavespeed balance Show your current credit balance
|
|
141
|
+
wavespeed usage [--since --until] Spend + request counts, per-model (last 7d)
|
|
142
|
+
wavespeed billings [--type --since …] Itemized charges and refunds
|
|
141
143
|
wavespeed price <model> -i k=v Estimate the cost of a run (no charge)
|
|
142
144
|
wavespeed top-up Open https://wavespeed.ai/top-up
|
|
143
145
|
wavespeed history [--limit --status …] List recent predictions (last 24h)
|
|
@@ -150,6 +152,16 @@ wavespeed init Create a wavespeed.json with defaults +
|
|
|
150
152
|
wavespeed skill install Drop SKILL.md for coding agents
|
|
151
153
|
```
|
|
152
154
|
|
|
155
|
+
**Pricing is an estimate.** `wavespeed price` quotes from the model's pricing
|
|
156
|
+
formula, and many models bill from an input — audio/video duration, frame count,
|
|
157
|
+
text length. Those inputs are optional in the quote: ask for a price without
|
|
158
|
+
them and the formula collapses to the model's **base price**, which is the floor
|
|
159
|
+
of its range, not a typical run. `wavespeed price wavespeed-ai/infinitetalk`
|
|
160
|
+
says `$0.15`; a 60s audio actually bills `$1.80`. The command names the inputs it
|
|
161
|
+
was blind to (`unpriced_inputs` / `at_base_price` in `--json`) — supply them for
|
|
162
|
+
a real quote. Whatever it says, the amount actually charged for a run is
|
|
163
|
+
authoritative.
|
|
164
|
+
|
|
153
165
|
## Auth
|
|
154
166
|
|
|
155
167
|
| | Use when |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { fetchBillings } from '../lib/api.js';
|
|
4
|
+
import { toIso } from '../lib/time.js';
|
|
4
5
|
function money(v) {
|
|
5
6
|
if (v === undefined)
|
|
6
7
|
return '';
|
|
@@ -35,8 +36,8 @@ export function registerBillings(program) {
|
|
|
35
36
|
type: opts.type,
|
|
36
37
|
model: opts.model,
|
|
37
38
|
accessKey: opts.key,
|
|
38
|
-
startTime: opts.since,
|
|
39
|
-
endTime: opts.until,
|
|
39
|
+
startTime: opts.since ? toIso(opts.since) : undefined,
|
|
40
|
+
endTime: opts.until ? toIso(opts.until, true) : undefined,
|
|
40
41
|
});
|
|
41
42
|
spinner?.stop();
|
|
42
43
|
if (opts.json) {
|
package/dist/commands/delete.js
CHANGED
|
@@ -7,7 +7,7 @@ export function registerDelete(program) {
|
|
|
7
7
|
.command('delete')
|
|
8
8
|
.description('Delete one or more predictions from your history')
|
|
9
9
|
.argument('<id...>', 'Prediction ID(s) to delete')
|
|
10
|
-
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
10
|
+
.option('-y, --yes', 'Skip the confirmation prompt (required in scripts / non-interactive use)')
|
|
11
11
|
.option('--json', 'Emit JSON: {deleted: [...ids]}')
|
|
12
12
|
.action(async (ids, opts) => {
|
|
13
13
|
// --json controls output format only; it must never stand in for
|
package/dist/commands/history.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { fetchHistory, PREDICTION_STATUSES } from '../lib/api.js';
|
|
4
|
+
import { toIso } from '../lib/time.js';
|
|
4
5
|
function statusBadge(status) {
|
|
5
6
|
const map = {
|
|
6
7
|
completed: chalk.bgGreen.black,
|
|
@@ -35,8 +36,8 @@ export function registerHistory(program) {
|
|
|
35
36
|
.option('--limit <n>', 'Page size (1-100)', '20')
|
|
36
37
|
.option('--model <id>', 'Filter by model ID')
|
|
37
38
|
.option('--status <s>', 'Filter by status (created, processing, completed, failed, cancelled, timeout)')
|
|
38
|
-
.option('--since <
|
|
39
|
-
.option('--until <
|
|
39
|
+
.option('--since <time>', 'Predictions created after this time (ISO or YYYY-MM-DD)')
|
|
40
|
+
.option('--until <time>', 'Predictions created before this time (ISO or YYYY-MM-DD)')
|
|
40
41
|
.option('--json', 'Emit JSON: {page, items: [...]}')
|
|
41
42
|
.action(async (opts) => {
|
|
42
43
|
if (opts.status && !PREDICTION_STATUSES.includes(opts.status)) {
|
|
@@ -54,8 +55,8 @@ export function registerHistory(program) {
|
|
|
54
55
|
pageSize: parseInt(opts.limit, 10),
|
|
55
56
|
model: opts.model,
|
|
56
57
|
status: opts.status,
|
|
57
|
-
createdAfter: opts.since,
|
|
58
|
-
createdBefore: opts.until,
|
|
58
|
+
createdAfter: opts.since ? toIso(opts.since) : undefined,
|
|
59
|
+
createdBefore: opts.until ? toIso(opts.until, true) : undefined,
|
|
59
60
|
});
|
|
60
61
|
spinner?.stop();
|
|
61
62
|
if (opts.json) {
|
package/dist/commands/price.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
-
import { fetchPricing } from '../lib/api.js';
|
|
3
|
+
import { fetchPricing, fetchModels } from '../lib/api.js';
|
|
4
|
+
import { missingPriceVars, isFloorQuote } from '../lib/pricing-vars.js';
|
|
4
5
|
import { parseInputs } from '../lib/inputs.js';
|
|
5
6
|
import { resolveModelToken } from '../lib/config.js';
|
|
6
7
|
import { collectLocalFiles, resolveLocalFiles } from '../lib/local-files.js';
|
|
@@ -13,7 +14,7 @@ export function registerPrice(program) {
|
|
|
13
14
|
.argument('<model>', 'Model ID or alias')
|
|
14
15
|
.option('-i, --input <pair...>', 'Inputs as key=value (same syntax as `wavespeed run`)', [])
|
|
15
16
|
.option('-p, --prompt <text>', 'Shorthand for --input prompt=<text>')
|
|
16
|
-
.option('--upload', 'Upload
|
|
17
|
+
.option('--upload', 'Upload @file inputs first so media-based pricing is exact (quota applies; identical bytes cached 24h)')
|
|
17
18
|
.option('--json', 'Emit JSON')
|
|
18
19
|
.action(async (modelArg, opts) => {
|
|
19
20
|
const resolved = resolveModelToken(modelArg);
|
|
@@ -73,9 +74,35 @@ export function registerPrice(program) {
|
|
|
73
74
|
const spinner = !opts.json ? ora('Calculating price…').start() : null;
|
|
74
75
|
try {
|
|
75
76
|
const data = await fetchPricing(resolved.model, inputs);
|
|
77
|
+
// /model/price evaluates the model's formula against whatever inputs we
|
|
78
|
+
// send and never complains about the ones we left out — a formula with
|
|
79
|
+
// every variable missing collapses to base_price. That is the floor of
|
|
80
|
+
// the model's range, not a representative charge, so name the inputs
|
|
81
|
+
// this quote could not see rather than presenting the floor as "the"
|
|
82
|
+
// price. Catalog is cached 1h; a lookup failure only costs the detail.
|
|
83
|
+
let unpriced = [];
|
|
84
|
+
let atFloor = false;
|
|
85
|
+
try {
|
|
86
|
+
const { models } = await fetchModels();
|
|
87
|
+
const meta = models.find((m) => m.model_id === resolved.model);
|
|
88
|
+
const props = meta?.api_schema?.api_schemas?.[0]?.request_schema?.properties;
|
|
89
|
+
unpriced = missingPriceVars(meta?.formula, inputs, props);
|
|
90
|
+
atFloor = isFloorQuote(meta?.formula, inputs);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* no catalog — fall back to the generic disclaimer below */
|
|
94
|
+
}
|
|
76
95
|
spinner?.stop();
|
|
96
|
+
const disclaimer = 'Estimate only, for reference — the amount actually charged for a run is authoritative.';
|
|
77
97
|
if (opts.json) {
|
|
78
|
-
process.stdout.write(JSON.stringify({
|
|
98
|
+
process.stdout.write(JSON.stringify({
|
|
99
|
+
...data,
|
|
100
|
+
estimate: true,
|
|
101
|
+
unpriced_inputs: unpriced,
|
|
102
|
+
at_base_price: atFloor,
|
|
103
|
+
disclaimer,
|
|
104
|
+
uploaded: uploadedUrls,
|
|
105
|
+
}, null, 2) + '\n');
|
|
79
106
|
return;
|
|
80
107
|
}
|
|
81
108
|
console.log();
|
|
@@ -86,6 +113,19 @@ export function registerPrice(program) {
|
|
|
86
113
|
(data.discounted_price > 0 && data.discounted_price !== data.price
|
|
87
114
|
? chalk.gray(` (list $${data.price})`)
|
|
88
115
|
: ''));
|
|
116
|
+
if (unpriced.length > 0) {
|
|
117
|
+
console.log(' ' +
|
|
118
|
+
chalk.yellow('note: ') +
|
|
119
|
+
chalk.yellow(`this model is priced from ${unpriced.join(', ')}, which ${unpriced.length === 1 ? 'was' : 'were'} not supplied.`));
|
|
120
|
+
console.log(' ' +
|
|
121
|
+
chalk.gray(atFloor
|
|
122
|
+
? 'The figure above is the base price — the floor of this model’s range, not a typical run.'
|
|
123
|
+
: 'The figure above prices the rest at their defaults and can move once these are set.'));
|
|
124
|
+
console.log(' ' +
|
|
125
|
+
chalk.gray('Pass ') +
|
|
126
|
+
chalk.cyan(unpriced.map((v) => `-i ${v}=…`).join(' ')) +
|
|
127
|
+
chalk.gray(' for a representative quote.'));
|
|
128
|
+
}
|
|
89
129
|
console.log();
|
|
90
130
|
for (const [file, url] of Object.entries(uploadedUrls)) {
|
|
91
131
|
console.log(' ' + chalk.gray('uploaded: ') + chalk.gray(file + ' → ') + chalk.cyan(url));
|
|
@@ -95,6 +135,7 @@ export function registerPrice(program) {
|
|
|
95
135
|
}
|
|
96
136
|
console.log();
|
|
97
137
|
console.log(chalk.gray('Run it: ') + chalk.cyan(`wavespeed run ${modelArg}` + (opts.prompt ? ` -p "${opts.prompt}"` : '')));
|
|
138
|
+
console.log(chalk.gray(disclaimer));
|
|
98
139
|
console.log();
|
|
99
140
|
}
|
|
100
141
|
catch (err) {
|
package/dist/commands/upload.js
CHANGED
|
@@ -6,7 +6,7 @@ import { uploadWithCache } from '../lib/upload-cache.js';
|
|
|
6
6
|
export function registerUpload(program) {
|
|
7
7
|
program
|
|
8
8
|
.command('upload')
|
|
9
|
-
.description('Upload one or more local files to Wavespeed and print their CDN URLs')
|
|
9
|
+
.description('Upload one or more local files to Wavespeed and print their CDN URLs (identical bytes reuse the same upload for 24h)')
|
|
10
10
|
.argument('<file...>', 'Path(s) to file(s)')
|
|
11
11
|
.option('--json', 'Emit a JSON object on stdout')
|
|
12
12
|
.action(async (files, opts) => {
|
package/dist/commands/usage.js
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { fetchUsageStats } from '../lib/api.js';
|
|
4
|
-
|
|
5
|
-
function toIso(value, endOfDay = false) {
|
|
6
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
7
|
-
return value + (endOfDay ? 'T23:59:59Z' : 'T00:00:00Z');
|
|
8
|
-
}
|
|
9
|
-
const d = new Date(value);
|
|
10
|
-
if (isNaN(d.getTime()))
|
|
11
|
-
throw new Error(`Invalid time "${value}". Use ISO 8601 or YYYY-MM-DD.`);
|
|
12
|
-
return d.toISOString();
|
|
13
|
-
}
|
|
4
|
+
import { toIso } from '../lib/time.js';
|
|
14
5
|
export function registerUsage(program) {
|
|
15
6
|
program
|
|
16
7
|
.command('usage')
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Pricing formulas are JSONata expressions returned by /api/v3/models, e.g.
|
|
2
|
+
// {"total_price": $max([5, $min([$ceil(get_duration_v3(audio)), 600])]) / 5 * base_price}
|
|
3
|
+
// {"total_price": base_price * duration / 5}
|
|
4
|
+
//
|
|
5
|
+
// The pricing endpoint evaluates them server-side and happily accepts a partial
|
|
6
|
+
// (or empty) `inputs` object — a formula whose variables are all missing
|
|
7
|
+
// collapses to `base_price`, which is the floor of the model's price range, not
|
|
8
|
+
// a typical charge. `wavespeed price wavespeed-ai/infinitetalk` with no inputs
|
|
9
|
+
// quotes $0.15 while a 60s audio actually costs $1.80. So we parse the formula
|
|
10
|
+
// to find which inputs move the price, and say which ones the quote is blind to.
|
|
11
|
+
const RESERVED = new Set(['base_price', 'total_price', 'true', 'false', 'null']);
|
|
12
|
+
/**
|
|
13
|
+
* Variable names a pricing formula reads from the model's inputs, in first-seen
|
|
14
|
+
* order. Excludes JSONata builtins (`$max`), helper calls (`get_duration_v3`),
|
|
15
|
+
* string literals, and `base_price`/`total_price`.
|
|
16
|
+
*
|
|
17
|
+
* Returns [] for a flat-priced model (`base_price * 1`) or an unparseable
|
|
18
|
+
* formula — callers should treat [] as "nothing input-dependent to warn about".
|
|
19
|
+
*/
|
|
20
|
+
export function extractPriceVars(formula) {
|
|
21
|
+
if (!formula)
|
|
22
|
+
return [];
|
|
23
|
+
// Drop string literals first so "720p" / "total_price" keys never register.
|
|
24
|
+
const stripped = formula.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
25
|
+
const vars = [];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const re = /(\$?)([A-Za-z_][A-Za-z0-9_]*)\s*(\()?/g;
|
|
28
|
+
let m;
|
|
29
|
+
while ((m = re.exec(stripped)) !== null) {
|
|
30
|
+
const [, dollar, name, call] = m;
|
|
31
|
+
if (dollar)
|
|
32
|
+
continue; // $max, $ceil, $length — JSONata builtin
|
|
33
|
+
if (call)
|
|
34
|
+
continue; // get_duration_v3(...) — server-side helper
|
|
35
|
+
if (RESERVED.has(name))
|
|
36
|
+
continue;
|
|
37
|
+
if (seen.has(name))
|
|
38
|
+
continue;
|
|
39
|
+
seen.add(name);
|
|
40
|
+
vars.push(name);
|
|
41
|
+
}
|
|
42
|
+
return vars;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Price-driving variables the quote is genuinely blind to.
|
|
46
|
+
*
|
|
47
|
+
* A driver the caller omitted but that carries a schema default is NOT blind —
|
|
48
|
+
* the server priced it at that default, which is exactly what a run would use.
|
|
49
|
+
* Only drivers with no default (infinitetalk's `audio`, seedance's
|
|
50
|
+
* `reference_videos`) leave the quote uninformed, and those are the ones that
|
|
51
|
+
* silently collapse the formula toward base_price.
|
|
52
|
+
*/
|
|
53
|
+
export function missingPriceVars(formula, inputs, schemaProps) {
|
|
54
|
+
return extractPriceVars(formula).filter((v) => {
|
|
55
|
+
const val = inputs[v];
|
|
56
|
+
if (val !== undefined && val !== null && val !== '')
|
|
57
|
+
return false;
|
|
58
|
+
const prop = schemaProps?.[v];
|
|
59
|
+
return !(prop && 'default' in prop);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* True when every price driver is unsupplied — the case where the formula
|
|
64
|
+
* degenerates to `base_price` and the quote is the floor of the model's range
|
|
65
|
+
* rather than a representative charge.
|
|
66
|
+
*/
|
|
67
|
+
export function isFloorQuote(formula, inputs) {
|
|
68
|
+
const vars = extractPriceVars(formula);
|
|
69
|
+
if (vars.length === 0)
|
|
70
|
+
return false;
|
|
71
|
+
return vars.every((v) => {
|
|
72
|
+
const val = inputs[v];
|
|
73
|
+
return val === undefined || val === null || val === '';
|
|
74
|
+
});
|
|
75
|
+
}
|
package/dist/lib/time.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The predictions-search API is strict RFC3339, so bare dates the CLI's own
|
|
2
|
+
// hints suggest ("--since 2026-01-01") must be widened here, not rejected
|
|
3
|
+
// there. Shared by history, billings, and usage so every time flag accepts
|
|
4
|
+
// the same two forms: full ISO 8601 or YYYY-MM-DD.
|
|
5
|
+
/** Accepts ISO 8601 or YYYY-MM-DD; returns RFC3339. Day-only values span the
|
|
6
|
+
* whole day: start-of-day normally, end-of-day when `endOfDay` is set. */
|
|
7
|
+
export function toIso(value, endOfDay = false) {
|
|
8
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
9
|
+
return value + (endOfDay ? 'T23:59:59Z' : 'T00:00:00Z');
|
|
10
|
+
}
|
|
11
|
+
const d = new Date(value);
|
|
12
|
+
if (isNaN(d.getTime())) {
|
|
13
|
+
throw new Error(`Invalid time "${value}". Use ISO 8601 or YYYY-MM-DD.`);
|
|
14
|
+
}
|
|
15
|
+
return d.toISOString();
|
|
16
|
+
}
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -23,7 +23,7 @@ wavespeed run google/nano-banana-2/text-to-image \
|
|
|
23
23
|
-i aspect_ratio="16:9" -i resolution="2k" --json
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
`run --json` returns `{ model, prompt, outputs: [url, ...], saved: [path, ...], elapsed_ms, raw }`. Use the URL when the user wants a link. Add `--download` if they need bytes on disk.
|
|
26
|
+
`run --json` returns `{ id, model, prompt, outputs: [url, ...], saved: [path, ...], elapsed_ms, raw }`. Keep `id` — it is the handle for `wavespeed show <id>` if anything is interrupted. Use the URL when the user wants a link. Add `--download` if they need bytes on disk.
|
|
27
27
|
|
|
28
28
|
## Recommended defaults
|
|
29
29
|
|
|
@@ -39,17 +39,18 @@ These are good starting points. Browse alternatives with `wavespeed models <quer
|
|
|
39
39
|
## Common recipes
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
# Edit an existing image —
|
|
43
|
-
URL=$(wavespeed upload ./input.jpg --json | jq -r .url)
|
|
42
|
+
# Edit an existing image — @path uploads the file and passes its URL (one step)
|
|
44
43
|
wavespeed run google/nano-banana-2/edit \
|
|
45
44
|
-p "replace the background with a sunlit kitchen" \
|
|
46
|
-
-i images=
|
|
45
|
+
-i images='["@./input.jpg"]' --json
|
|
47
46
|
|
|
48
|
-
# Image-to-video — same
|
|
49
|
-
URL=$(wavespeed upload ./hero.jpg --json | jq -r .url)
|
|
47
|
+
# Image-to-video — same @ marker for single-URL fields
|
|
50
48
|
wavespeed run bytedance/seedance-2.0/image-to-video \
|
|
51
49
|
-p "subtle parallax, gentle wind" \
|
|
52
|
-
-i image
|
|
50
|
+
-i image=@./hero.jpg -i duration=5 --json
|
|
51
|
+
|
|
52
|
+
# Or upload separately when you need the URL itself
|
|
53
|
+
URL=$(wavespeed upload ./hero.jpg --json | jq -r .url)
|
|
53
54
|
|
|
54
55
|
# Save outputs locally with a template
|
|
55
56
|
wavespeed run ... -p "..." --download "./out/{index}.{ext}"
|
|
@@ -62,7 +63,7 @@ If `wavespeed.json` exists (created by `wavespeed init`):
|
|
|
62
63
|
- **`defaultModel`** — lets `wavespeed run -p "…"` (no model arg) work.
|
|
63
64
|
- **Aliases** — named shortcuts that bundle model + default inputs. Run `wavespeed aliases` to see what's defined. `wavespeed run <alias> -h` shows the resolved schema. CLI `-i k=v` overrides alias defaults.
|
|
64
65
|
|
|
65
|
-
The CLI never modifies the user's prompt or inputs.
|
|
66
|
+
The CLI never modifies the user's prompt or inputs. The single exception is explicit: an `@path` value uploads that file and substitutes its hosted URL. Bare paths are never uploaded.
|
|
66
67
|
|
|
67
68
|
## Auth
|
|
68
69
|
|
|
@@ -70,6 +71,8 @@ The CLI never modifies the user's prompt or inputs. What you typed is what hits
|
|
|
70
71
|
|
|
71
72
|
## Pitfalls
|
|
72
73
|
|
|
73
|
-
- Local file
|
|
74
|
+
- Local files: use `@./file.jpg` in `-i` values. Bare paths are NOT uploaded and the model will reject them.
|
|
74
75
|
- Don't invent model IDs. Always confirm via `wavespeed models` or `wavespeed schema <id>` before running.
|
|
75
76
|
- Use `--json` on every `run` so you can read `outputs[0]` programmatically.
|
|
77
|
+
- `wavespeed delete` requires `--yes` when run non-interactively (that includes you).
|
|
78
|
+
- Spend questions: `wavespeed usage` (totals, per-model) and `wavespeed billings` (per-charge records).
|