@wavespeed/cli 0.4.10 → 0.4.12
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 +4 -8
- package/dist/commands/auth.js +2 -0
- package/dist/commands/price.js +3 -35
- package/dist/commands/schema.js +2 -1
- package/dist/lib/api.js +30 -10
- package/package.json +2 -2
- package/dist/lib/pricing-vars.js +0 -75
package/README.md
CHANGED
|
@@ -165,14 +165,10 @@ wavespeed init Create a wavespeed.json with defaults +
|
|
|
165
165
|
wavespeed skill install Drop SKILL.md for coding agents
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
**Pricing is an estimate.** `wavespeed price` quotes
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
of its range, not a typical run. `wavespeed price wavespeed-ai/infinitetalk`
|
|
173
|
-
says `$0.15`; a 60s audio actually bills `$1.80`. The command names the inputs it
|
|
174
|
-
was blind to (`unpriced_inputs` / `at_base_price` in `--json`) — supply them for
|
|
175
|
-
a real quote. Whatever it says, the amount actually charged for a run is
|
|
168
|
+
**Pricing is an estimate.** `wavespeed price` quotes the inputs you pass, and many
|
|
169
|
+
models bill from an input — audio/video duration, frame count, text length. Pass
|
|
170
|
+
the same inputs you would pass to `wavespeed run`: a quote made without them can
|
|
171
|
+
be lower than the real charge. The amount actually charged for a run is
|
|
176
172
|
authoritative.
|
|
177
173
|
|
|
178
174
|
## Auth
|
package/dist/commands/auth.js
CHANGED
|
@@ -6,6 +6,7 @@ import clipboard from 'clipboardy';
|
|
|
6
6
|
import { userConfig, getBaseUrl, getApiKey, loadProjectConfig, PROJECT_FILE } from '../lib/config.js';
|
|
7
7
|
import { LINKS } from '../lib/links.js';
|
|
8
8
|
import { clientAttributionHeaders } from '../lib/client-headers.js';
|
|
9
|
+
import { GET_TIMEOUT_MS } from '../lib/api.js';
|
|
9
10
|
const KEY_URL = 'https://wavespeed.ai/accesskey';
|
|
10
11
|
function mask(key) {
|
|
11
12
|
if (key.length < 12)
|
|
@@ -42,6 +43,7 @@ async function validateKey(apiKey, baseUrl) {
|
|
|
42
43
|
try {
|
|
43
44
|
res = await fetch(`${baseUrl}/api/v3/balance`, {
|
|
44
45
|
headers: { Authorization: `Bearer ${apiKey}`, ...clientAttributionHeaders() },
|
|
46
|
+
signal: AbortSignal.timeout(GET_TIMEOUT_MS),
|
|
45
47
|
});
|
|
46
48
|
}
|
|
47
49
|
catch (err) {
|
package/dist/commands/price.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
-
import { fetchPricing,
|
|
4
|
-
import { missingPriceVars, isFloorQuote } from '../lib/pricing-vars.js';
|
|
3
|
+
import { fetchPricing, modelLlmsTxtUrl } from '../lib/api.js';
|
|
5
4
|
import { parseInputs } from '../lib/inputs.js';
|
|
6
5
|
import { resolveModelToken } from '../lib/config.js';
|
|
7
6
|
import { collectLocalFiles, resolveLocalFiles } from '../lib/local-files.js';
|
|
@@ -74,33 +73,14 @@ export function registerPrice(program) {
|
|
|
74
73
|
const spinner = !opts.json ? ora('Calculating price…').start() : null;
|
|
75
74
|
try {
|
|
76
75
|
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
|
-
}
|
|
95
76
|
spinner?.stop();
|
|
96
77
|
const disclaimer = 'Estimate only, for reference — the amount actually charged for a run is authoritative.';
|
|
97
78
|
if (opts.json) {
|
|
98
79
|
process.stdout.write(JSON.stringify({
|
|
99
80
|
...data,
|
|
100
81
|
estimate: true,
|
|
101
|
-
unpriced_inputs: unpriced,
|
|
102
|
-
at_base_price: atFloor,
|
|
103
82
|
disclaimer,
|
|
83
|
+
llms_txt: modelLlmsTxtUrl(resolved.model),
|
|
104
84
|
uploaded: uploadedUrls,
|
|
105
85
|
}, null, 2) + '\n');
|
|
106
86
|
return;
|
|
@@ -113,19 +93,6 @@ export function registerPrice(program) {
|
|
|
113
93
|
(data.discounted_price > 0 && data.discounted_price !== data.price
|
|
114
94
|
? chalk.gray(` (list $${data.price})`)
|
|
115
95
|
: ''));
|
|
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
|
-
}
|
|
129
96
|
console.log();
|
|
130
97
|
for (const [file, url] of Object.entries(uploadedUrls)) {
|
|
131
98
|
console.log(' ' + chalk.gray('uploaded: ') + chalk.gray(file + ' → ') + chalk.cyan(url));
|
|
@@ -135,6 +102,7 @@ export function registerPrice(program) {
|
|
|
135
102
|
}
|
|
136
103
|
console.log();
|
|
137
104
|
console.log(chalk.gray('Run it: ') + chalk.cyan(`wavespeed run ${modelArg}` + (opts.prompt ? ` -p "${opts.prompt}"` : '')));
|
|
105
|
+
console.log(chalk.gray('Model inputs: ') + chalk.cyan(modelLlmsTxtUrl(resolved.model)));
|
|
138
106
|
console.log(chalk.gray(disclaimer));
|
|
139
107
|
console.log();
|
|
140
108
|
}
|
package/dist/commands/schema.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
-
import { fetchModels } from '../lib/api.js';
|
|
3
|
+
import { fetchModels, modelLlmsTxtUrl } from '../lib/api.js';
|
|
4
4
|
function getRequestSchema(model) {
|
|
5
5
|
return model.api_schema?.api_schemas?.[0]?.request_schema;
|
|
6
6
|
}
|
|
@@ -116,6 +116,7 @@ export function registerSchema(program) {
|
|
|
116
116
|
chalk.white(model.type ?? 'unknown') +
|
|
117
117
|
(model.base_price !== undefined ? chalk.gray(' base price: ') + chalk.green(`$${model.base_price}`) : ''));
|
|
118
118
|
console.log(chalk.gray(' page: ') + chalk.cyan(`https://wavespeed.ai/models/${model.model_id}`));
|
|
119
|
+
console.log(chalk.gray(' llms: ') + chalk.cyan(modelLlmsTxtUrl(model.model_id)));
|
|
119
120
|
if (model.description) {
|
|
120
121
|
console.log(wrap(model.description, ' '));
|
|
121
122
|
}
|
package/dist/lib/api.js
CHANGED
|
@@ -46,6 +46,7 @@ export async function fetchModels(opts = {}) {
|
|
|
46
46
|
throw new Error('No API key configured. Run `wavespeed login`.');
|
|
47
47
|
const res = await fetch(`${baseUrl}/api/v3/models`, {
|
|
48
48
|
headers: { Authorization: `Bearer ${apiKey}`, ...clientAttributionHeaders() },
|
|
49
|
+
signal: AbortSignal.timeout(GET_TIMEOUT_MS),
|
|
49
50
|
});
|
|
50
51
|
if (!res.ok) {
|
|
51
52
|
throw await httpError(res, 'GET', '/api/v3/models');
|
|
@@ -66,6 +67,12 @@ export function clearModelsCache() {
|
|
|
66
67
|
/* nothing to clear */
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
// Per-request timeouts (no overall cap). A hung socket would otherwise stall
|
|
71
|
+
// the CLI forever. Read-only GETs are far below 30s in practice (result p99.9
|
|
72
|
+
// < 1s); the async submit gets more headroom because a timeout there is an
|
|
73
|
+
// ambiguous outcome, not a clean failure.
|
|
74
|
+
export const GET_TIMEOUT_MS = 30_000;
|
|
75
|
+
export const SUBMIT_TIMEOUT_MS = 60_000;
|
|
69
76
|
// --- Account + prediction endpoints (not exposed by the wavespeed SDK) ---
|
|
70
77
|
function authHeaders() {
|
|
71
78
|
const apiKey = getApiKey();
|
|
@@ -94,10 +101,10 @@ async function httpError(res, method, path) {
|
|
|
94
101
|
err.status = res.status;
|
|
95
102
|
return err;
|
|
96
103
|
}
|
|
97
|
-
async function apiGet(path
|
|
104
|
+
async function apiGet(path) {
|
|
98
105
|
const res = await fetch(`${getBaseUrl()}${path}`, {
|
|
99
106
|
headers: authHeaders(),
|
|
100
|
-
signal:
|
|
107
|
+
signal: AbortSignal.timeout(GET_TIMEOUT_MS),
|
|
101
108
|
});
|
|
102
109
|
if (!res.ok)
|
|
103
110
|
throw await httpError(res, 'GET', path);
|
|
@@ -106,11 +113,12 @@ async function apiGet(path, opts = {}) {
|
|
|
106
113
|
throw new Error(json.message || `API returned code ${json.code}`);
|
|
107
114
|
return json.data;
|
|
108
115
|
}
|
|
109
|
-
async function apiPost(path, body) {
|
|
116
|
+
async function apiPost(path, body, opts = {}) {
|
|
110
117
|
const res = await fetch(`${getBaseUrl()}${path}`, {
|
|
111
118
|
method: 'POST',
|
|
112
119
|
headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
|
113
120
|
body: JSON.stringify(body),
|
|
121
|
+
signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined,
|
|
114
122
|
});
|
|
115
123
|
if (!res.ok)
|
|
116
124
|
throw await httpError(res, 'POST', path);
|
|
@@ -122,6 +130,10 @@ async function apiPost(path, body) {
|
|
|
122
130
|
export async function fetchBalance() {
|
|
123
131
|
return apiGet('/api/v3/balance');
|
|
124
132
|
}
|
|
133
|
+
// The model page's markdown summary for agents (inputs, endpoint, examples).
|
|
134
|
+
export function modelLlmsTxtUrl(modelId) {
|
|
135
|
+
return `https://wavespeed.ai/models/${modelId}/llms.txt`;
|
|
136
|
+
}
|
|
125
137
|
export async function fetchPricing(modelId, inputs) {
|
|
126
138
|
// /model/price is the documented pricing endpoint; /model/pricing is a
|
|
127
139
|
// legacy contract kept for old clients and absent from the public docs.
|
|
@@ -142,13 +154,8 @@ export async function fetchHistory(query = {}) {
|
|
|
142
154
|
body.status = query.status;
|
|
143
155
|
return apiPost('/api/v3/predictions', body);
|
|
144
156
|
}
|
|
145
|
-
// A hung socket would otherwise stall the CLI forever. 30s is far above a
|
|
146
|
-
// healthy status read, so it only trips on a dead connection.
|
|
147
|
-
const POLL_REQUEST_TIMEOUT_MS = 30_000;
|
|
148
157
|
export async function fetchPrediction(id) {
|
|
149
|
-
return apiGet(`/api/v3/predictions/${id}/result
|
|
150
|
-
timeoutMs: POLL_REQUEST_TIMEOUT_MS,
|
|
151
|
-
});
|
|
158
|
+
return apiGet(`/api/v3/predictions/${id}/result`);
|
|
152
159
|
}
|
|
153
160
|
// Same set as wavespeed-python. A thrown error with no HTTP status is a
|
|
154
161
|
// network failure or timeout; envelope/JSON errors carry no status either but
|
|
@@ -200,7 +207,20 @@ export async function submitPrediction(model, input, opts = {}) {
|
|
|
200
207
|
const body = { ...input };
|
|
201
208
|
if (opts.sync)
|
|
202
209
|
body.enable_sync_mode = true;
|
|
203
|
-
|
|
210
|
+
// sync mode legitimately holds the request until the task finishes, so only
|
|
211
|
+
// async submits get the per-request timeout.
|
|
212
|
+
try {
|
|
213
|
+
return await apiPost(`/api/v3/${model}`, body, {
|
|
214
|
+
timeoutMs: opts.sync ? undefined : SUBMIT_TIMEOUT_MS,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
// Never retried: the server may already have created (and billed) the task.
|
|
219
|
+
if (err?.name === 'TimeoutError') {
|
|
220
|
+
throw new Error(`Submission outcome unknown: no response within ${SUBMIT_TIMEOUT_MS / 1000}s. The task may have been created — run \`wavespeed history\` to check before retrying.`);
|
|
221
|
+
}
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
204
224
|
}
|
|
205
225
|
/**
|
|
206
226
|
* Poll a prediction until it reaches a terminal status. `onTick` fires each
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavespeed/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"conf": "^13.0.0",
|
|
57
57
|
"open": "^10.1.0",
|
|
58
58
|
"ora": "^8.1.0",
|
|
59
|
-
"wavespeed": "^0.3.
|
|
59
|
+
"wavespeed": "^0.3.4"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^20.14.0",
|
package/dist/lib/pricing-vars.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
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
|
-
}
|