segmind 0.2.4 → 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 +50 -2
- package/dist/index.cjs +221 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +226 -80
- package/dist/index.d.ts +226 -80
- package/dist/index.js +221 -48
- 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
|
|
@@ -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 {
|
|
@@ -859,14 +951,17 @@ var Segmind = class {
|
|
|
859
951
|
/** @internal */
|
|
860
952
|
http;
|
|
861
953
|
constructor(options = {}) {
|
|
862
|
-
|
|
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"));
|
|
863
958
|
const runtime = detectRuntime();
|
|
864
959
|
if (apiKey && (runtime === "browser" || runtime === "react-native") && !options.dangerouslyAllowBrowser) {
|
|
865
960
|
throw new SegmindError(
|
|
866
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."
|
|
867
962
|
);
|
|
868
963
|
}
|
|
869
|
-
if (!apiKey) {
|
|
964
|
+
if (!apiKey && !options.tokenProvider) {
|
|
870
965
|
throw new SegmindError("Missing API key: pass { apiKey } or set the SEGMIND_API_KEY environment variable.");
|
|
871
966
|
}
|
|
872
967
|
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
@@ -875,11 +970,17 @@ var Segmind = class {
|
|
|
875
970
|
this.spotURL = trimSlash(options.spotURL ?? DEFAULT_SPOT_URL);
|
|
876
971
|
this.consoleURL = trimSlash(options.consoleURL ?? DEFAULT_CONSOLE_URL);
|
|
877
972
|
this.assetsURL = trimSlash(options.assetsURL ?? DEFAULT_ASSETS_URL);
|
|
878
|
-
const headers = {
|
|
973
|
+
const headers = {
|
|
974
|
+
"X-Initiator": "SDK-JS",
|
|
975
|
+
...initiatorHeaders(options),
|
|
976
|
+
...options.defaultHeaders
|
|
977
|
+
};
|
|
879
978
|
const node = nodeVersion();
|
|
880
979
|
if (runtime === "node" && node) headers["User-Agent"] = `segmind-js/${VERSION} node/${node}`;
|
|
881
980
|
this.http = new HttpClient({
|
|
882
981
|
apiKey,
|
|
982
|
+
tokenProvider: options.tokenProvider,
|
|
983
|
+
onUnauthorized: options.onUnauthorized,
|
|
883
984
|
fetch: fetchImpl,
|
|
884
985
|
timeout: options.timeout ?? 6e5,
|
|
885
986
|
maxRetries: options.maxRetries ?? 2,
|
|
@@ -897,8 +998,8 @@ var Segmind = class {
|
|
|
897
998
|
* until it finishes. `params` go to the model unchanged.
|
|
898
999
|
*/
|
|
899
1000
|
async run(slug, params = {}, options = {}) {
|
|
900
|
-
const
|
|
901
|
-
return
|
|
1001
|
+
const job2 = await this.submit(slug, params, options);
|
|
1002
|
+
return job2.wait(options);
|
|
902
1003
|
}
|
|
903
1004
|
/**
|
|
904
1005
|
* One blocking `POST /v1/{slug}`. Returns the fetch `Response`: call
|
|
@@ -910,6 +1011,7 @@ var Segmind = class {
|
|
|
910
1011
|
method: "POST",
|
|
911
1012
|
url: `${this.baseURL}/v1/${path(slug)}`,
|
|
912
1013
|
body: params,
|
|
1014
|
+
headers: initiatorHeaders(options),
|
|
913
1015
|
signal: options.signal,
|
|
914
1016
|
timeout: options.timeout
|
|
915
1017
|
});
|
|
@@ -922,10 +1024,27 @@ var Segmind = class {
|
|
|
922
1024
|
async submit(slug, params = {}, options = {}) {
|
|
923
1025
|
return this.submitJob(slug, params, options, (body) => body);
|
|
924
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
|
+
}
|
|
925
1044
|
/** Chat with an LLM through the v2 queue. Returns a provider-neutral reply. */
|
|
926
1045
|
async chat(slug, params, options = {}) {
|
|
927
|
-
const
|
|
928
|
-
return
|
|
1046
|
+
const job2 = await this.submitChat(slug, params, options);
|
|
1047
|
+
return job2.wait(options);
|
|
929
1048
|
}
|
|
930
1049
|
/** Chat with one blocking `POST /v1/{slug}`. */
|
|
931
1050
|
async chatSync(slug, params, options = {}) {
|
|
@@ -948,7 +1067,7 @@ var Segmind = class {
|
|
|
948
1067
|
method: "POST",
|
|
949
1068
|
url,
|
|
950
1069
|
body: params,
|
|
951
|
-
headers: { "X-Request-ID": requestId },
|
|
1070
|
+
headers: { "X-Request-ID": requestId, ...initiatorHeaders(options) },
|
|
952
1071
|
signal: options.signal
|
|
953
1072
|
});
|
|
954
1073
|
} catch (err) {
|
|
@@ -984,21 +1103,62 @@ var Segmind = class {
|
|
|
984
1103
|
}
|
|
985
1104
|
}
|
|
986
1105
|
};
|
|
1106
|
+
var MODELS_PAGE_LIMIT = 100;
|
|
987
1107
|
var Models = class {
|
|
988
1108
|
constructor(client2) {
|
|
989
1109
|
this.client = client2;
|
|
990
1110
|
}
|
|
991
1111
|
client;
|
|
992
|
-
/**
|
|
993
|
-
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 = {}) {
|
|
994
1114
|
return this.client.http.json({
|
|
995
1115
|
method: "GET",
|
|
996
|
-
url: `${this.client.
|
|
1116
|
+
url: `${this.client.baseURL}/models`,
|
|
1117
|
+
query: modelsQuery(params),
|
|
997
1118
|
signal: options.signal,
|
|
998
1119
|
retry: true
|
|
999
1120
|
});
|
|
1000
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
|
+
}
|
|
1001
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
|
+
}
|
|
1002
1162
|
var Accounts = class {
|
|
1003
1163
|
constructor(client2) {
|
|
1004
1164
|
this.client = client2;
|
|
@@ -1013,6 +1173,16 @@ var Accounts = class {
|
|
|
1013
1173
|
retry: true
|
|
1014
1174
|
});
|
|
1015
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
|
+
}
|
|
1016
1186
|
};
|
|
1017
1187
|
function validateSubmit(body, status) {
|
|
1018
1188
|
if (!body.request_id || !body.status_url || !body.response_url) {
|
|
@@ -1061,14 +1231,17 @@ function client() {
|
|
|
1061
1231
|
var run = (slug, params, options) => client().run(slug, params, options);
|
|
1062
1232
|
var runSync = (slug, params, options) => client().runSync(slug, params, options);
|
|
1063
1233
|
var submit = (slug, params, options) => client().submit(slug, params, options);
|
|
1234
|
+
var job = (requestId) => client().job(requestId);
|
|
1064
1235
|
var chat = (slug, params, options) => client().chat(slug, params, options);
|
|
1065
1236
|
var chatSync = (slug, params, options) => client().chatSync(slug, params, options);
|
|
1066
1237
|
var submitChat = (slug, params, options) => client().submitChat(slug, params, options);
|
|
1067
1238
|
var models = {
|
|
1068
|
-
list: (options) => client().models.list(options)
|
|
1239
|
+
list: (params, options) => client().models.list(params, options),
|
|
1240
|
+
listAll: (params, options) => client().models.listAll(params, options)
|
|
1069
1241
|
};
|
|
1070
1242
|
var accounts = {
|
|
1071
|
-
current: (options) => client().accounts.current(options)
|
|
1243
|
+
current: (options) => client().accounts.current(options),
|
|
1244
|
+
credits: (options) => client().accounts.credits(options)
|
|
1072
1245
|
};
|
|
1073
1246
|
var files = {
|
|
1074
1247
|
upload: ((input, options) => client().files.upload(input, options))
|