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 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({ timeout: 15 * 60_000, interval: 2_000 });
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', ['PIXELFLOW']);
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 polls retry connection errors and `408`, `429`, `502`, `503` and `504`, honouring `Retry-After`.
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 _SegmindError extends Error {
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 async fromResponse(response, requestId) {
89
- const text = await response.text().catch(() => "");
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
- if (retryAfter) {
272
- const seconds = Number(retryAfter);
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
- for (let attempt = 0; ; attempt++) {
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 attempt(req) {
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("node:fs/promises");
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, and
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 new InferenceFailed(this.requestId, body);
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 new InferenceFailed(this.requestId, status);
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
- await sleep(Math.min(interval, timeout - elapsed), options.signal);
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 job = await this.submit(slug, inputs, options);
680
- return job.wait(options);
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 new InferenceFailed(this.requestId, state.raw, failureDetail(state.raw));
827
+ if (FAILED.has(state.status)) throw requestFailure(this.requestId, state.raw, failureDetail(state.raw));
737
828
  const elapsed = Date.now() - started;
738
- if (elapsed + interval > timeout) throw new InferenceTimeout(this.requestId, elapsed);
739
- await sleep(interval, options.signal);
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.2.3";
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
- /** Changes a registration's URL and event types. */
804
- async update(webhookId, webhookUrl, eventTypes, options = {}) {
805
- return this.call("POST", "/update", {
806
- body: { webhook_id: webhookId, webhook_url: webhookUrl, event: { types: requireTypes(eventTypes) } },
807
- signal: options.signal
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
- /** Deactivates a registration. */
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("GET", "/inactive", { query: { webhook_id: webhookId }, signal: options.signal, retry: true });
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
- const apiKey = options.apiKey ?? readEnv("SEGMIND_API_KEY");
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 = { "X-Initiator": "SDK-JS", ...options.defaultHeaders };
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 job = await this.submit(slug, params, options);
891
- return job.wait(options);
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 job = await this.submitChat(slug, params, options);
918
- return job.wait(options);
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
- /** Every model in the catalog, under the key `Inference Model Information List`. */
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.spotURL}/inference-model-information/list`,
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))