@superlinked/sie-sdk 0.6.15 → 0.6.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -87,6 +87,18 @@ var ModelLoadingError = class extends SIEError {
87
87
  this.model = model;
88
88
  }
89
89
  };
90
+ var ResourceExhaustedError = class extends ServerError {
91
+ /** The model that was requested */
92
+ model;
93
+ /** Number of retry attempts made before giving up */
94
+ retries;
95
+ constructor(message, options) {
96
+ super(message, "RESOURCE_EXHAUSTED", 503);
97
+ this.name = "ResourceExhaustedError";
98
+ this.model = options?.model;
99
+ this.retries = options?.retries ?? 0;
100
+ }
101
+ };
90
102
  var SIEStreamError = class extends SIEError {
91
103
  /** SIE-native error code (e.g. `context_exceeded`, `cancelled`). */
92
104
  code;
@@ -193,16 +205,24 @@ var HTTP_CLIENT_ERROR_MIN = 400;
193
205
  var HTTP_CLIENT_ERROR_MAX = 499;
194
206
  var HTTP_SERVER_ERROR_MIN = 500;
195
207
  var HTTP_SERVER_ERROR_MAX = 599;
208
+ var HTTP_GATEWAY_TIMEOUT = 504;
196
209
  var DEFAULT_TIMEOUT = 3e4;
210
+ var DEFAULT_LONG_RUNNING_TIMEOUT = 12e4;
197
211
  var DEFAULT_PROVISION_TIMEOUT = 3e5;
198
212
  var DEFAULT_RETRY_DELAY = 5e3;
199
213
  var DEFAULT_LEASE_RENEWAL_INTERVAL = 6e4;
214
+ var DEFAULT_JOB_WAIT_TIMEOUT = 6e5;
215
+ var DEFAULT_JOB_WAIT_POLL = 2e3;
200
216
  var LORA_LOADING_MAX_RETRIES = 10;
201
217
  var LORA_LOADING_DEFAULT_DELAY = 1e3;
202
218
  var LORA_LOADING_ERROR_CODE = "LORA_LOADING";
203
219
  var MODEL_LOADING_DEFAULT_DELAY = 5e3;
204
220
  var MODEL_LOADING_ERROR_CODE = "MODEL_LOADING";
205
221
  var PROVISIONING_ERROR_CODE = "PROVISIONING";
222
+ var RESOURCE_EXHAUSTED_MAX_RETRIES = 3;
223
+ var RESOURCE_EXHAUSTED_DEFAULT_DELAY = 5e3;
224
+ var RESOURCE_EXHAUSTED_MAX_DELAY = 3e4;
225
+ var RESOURCE_EXHAUSTED_ERROR_CODE = "RESOURCE_EXHAUSTED";
206
226
  var SDK_VERSION_HEADER = "X-SIE-SDK-Version";
207
227
  var SERVER_VERSION_HEADER = "X-SIE-Server-Version";
208
228
  var EXT_TYPE_NUMPY = 78;
@@ -392,9 +412,19 @@ function unpackMessage(data) {
392
412
  // src/internal/retry.ts
393
413
  var RETRY_JITTER_FRACTION = 0.25;
394
414
  function applyRetryJitter(delay) {
415
+ if (delay <= 0) return Math.max(delay, 0);
395
416
  const low = delay * (1 - RETRY_JITTER_FRACTION);
396
417
  return Math.max(0, low + Math.random() * (delay - low));
397
418
  }
419
+ function computeOomBackoff(retryAfter, attempt, baseDelay = RESOURCE_EXHAUSTED_DEFAULT_DELAY, maxDelay = RESOURCE_EXHAUSTED_MAX_DELAY) {
420
+ const safeRetryAfter = retryAfter !== void 0 ? Math.max(retryAfter, 0) : void 0;
421
+ if (safeRetryAfter !== void 0 && attempt === 0) {
422
+ return Math.min(safeRetryAfter, maxDelay);
423
+ }
424
+ const base = safeRetryAfter !== void 0 ? Math.max(baseDelay, safeRetryAfter) : baseDelay;
425
+ const capped = Math.max(0, Math.min(base * 2 ** attempt, maxDelay));
426
+ return applyRetryJitter(capped);
427
+ }
398
428
  function getRetryAfter(header) {
399
429
  if (!header) return void 0;
400
430
  const seconds = Number.parseInt(header, 10);
@@ -650,8 +680,12 @@ function parseCapacityInfo(data, gpuFilter) {
650
680
  const parsedWorkers = workers.map((w) => ({
651
681
  url: w.url,
652
682
  gpu: w.gpu,
683
+ gpuCount: w.gpu_count ?? 0,
684
+ readyGpuSlots: w.ready_gpu_slots ?? w.gpu_count ?? (w.healthy ? 1 : 0),
653
685
  healthy: w.healthy,
654
686
  queueDepth: w.queue_depth,
687
+ pendingCost: w.pending_cost ?? 0,
688
+ inflightBatches: w.inflight_batches ?? 0,
655
689
  loadedModels: w.loaded_models
656
690
  }));
657
691
  return {
@@ -669,8 +703,21 @@ function parseCapacityInfo(data, gpuFilter) {
669
703
  function sleep(ms) {
670
704
  return new Promise((resolve) => setTimeout(resolve, ms));
671
705
  }
706
+ function nextOomRetryDelay(opts) {
707
+ const { retryAfter, oomRetries, maxOomRetries, elapsedMs, provisionTimeoutMs, model } = opts;
708
+ const message = `Server resource exhausted after ${oomRetries} retry attempt(s) for model '${model}'`;
709
+ if (oomRetries >= maxOomRetries || elapsedMs >= provisionTimeoutMs) {
710
+ throw new ResourceExhaustedError(message, { model, retries: oomRetries });
711
+ }
712
+ const delay = computeOomBackoff(retryAfter, oomRetries);
713
+ if (delay >= provisionTimeoutMs - elapsedMs) {
714
+ throw new ResourceExhaustedError(message, { model, retries: oomRetries });
715
+ }
716
+ return delay;
717
+ }
672
718
  async function withProvisioningRetry(performFetch, opts) {
673
719
  const startTime = Date.now();
720
+ let oomRetries = 0;
674
721
  while (true) {
675
722
  const response = await performFetch();
676
723
  await throwIfModelLoadFailed(response, opts.model);
@@ -706,6 +753,26 @@ async function withProvisioningRetry(performFetch, opts) {
706
753
  await sleep(Math.min(delay, opts.provisionTimeoutMs - elapsed));
707
754
  continue;
708
755
  }
756
+ if (errorCode === RESOURCE_EXHAUSTED_ERROR_CODE) {
757
+ const delay = nextOomRetryDelay({
758
+ retryAfter: getRetryAfter2(response),
759
+ oomRetries,
760
+ maxOomRetries: RESOURCE_EXHAUSTED_MAX_RETRIES,
761
+ elapsedMs: Date.now() - startTime,
762
+ provisionTimeoutMs: opts.provisionTimeoutMs,
763
+ model: opts.model
764
+ });
765
+ oomRetries += 1;
766
+ await sleep(delay);
767
+ continue;
768
+ }
769
+ }
770
+ if (response.status === HTTP_GATEWAY_TIMEOUT) {
771
+ throw new ServerError(
772
+ "Gateway timed out (504) after the request was published to the queue; a worker may already be generating. Not retried because generation is non-idempotent (retrying could double-bill). Re-issue manually if needed.",
773
+ await getErrorCode(response.clone()),
774
+ HTTP_GATEWAY_TIMEOUT
775
+ );
709
776
  }
710
777
  if (!response.ok) {
711
778
  await handleError(response);
@@ -717,6 +784,255 @@ async function withProvisioningRetry(performFetch, opts) {
717
784
  }
718
785
  }
719
786
 
787
+ // src/jobs.ts
788
+ var TERMINAL_JOB_STATES = /* @__PURE__ */ new Set([
789
+ "succeeded",
790
+ "failed",
791
+ "suspended",
792
+ "cancelled"
793
+ ]);
794
+ var SINK_RETURN = /* @__PURE__ */ new Set(["return", "default"]);
795
+ var SINK_INPLACE = /* @__PURE__ */ new Set(["inplace", "in_place", "in place"]);
796
+ var INTERNAL_SCHEMES = /* @__PURE__ */ new Set(["upload"]);
797
+ var FIELD_MAP_KEYS = /* @__PURE__ */ new Set(["id_field", "input_field", "carry", "input_type"]);
798
+ var INPUT_TYPES = /* @__PURE__ */ new Set(["text", "document"]);
799
+ function isConnectorUri(value) {
800
+ return value.includes("://");
801
+ }
802
+ function isInternalUri(uri) {
803
+ return INTERNAL_SCHEMES.has(uri.split("://", 1)[0] ?? "");
804
+ }
805
+ function normItem(item, index) {
806
+ if (typeof item === "string") return { text: item };
807
+ if (item !== null && typeof item === "object") return item;
808
+ throw new RequestError(`item ${index} must be a string or an object`, "invalid_request", 400);
809
+ }
810
+ function connectionName(uri) {
811
+ const afterScheme = uri.split("://", 2)[1] ?? "";
812
+ const authority = afterScheme.split(/[/?#]/, 1)[0] ?? "";
813
+ const name = authority.trim();
814
+ if (!name) {
815
+ throw new RequestError(
816
+ `connector URI ${JSON.stringify(uri)} names no connection (expected 'scheme://<connection>/\u2026')`,
817
+ "invalid_request",
818
+ 400
819
+ );
820
+ }
821
+ return name;
822
+ }
823
+ function resolveSource(source, connection) {
824
+ if (Array.isArray(source)) {
825
+ if (source.length === 0) {
826
+ throw new RequestError("inline source has no items", "invalid_request", 400);
827
+ }
828
+ return { items: source.map((item, i) => normItem(item, i)) };
829
+ }
830
+ if (isConnectorUri(source)) {
831
+ if (isInternalUri(source)) {
832
+ return connection ? { src: source, connection } : { src: source };
833
+ }
834
+ return { src: source, connection: connection ?? connectionName(source) };
835
+ }
836
+ if (typeof source === "string" && source.trim()) {
837
+ return { items: [{ text: source }] };
838
+ }
839
+ throw new RequestError(
840
+ "source must be inline items (a list/string) or a connector URI (scheme://<connection>/\u2026)",
841
+ "invalid_request",
842
+ 400
843
+ );
844
+ }
845
+ function resolveSink(sink, sourceConnection, sinkConnection) {
846
+ if (sink === null || sink === void 0 || SINK_RETURN.has(sink.trim().toLowerCase())) {
847
+ return {};
848
+ }
849
+ if (SINK_INPLACE.has(sink.trim().toLowerCase())) {
850
+ return { sink: "inplace" };
851
+ }
852
+ if (isConnectorUri(sink)) {
853
+ const body = { sink };
854
+ if (isInternalUri(sink)) {
855
+ if (sinkConnection != null) body.sink_connection = sinkConnection;
856
+ return body;
857
+ }
858
+ const resolved = sinkConnection ?? connectionName(sink);
859
+ if (sinkConnection != null || resolved !== sourceConnection) {
860
+ body.sink_connection = resolved;
861
+ }
862
+ return body;
863
+ }
864
+ throw new RequestError(
865
+ `sink must be 'return', 'inplace', or a connector URI (got ${JSON.stringify(sink)})`,
866
+ "invalid_request",
867
+ 400
868
+ );
869
+ }
870
+ function resolveFieldMap(fieldMap, outputField) {
871
+ const body = {};
872
+ if (fieldMap != null) {
873
+ const unknown = Object.keys(fieldMap).filter((key) => !FIELD_MAP_KEYS.has(key));
874
+ if (unknown.length > 0) {
875
+ throw new RequestError(
876
+ `unknown field_map key(s) ${JSON.stringify(unknown)} (known: ${[...FIELD_MAP_KEYS].join(", ")})`,
877
+ "invalid_request",
878
+ 400
879
+ );
880
+ }
881
+ if (fieldMap.carry != null && (!Array.isArray(fieldMap.carry) || fieldMap.carry.some((c) => typeof c !== "string" || !c))) {
882
+ throw new RequestError(
883
+ `field_map.carry must be a list of field names (got ${JSON.stringify(fieldMap.carry)})`,
884
+ "invalid_request",
885
+ 400
886
+ );
887
+ }
888
+ if (fieldMap.input_type != null && !INPUT_TYPES.has(fieldMap.input_type)) {
889
+ throw new RequestError(
890
+ `field_map.input_type must be one of ${[...INPUT_TYPES].join(", ")} (got ${JSON.stringify(fieldMap.input_type)})`,
891
+ "invalid_request",
892
+ 400
893
+ );
894
+ }
895
+ const mapped = {};
896
+ if (fieldMap.id_field != null) mapped.id_field = fieldMap.id_field;
897
+ if (fieldMap.input_field != null) mapped.input_field = fieldMap.input_field;
898
+ if (fieldMap.input_type != null) mapped.input_type = fieldMap.input_type;
899
+ if (fieldMap.carry != null && fieldMap.carry.length > 0) mapped.carry = fieldMap.carry;
900
+ if (Object.keys(mapped).length > 0) body.field_map = mapped;
901
+ }
902
+ if (outputField != null) {
903
+ if (typeof outputField !== "string" || !outputField) {
904
+ throw new RequestError(
905
+ `output_field must be a non-empty string (got ${JSON.stringify(outputField)})`,
906
+ "invalid_request",
907
+ 400
908
+ );
909
+ }
910
+ body.output_field = outputField;
911
+ }
912
+ return body;
913
+ }
914
+ function resolveWhen(when) {
915
+ if (when == null || when.trim() === "" || when.trim().toLowerCase() === "now") {
916
+ return {};
917
+ }
918
+ const text = when.trim();
919
+ if (text.toLowerCase().startsWith("schedule:")) {
920
+ return { when: "schedule", schedule: text.slice("schedule:".length).trim() };
921
+ }
922
+ if (text.toLowerCase().startsWith("watch:")) {
923
+ return { when: "watch", watch: text.slice("watch:".length).trim() };
924
+ }
925
+ if (text.toLowerCase() === "schedule") {
926
+ throw new RequestError(
927
+ "schedule trigger needs a cron expr: when='schedule:<cron>'",
928
+ "invalid_request",
929
+ 400
930
+ );
931
+ }
932
+ if (text.split(/\s+/).length === 5) {
933
+ return { when: "schedule", schedule: text };
934
+ }
935
+ throw new RequestError(
936
+ `unrecognized when ${JSON.stringify(when)}: use 'now', 'schedule:<cron>', or 'watch:<source>'`,
937
+ "invalid_request",
938
+ 400
939
+ );
940
+ }
941
+ function buildJobBody(options) {
942
+ const operation = options.operation ?? "encode";
943
+ const body = { operation, model: options.model };
944
+ const sourceFields = resolveSource(options.source, options.connection);
945
+ Object.assign(body, sourceFields);
946
+ Object.assign(
947
+ body,
948
+ resolveSink(
949
+ options.sink,
950
+ sourceFields.connection,
951
+ options.sinkConnection
952
+ )
953
+ );
954
+ const mappingFields = resolveFieldMap(options.fieldMap, options.outputField);
955
+ if (Object.keys(mappingFields).length > 0 && !("src" in body)) {
956
+ throw new RequestError(
957
+ "fieldMap/outputField apply to connector-src jobs; an inline items job maps nothing",
958
+ "invalid_request",
959
+ 400
960
+ );
961
+ }
962
+ Object.assign(body, mappingFields);
963
+ Object.assign(body, resolveWhen(options.when));
964
+ if (options.outputTypes && options.outputTypes.length > 0) {
965
+ body.output_types = options.outputTypes;
966
+ }
967
+ if (options.options && Object.keys(options.options).length > 0) {
968
+ body.options = options.options;
969
+ }
970
+ return body;
971
+ }
972
+ function jobChunks(jobDoc) {
973
+ const raw = jobDoc.output?.chunks ?? [];
974
+ return raw.map((chunk) => ({
975
+ seq: chunk.seq,
976
+ items: chunk.items,
977
+ state: chunk.state,
978
+ ref: chunk.ref,
979
+ units: chunk.units,
980
+ credits: chunk.credits,
981
+ error: chunk.error ?? null
982
+ }));
983
+ }
984
+ function toNumberArrayLike(value) {
985
+ if (value == null) return null;
986
+ if (value instanceof Float32Array) return value;
987
+ if (Array.isArray(value)) return value;
988
+ if (ArrayBuffer.isView(value)) return value;
989
+ return null;
990
+ }
991
+ function denseInfo(dense) {
992
+ if (dense == null) return { dims: null, vector: null };
993
+ if (typeof dense === "object" && !Array.isArray(dense) && !ArrayBuffer.isView(dense)) {
994
+ const rec = dense;
995
+ let raw = null;
996
+ for (const key of ["values", "vector", "dense"]) {
997
+ if (rec[key] != null) {
998
+ raw = rec[key];
999
+ break;
1000
+ }
1001
+ }
1002
+ const vector2 = toNumberArrayLike(raw);
1003
+ let dims = typeof rec.dims === "number" ? rec.dims : null;
1004
+ if (dims == null && vector2 != null) dims = vector2.length;
1005
+ return { dims, vector: vector2 };
1006
+ }
1007
+ const vector = toNumberArrayLike(dense);
1008
+ return { dims: vector != null ? vector.length : null, vector };
1009
+ }
1010
+ function decodeResultItem(result) {
1011
+ const payload = result.result_msgpack;
1012
+ let decoded = null;
1013
+ if (payload instanceof Uint8Array) {
1014
+ try {
1015
+ decoded = unpackMessage(payload);
1016
+ } catch {
1017
+ decoded = null;
1018
+ }
1019
+ }
1020
+ const dense = decoded && typeof decoded === "object" ? decoded.dense : null;
1021
+ const { dims, vector } = denseInfo(dense);
1022
+ return {
1023
+ id: result.id ?? null,
1024
+ success: result.success ?? null,
1025
+ units: result.units ?? null,
1026
+ dims,
1027
+ dense: vector
1028
+ };
1029
+ }
1030
+ function decodeChunkBytes(raw) {
1031
+ const results = unpackMessage(raw);
1032
+ if (!Array.isArray(results)) return [];
1033
+ return results.map((r) => decodeResultItem(r));
1034
+ }
1035
+
720
1036
  // src/sse.ts
721
1037
  var SSE_DONE = "[DONE]";
722
1038
  var MAX_SSE_BUFFER_CHARS = 8 * 1024 * 1024;
@@ -856,12 +1172,20 @@ function extractDataPayload(block) {
856
1172
  }
857
1173
 
858
1174
  // src/version.ts
859
- var SDK_VERSION = "0.6.15";
1175
+ var SDK_VERSION = "0.6.17";
860
1176
 
861
1177
  // src/client.ts
862
1178
  function sleep2(ms) {
863
1179
  return new Promise((resolve) => setTimeout(resolve, ms));
864
1180
  }
1181
+ function resolveUploadFilename(file, filename) {
1182
+ if (filename) return filename;
1183
+ const name = file.name;
1184
+ if (typeof name === "string" && name.length > 0) {
1185
+ return name.split(/[/\\]/).pop() || "upload.jsonl";
1186
+ }
1187
+ return "upload.jsonl";
1188
+ }
865
1189
  function abortableSleep(ms, signal) {
866
1190
  if (signal.aborted) return Promise.resolve(true);
867
1191
  return new Promise((resolve) => {
@@ -915,6 +1239,16 @@ var SIEClient = class {
915
1239
  apiKey;
916
1240
  defaultWaitForCapacity;
917
1241
  provisionTimeout;
1242
+ controlPlaneUrl;
1243
+ org;
1244
+ /** Batch class — `POST/GET /v1/jobs` on the keyed gateway. */
1245
+ jobs;
1246
+ /** Org-scoped connections (connector auth by name) on the control plane. */
1247
+ connections;
1248
+ /** OpenAI-compatible Files API — `POST/GET /v1/files`. */
1249
+ files;
1250
+ /** OpenAI-compatible Batch API — `POST/GET /v1/batches`. */
1251
+ batches;
918
1252
  // Pool state: track created pools and their lease renewal scheduling
919
1253
  pools = /* @__PURE__ */ new Map();
920
1254
  // Version negotiation state
@@ -932,8 +1266,36 @@ var SIEClient = class {
932
1266
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
933
1267
  this.gpu = options.gpu;
934
1268
  this.apiKey = options.apiKey;
935
- this.defaultWaitForCapacity = options.waitForCapacity ?? false;
1269
+ this.defaultWaitForCapacity = options.waitForCapacity ?? true;
936
1270
  this.provisionTimeout = options.provisionTimeout ?? DEFAULT_PROVISION_TIMEOUT;
1271
+ this.controlPlaneUrl = options.controlPlaneUrl?.replace(/\/$/, "");
1272
+ this.org = options.org;
1273
+ this.jobs = {
1274
+ submit: (submitOptions) => this.jobSubmit(submitOptions),
1275
+ get: (jobId) => this.jobGet(jobId),
1276
+ list: () => this.jobList(),
1277
+ cancel: (jobId) => this.jobCancel(jobId),
1278
+ results: (jobId) => this.jobResults(jobId),
1279
+ wait: (jobId, options2) => this.jobWait(jobId, options2)
1280
+ };
1281
+ this.connections = {
1282
+ add: (name, type, secret) => this.connectionAdd(name, type, secret),
1283
+ list: () => this.connectionList(),
1284
+ revoke: (name) => this.connectionRevoke(name)
1285
+ };
1286
+ this.files = {
1287
+ upload: (file, options2) => this.fileUpload(file, options2),
1288
+ create: (options2) => this.fileUpload(options2.file, options2),
1289
+ retrieve: (fileId) => this.fileRetrieve(fileId),
1290
+ content: (fileId) => this.fileContent(fileId),
1291
+ delete: (fileId) => this.fileDelete(fileId)
1292
+ };
1293
+ this.batches = {
1294
+ create: (options2) => this.batchCreate(options2),
1295
+ retrieve: (batchId) => this.batchRetrieve(batchId),
1296
+ list: () => this.batchList(),
1297
+ cancel: (batchId) => this.batchCancel(batchId)
1298
+ };
937
1299
  }
938
1300
  /**
939
1301
  * Get the base URL of the SIE server.
@@ -1396,8 +1758,10 @@ var SIEClient = class {
1396
1758
  * generator throws it instead of yielding the chunk.
1397
1759
  *
1398
1760
  * Retry policy mirrors {@link generate}: only explicit SAFE
1399
- * pre-execution capacity signals — `503 PROVISIONING` and
1400
- * `503 MODEL_LOADING` are retried while the provision budget remains.
1761
+ * pre-execution capacity signals — `503 PROVISIONING`,
1762
+ * `503 MODEL_LOADING` and `503 RESOURCE_EXHAUSTED` (the latter only
1763
+ * under `waitForCapacity`) — are retried while the provision budget
1764
+ * remains; a `504` is post-publish and therefore terminal.
1401
1765
  * Once the body opens we never retry (the call is non-idempotent; a
1402
1766
  * mid-stream failure must not re-issue generation).
1403
1767
  *
@@ -1419,6 +1783,7 @@ var SIEClient = class {
1419
1783
  }
1420
1784
  try {
1421
1785
  const startTime = Date.now();
1786
+ let oomRetries = 0;
1422
1787
  let response;
1423
1788
  while (true) {
1424
1789
  if (signal?.aborted) {
@@ -1490,6 +1855,34 @@ var SIEClient = class {
1490
1855
  }
1491
1856
  continue;
1492
1857
  }
1858
+ if (errorCode === RESOURCE_EXHAUSTED_ERROR_CODE) {
1859
+ if (!waitForCapacity) {
1860
+ throw new ResourceExhaustedError(
1861
+ `Server resource exhausted after ${oomRetries} retry attempt(s) for model '${model}'`,
1862
+ { model, retries: oomRetries }
1863
+ );
1864
+ }
1865
+ const delay = nextOomRetryDelay({
1866
+ retryAfter: getRetryAfter2(attemptResponse),
1867
+ oomRetries,
1868
+ maxOomRetries: RESOURCE_EXHAUSTED_MAX_RETRIES,
1869
+ elapsedMs: Date.now() - startTime,
1870
+ provisionTimeoutMs: this.provisionTimeout,
1871
+ model
1872
+ });
1873
+ oomRetries += 1;
1874
+ if (await abortableSleep(delay, controller.signal)) {
1875
+ throw new SIEConnectionError("Stream aborted while provisioning", "other");
1876
+ }
1877
+ continue;
1878
+ }
1879
+ }
1880
+ if (attemptResponse.status === HTTP_GATEWAY_TIMEOUT) {
1881
+ throw new ServerError(
1882
+ "Gateway timed out (504) after the request was published to the queue; a worker may already be generating. Not retried because generation is non-idempotent (retrying could double-bill).",
1883
+ await getErrorCode(attemptResponse.clone()),
1884
+ HTTP_GATEWAY_TIMEOUT
1885
+ );
1493
1886
  }
1494
1887
  if (attemptResponse.status !== 200) {
1495
1888
  await handleError(attemptResponse);
@@ -1642,12 +2035,21 @@ var SIEClient = class {
1642
2035
  * @param gpus - Optional machine profile requirements for pool readiness, e.g., { "l4": 2, "l4-spot": 1 }
1643
2036
  * @param gpuCaps - Optional maximum assigned workers per machine profile
1644
2037
  * @param queuePool - Optional Helm/NATS queue namespace backing this logical pool. Defaults to "default".
2038
+ * @param options - Optional bundle filter, warm floor, and pinned models
2039
+ * (Python SDK `create_pool` parity)
1645
2040
  *
1646
2041
  * @example
1647
2042
  * ```typescript
1648
2043
  * // Create or update a pool with 2 L4 GPUs
1649
2044
  * await client.createPool("eval-bench", { l4: 2 });
1650
2045
  *
2046
+ * // With a bundle filter, warm floor, and pinned models
2047
+ * await client.createPool("eval-bench", { l4: 2 }, undefined, undefined, {
2048
+ * bundle: "default",
2049
+ * minimumWorkerCount: 1,
2050
+ * pinnedModels: ["bge-m3"],
2051
+ * });
2052
+ *
1651
2053
  * // Use the pool for requests
1652
2054
  * await client.encode("bge-m3", { text: "Hello" }, { gpu: "eval-bench/l4" });
1653
2055
  *
@@ -1655,8 +2057,11 @@ var SIEClient = class {
1655
2057
  * await client.deletePool("eval-bench");
1656
2058
  * ```
1657
2059
  */
1658
- async createPool(name, gpus, gpuCaps, queuePool) {
2060
+ async createPool(name, gpus, gpuCaps, queuePool, options = {}) {
1659
2061
  const alreadyTracking = this.pools.has(name);
2062
+ if (options.minimumWorkerCount !== void 0 && options.minimumWorkerCount < 0) {
2063
+ throw new RangeError("minimumWorkerCount must be >= 0");
2064
+ }
1660
2065
  const requestBody = {
1661
2066
  name
1662
2067
  };
@@ -1669,6 +2074,15 @@ var SIEClient = class {
1669
2074
  if (queuePool) {
1670
2075
  requestBody.queue_pool = queuePool;
1671
2076
  }
2077
+ if (options.bundle) {
2078
+ requestBody.bundle = options.bundle;
2079
+ }
2080
+ if (options.minimumWorkerCount !== void 0) {
2081
+ requestBody.minimum_worker_count = options.minimumWorkerCount;
2082
+ }
2083
+ if (options.pinnedModels !== void 0) {
2084
+ requestBody.pinned_models = options.pinnedModels;
2085
+ }
1672
2086
  const url = `${this.baseUrl}/v1/pools`;
1673
2087
  const headers = {
1674
2088
  "Content-Type": JSON_CONTENT_TYPE,
@@ -1976,6 +2390,11 @@ var SIEClient = class {
1976
2390
  * Retried (capped by `provisionTimeout`):
1977
2391
  * - 503 `PROVISIONING` when `waitForCapacity: true`
1978
2392
  * - 503 `MODEL_LOADING` / `LORA_LOADING`
2393
+ * - 503 `RESOURCE_EXHAUSTED` regardless of `waitForCapacity` (bounded
2394
+ * exponential backoff, at most `RESOURCE_EXHAUSTED_MAX_RETRIES`)
2395
+ * - 504 gateway timeout when `waitForCapacity: true` — encode/score/
2396
+ * extract are idempotent queue paths, so a post-publish retry is safe
2397
+ * (unlike generate/chat, where a 504 is terminal)
1979
2398
  * - `SIEConnectionError` with `kind === "connect"` (issue #95)
1980
2399
  *
1981
2400
  * `kind === "timeout"` is NOT retried — would extend the user-visible
@@ -1984,6 +2403,7 @@ var SIEClient = class {
1984
2403
  async requestWithRetry(path, body, pool, gpu, waitForCapacity, model) {
1985
2404
  const startTime = Date.now();
1986
2405
  let loraRetries = 0;
2406
+ let oomRetries = 0;
1987
2407
  while (true) {
1988
2408
  let response;
1989
2409
  try {
@@ -2058,6 +2478,27 @@ var SIEClient = class {
2058
2478
  await sleep2(actualDelay);
2059
2479
  continue;
2060
2480
  }
2481
+ if (errorCode === RESOURCE_EXHAUSTED_ERROR_CODE) {
2482
+ const delay = nextOomRetryDelay({
2483
+ retryAfter: getRetryAfter2(response),
2484
+ oomRetries,
2485
+ maxOomRetries: RESOURCE_EXHAUSTED_MAX_RETRIES,
2486
+ elapsedMs: Date.now() - startTime,
2487
+ provisionTimeoutMs: this.provisionTimeout,
2488
+ model
2489
+ });
2490
+ oomRetries += 1;
2491
+ await sleep2(delay);
2492
+ continue;
2493
+ }
2494
+ }
2495
+ if (response.status === HTTP_GATEWAY_TIMEOUT && waitForCapacity) {
2496
+ const elapsed = Date.now() - startTime;
2497
+ if (elapsed < this.provisionTimeout) {
2498
+ const delay = getRetryAfter2(response) ?? MODEL_LOADING_DEFAULT_DELAY;
2499
+ await sleep2(Math.min(delay, this.provisionTimeout - elapsed));
2500
+ continue;
2501
+ }
2061
2502
  }
2062
2503
  if (!response.ok) {
2063
2504
  await handleError(response, gpu);
@@ -2146,6 +2587,291 @@ var SIEClient = class {
2146
2587
  clearTimeout(timeoutId);
2147
2588
  }
2148
2589
  }
2590
+ // ---------------------------------------------------------------------------
2591
+ // Jobs + connections namespaces. Jobs ride the keyed gateway
2592
+ // (`/v1/jobs`); connections ride the control plane (`/internal/orgs/{org}/…`).
2593
+ // ---------------------------------------------------------------------------
2594
+ /** One JSON request over `fetch` (bearer auth reused; absolute or base-relative URL). */
2595
+ async jsonRequest(target, method, body, timeoutMs = this.timeout) {
2596
+ const url = target.startsWith("http") ? target : `${this.baseUrl}${target}`;
2597
+ const headers = {
2598
+ Accept: JSON_CONTENT_TYPE,
2599
+ [SDK_VERSION_HEADER]: SDK_VERSION
2600
+ };
2601
+ if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
2602
+ const init = { method, headers };
2603
+ if (body !== void 0) {
2604
+ headers["Content-Type"] = JSON_CONTENT_TYPE;
2605
+ init.body = JSON.stringify(body);
2606
+ }
2607
+ const controller = new AbortController();
2608
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2609
+ init.signal = controller.signal;
2610
+ try {
2611
+ const response = await fetch(url, init);
2612
+ if (!response.ok) {
2613
+ await handleError(response);
2614
+ }
2615
+ this.checkServerVersion(response);
2616
+ const text = await response.text();
2617
+ return text ? JSON.parse(text) : {};
2618
+ } catch (error) {
2619
+ if (error instanceof Error && error.name === "AbortError") {
2620
+ throw new SIEConnectionError(`Request timeout after ${timeoutMs}ms`, "timeout");
2621
+ }
2622
+ if (error instanceof TypeError) {
2623
+ throw new SIEConnectionError(`Connection failed: ${error.message}`, "connect");
2624
+ }
2625
+ throw error;
2626
+ } finally {
2627
+ clearTimeout(timeoutId);
2628
+ }
2629
+ }
2630
+ async jobSubmit(options) {
2631
+ return this.jsonRequest(
2632
+ "/v1/jobs",
2633
+ "POST",
2634
+ buildJobBody(options),
2635
+ Math.max(this.timeout, DEFAULT_LONG_RUNNING_TIMEOUT)
2636
+ );
2637
+ }
2638
+ async jobGet(jobId) {
2639
+ return this.jsonRequest(`/v1/jobs/${encodeURIComponent(jobId)}`, "GET");
2640
+ }
2641
+ async jobList() {
2642
+ const data = await this.jsonRequest("/v1/jobs", "GET");
2643
+ return Array.isArray(data) ? data : data.data ?? [];
2644
+ }
2645
+ async jobCancel(jobId) {
2646
+ return this.jsonRequest(`/v1/jobs/${encodeURIComponent(jobId)}/cancel`, "POST");
2647
+ }
2648
+ async jobResults(jobId) {
2649
+ const job = await this.jobGet(jobId);
2650
+ const chunks = jobChunks(job);
2651
+ const items = [];
2652
+ for (const chunk of chunks) {
2653
+ if (chunk.state !== "succeeded" || !chunk.ref) continue;
2654
+ const raw = await this.readRef(chunk.ref);
2655
+ items.push(...decodeChunkBytes(raw));
2656
+ }
2657
+ const withDims = items.find((it) => it.dims != null);
2658
+ return {
2659
+ job_id: job.id ?? jobId,
2660
+ state: job.state,
2661
+ total_items: job.total_items,
2662
+ settled_credits: job.settled_credits,
2663
+ chunks,
2664
+ retrieved: items.length,
2665
+ dims: withDims ? withDims.dims : null,
2666
+ items
2667
+ };
2668
+ }
2669
+ async jobWait(jobId, options) {
2670
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_JOB_WAIT_TIMEOUT;
2671
+ const pollMs = options?.pollMs ?? DEFAULT_JOB_WAIT_POLL;
2672
+ const deadline = Date.now() + timeoutMs;
2673
+ for (; ; ) {
2674
+ const job = await this.jobGet(jobId);
2675
+ if (job.state && TERMINAL_JOB_STATES.has(job.state)) {
2676
+ return job;
2677
+ }
2678
+ if (Date.now() >= deadline) {
2679
+ throw new RequestError(
2680
+ `job ${jobId} still ${JSON.stringify(job.state)} after ${timeoutMs}ms`,
2681
+ "job_wait_timeout",
2682
+ 504
2683
+ );
2684
+ }
2685
+ await sleep2(pollMs);
2686
+ }
2687
+ }
2688
+ /** Retrieve a chunk's payload-store ref (http(s) URL). */
2689
+ async readRef(ref) {
2690
+ if (!ref.startsWith("http://") && !ref.startsWith("https://")) {
2691
+ throw new RequestError(
2692
+ `cannot retrieve payload-store ref ${JSON.stringify(ref)} (the TS SDK reads http(s) refs)`,
2693
+ "bad_ref",
2694
+ 400
2695
+ );
2696
+ }
2697
+ const controller = new AbortController();
2698
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2699
+ try {
2700
+ const response = await fetch(ref, {
2701
+ headers: { Accept: "application/octet-stream" },
2702
+ signal: controller.signal
2703
+ });
2704
+ if (!response.ok) {
2705
+ await handleError(response);
2706
+ }
2707
+ return new Uint8Array(await response.arrayBuffer());
2708
+ } catch (error) {
2709
+ if (error instanceof Error && error.name === "AbortError") {
2710
+ throw new SIEConnectionError(`Request timeout after ${this.timeout}ms`, "timeout");
2711
+ }
2712
+ if (error instanceof TypeError) {
2713
+ throw new SIEConnectionError(`Connection failed: ${error.message}`, "connect");
2714
+ }
2715
+ throw error;
2716
+ } finally {
2717
+ clearTimeout(timeoutId);
2718
+ }
2719
+ }
2720
+ connectionsBase() {
2721
+ if (!this.controlPlaneUrl) {
2722
+ throw new RequestError(
2723
+ "connections require controlPlaneUrl on the client: new SIEClient(url, { controlPlaneUrl, org })",
2724
+ "missing_control_plane_url",
2725
+ 400
2726
+ );
2727
+ }
2728
+ if (!this.org) {
2729
+ throw new RequestError(
2730
+ "connections require org on the client: new SIEClient(url, { controlPlaneUrl, org })",
2731
+ "missing_org",
2732
+ 400
2733
+ );
2734
+ }
2735
+ return `${this.controlPlaneUrl}/internal/orgs/${encodeURIComponent(this.org)}/connections`;
2736
+ }
2737
+ async connectionAdd(name, type, secret) {
2738
+ return this.jsonRequest(this.connectionsBase(), "POST", {
2739
+ type,
2740
+ name,
2741
+ secret
2742
+ });
2743
+ }
2744
+ async connectionList() {
2745
+ const data = await this.jsonRequest(
2746
+ this.connectionsBase(),
2747
+ "GET"
2748
+ );
2749
+ return Array.isArray(data) ? data : data.connections ?? [];
2750
+ }
2751
+ async connectionRevoke(name) {
2752
+ return this.jsonRequest(
2753
+ `${this.connectionsBase()}/${encodeURIComponent(name)}`,
2754
+ "DELETE"
2755
+ );
2756
+ }
2757
+ // ---------------------------------------------------------------------------
2758
+ // Files + batches namespaces — the OpenAI-compatible file /
2759
+ // batch surface on the keyed gateway. Method names/args mirror `openai.files`
2760
+ // / `openai.batches` so switching an OpenAI-batch caller to the SDK is
2761
+ // mechanical.
2762
+ // ---------------------------------------------------------------------------
2763
+ /** POST a raw body and parse the JSON response (bearer auth reused). */
2764
+ async rawPostJson(path, body, contentType, timeoutMs = this.timeout) {
2765
+ const url = `${this.baseUrl}${path}`;
2766
+ const headers = {
2767
+ Accept: JSON_CONTENT_TYPE,
2768
+ "Content-Type": contentType,
2769
+ [SDK_VERSION_HEADER]: SDK_VERSION
2770
+ };
2771
+ if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
2772
+ const controller = new AbortController();
2773
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2774
+ try {
2775
+ const response = await fetch(url, {
2776
+ method: "POST",
2777
+ headers,
2778
+ body,
2779
+ signal: controller.signal
2780
+ });
2781
+ if (!response.ok) {
2782
+ await handleError(response);
2783
+ }
2784
+ this.checkServerVersion(response);
2785
+ const text = await response.text();
2786
+ return text ? JSON.parse(text) : {};
2787
+ } catch (error) {
2788
+ if (error instanceof Error && error.name === "AbortError") {
2789
+ throw new SIEConnectionError(`Request timeout after ${timeoutMs}ms`, "timeout");
2790
+ }
2791
+ if (error instanceof TypeError) {
2792
+ throw new SIEConnectionError(`Connection failed: ${error.message}`, "connect");
2793
+ }
2794
+ throw error;
2795
+ } finally {
2796
+ clearTimeout(timeoutId);
2797
+ }
2798
+ }
2799
+ /** GET raw bytes (bearer auth reused); used to download a file's content. */
2800
+ async rawGetBytes(path) {
2801
+ const url = `${this.baseUrl}${path}`;
2802
+ const headers = {
2803
+ Accept: "application/jsonl",
2804
+ [SDK_VERSION_HEADER]: SDK_VERSION
2805
+ };
2806
+ if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
2807
+ const controller = new AbortController();
2808
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2809
+ try {
2810
+ const response = await fetch(url, { method: "GET", headers, signal: controller.signal });
2811
+ if (!response.ok) {
2812
+ await handleError(response);
2813
+ }
2814
+ this.checkServerVersion(response);
2815
+ return new Uint8Array(await response.arrayBuffer());
2816
+ } catch (error) {
2817
+ if (error instanceof Error && error.name === "AbortError") {
2818
+ throw new SIEConnectionError(`Request timeout after ${this.timeout}ms`, "timeout");
2819
+ }
2820
+ if (error instanceof TypeError) {
2821
+ throw new SIEConnectionError(`Connection failed: ${error.message}`, "connect");
2822
+ }
2823
+ throw error;
2824
+ } finally {
2825
+ clearTimeout(timeoutId);
2826
+ }
2827
+ }
2828
+ async fileUpload(file, options) {
2829
+ const purpose = options?.purpose ?? "batch";
2830
+ const filename = resolveUploadFilename(file, options?.filename);
2831
+ const query = new URLSearchParams({ purpose, filename }).toString();
2832
+ const body = file instanceof ArrayBuffer ? new Uint8Array(file) : file;
2833
+ return this.rawPostJson(
2834
+ `/v1/files?${query}`,
2835
+ body,
2836
+ "application/jsonl",
2837
+ Math.max(this.timeout, DEFAULT_LONG_RUNNING_TIMEOUT)
2838
+ );
2839
+ }
2840
+ async fileRetrieve(fileId) {
2841
+ return this.jsonRequest(`/v1/files/${encodeURIComponent(fileId)}`, "GET");
2842
+ }
2843
+ async fileContent(fileId) {
2844
+ return this.rawGetBytes(`/v1/files/${encodeURIComponent(fileId)}/content`);
2845
+ }
2846
+ async fileDelete(fileId) {
2847
+ return this.jsonRequest(`/v1/files/${encodeURIComponent(fileId)}`, "DELETE");
2848
+ }
2849
+ async batchCreate(options) {
2850
+ const body = {
2851
+ input_file_id: options.input_file_id,
2852
+ endpoint: options.endpoint ?? "/v1/embeddings",
2853
+ completion_window: options.completion_window ?? "24h"
2854
+ };
2855
+ if (options.metadata !== void 0) {
2856
+ body.metadata = options.metadata;
2857
+ }
2858
+ return this.jsonRequest(
2859
+ "/v1/batches",
2860
+ "POST",
2861
+ body,
2862
+ Math.max(this.timeout, DEFAULT_LONG_RUNNING_TIMEOUT)
2863
+ );
2864
+ }
2865
+ async batchRetrieve(batchId) {
2866
+ return this.jsonRequest(`/v1/batches/${encodeURIComponent(batchId)}`, "GET");
2867
+ }
2868
+ async batchList() {
2869
+ const data = await this.jsonRequest("/v1/batches", "GET");
2870
+ return Array.isArray(data) ? data : data.data ?? [];
2871
+ }
2872
+ async batchCancel(batchId) {
2873
+ return this.jsonRequest(`/v1/batches/${encodeURIComponent(batchId)}/cancel`, "POST");
2874
+ }
2149
2875
  buildWsUrl(path) {
2150
2876
  const url = new URL(this.baseUrl);
2151
2877
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
@@ -2291,12 +3017,16 @@ exports.ModelLoadingError = ModelLoadingError;
2291
3017
  exports.PoolError = PoolError;
2292
3018
  exports.ProvisioningError = ProvisioningError;
2293
3019
  exports.RequestError = RequestError;
3020
+ exports.ResourceExhaustedError = ResourceExhaustedError;
2294
3021
  exports.SDK_VERSION = SDK_VERSION;
2295
3022
  exports.SIEClient = SIEClient;
2296
3023
  exports.SIEConnectionError = SIEConnectionError;
2297
3024
  exports.SIEError = SIEError;
2298
3025
  exports.SIEStreamError = SIEStreamError;
2299
3026
  exports.ServerError = ServerError;
3027
+ exports.TERMINAL_JOB_STATES = TERMINAL_JOB_STATES;
3028
+ exports.buildJobBody = buildJobBody;
3029
+ exports.connectionName = connectionName;
2300
3030
  exports.denseEmbedding = denseEmbedding;
2301
3031
  exports.detectImageFormat = detectImageFormat;
2302
3032
  exports.maxsim = maxsim;