@wavespeed/cli 0.4.9 → 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.
- package/dist/lib/api.js +45 -8
- package/dist/lib/validate-inputs.js +27 -1
- package/package.json +1 -1
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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}`, {
|
|
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
|
|
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
|
|
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();
|