@eyepop.ai/eyepop 3.25.3 → 3.25.5

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.
@@ -8928,14 +8928,25 @@ if ("document" in globalThis && "implementation" in globalThis.document) {
8928
8928
  }
8929
8929
 
8930
8930
  // endpoint.ts
8931
+ var ACCOUNT_CLAIM = "https://ident.eyepop.ai/account-uuid";
8932
+ var M2M_REFUSAL = "API key or user login required";
8933
+ function carriesAccount(token) {
8934
+ try {
8935
+ const payload = (token.split(".")[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
8936
+ return Boolean(JSON.parse(atob(payload))?.[ACCOUNT_CLAIM]);
8937
+ } catch {
8938
+ return false;
8939
+ }
8940
+ }
8931
8941
  var Endpoint = class {
8932
8942
  constructor(options) {
8943
+ this._exchanging = null;
8933
8944
  this._options = options;
8934
8945
  this._token = null;
8935
8946
  this._expire_token_time = null;
8936
8947
  this._client = null;
8937
8948
  this._statusRetryHandlers = /* @__PURE__ */ new Map();
8938
- this._statusRetryHandlers.set(401, this.statusHandler401);
8949
+ this._statusRetryHandlers.set(401, (statusCode, failedToken) => this.statusHandler401(statusCode, failedToken));
8939
8950
  this._limit = new Semaphore(this._options.jobQueueLength ?? 1024);
8940
8951
  this._state = "Idle" /* Idle */;
8941
8952
  this._message = null;
@@ -8951,7 +8962,7 @@ var Endpoint = class {
8951
8962
  }
8952
8963
  setStatusRetryHandlers(statusRetryHandlers = void 0) {
8953
8964
  this._statusRetryHandlers = new Map(statusRetryHandlers);
8954
- this._statusRetryHandlers.set(401, this.statusHandler401);
8965
+ this._statusRetryHandlers.set(401, (statusCode, failedToken) => this.statusHandler401(statusCode, failedToken));
8955
8966
  }
8956
8967
  eyepopUrl() {
8957
8968
  if (this._options.eyepopUrl) {
@@ -9048,12 +9059,15 @@ var Endpoint = class {
9048
9059
  validUntil: this._expire_token_time * 1e3
9049
9060
  };
9050
9061
  }
9051
- statusHandler401(_statusCode) {
9052
- this._token = null;
9053
- this._expire_token_time = null;
9062
+ statusHandler401(_statusCode, failedToken = this._token) {
9063
+ if (this._token === failedToken) {
9064
+ this._token = null;
9065
+ this._expire_token_time = null;
9066
+ }
9054
9067
  }
9055
9068
  async fetchWithRetry(fetcher, retries = 1) {
9056
9069
  while (true) {
9070
+ const sentToken = this._token ?? void 0;
9057
9071
  try {
9058
9072
  const response = await fetcher();
9059
9073
  if (response.ok) {
@@ -9061,7 +9075,7 @@ var Endpoint = class {
9061
9075
  } else {
9062
9076
  const handler = this._statusRetryHandlers.get(response.status);
9063
9077
  if (handler != null) {
9064
- handler(response.status);
9078
+ handler(response.status, sentToken);
9065
9079
  if (retries--) {
9066
9080
  this._requestLogger.info("received %d response, retry", response.status);
9067
9081
  } else {
@@ -9095,54 +9109,74 @@ var Endpoint = class {
9095
9109
  }
9096
9110
  return `Bearer ${await this.currentAccessToken()}`;
9097
9111
  }
9112
+ async exchange(client, credential, accountUuid) {
9113
+ const query = accountUuid ? `?account_uuid=${encodeURIComponent(accountUuid)}` : "";
9114
+ return await client.fetch(`${this.eyepopUrl()}/v1/auth/authenticate${query}`, {
9115
+ method: "POST",
9116
+ headers: { Authorization: `Bearer ${credential}` }
9117
+ });
9118
+ }
9119
+ async storeExchangedToken(response, now) {
9120
+ if (response.status != 200) {
9121
+ const message = await response.text();
9122
+ return Promise.reject(`Unexpected status ${response.status}: ${message}`);
9123
+ }
9124
+ const token = await response.json();
9125
+ this._token = token.access_token;
9126
+ this._expire_token_time = now + token.expires_in - 60;
9127
+ }
9128
+ /**
9129
+ * A token that already carries an account, or that the compute API refuses as an exchanged
9130
+ * token ("API key or user login required"), is used as is. Any other refusal is raised.
9131
+ */
9132
+ async exchangeLoginToken(client, loginToken, validUntil, now) {
9133
+ if (!carriesAccount(loginToken)) {
9134
+ const response = await this.exchange(client, loginToken, this._options.accountId);
9135
+ if (response.status != 401 || !(await response.clone().text()).includes(M2M_REFUSAL)) {
9136
+ await this.storeExchangedToken(response, now);
9137
+ return;
9138
+ }
9139
+ }
9140
+ this._token = loginToken;
9141
+ this._expire_token_time = validUntil;
9142
+ }
9098
9143
  async currentAccessToken() {
9144
+ if (!this._token || this._expire_token_time < Date.now() / 1e3) {
9145
+ this._exchanging ?? (this._exchanging = this.authenticate().finally(() => this._exchanging = null));
9146
+ await this._exchanging;
9147
+ }
9148
+ this._logger.debug("using access token, valid for at least %d seconds", this._expire_token_time - Date.now() / 1e3);
9149
+ return this._token;
9150
+ }
9151
+ async authenticate() {
9099
9152
  const now = Date.now() / 1e3;
9100
- if (!this._token || this._expire_token_time < now) {
9101
- this.updateState("Authenticating" /* Authenticating */);
9102
- try {
9103
- const auth = resolveAuth(this._options);
9104
- if (!this._client) {
9105
- return Promise.reject("endpoint not connected");
9106
- } else if (auth?.isLocal !== void 0) {
9107
- this._token = "implicit";
9108
- this._expire_token_time = Number.MAX_VALUE;
9109
- } else if (auth?.session !== void 0) {
9110
- return Promise.reject("temporary access token expired");
9111
- } else if (auth?.oAuth2 !== void 0 && this._options.eyepopUrl) {
9112
- const session = await authenticateBrowserSession(auth.oAuth2, this._options);
9113
- this._token = session.accessToken;
9114
- this._expire_token_time = session.validUntil / 1e3;
9115
- } else if (accessTokenCredential(auth) !== void 0) {
9116
- this._token = accessTokenCredential(auth);
9117
- this._expire_token_time = Number.MAX_VALUE;
9118
- } else if (this._options.eyepopUrl) {
9119
- const credential = apiKeyCredential(auth);
9120
- if (credential === void 0) {
9121
- return Promise.reject("no valid auth option");
9122
- }
9123
- const headers = { Authorization: `Bearer ${credential}` };
9124
- const post_url = `${this.eyepopUrl()}/v1/auth/authenticate`;
9125
- const response = await this._client.fetch(post_url, {
9126
- method: "POST",
9127
- headers
9128
- });
9129
- if (response.status != 200) {
9130
- const message = await response.text();
9131
- return Promise.reject(`Unexpected status ${response.status}: ${message}`);
9132
- }
9133
- const data = await response.json();
9134
- const token = data;
9135
- this._token = token.access_token;
9136
- this._expire_token_time = now + token.expires_in - 60;
9137
- } else {
9153
+ this.updateState("Authenticating" /* Authenticating */);
9154
+ try {
9155
+ const auth = resolveAuth(this._options);
9156
+ if (!this._client) {
9157
+ return Promise.reject("endpoint not connected");
9158
+ } else if (auth?.isLocal !== void 0) {
9159
+ this._token = "implicit";
9160
+ this._expire_token_time = Number.MAX_VALUE;
9161
+ } else if (auth?.session !== void 0) {
9162
+ return Promise.reject("temporary access token expired");
9163
+ } else if (auth?.oAuth2 !== void 0 && this._options.eyepopUrl) {
9164
+ const login = await authenticateBrowserSession(auth.oAuth2, this._options);
9165
+ await this.exchangeLoginToken(this._client, login.accessToken, login.validUntil / 1e3, now);
9166
+ } else if (accessTokenCredential(auth) !== void 0) {
9167
+ await this.exchangeLoginToken(this._client, accessTokenCredential(auth), Number.MAX_VALUE, now);
9168
+ } else if (this._options.eyepopUrl) {
9169
+ const credential = apiKeyCredential(auth);
9170
+ if (credential === void 0) {
9138
9171
  return Promise.reject("no valid auth option");
9139
9172
  }
9140
- } finally {
9141
- this.updateState();
9173
+ await this.storeExchangedToken(await this.exchange(this._client, credential), now);
9174
+ } else {
9175
+ return Promise.reject("no valid auth option");
9142
9176
  }
9177
+ } finally {
9178
+ this.updateState();
9143
9179
  }
9144
- this._logger.debug("using access token, valid for at least %d seconds", this._expire_token_time - now);
9145
- return this._token;
9146
9180
  }
9147
9181
  };
9148
9182
 
@@ -11497,6 +11531,8 @@ if (isBrowser) {
11497
11531
  var PIPELINE_PATH = "/api/v1/pipeline";
11498
11532
  var SUBPROTOCOL = "eyepop.v1";
11499
11533
  var MAX_OPEN_SOURCES = 256;
11534
+ var PIPELINE_SOCKETS = 4;
11535
+ var EXTRA_SOCKET_RETRY_MS = 10 * 1e3;
11500
11536
  var COMBINED_UPLOAD_MAX_BYTES = 8 * 1024 * 1024;
11501
11537
  var CHUNK = 1;
11502
11538
  var COMBINED = 2;
@@ -11616,6 +11652,8 @@ var PipelineSocket = class {
11616
11652
  // The ids of sources sent on this socket that have neither ended nor been refused, as the
11617
11653
  // pipeline API counts them, cancelled ones included.
11618
11654
  this.held = /* @__PURE__ */ new Set();
11655
+ // The held sources that are live. A live source never ends, so nothing queued behind it plays.
11656
+ this.lives = /* @__PURE__ */ new Set();
11619
11657
  // The held sources the pipeline API took: only these end, so only these make room.
11620
11658
  this.accepted = /* @__PURE__ */ new Set();
11621
11659
  // The ends on this socket so far.
@@ -11624,6 +11662,7 @@ var PipelineSocket = class {
11624
11662
  // the next send waits. Null once none is held.
11625
11663
  this.cap = null;
11626
11664
  this._roomWaiters = [];
11665
+ this.heartbeat = null;
11627
11666
  this.closed = new Promise((resolve) => this._markClosed = resolve);
11628
11667
  }
11629
11668
  /** A source ended or the socket closed: wake the send waiting for room. */
@@ -11657,6 +11696,10 @@ var PipelineSocket = class {
11657
11696
  return !this._closed && this.ws.readyState === 1;
11658
11697
  }
11659
11698
  markClosed() {
11699
+ if (this.heartbeat !== null) {
11700
+ clearInterval(this.heartbeat);
11701
+ this.heartbeat = null;
11702
+ }
11660
11703
  this._closed = true;
11661
11704
  this._markClosed();
11662
11705
  this.freed();
@@ -11683,10 +11726,22 @@ var PipelineJob = class {
11683
11726
  this.openSource = null;
11684
11727
  // Stops the job's source on the pipeline, once it was sent.
11685
11728
  this.stopOnPipeline = null;
11729
+ this._milliComputeUnits = void 0;
11686
11730
  }
11687
11731
  get cancelled() {
11688
11732
  return this._cancelled;
11689
11733
  }
11734
+ /** The source's milli compute units: the latest total of its `usage` messages and its `end`. */
11735
+ get milliComputeUnits() {
11736
+ return this._milliComputeUnits;
11737
+ }
11738
+ /** Keep a usage's or an end's milliComputeUnits: each carries the source's total so far. */
11739
+ noteMilliComputeUnits(message) {
11740
+ const value = message["milliComputeUnits"];
11741
+ if (Number.isInteger(value) && value >= 0) {
11742
+ this._milliComputeUnits = value;
11743
+ }
11744
+ }
11690
11745
  command(id) {
11691
11746
  const command = { type: "setSource", id, ...this.source };
11692
11747
  if (this.params.componentParams !== void 0) {
@@ -11745,14 +11800,14 @@ var PipelineBackend = class {
11745
11800
  constructor(ctx) {
11746
11801
  this.ctx = ctx;
11747
11802
  this._pipelineApiUrl = null;
11748
- this._socket = null;
11803
+ this._sockets = [];
11749
11804
  this._connecting = null;
11750
11805
  // Every job's setSource and every changePop goes out through this chain, in the order submitted.
11751
11806
  this._sendChain = Promise.resolve();
11752
11807
  this._disconnecting = false;
11753
11808
  this._nextId = 0;
11754
11809
  this._warned = /* @__PURE__ */ new Set();
11755
- this._heartbeat = null;
11810
+ this._noExtraSocketUntil = 0;
11756
11811
  // Reconnects at once since a source last played.
11757
11812
  this._reconnects = 0;
11758
11813
  this._currentPop = ctx.options.pop ?? null;
@@ -11766,13 +11821,9 @@ var PipelineBackend = class {
11766
11821
  async disconnect(closeEndpoint) {
11767
11822
  await closeEndpoint();
11768
11823
  this._disconnecting = true;
11769
- if (this._heartbeat !== null) {
11770
- clearInterval(this._heartbeat);
11771
- this._heartbeat = null;
11772
- }
11773
- const socket = this._socket;
11774
- this._socket = null;
11775
- if (socket !== null) {
11824
+ const sockets = this._sockets;
11825
+ this._sockets = [];
11826
+ for (const socket of sockets) {
11776
11827
  socket.ws.close(1e3);
11777
11828
  await socket.closed;
11778
11829
  }
@@ -11790,21 +11841,55 @@ var PipelineBackend = class {
11790
11841
  this._sendChain = result.catch(() => void 0);
11791
11842
  return result;
11792
11843
  }
11793
- /** The open socket, or a new one with the last pop set on it. */
11844
+ openSockets() {
11845
+ return this._sockets.filter((socket) => socket.open);
11846
+ }
11847
+ /** An open socket, or a new one with the last pop set on it. */
11794
11848
  async ensureSocket() {
11795
- if (this._socket !== null && this._socket.open) {
11796
- return this._socket;
11849
+ const open = this.openSockets()[0];
11850
+ if (open !== void 0) {
11851
+ return open;
11797
11852
  }
11798
11853
  if (this._connecting === null) {
11799
- this._connecting = this.openSocket().finally(() => this._connecting = null);
11854
+ this._connecting = this.openSocket(true).finally(() => this._connecting = null);
11800
11855
  }
11801
11856
  return await this._connecting;
11802
11857
  }
11803
- async openSocket() {
11858
+ /**
11859
+ * The socket for the next job; the caller runs it in submission order. A live job goes on a socket
11860
+ * of its own, an idle one or a new one, which `pipelineSockets` does not count. Any other job skips
11861
+ * the sockets with a live source: a new one while every other socket has a source and fewer than
11862
+ * `pipelineSockets` are open, else the one with room and the fewest sources.
11863
+ */
11864
+ async socketForJob(job) {
11865
+ if (job.live) {
11866
+ return this.openSockets().find((socket) => socket.held.size === 0) ?? await this.openSocket(true);
11867
+ }
11868
+ const sockets = this.openSockets().filter((socket) => socket.lives.size === 0);
11869
+ if (sockets.length === 0) {
11870
+ return await this.openSocket(true);
11871
+ }
11872
+ if (sockets.length < (this.ctx.options.pipelineSockets ?? PIPELINE_SOCKETS) && sockets.every((socket) => socket.held.size > 0) && Date.now() >= this._noExtraSocketUntil) {
11873
+ try {
11874
+ return await this.openSocket(false);
11875
+ } catch (error) {
11876
+ this._noExtraSocketUntil = Date.now() + EXTRA_SOCKET_RETRY_MS;
11877
+ this.ctx.requestLogger.info("could not open another pipeline API socket: %s", error);
11878
+ }
11879
+ }
11880
+ const rank = (socket) => (socket.hasRoom ? 0 : MAX_OPEN_SOURCES + 1) + socket.held.size;
11881
+ return sockets.reduce((best, socket) => rank(socket) < rank(best) ? socket : best);
11882
+ }
11883
+ /** A new socket with the last pop set on it; `patient` waits through 503 with Retry-After. */
11884
+ async openSocket(patient) {
11804
11885
  if (this.ctx.client() === null) {
11805
11886
  throw new PipelineSocketClosedError(void 0, "the endpoint is not connected");
11806
11887
  }
11807
- const ws = await this.upgrade(await this.socketUrl());
11888
+ const ws = await this.upgrade(await this.socketUrl(), patient);
11889
+ if (this._disconnecting) {
11890
+ ws.close(1e3);
11891
+ throw new PipelineSocketClosedError(void 0, "the endpoint disconnected");
11892
+ }
11808
11893
  const socket = new PipelineSocket(ws);
11809
11894
  ws.onmessage = (event) => {
11810
11895
  if (typeof event.data === "string") {
@@ -11818,31 +11903,29 @@ var PipelineBackend = class {
11818
11903
  ws.onerror = () => {
11819
11904
  };
11820
11905
  ws.onclose = (event) => this.closed(socket, event.code, event.reason);
11821
- this._socket = socket;
11822
- this.startHeartbeat(ws);
11906
+ this._sockets.push(socket);
11907
+ this.startHeartbeat(socket);
11823
11908
  if (this._currentPop !== null) {
11824
11909
  try {
11825
11910
  await this.setPopOn(socket, this._currentPop);
11826
11911
  } catch (error) {
11827
- this._socket = null;
11912
+ this._sockets = this._sockets.filter((other) => other !== socket);
11828
11913
  ws.close(1e3);
11829
11914
  throw error;
11830
11915
  }
11831
11916
  }
11832
11917
  return socket;
11833
11918
  }
11834
- startHeartbeat(ws) {
11835
- if (this._heartbeat !== null) {
11836
- clearInterval(this._heartbeat);
11837
- }
11919
+ startHeartbeat(socket) {
11920
+ const ws = socket.ws;
11838
11921
  const ping = ws.ping;
11839
11922
  if (typeof ping === "function") {
11840
- this._heartbeat = setInterval(() => {
11923
+ socket.heartbeat = setInterval(() => {
11841
11924
  if (ws.readyState === 1) {
11842
11925
  ping.call(ws);
11843
11926
  }
11844
11927
  }, HEARTBEAT_MS);
11845
- this._heartbeat.unref?.();
11928
+ socket.heartbeat.unref?.();
11846
11929
  }
11847
11930
  }
11848
11931
  async socketUrl() {
@@ -11880,7 +11963,7 @@ var PipelineBackend = class {
11880
11963
  return (await this.ctx.credentialHeader()).replace(/^Bearer /, "");
11881
11964
  }
11882
11965
  /** Open the socket, waiting through 503 with Retry-After and exchanging the API key again after a 401. */
11883
- async upgrade(url) {
11966
+ async upgrade(url, patient) {
11884
11967
  const deadline = Date.now() + MAX_RETRY_MS;
11885
11968
  let rejected = false;
11886
11969
  while (true) {
@@ -11898,7 +11981,7 @@ var PipelineBackend = class {
11898
11981
  continue;
11899
11982
  }
11900
11983
  const wait = error.status === 503 && error.retryAfter !== void 0 ? Math.max(error.retryAfter, 1) * 1e3 : void 0;
11901
- if (wait === void 0 || Date.now() + wait > deadline) {
11984
+ if (!patient || wait === void 0 || Date.now() + wait > deadline) {
11902
11985
  throw error;
11903
11986
  }
11904
11987
  this.ctx.requestLogger.info("the pipeline API answered 503, retrying in %d ms", wait);
@@ -11908,9 +11991,7 @@ var PipelineBackend = class {
11908
11991
  }
11909
11992
  closed(socket, code, reason) {
11910
11993
  socket.markClosed();
11911
- if (this._socket === socket) {
11912
- this._socket = null;
11913
- }
11994
+ this._sockets = this._sockets.filter((other) => other !== socket);
11914
11995
  const error = new PipelineSocketClosedError(code, reason);
11915
11996
  socket.sources.forEach((source) => source.close(error));
11916
11997
  socket.commands.forEach((command) => command.reject(error));
@@ -11936,12 +12017,14 @@ var PipelineBackend = class {
11936
12017
  const source = typeof id === "string" ? socket.sources.get(id) : void 0;
11937
12018
  if (type === "end") {
11938
12019
  socket.held.delete(id);
12020
+ socket.lives.delete(id);
11939
12021
  socket.accepted.delete(id);
11940
12022
  socket.freed();
11941
12023
  } else if (type === "result" && socket.held.has(id)) {
11942
12024
  socket.accepted.add(id);
11943
12025
  } else if (type === "error" && message["error"] !== "unknown_command") {
11944
12026
  socket.held.delete(id);
12027
+ socket.lives.delete(id);
11945
12028
  socket.wake();
11946
12029
  }
11947
12030
  if (type === "send" || type === "credit") {
@@ -11983,6 +12066,10 @@ var PipelineBackend = class {
11983
12066
  command(socket, message) {
11984
12067
  const id = this.newId(message["type"]);
11985
12068
  return new Promise((resolve, reject) => {
12069
+ if (!socket.open) {
12070
+ reject(new PipelineSocketClosedError(void 0, "the socket closed"));
12071
+ return;
12072
+ }
11986
12073
  socket.commands.set(id, { resolve, reject });
11987
12074
  socket.sendJson({ ...message, id });
11988
12075
  });
@@ -11991,15 +12078,28 @@ var PipelineBackend = class {
11991
12078
  pop() {
11992
12079
  return this._currentPop;
11993
12080
  }
11994
- /** Set the pop on the socket's pipeline; a pop it refuses rejects, and the last pop it took stays. */
12081
+ /**
12082
+ * Set the pop on every open socket's pipeline; a pop the first refuses rejects, and the last pop it
12083
+ * took stays. shortcut: a refusal from a later socket, which the first took, leaves the earlier ones
12084
+ * on the new pop; a refusal is the pop's own fault, so every socket answers it alike.
12085
+ */
11995
12086
  async changePop(pop) {
11996
12087
  if (this.ctx.client() === null) {
11997
12088
  this._currentPop = pop;
11998
12089
  return;
11999
12090
  }
12000
12091
  await this.inOrder(async () => {
12001
- if (this._socket !== null && this._socket.open) {
12002
- await this.setPopOn(this._socket, pop);
12092
+ const sockets = this.openSockets();
12093
+ if (sockets.length > 0) {
12094
+ for (const socket of sockets) {
12095
+ try {
12096
+ await this.setPopOn(socket, pop);
12097
+ } catch (error) {
12098
+ if (!(error instanceof PipelineSocketClosedError)) {
12099
+ throw error;
12100
+ }
12101
+ }
12102
+ }
12003
12103
  this._currentPop = pop;
12004
12104
  return;
12005
12105
  }
@@ -12160,22 +12260,30 @@ var PipelineBackend = class {
12160
12260
  }
12161
12261
  /** Send the job's setSource; the caller runs it in submission order. */
12162
12262
  async setSource(job) {
12163
- if (job.cancelled) {
12164
- return null;
12165
- }
12166
- let socket = await this.ensureSocket();
12167
- await socket.room(job);
12168
12263
  if (job.cancelled) {
12169
12264
  return null;
12170
12265
  }
12171
12266
  const combined = await combinedMedia(job);
12172
- socket = await this.ensureSocket();
12267
+ let socket;
12268
+ while (true) {
12269
+ socket = await this.socketForJob(job);
12270
+ await socket.room(job);
12271
+ if (job.cancelled) {
12272
+ return null;
12273
+ }
12274
+ if (socket.open) {
12275
+ break;
12276
+ }
12277
+ }
12173
12278
  const source = new OpenSource(this.newId("source"));
12174
12279
  if (combined === null) {
12175
12280
  uploadKeys(job).forEach((key) => source.uploads.set(key, new Credit()));
12176
12281
  }
12177
12282
  socket.sources.set(source.id, source);
12178
12283
  socket.held.add(source.id);
12284
+ if (job.live) {
12285
+ socket.lives.add(source.id);
12286
+ }
12179
12287
  source.endedBefore = socket.ended;
12180
12288
  job.openSource = source;
12181
12289
  job.stopOnPipeline = () => {
@@ -12211,7 +12319,11 @@ var PipelineBackend = class {
12211
12319
  case "prediction":
12212
12320
  job.predicted(message["prediction"]);
12213
12321
  break;
12322
+ case "usage":
12323
+ job.noteMilliComputeUnits(message);
12324
+ break;
12214
12325
  case "end":
12326
+ job.noteMilliComputeUnits(message);
12215
12327
  if (message["reason"] === "eos") {
12216
12328
  return null;
12217
12329
  }
@@ -12451,6 +12563,9 @@ var WorkerEndpoint = class extends Endpoint {
12451
12563
  if (options.jobQueueLength > MAX_OPEN_SOURCES) {
12452
12564
  throw new PipelineConfigurationError("job_queue_length", `jobQueueLength ${options.jobQueueLength} exceeds ${MAX_OPEN_SOURCES}, the open sources a pipeline API socket takes`);
12453
12565
  }
12566
+ if (options.pipelineSockets !== void 0 && !(Number.isInteger(options.pipelineSockets) && options.pipelineSockets > 0)) {
12567
+ throw new PipelineConfigurationError("pipeline_sockets", `pipelineSockets ${options.pipelineSockets} is not a positive whole number`);
12568
+ }
12454
12569
  }
12455
12570
  super(options);
12456
12571
  const context = {
@@ -12772,6 +12887,7 @@ var InferAssetJob = class {
12772
12887
  constructor(assetUrl, inferRequest, vlmFetch, requestLogger) {
12773
12888
  this._response = null;
12774
12889
  this._runInfo = null;
12890
+ this._milliComputeUnits = void 0;
12775
12891
  this._controller = new AbortController();
12776
12892
  this._vlmFetch = vlmFetch;
12777
12893
  this._assetUrl = assetUrl;
@@ -12808,6 +12924,13 @@ var InferAssetJob = class {
12808
12924
  }
12809
12925
  return iterator();
12810
12926
  }
12927
+ /**
12928
+ * The milli compute units vlm-server billed for the inference, 0 on a cache hit; undefined before it answered
12929
+ * or when it reported none.
12930
+ */
12931
+ get milliComputeUnits() {
12932
+ return this._milliComputeUnits;
12933
+ }
12811
12934
  start(done) {
12812
12935
  this._response = new Promise(async (resolve, reject) => {
12813
12936
  try {
@@ -12832,6 +12955,8 @@ var InferAssetJob = class {
12832
12955
  const runInfo = response["run_info"];
12833
12956
  if (predictions) {
12834
12957
  this._runInfo = runInfo;
12958
+ const used = response["milli_compute_units"] ?? response["compute_units"];
12959
+ this._milliComputeUnits = typeof used === "number" ? used : void 0;
12835
12960
  resolve(predictions);
12836
12961
  break;
12837
12962
  } else {
@@ -14425,6 +14550,7 @@ export {
14425
14550
  ModelType,
14426
14551
  MotionModel,
14427
14552
  PIPELINE_PATH,
14553
+ PIPELINE_SOCKETS,
14428
14554
  PipelineBackend,
14429
14555
  PipelineCommandError,
14430
14556
  PipelineConfigurationError,