@wavespeed/cli 0.4.8 → 0.4.10

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.
@@ -28,9 +28,8 @@ export function registerBalance(program) {
28
28
  process.stdout.write(JSON.stringify({ balance, currency: 'USD', top_up_url: TOPUP_URL }, null, 2) + '\n');
29
29
  return;
30
30
  }
31
- const display = balance.toFixed(balance < 10 ? 3 : 2);
32
31
  console.log();
33
- console.log(chalk.bold('Balance: ') + chalk.green(`$${display}`));
32
+ console.log(chalk.bold('Balance: ') + chalk.green(`$${balance}`));
34
33
  console.log(chalk.gray('Top up: ') + chalk.cyan(TOPUP_URL));
35
34
  console.log();
36
35
  if (opts.topUp) {
package/dist/lib/api.js CHANGED
@@ -88,12 +88,17 @@ async function httpError(res, method, path) {
88
88
  catch {
89
89
  /* non-JSON body (gateway error page, empty response) — fall back below */
90
90
  }
91
- if (detail)
92
- return new Error(detail);
93
- return new Error(`${method} ${path} failed: ${res.status} ${res.statusText}`);
91
+ const err = detail
92
+ ? new Error(detail)
93
+ : new Error(`${method} ${path} failed: ${res.status} ${res.statusText}`);
94
+ err.status = res.status;
95
+ return err;
94
96
  }
95
- async function apiGet(path) {
96
- const res = await fetch(`${getBaseUrl()}${path}`, { headers: authHeaders() });
97
+ async function apiGet(path, opts = {}) {
98
+ const res = await fetch(`${getBaseUrl()}${path}`, {
99
+ headers: authHeaders(),
100
+ signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined,
101
+ });
97
102
  if (!res.ok)
98
103
  throw await httpError(res, 'GET', path);
99
104
  const json = (await res.json());
@@ -137,8 +142,39 @@ export async function fetchHistory(query = {}) {
137
142
  body.status = query.status;
138
143
  return apiPost('/api/v3/predictions', body);
139
144
  }
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;
140
148
  export async function fetchPrediction(id) {
141
- return apiGet(`/api/v3/predictions/${id}/result`);
149
+ return apiGet(`/api/v3/predictions/${id}/result`, {
150
+ timeoutMs: POLL_REQUEST_TIMEOUT_MS,
151
+ });
152
+ }
153
+ // Same set as wavespeed-python. A thrown error with no HTTP status is a
154
+ // network failure or timeout; envelope/JSON errors carry no status either but
155
+ // never reach here as a transient fault, so only fetch-level errors count.
156
+ const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
157
+ const MAX_POLL_RETRIES = 5;
158
+ function isTransient(err) {
159
+ if (typeof err?.status === 'number')
160
+ return RETRYABLE_STATUS.has(err.status);
161
+ return err instanceof TypeError || err?.name === 'TimeoutError' || err?.name === 'AbortError';
162
+ }
163
+ async function fetchPredictionWithRetry(id, retryDelayMs) {
164
+ for (let attempt = 0;; attempt++) {
165
+ try {
166
+ return await fetchPrediction(id);
167
+ }
168
+ catch (err) {
169
+ const reason = err?.message ?? String(err);
170
+ if (!isTransient(err))
171
+ throw new Error(`${reason} (task_id: ${id})`);
172
+ if (attempt >= MAX_POLL_RETRIES) {
173
+ throw new Error(`Status check failed after ${attempt + 1} attempts: ${reason} (task_id: ${id}) — the task may still be running`);
174
+ }
175
+ await new Promise((r) => setTimeout(r, retryDelayMs * (attempt + 1)));
176
+ }
177
+ }
142
178
  }
143
179
  export async function deletePredictions(ids) {
144
180
  await apiPost('/api/v3/predictions/delete', { ids });
@@ -168,12 +204,13 @@ export async function submitPrediction(model, input, opts = {}) {
168
204
  }
169
205
  /**
170
206
  * Poll a prediction until it reaches a terminal status. `onTick` fires each
171
- * round so the caller can keep a spinner honest.
207
+ * round so the caller can keep a spinner honest. Transient status-read
208
+ * failures are retried; submit is never retried.
172
209
  */
173
210
  export async function waitForPrediction(id, opts = {}) {
174
211
  const interval = opts.intervalMs ?? 1000;
175
212
  for (;;) {
176
- const item = await fetchPrediction(id);
213
+ const item = await fetchPredictionWithRetry(id, opts.retryDelayMs ?? 1000);
177
214
  opts.onTick?.(item.status);
178
215
  if (item.status === 'completed')
179
216
  return item;
@@ -16,6 +16,31 @@ export function requestSchemaProps(model) {
16
16
  const keys = Object.keys(props);
17
17
  return keys.length > 0 ? keys : null;
18
18
  }
19
+ /**
20
+ * Legacy field names a model still accepts, as `legacy -> canonical`.
21
+ *
22
+ * The API renames public fields without breaking old callers by declaring the
23
+ * old name in the property's `x-aliases` (e.g. `last_image` carries
24
+ * `["end_image"]`) and folding it into the canonical name before validation.
25
+ * A legacy name is therefore a valid input, not a typo — it just is not listed
26
+ * as an accepted input, so schema-driven UIs show only the current name.
27
+ */
28
+ export function requestSchemaAliases(model) {
29
+ const aliases = new Map();
30
+ const props = model?.api_schema?.api_schemas?.[0]?.request_schema?.properties;
31
+ if (!props)
32
+ return aliases;
33
+ for (const [name, def] of Object.entries(props)) {
34
+ const declared = def?.['x-aliases'];
35
+ if (!Array.isArray(declared))
36
+ continue;
37
+ for (const alias of declared) {
38
+ if (typeof alias === 'string' && alias !== name)
39
+ aliases.set(alias, name);
40
+ }
41
+ }
42
+ return aliases;
43
+ }
19
44
  // Small edit distance for typo hints — inputs are short snake_case tokens, so
20
45
  // a full Levenshtein over ≤40 keys is nothing.
21
46
  function editDistance(a, b) {
@@ -49,7 +74,8 @@ export function findUnknownInputs(input, model) {
49
74
  if (!known)
50
75
  return null;
51
76
  const knownSet = new Set(known);
52
- const unknown = Object.keys(input).filter((k) => !knownSet.has(k));
77
+ const aliases = requestSchemaAliases(model);
78
+ const unknown = Object.keys(input).filter((k) => !knownSet.has(k) && !aliases.has(k));
53
79
  if (unknown.length === 0)
54
80
  return null;
55
81
  const suggestions = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavespeed/cli",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
5
5
  "publishConfig": {
6
6
  "access": "public"