segmind 0.2.3 → 0.3.0
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 +52 -4
- package/dist/index.cjs +247 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +238 -83
- package/dist/index.d.ts +238 -83
- package/dist/index.js +247 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,11 +51,24 @@ const image = new Uint8Array(await response.arrayBuffer()); // or response.json(
|
|
|
51
51
|
const job = await segmind.submit('seedance-2.0', { prompt: 'A sunset timelapse' });
|
|
52
52
|
console.log(job.requestId);
|
|
53
53
|
|
|
54
|
-
const result = await job.wait({
|
|
54
|
+
const result = await job.wait({
|
|
55
|
+
timeout: 15 * 60_000,
|
|
56
|
+
interval: (elapsed) => (elapsed < 30_000 ? 2_000 : 5_000), // or a fixed number of ms
|
|
57
|
+
onStatus: (status) => console.log(status.status),
|
|
58
|
+
});
|
|
55
59
|
```
|
|
56
60
|
|
|
57
61
|
A submit carries an `X-Request-ID`. If the connection drops, the SDK sends it again with the same id, and the gateway returns the existing request instead of starting (and billing) a second one. Pass your own `requestId` to make a submit idempotent across restarts.
|
|
58
62
|
|
|
63
|
+
To pick a job up later, such as after a page reload, keep its id and rebuild it:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const job = segmind.job(savedRequestId);
|
|
67
|
+
const result = await job.wait();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The result is kept for about an hour, and only for the user who submitted the job. Anything else reads as a `SegmindError` with status 404.
|
|
71
|
+
|
|
59
72
|
## Chat with an LLM
|
|
60
73
|
|
|
61
74
|
```ts
|
|
@@ -109,6 +122,16 @@ logo.persistent; // true
|
|
|
109
122
|
|
|
110
123
|
Persistent uploads take images, video, audio, PDF and common document types, up to 2 GB each.
|
|
111
124
|
|
|
125
|
+
## Models
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const page = await segmind.models.list({ q: 'upscale', output: 'image', limit: 20 });
|
|
129
|
+
page.models; // slug, name, category, average_cost, latency_seconds, ...
|
|
130
|
+
const videoModels = await segmind.models.listAll({ output: 'video', sort: 'cheapest' });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`list` returns one page (`has_more`, `offset`, `total`); `listAll` reads every page. `average_cost` is observed spend, not the list price.
|
|
134
|
+
|
|
112
135
|
## Generations
|
|
113
136
|
|
|
114
137
|
```ts
|
|
@@ -125,12 +148,12 @@ const latest = await segmind.generations.recent('flux-schnell');
|
|
|
125
148
|
```ts
|
|
126
149
|
const hook = await segmind.webhooks.add('https://example.com/hooks/segmind', ['PIXELFLOW']);
|
|
127
150
|
const hooks = await segmind.webhooks.get(); // each with its signing_secret
|
|
128
|
-
await segmind.webhooks.update(hook.id, 'https://example.com/hooks/v2'
|
|
151
|
+
await segmind.webhooks.update(hook.id, 'https://example.com/hooks/v2'); // URL only
|
|
129
152
|
await segmind.webhooks.logs(hook.id);
|
|
130
153
|
await segmind.webhooks.delete(hook.id);
|
|
131
154
|
```
|
|
132
155
|
|
|
133
|
-
Event types are subscription types: `PIXELFLOW` for PixelFlow runs, plus `WORKFLOW`, `INFERENCE` and `FINETUNE`.
|
|
156
|
+
Event types are subscription types: `PIXELFLOW` for PixelFlow runs, plus `WORKFLOW`, `INFERENCE` and `FINETUNE`. `update` changes the URL only; to change the event types, `delete` the registration and `add` a new one. Before 0.2.4, `update` registered a second hook and `delete` deactivated nothing, so check `webhooks.get()` for registrations left behind.
|
|
134
157
|
|
|
135
158
|
## PixelFlow workflows
|
|
136
159
|
|
|
@@ -144,6 +167,14 @@ const done = await job.wait({ timeout: 20 * 60_000, interval: 5_000 });
|
|
|
144
167
|
|
|
145
168
|
The slug is the workflow's API slug from its API tab, and inputs are keyed by each input's keyname. Runs use the current workflow engine; pass `{ engine: 'v1' }` for the legacy one. A failed run throws `InferenceFailed` whose `detail` names the node that failed. A submit is never retried, because a run bills.
|
|
146
169
|
|
|
170
|
+
## Credits
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const { credits, freeCredits } = await segmind.accounts.credits();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`credits` is the stored balance of the account or team making the request. Credits held by requests still running are not taken off.
|
|
177
|
+
|
|
147
178
|
## Errors
|
|
148
179
|
|
|
149
180
|
| Error | When |
|
|
@@ -152,8 +183,10 @@ The slug is the workflow's API slug from its API tab, and inputs are keyed by ea
|
|
|
152
183
|
| `APIConnectionError` | No response: network failure or the client timeout |
|
|
153
184
|
| `InferenceFailed` | A queued request reached `FAILED`; `detail` holds the reason |
|
|
154
185
|
| `InferenceTimeout` | `wait()` passed its timeout. The request may still finish: call `job.wait()` again |
|
|
186
|
+
| `InsufficientCredits` | The account can't pay for the request, at submit or as a failed job. `detail` holds the reason |
|
|
187
|
+
| `RateLimited` | Too many requests, after any retries. `retryAfterMs` is set when the server sent `Retry-After` and the runtime could read it |
|
|
155
188
|
|
|
156
|
-
Reads and
|
|
189
|
+
Reads, polls and queued submits retry connection errors and `429`, `502`, `503` and `504`, honouring `Retry-After`; reads also retry `408`.
|
|
157
190
|
|
|
158
191
|
## Options
|
|
159
192
|
|
|
@@ -164,6 +197,8 @@ new Segmind({
|
|
|
164
197
|
timeout: 600_000, // ms per HTTP request
|
|
165
198
|
maxRetries: 2,
|
|
166
199
|
fetch: customFetch, // any fetch implementation
|
|
200
|
+
initiator: 'API', // how these requests are labelled in usage history; default SDK-JS
|
|
201
|
+
initiatorId: 'batch-42', // what they belong to, also settable per call
|
|
167
202
|
});
|
|
168
203
|
```
|
|
169
204
|
|
|
@@ -171,6 +206,19 @@ new Segmind({
|
|
|
171
206
|
|
|
172
207
|
The constructor refuses an API key in a browser or React Native app, because every user of the app could read it. Call Segmind from your backend. Pass `dangerouslyAllowBrowser: true` only for internal tools where that is acceptable.
|
|
173
208
|
|
|
209
|
+
### Signed-in Segmind users
|
|
210
|
+
|
|
211
|
+
An app where people sign in with their Segmind account can send the user's own session token instead of an API key. This works in a browser, because the token only reaches what that user could do anyway.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const segmind = new Segmind({
|
|
215
|
+
tokenProvider: () => auth.getToken(), // called before every request
|
|
216
|
+
onUnauthorized: () => auth.refresh(), // on a 401; a new token retries the request once
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Pass `apiKey` or `tokenProvider`, not both.
|
|
221
|
+
|
|
174
222
|
## License
|
|
175
223
|
|
|
176
224
|
Apache-2.0
|
package/dist/index.cjs
CHANGED
|
@@ -42,10 +42,12 @@ __export(index_exports, {
|
|
|
42
42
|
Generations: () => Generations,
|
|
43
43
|
InferenceFailed: () => InferenceFailed,
|
|
44
44
|
InferenceTimeout: () => InferenceTimeout,
|
|
45
|
+
InsufficientCredits: () => InsufficientCredits,
|
|
45
46
|
Job: () => Job,
|
|
46
47
|
Models: () => Models,
|
|
47
48
|
PixelFlows: () => PixelFlows,
|
|
48
49
|
PixelflowJob: () => PixelflowJob,
|
|
50
|
+
RateLimited: () => RateLimited,
|
|
49
51
|
Segmind: () => Segmind,
|
|
50
52
|
SegmindError: () => SegmindError,
|
|
51
53
|
VERSION: () => VERSION,
|
|
@@ -56,6 +58,7 @@ __export(index_exports, {
|
|
|
56
58
|
files: () => files,
|
|
57
59
|
generations: () => generations,
|
|
58
60
|
imageUrl: () => imageUrl,
|
|
61
|
+
job: () => job,
|
|
59
62
|
models: () => models,
|
|
60
63
|
pixelflows: () => pixelflows,
|
|
61
64
|
run: () => run,
|
|
@@ -67,7 +70,7 @@ __export(index_exports, {
|
|
|
67
70
|
module.exports = __toCommonJS(index_exports);
|
|
68
71
|
|
|
69
72
|
// src/core/errors.ts
|
|
70
|
-
var SegmindError = class
|
|
73
|
+
var SegmindError = class extends Error {
|
|
71
74
|
/** HTTP status, when the error came from a response. */
|
|
72
75
|
status;
|
|
73
76
|
/** The server's error text (`error` in the response body), when it sent one. */
|
|
@@ -85,20 +88,21 @@ var SegmindError = class _SegmindError extends Error {
|
|
|
85
88
|
this.body = options.body;
|
|
86
89
|
}
|
|
87
90
|
/** Reads the response body and builds the error. Consumes the body. */
|
|
88
|
-
static
|
|
89
|
-
|
|
90
|
-
let body = text;
|
|
91
|
-
try {
|
|
92
|
-
body = text ? JSON.parse(text) : void 0;
|
|
93
|
-
} catch {
|
|
94
|
-
}
|
|
95
|
-
const detail = errorDetail(body) ?? (typeof body === "string" && body ? body.slice(0, 500) : void 0);
|
|
96
|
-
const message = detail ? `${response.status} ${detail}` : `${response.status} ${response.statusText || "request failed"}`;
|
|
97
|
-
return new _SegmindError(message, { status: response.status, detail, requestId, body });
|
|
91
|
+
static fromResponse(response, requestId) {
|
|
92
|
+
return errorFromResponse(response, requestId);
|
|
98
93
|
}
|
|
99
94
|
};
|
|
100
95
|
var APIConnectionError = class extends SegmindError {
|
|
101
96
|
};
|
|
97
|
+
var InsufficientCredits = class extends SegmindError {
|
|
98
|
+
};
|
|
99
|
+
var RateLimited = class extends SegmindError {
|
|
100
|
+
retryAfterMs;
|
|
101
|
+
constructor(message, options = {}) {
|
|
102
|
+
super(message, options);
|
|
103
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
102
106
|
var InferenceFailed = class extends SegmindError {
|
|
103
107
|
/** The status body that reported the failure. */
|
|
104
108
|
statusBody;
|
|
@@ -119,6 +123,43 @@ var InferenceTimeout = class extends SegmindError {
|
|
|
119
123
|
this.elapsedMs = elapsedMs;
|
|
120
124
|
}
|
|
121
125
|
};
|
|
126
|
+
async function errorFromResponse(response, requestId) {
|
|
127
|
+
const text = await response.text().catch(() => "");
|
|
128
|
+
let body = text;
|
|
129
|
+
try {
|
|
130
|
+
body = text ? JSON.parse(text) : void 0;
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
const detail = errorDetail(body) ?? plainText(body);
|
|
134
|
+
const message = detail ? `${response.status} ${detail}` : `${response.status} ${response.statusText || "request failed"}`;
|
|
135
|
+
const options = { status: response.status, detail, requestId, body };
|
|
136
|
+
if (response.status === 429) {
|
|
137
|
+
return new RateLimited(message, { ...options, retryAfterMs: parseRetryAfter(response.headers.get("retry-after")) });
|
|
138
|
+
}
|
|
139
|
+
if (response.status === 402 || isOutOfCredits(detail)) return new InsufficientCredits(message, options);
|
|
140
|
+
return new SegmindError(message, options);
|
|
141
|
+
}
|
|
142
|
+
function requestFailure(requestId, statusBody, detail) {
|
|
143
|
+
detail ??= typeof statusBody.error === "string" ? statusBody.error : void 0;
|
|
144
|
+
if (isOutOfCredits(detail)) {
|
|
145
|
+
return new InsufficientCredits(`request ${requestId} failed: ${detail}`, { detail, requestId, body: statusBody });
|
|
146
|
+
}
|
|
147
|
+
return new InferenceFailed(requestId, statusBody, detail);
|
|
148
|
+
}
|
|
149
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
150
|
+
if (!value) return void 0;
|
|
151
|
+
const seconds = Number(value);
|
|
152
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
153
|
+
const date = Date.parse(value);
|
|
154
|
+
return Number.isNaN(date) ? void 0 : Math.max(date - now, 0);
|
|
155
|
+
}
|
|
156
|
+
function isOutOfCredits(detail) {
|
|
157
|
+
return detail?.startsWith("Insufficient credits.") ?? false;
|
|
158
|
+
}
|
|
159
|
+
function plainText(body) {
|
|
160
|
+
if (typeof body !== "string" || !body || body.trimStart().startsWith("<")) return void 0;
|
|
161
|
+
return body.slice(0, 500);
|
|
162
|
+
}
|
|
122
163
|
function errorDetail(body) {
|
|
123
164
|
if (!body || typeof body !== "object") return void 0;
|
|
124
165
|
const record2 = body;
|
|
@@ -236,6 +277,12 @@ function detectRuntime() {
|
|
|
236
277
|
if (g.process?.versions?.node) return "node";
|
|
237
278
|
return "other";
|
|
238
279
|
}
|
|
280
|
+
function initiatorHeaders(options) {
|
|
281
|
+
return {
|
|
282
|
+
...options.initiator ? { "X-Initiator": options.initiator } : {},
|
|
283
|
+
...options.initiatorId ? { "X-Initiator-ID": options.initiatorId } : {}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
239
286
|
function readEnv(name) {
|
|
240
287
|
try {
|
|
241
288
|
const value = g.process?.env?.[name];
|
|
@@ -268,15 +315,15 @@ function toBase64(bytes) {
|
|
|
268
315
|
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 502, 503, 504]);
|
|
269
316
|
var MAX_RETRY_DELAY_MS = 6e4;
|
|
270
317
|
function retryDelay(attempt, retryAfter, now = Date.now()) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, MAX_RETRY_DELAY_MS);
|
|
274
|
-
const date = Date.parse(retryAfter);
|
|
275
|
-
if (!Number.isNaN(date)) return Math.min(Math.max(date - now, 0), MAX_RETRY_DELAY_MS);
|
|
276
|
-
}
|
|
318
|
+
const asked = parseRetryAfter(retryAfter, now);
|
|
319
|
+
if (asked !== void 0) return Math.min(asked, MAX_RETRY_DELAY_MS);
|
|
277
320
|
const base = Math.min(500 * 2 ** attempt, 8e3);
|
|
278
321
|
return base * (0.75 + Math.random() * 0.25);
|
|
279
322
|
}
|
|
323
|
+
function pollDelay(interval, elapsedMs, fallback) {
|
|
324
|
+
const delay = typeof interval === "function" ? interval(elapsedMs) : interval ?? fallback;
|
|
325
|
+
return Number.isFinite(delay) && delay >= 0 ? delay : fallback;
|
|
326
|
+
}
|
|
280
327
|
function sleep(ms, signal) {
|
|
281
328
|
return new Promise((resolve, reject) => {
|
|
282
329
|
if (signal?.aborted) return reject(signal.reason);
|
|
@@ -296,23 +343,35 @@ var HttpClient = class {
|
|
|
296
343
|
this.config = config;
|
|
297
344
|
}
|
|
298
345
|
config;
|
|
346
|
+
/** The hook call in flight, so that requests failing together share one refresh. */
|
|
347
|
+
unauthorized;
|
|
299
348
|
/** The response whatever its status, after retries when `retry` is set. */
|
|
300
349
|
async fetchRaw(req) {
|
|
301
|
-
|
|
350
|
+
let attempt = 0;
|
|
351
|
+
let reauthenticated = false;
|
|
352
|
+
for (; ; ) {
|
|
302
353
|
const canRetry = req.retry === true && attempt < this.config.maxRetries;
|
|
354
|
+
const token = await this.sessionToken(req);
|
|
303
355
|
let response;
|
|
304
356
|
try {
|
|
305
|
-
response = await this.attempt(req);
|
|
357
|
+
response = await this.attempt(req, token);
|
|
306
358
|
} catch (err) {
|
|
307
359
|
if (canRetry && err instanceof APIConnectionError) {
|
|
308
360
|
await sleep(retryDelay(attempt, null), req.signal);
|
|
361
|
+
attempt++;
|
|
309
362
|
continue;
|
|
310
363
|
}
|
|
311
364
|
throw err;
|
|
312
365
|
}
|
|
366
|
+
if (response.status === 401 && !reauthenticated && await this.reauthenticate(req, token)) {
|
|
367
|
+
reauthenticated = true;
|
|
368
|
+
await response.body?.cancel().catch(() => void 0);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
313
371
|
if (canRetry && RETRYABLE_STATUS.has(response.status)) {
|
|
314
372
|
await response.body?.cancel().catch(() => void 0);
|
|
315
373
|
await sleep(retryDelay(attempt, response.headers.get("retry-after")), req.signal);
|
|
374
|
+
attempt++;
|
|
316
375
|
continue;
|
|
317
376
|
}
|
|
318
377
|
return response;
|
|
@@ -328,7 +387,31 @@ var HttpClient = class {
|
|
|
328
387
|
async json(req) {
|
|
329
388
|
return readJson(await this.request(req));
|
|
330
389
|
}
|
|
331
|
-
async
|
|
390
|
+
async sessionToken(req) {
|
|
391
|
+
if (!this.config.tokenProvider || req.bare) return void 0;
|
|
392
|
+
try {
|
|
393
|
+
return await this.config.tokenProvider() ?? void 0;
|
|
394
|
+
} catch (err) {
|
|
395
|
+
throw new SegmindError(`tokenProvider failed: ${describe(err)}`, { cause: err });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/** Tells the app its token was refused. True when it now has a different one to try. */
|
|
399
|
+
async reauthenticate(req, refused) {
|
|
400
|
+
if (!this.config.tokenProvider || req.bare) return false;
|
|
401
|
+
try {
|
|
402
|
+
if (this.config.onUnauthorized) {
|
|
403
|
+
this.unauthorized ??= Promise.resolve(this.config.onUnauthorized()).finally(() => {
|
|
404
|
+
this.unauthorized = void 0;
|
|
405
|
+
});
|
|
406
|
+
await this.unauthorized;
|
|
407
|
+
}
|
|
408
|
+
const next = await this.config.tokenProvider();
|
|
409
|
+
return Boolean(next) && next !== refused;
|
|
410
|
+
} catch {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async attempt(req, token) {
|
|
332
415
|
const { signal } = req;
|
|
333
416
|
if (signal?.aborted) throw signal.reason;
|
|
334
417
|
const timeout = req.timeout ?? this.config.timeout;
|
|
@@ -342,6 +425,7 @@ var HttpClient = class {
|
|
|
342
425
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
343
426
|
const headers = req.bare ? { ...req.headers } : { ...this.config.headers, ...req.headers };
|
|
344
427
|
if (this.config.apiKey && !req.bare) headers["x-api-key"] = this.config.apiKey;
|
|
428
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
345
429
|
if (req.body !== void 0) headers["content-type"] = "application/json";
|
|
346
430
|
const body = req.rawBody ?? (req.body === void 0 ? void 0 : JSON.stringify(req.body));
|
|
347
431
|
try {
|
|
@@ -436,7 +520,12 @@ async function readFileInput(input, options) {
|
|
|
436
520
|
if (detectRuntime() !== "node") {
|
|
437
521
|
throw new SegmindError(`${options.caller}: file paths work in Node only; pass a Blob or bytes`);
|
|
438
522
|
}
|
|
439
|
-
const { readFile } = await import(
|
|
523
|
+
const { readFile } = await import(
|
|
524
|
+
/* webpackIgnore: true */
|
|
525
|
+
/* turbopackIgnore: true */
|
|
526
|
+
/* @vite-ignore */
|
|
527
|
+
"node:fs/promises"
|
|
528
|
+
);
|
|
440
529
|
try {
|
|
441
530
|
bytes = new Uint8Array(await readFile(input));
|
|
442
531
|
} catch (err) {
|
|
@@ -619,12 +708,12 @@ var Job = class {
|
|
|
619
708
|
return await this.read(this.statusUrl, options.signal);
|
|
620
709
|
}
|
|
621
710
|
/**
|
|
622
|
-
* The finished result. Throws `InferenceFailed` when the request failed
|
|
623
|
-
* a `SegmindError` when it has not finished yet (use `wait()` for that).
|
|
711
|
+
* The finished result. Throws `InferenceFailed` when the request failed
|
|
712
|
+
* (`InsufficientCredits` when that was for lack of credits), and a `SegmindError` when it has not finished yet (use `wait()` for that).
|
|
624
713
|
*/
|
|
625
714
|
async result(options = {}) {
|
|
626
715
|
const body = await this.read(this.responseUrl, options.signal);
|
|
627
|
-
if (body.status === "FAILED") throw
|
|
716
|
+
if (body.status === "FAILED") throw requestFailure(this.requestId, body);
|
|
628
717
|
if (body.status !== void 0 && body.status !== "COMPLETED") {
|
|
629
718
|
throw new SegmindError(`request ${this.requestId} is still ${String(body.status)}; use wait()`, {
|
|
630
719
|
requestId: this.requestId,
|
|
@@ -636,15 +725,16 @@ var Job = class {
|
|
|
636
725
|
/** Polls until the request finishes, then returns its result. */
|
|
637
726
|
async wait(options = {}) {
|
|
638
727
|
const timeout = options.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
639
|
-
const interval = options.interval ?? DEFAULT_WAIT_INTERVAL_MS;
|
|
640
728
|
const started = Date.now();
|
|
641
729
|
for (; ; ) {
|
|
642
730
|
const status = await this.status({ signal: options.signal });
|
|
731
|
+
options.onStatus?.(status);
|
|
643
732
|
if (status.status === "COMPLETED") return this.result({ signal: options.signal });
|
|
644
|
-
if (status.status === "FAILED") throw
|
|
733
|
+
if (status.status === "FAILED") throw requestFailure(this.requestId, status);
|
|
645
734
|
const elapsed = Date.now() - started;
|
|
646
735
|
if (elapsed >= timeout) throw new InferenceTimeout(this.requestId, elapsed);
|
|
647
|
-
|
|
736
|
+
const delay = pollDelay(options.interval, elapsed, DEFAULT_WAIT_INTERVAL_MS);
|
|
737
|
+
await sleep(Math.min(delay, timeout - elapsed), options.signal);
|
|
648
738
|
}
|
|
649
739
|
}
|
|
650
740
|
/**
|
|
@@ -676,8 +766,8 @@ var PixelFlows = class {
|
|
|
676
766
|
client;
|
|
677
767
|
/** Runs a workflow and waits for its outputs. */
|
|
678
768
|
async run(slug, inputs = {}, options = {}) {
|
|
679
|
-
const
|
|
680
|
-
return
|
|
769
|
+
const job2 = await this.submit(slug, inputs, options);
|
|
770
|
+
return job2.wait(options);
|
|
681
771
|
}
|
|
682
772
|
/** Starts a workflow and returns a job to wait on. Never retried: a run bills. */
|
|
683
773
|
async submit(slug, inputs = {}, options = {}) {
|
|
@@ -688,6 +778,7 @@ var PixelFlows = class {
|
|
|
688
778
|
method: "POST",
|
|
689
779
|
url: `${this.client.baseURL}${route}${clean}`,
|
|
690
780
|
body: inputs,
|
|
781
|
+
headers: initiatorHeaders(options),
|
|
691
782
|
signal: options.signal
|
|
692
783
|
});
|
|
693
784
|
const requestId = body.request_id;
|
|
@@ -723,20 +814,21 @@ var PixelflowJob = class {
|
|
|
723
814
|
}
|
|
724
815
|
/**
|
|
725
816
|
* Polls until the run finishes. Throws `InferenceFailed` (its `detail` names
|
|
726
|
-
* the failing node) or `InferenceTimeout`; the run may still finish after a
|
|
817
|
+
* the failing node), `InsufficientCredits` or `InferenceTimeout`; the run may still finish after a
|
|
727
818
|
* timeout, so `wait()` can be called again.
|
|
728
819
|
*/
|
|
729
820
|
async wait(options = {}) {
|
|
730
821
|
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
731
|
-
const interval = options.interval ?? DEFAULT_INTERVAL_MS;
|
|
732
822
|
const started = Date.now();
|
|
733
823
|
for (; ; ) {
|
|
734
824
|
const state = await this.status({ signal: options.signal });
|
|
825
|
+
options.onStatus?.(state);
|
|
735
826
|
if (DONE.has(state.status)) return state;
|
|
736
|
-
if (FAILED.has(state.status)) throw
|
|
827
|
+
if (FAILED.has(state.status)) throw requestFailure(this.requestId, state.raw, failureDetail(state.raw));
|
|
737
828
|
const elapsed = Date.now() - started;
|
|
738
|
-
|
|
739
|
-
|
|
829
|
+
const delay = pollDelay(options.interval, elapsed, DEFAULT_INTERVAL_MS);
|
|
830
|
+
if (elapsed + delay > timeout) throw new InferenceTimeout(this.requestId, elapsed);
|
|
831
|
+
await sleep(delay, options.signal);
|
|
740
832
|
}
|
|
741
833
|
}
|
|
742
834
|
};
|
|
@@ -781,7 +873,7 @@ function failureDetail(body) {
|
|
|
781
873
|
}
|
|
782
874
|
|
|
783
875
|
// src/version.ts
|
|
784
|
-
var VERSION = "0.
|
|
876
|
+
var VERSION = "0.3.0";
|
|
785
877
|
|
|
786
878
|
// src/webhooks.ts
|
|
787
879
|
var Webhooks = class {
|
|
@@ -800,16 +892,26 @@ var Webhooks = class {
|
|
|
800
892
|
signal: options.signal
|
|
801
893
|
});
|
|
802
894
|
}
|
|
803
|
-
/**
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
895
|
+
/**
|
|
896
|
+
* Points an active registration at a new URL. The event types stay as
|
|
897
|
+
* registered: to change them, `delete` the registration and `add` a new one.
|
|
898
|
+
*
|
|
899
|
+
* @param _eventTypes Ignored. Kept so 0.2 calls still compile; the API never
|
|
900
|
+
* changed event types in place.
|
|
901
|
+
*/
|
|
902
|
+
async update(webhookId, webhookUrl, _eventTypes, options = {}) {
|
|
903
|
+
return this.call("PUT", "/update", {
|
|
904
|
+
body: { webhook_id: webhookId, webhook_url: webhookUrl },
|
|
905
|
+
signal: options.signal,
|
|
906
|
+
retry: true
|
|
808
907
|
});
|
|
809
908
|
}
|
|
810
|
-
/**
|
|
909
|
+
/**
|
|
910
|
+
* Deactivates a registration. Not retried: a second attempt after a lost
|
|
911
|
+
* response would report the registration as already deactivated.
|
|
912
|
+
*/
|
|
811
913
|
async delete(webhookId, options = {}) {
|
|
812
|
-
return this.call("
|
|
914
|
+
return this.call("DELETE", "/inactive", { query: { webhook_id: webhookId }, signal: options.signal });
|
|
813
915
|
}
|
|
814
916
|
/** Delivery history for one registration. */
|
|
815
917
|
async logs(webhookId, options = {}) {
|
|
@@ -849,14 +951,17 @@ var Segmind = class {
|
|
|
849
951
|
/** @internal */
|
|
850
952
|
http;
|
|
851
953
|
constructor(options = {}) {
|
|
852
|
-
|
|
954
|
+
if (options.apiKey && options.tokenProvider) {
|
|
955
|
+
throw new SegmindError("Pass either apiKey or tokenProvider, not both.");
|
|
956
|
+
}
|
|
957
|
+
const apiKey = options.apiKey ?? (options.tokenProvider ? void 0 : readEnv("SEGMIND_API_KEY"));
|
|
853
958
|
const runtime = detectRuntime();
|
|
854
959
|
if (apiKey && (runtime === "browser" || runtime === "react-native") && !options.dangerouslyAllowBrowser) {
|
|
855
960
|
throw new SegmindError(
|
|
856
961
|
"An API key in a browser or mobile app exposes it to every user. Call Segmind from your backend instead, or pass dangerouslyAllowBrowser: true if you accept that risk."
|
|
857
962
|
);
|
|
858
963
|
}
|
|
859
|
-
if (!apiKey) {
|
|
964
|
+
if (!apiKey && !options.tokenProvider) {
|
|
860
965
|
throw new SegmindError("Missing API key: pass { apiKey } or set the SEGMIND_API_KEY environment variable.");
|
|
861
966
|
}
|
|
862
967
|
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
@@ -865,11 +970,17 @@ var Segmind = class {
|
|
|
865
970
|
this.spotURL = trimSlash(options.spotURL ?? DEFAULT_SPOT_URL);
|
|
866
971
|
this.consoleURL = trimSlash(options.consoleURL ?? DEFAULT_CONSOLE_URL);
|
|
867
972
|
this.assetsURL = trimSlash(options.assetsURL ?? DEFAULT_ASSETS_URL);
|
|
868
|
-
const headers = {
|
|
973
|
+
const headers = {
|
|
974
|
+
"X-Initiator": "SDK-JS",
|
|
975
|
+
...initiatorHeaders(options),
|
|
976
|
+
...options.defaultHeaders
|
|
977
|
+
};
|
|
869
978
|
const node = nodeVersion();
|
|
870
979
|
if (runtime === "node" && node) headers["User-Agent"] = `segmind-js/${VERSION} node/${node}`;
|
|
871
980
|
this.http = new HttpClient({
|
|
872
981
|
apiKey,
|
|
982
|
+
tokenProvider: options.tokenProvider,
|
|
983
|
+
onUnauthorized: options.onUnauthorized,
|
|
873
984
|
fetch: fetchImpl,
|
|
874
985
|
timeout: options.timeout ?? 6e5,
|
|
875
986
|
maxRetries: options.maxRetries ?? 2,
|
|
@@ -887,8 +998,8 @@ var Segmind = class {
|
|
|
887
998
|
* until it finishes. `params` go to the model unchanged.
|
|
888
999
|
*/
|
|
889
1000
|
async run(slug, params = {}, options = {}) {
|
|
890
|
-
const
|
|
891
|
-
return
|
|
1001
|
+
const job2 = await this.submit(slug, params, options);
|
|
1002
|
+
return job2.wait(options);
|
|
892
1003
|
}
|
|
893
1004
|
/**
|
|
894
1005
|
* One blocking `POST /v1/{slug}`. Returns the fetch `Response`: call
|
|
@@ -900,6 +1011,7 @@ var Segmind = class {
|
|
|
900
1011
|
method: "POST",
|
|
901
1012
|
url: `${this.baseURL}/v1/${path(slug)}`,
|
|
902
1013
|
body: params,
|
|
1014
|
+
headers: initiatorHeaders(options),
|
|
903
1015
|
signal: options.signal,
|
|
904
1016
|
timeout: options.timeout
|
|
905
1017
|
});
|
|
@@ -912,10 +1024,27 @@ var Segmind = class {
|
|
|
912
1024
|
async submit(slug, params = {}, options = {}) {
|
|
913
1025
|
return this.submitJob(slug, params, options, (body) => body);
|
|
914
1026
|
}
|
|
1027
|
+
/**
|
|
1028
|
+
* A job submitted earlier, from its id alone, for picking one up after a
|
|
1029
|
+
* reload or in another process. Makes no request. The result is kept for about
|
|
1030
|
+
* an hour, and only for the user who submitted it: anything else reads as a
|
|
1031
|
+
* `SegmindError` with status 404.
|
|
1032
|
+
*/
|
|
1033
|
+
job(requestId) {
|
|
1034
|
+
const clean = requestId.trim();
|
|
1035
|
+
if (!clean) throw new SegmindError("A request id is required.");
|
|
1036
|
+
const url = `${this.baseURL}/v2/requests/${encodeURIComponent(clean)}`;
|
|
1037
|
+
return new Job(this.http, {
|
|
1038
|
+
request_id: clean,
|
|
1039
|
+
status: "UNKNOWN",
|
|
1040
|
+
status_url: `${url}/status`,
|
|
1041
|
+
response_url: url
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
915
1044
|
/** Chat with an LLM through the v2 queue. Returns a provider-neutral reply. */
|
|
916
1045
|
async chat(slug, params, options = {}) {
|
|
917
|
-
const
|
|
918
|
-
return
|
|
1046
|
+
const job2 = await this.submitChat(slug, params, options);
|
|
1047
|
+
return job2.wait(options);
|
|
919
1048
|
}
|
|
920
1049
|
/** Chat with one blocking `POST /v1/{slug}`. */
|
|
921
1050
|
async chatSync(slug, params, options = {}) {
|
|
@@ -938,7 +1067,7 @@ var Segmind = class {
|
|
|
938
1067
|
method: "POST",
|
|
939
1068
|
url,
|
|
940
1069
|
body: params,
|
|
941
|
-
headers: { "X-Request-ID": requestId },
|
|
1070
|
+
headers: { "X-Request-ID": requestId, ...initiatorHeaders(options) },
|
|
942
1071
|
signal: options.signal
|
|
943
1072
|
});
|
|
944
1073
|
} catch (err) {
|
|
@@ -957,7 +1086,7 @@ var Segmind = class {
|
|
|
957
1086
|
body
|
|
958
1087
|
});
|
|
959
1088
|
}
|
|
960
|
-
return new Job(this.http, validateSubmit(body, response.status), transform);
|
|
1089
|
+
return new Job(this.http, onBase(validateSubmit(body, response.status), this.baseURL), transform);
|
|
961
1090
|
}
|
|
962
1091
|
if (canRetry && response.status === 503) {
|
|
963
1092
|
await response.body?.cancel().catch(() => void 0);
|
|
@@ -974,21 +1103,62 @@ var Segmind = class {
|
|
|
974
1103
|
}
|
|
975
1104
|
}
|
|
976
1105
|
};
|
|
1106
|
+
var MODELS_PAGE_LIMIT = 100;
|
|
977
1107
|
var Models = class {
|
|
978
1108
|
constructor(client2) {
|
|
979
1109
|
this.client = client2;
|
|
980
1110
|
}
|
|
981
1111
|
client;
|
|
982
|
-
/**
|
|
983
|
-
async list(options = {}) {
|
|
1112
|
+
/** One page of the catalog. Pass `offset` from the previous page while `has_more` is true. */
|
|
1113
|
+
async list(params = {}, options = {}) {
|
|
984
1114
|
return this.client.http.json({
|
|
985
1115
|
method: "GET",
|
|
986
|
-
url: `${this.client.
|
|
1116
|
+
url: `${this.client.baseURL}/models`,
|
|
1117
|
+
query: modelsQuery(params),
|
|
987
1118
|
signal: options.signal,
|
|
988
1119
|
retry: true
|
|
989
1120
|
});
|
|
990
1121
|
}
|
|
1122
|
+
/** Every model matching `params`, reading page after page. */
|
|
1123
|
+
async listAll(params = {}, options = {}) {
|
|
1124
|
+
const models2 = [];
|
|
1125
|
+
for (let offset = 0; ; ) {
|
|
1126
|
+
const page = await this.list({ ...params, limit: MODELS_PAGE_LIMIT, offset }, options);
|
|
1127
|
+
models2.push(...page.models);
|
|
1128
|
+
offset += page.models.length;
|
|
1129
|
+
if (!page.has_more || !page.models.length || offset >= page.total) return models2;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
991
1132
|
};
|
|
1133
|
+
function modelsQuery(p) {
|
|
1134
|
+
return {
|
|
1135
|
+
q: p.q,
|
|
1136
|
+
sort: p.sort,
|
|
1137
|
+
category: p.category,
|
|
1138
|
+
modality: p.modality,
|
|
1139
|
+
input: joinList(p.input),
|
|
1140
|
+
output: joinList(p.output),
|
|
1141
|
+
provider: p.provider,
|
|
1142
|
+
slugs: joinList(p.slugs),
|
|
1143
|
+
operations: joinList(p.operations),
|
|
1144
|
+
features: joinList(p.features),
|
|
1145
|
+
min_popularity: p.minPopularity,
|
|
1146
|
+
min_category_percentile: p.minCategoryPercentile,
|
|
1147
|
+
min_cost: p.minCost,
|
|
1148
|
+
max_cost: p.maxCost,
|
|
1149
|
+
max_latency: p.maxLatency,
|
|
1150
|
+
is_new: p.isNew,
|
|
1151
|
+
created_after: p.createdAfter,
|
|
1152
|
+
include_coming_soon: p.includeComingSoon,
|
|
1153
|
+
facets: p.facets,
|
|
1154
|
+
limit: p.limit,
|
|
1155
|
+
offset: p.offset
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
function joinList(value) {
|
|
1159
|
+
if (value === void 0) return void 0;
|
|
1160
|
+
return Array.isArray(value) ? value.join(",") : value;
|
|
1161
|
+
}
|
|
992
1162
|
var Accounts = class {
|
|
993
1163
|
constructor(client2) {
|
|
994
1164
|
this.client = client2;
|
|
@@ -1003,6 +1173,16 @@ var Accounts = class {
|
|
|
1003
1173
|
retry: true
|
|
1004
1174
|
});
|
|
1005
1175
|
}
|
|
1176
|
+
/** The current credit balance. */
|
|
1177
|
+
async credits(options = {}) {
|
|
1178
|
+
const { "free-credits": freeCredits, ...rest } = await this.client.http.json({
|
|
1179
|
+
method: "GET",
|
|
1180
|
+
url: `${this.client.baseURL}/v1/get-user-credits`,
|
|
1181
|
+
signal: options.signal,
|
|
1182
|
+
retry: true
|
|
1183
|
+
});
|
|
1184
|
+
return { ...rest, credits: Number(rest.credits), freeCredits: Number(freeCredits) };
|
|
1185
|
+
}
|
|
1006
1186
|
};
|
|
1007
1187
|
function validateSubmit(body, status) {
|
|
1008
1188
|
if (!body.request_id || !body.status_url || !body.response_url) {
|
|
@@ -1018,6 +1198,14 @@ function path(slug) {
|
|
|
1018
1198
|
if (!clean) throw new SegmindError("A model slug is required.");
|
|
1019
1199
|
return clean;
|
|
1020
1200
|
}
|
|
1201
|
+
function onBase(body, baseURL) {
|
|
1202
|
+
const origin = new URL(baseURL).origin;
|
|
1203
|
+
const pin = (url) => {
|
|
1204
|
+
const target = new URL(url, `${baseURL}/`);
|
|
1205
|
+
return target.origin === origin ? target.href : `${baseURL}${target.pathname}${target.search}`;
|
|
1206
|
+
};
|
|
1207
|
+
return { ...body, status_url: pin(body.status_url), response_url: pin(body.response_url) };
|
|
1208
|
+
}
|
|
1021
1209
|
function trimSlash(url) {
|
|
1022
1210
|
return url.replace(/\/+$/, "");
|
|
1023
1211
|
}
|
|
@@ -1043,14 +1231,17 @@ function client() {
|
|
|
1043
1231
|
var run = (slug, params, options) => client().run(slug, params, options);
|
|
1044
1232
|
var runSync = (slug, params, options) => client().runSync(slug, params, options);
|
|
1045
1233
|
var submit = (slug, params, options) => client().submit(slug, params, options);
|
|
1234
|
+
var job = (requestId) => client().job(requestId);
|
|
1046
1235
|
var chat = (slug, params, options) => client().chat(slug, params, options);
|
|
1047
1236
|
var chatSync = (slug, params, options) => client().chatSync(slug, params, options);
|
|
1048
1237
|
var submitChat = (slug, params, options) => client().submitChat(slug, params, options);
|
|
1049
1238
|
var models = {
|
|
1050
|
-
list: (options) => client().models.list(options)
|
|
1239
|
+
list: (params, options) => client().models.list(params, options),
|
|
1240
|
+
listAll: (params, options) => client().models.listAll(params, options)
|
|
1051
1241
|
};
|
|
1052
1242
|
var accounts = {
|
|
1053
|
-
current: (options) => client().accounts.current(options)
|
|
1243
|
+
current: (options) => client().accounts.current(options),
|
|
1244
|
+
credits: (options) => client().accounts.credits(options)
|
|
1054
1245
|
};
|
|
1055
1246
|
var files = {
|
|
1056
1247
|
upload: ((input, options) => client().files.upload(input, options))
|