@plasius/gpu-model-runtime 0.2.0 → 0.2.2

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.js CHANGED
@@ -29,8 +29,117 @@ function createModelCacheKey(contentHash, formatId, adapterOptions) {
29
29
  return `${formatId}:${contentHash}:${JSON.stringify(stableValue(adapterOptions ?? null))}`;
30
30
  }
31
31
 
32
+ // src/types.ts
33
+ var DEFAULT_MODEL_MAX_BYTES = 64 * 1024 * 1024;
34
+
35
+ // src/bytes.ts
36
+ var ModelResourceTooLargeError = class extends Error {
37
+ code = "MODEL_RESOURCE_TOO_LARGE";
38
+ constructor() {
39
+ super("Model resource exceeds maxBytes");
40
+ this.name = "ModelResourceTooLargeError";
41
+ }
42
+ };
43
+ function byteLimit(value = DEFAULT_MODEL_MAX_BYTES) {
44
+ if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError("maxBytes must be a positive safe integer");
45
+ return value;
46
+ }
47
+ function checkSize(size, maxBytes) {
48
+ if (size > maxBytes) throw new ModelResourceTooLargeError();
49
+ }
50
+ function checkAbort(signal) {
51
+ if (signal?.aborted) throw new DOMException("The model load was aborted", "AbortError");
52
+ }
53
+ function acquisitionController(signal, timeoutMs = 3e4, parent) {
54
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
55
+ throw new RangeError("timeoutMs must be a positive timer-safe integer");
56
+ }
57
+ const controller = new AbortController();
58
+ const abort = () => controller.abort();
59
+ signal?.addEventListener("abort", abort, { once: true });
60
+ parent?.addEventListener("abort", abort, { once: true });
61
+ if (signal?.aborted || parent?.aborted) controller.abort();
62
+ const timer = setTimeout(abort, timeoutMs);
63
+ return {
64
+ signal: controller.signal,
65
+ dispose() {
66
+ clearTimeout(timer);
67
+ signal?.removeEventListener("abort", abort);
68
+ parent?.removeEventListener("abort", abort);
69
+ }
70
+ };
71
+ }
72
+ async function abortable(work, signal) {
73
+ return new Promise((resolve, reject) => {
74
+ const abort = () => {
75
+ signal.removeEventListener("abort", abort);
76
+ reject(new DOMException("The model load was aborted", "AbortError"));
77
+ };
78
+ signal.addEventListener("abort", abort, { once: true });
79
+ if (signal.aborted) abort();
80
+ work.then(
81
+ (value) => {
82
+ signal.removeEventListener("abort", abort);
83
+ resolve(value);
84
+ },
85
+ (error) => {
86
+ signal.removeEventListener("abort", abort);
87
+ reject(error);
88
+ }
89
+ );
90
+ });
91
+ }
92
+ function cleanup(work) {
93
+ try {
94
+ void Promise.resolve(work()).catch(() => {
95
+ });
96
+ } catch {
97
+ }
98
+ }
99
+ async function readBoundedStream(source, maxBytes, signal) {
100
+ let bytes = new Uint8Array(0);
101
+ let length = 0;
102
+ let chunks = 0;
103
+ const reader = "getReader" in source ? source.getReader() : void 0;
104
+ const iterator = reader ? void 0 : source[Symbol.asyncIterator]();
105
+ try {
106
+ while (true) {
107
+ checkAbort(signal);
108
+ if (++chunks % 256 === 0) await abortable(new Promise((resolve) => setTimeout(resolve, 0)), signal);
109
+ const next = await abortable(reader ? reader.read() : iterator.next(), signal);
110
+ checkAbort(signal);
111
+ if (next.done) break;
112
+ if (!(next.value instanceof Uint8Array)) throw new TypeError("Model stream chunks must be Uint8Array");
113
+ const chunk = next.value;
114
+ if (chunk.byteLength > maxBytes - length) throw new ModelResourceTooLargeError();
115
+ const required = length + chunk.byteLength;
116
+ if (required > bytes.length) {
117
+ const grown = new Uint8Array(Math.min(maxBytes, Math.max(required, bytes.length * 2, 4096)));
118
+ grown.set(bytes.subarray(0, length));
119
+ bytes = grown;
120
+ }
121
+ bytes.set(chunk, length);
122
+ length = required;
123
+ }
124
+ return bytes.length === length ? bytes : bytes.slice(0, length);
125
+ } catch (error) {
126
+ if (reader) cleanup(() => reader.cancel());
127
+ else cleanup(() => iterator?.return?.());
128
+ throw error;
129
+ } finally {
130
+ reader?.releaseLock();
131
+ }
132
+ }
133
+ async function boundedBytes(input, maxBytes, signal) {
134
+ checkAbort(signal);
135
+ checkSize(input instanceof Blob ? input.size : input.byteLength, maxBytes);
136
+ if (input instanceof Blob) return readBoundedStream(input.stream(), maxBytes, signal);
137
+ return new Uint8Array(input);
138
+ }
139
+
32
140
  // src/fetch.ts
33
141
  var DEFAULT_POLICY = {
142
+ maxBytes: DEFAULT_MODEL_MAX_BYTES,
34
143
  maxAttempts: 3,
35
144
  baseDelayMs: 100,
36
145
  maxDelayMs: 1e3,
@@ -61,6 +170,7 @@ async function sha256Hex(bytes) {
61
170
  }
62
171
  async function fetchResource(url, options) {
63
172
  const policy = { ...DEFAULT_POLICY, ...options.policy };
173
+ const maxBytes = Math.min(byteLimit(policy.maxBytes), byteLimit(options.resolveOptions?.maxBytes ?? policy.maxBytes));
64
174
  const headers = new Headers(options.headers);
65
175
  if (options.resolveOptions?.range) {
66
176
  const { start, end } = options.resolveOptions.range;
@@ -68,13 +178,21 @@ async function fetchResource(url, options) {
68
178
  }
69
179
  let lastError;
70
180
  for (let attempt = 1; attempt <= policy.maxAttempts; attempt += 1) {
71
- const controller = createRequestController(options.resolveOptions?.signal, policy.timeoutMs);
181
+ const controller = acquisitionController(options.resolveOptions?.signal, policy.timeoutMs);
72
182
  try {
73
- const response = await options.fetch(url, {
183
+ checkAbort(controller.signal);
184
+ const response = await abortable(options.fetch(url, {
74
185
  headers,
75
186
  signal: controller.signal
76
- });
187
+ }).then((response2) => {
188
+ if (controller.signal.aborted) {
189
+ cleanup(() => response2.body?.cancel());
190
+ checkAbort(controller.signal);
191
+ }
192
+ return response2;
193
+ }), controller.signal);
77
194
  if (!response.ok) {
195
+ cleanup(() => response.body?.cancel());
78
196
  if (!RETRYABLE_STATUSES.has(response.status)) {
79
197
  throw new NonRetryableResourceError(`Model resource request failed with HTTP ${response.status}`);
80
198
  }
@@ -82,11 +200,20 @@ async function fetchResource(url, options) {
82
200
  throw new NonRetryableResourceError(`Model resource request failed with HTTP ${response.status}`);
83
201
  }
84
202
  lastError = new Error(`Retryable model resource response: HTTP ${response.status}`);
85
- await waitForRetry(options.sleep, attempt, policy, options.resolveOptions?.signal);
203
+ await waitForRetry(options.sleep, attempt, policy, controller.signal);
86
204
  continue;
87
205
  }
88
- const bytes = new Uint8Array(await response.arrayBuffer());
89
- await verifyIntegrity(bytes, options.integrity, options.hashBytes);
206
+ let bytes;
207
+ try {
208
+ const declared = response.headers.get("content-length");
209
+ if (declared !== null && /^\d+$/u.test(declared)) checkSize(Number(declared), maxBytes);
210
+ bytes = response.body ? await readBoundedStream(response.body, maxBytes, controller.signal) : new Uint8Array(0);
211
+ checkAbort(controller.signal);
212
+ } catch (error) {
213
+ if (!response.body?.locked) cleanup(() => response.body?.cancel());
214
+ throw error;
215
+ }
216
+ await abortable(verifyIntegrity(bytes, options.integrity, options.hashBytes), controller.signal);
90
217
  return {
91
218
  bytes,
92
219
  contentType: response.headers.get("content-type") ?? void 0,
@@ -96,14 +223,14 @@ async function fetchResource(url, options) {
96
223
  if (isAbortError(error) || options.resolveOptions?.signal?.aborted) {
97
224
  throw createAbortError();
98
225
  }
99
- if (error instanceof NonRetryableResourceError) {
226
+ if (error instanceof NonRetryableResourceError || error instanceof ModelResourceTooLargeError) {
100
227
  throw error;
101
228
  }
102
229
  lastError = error;
103
230
  if (attempt === policy.maxAttempts) {
104
231
  throw error;
105
232
  }
106
- await waitForRetry(options.sleep, attempt, policy, options.resolveOptions?.signal);
233
+ await waitForRetry(options.sleep, attempt, policy, controller.signal);
107
234
  } finally {
108
235
  controller.dispose();
109
236
  }
@@ -140,20 +267,7 @@ function bytesToBase64(bytes) {
140
267
  }
141
268
  async function waitForRetry(sleep, attempt, policy, signal) {
142
269
  const delay = Math.min(policy.maxDelayMs, policy.baseDelayMs * 2 ** (attempt - 1));
143
- await sleep(delay, signal);
144
- }
145
- function createRequestController(signal, timeoutMs) {
146
- const controller = new AbortController();
147
- const onAbort = () => controller.abort();
148
- signal?.addEventListener("abort", onAbort, { once: true });
149
- const timer = setTimeout(() => controller.abort(), timeoutMs);
150
- return {
151
- signal: controller.signal,
152
- dispose: () => {
153
- clearTimeout(timer);
154
- signal?.removeEventListener("abort", onAbort);
155
- }
156
- };
270
+ await abortable(sleep(delay, signal), signal);
157
271
  }
158
272
  function createAbortError() {
159
273
  return new DOMException("The model load was aborted", "AbortError");
@@ -189,6 +303,7 @@ var AdapterRegistry = class {
189
303
  }
190
304
  };
191
305
  var PackageResourceResolver = class {
306
+ parentSignal;
192
307
  memoryResources;
193
308
  baseUrl;
194
309
  sourceKind;
@@ -199,48 +314,58 @@ var PackageResourceResolver = class {
199
314
  headers;
200
315
  blobStorageUrlResolver;
201
316
  constructor(options) {
317
+ this.parentSignal = options.signal;
202
318
  this.sourceKind = options.sourceKind;
203
319
  this.baseUrl = options.package?.baseUrl ?? options.baseUrl;
204
320
  this.memoryResources = options.package?.resources ?? {};
205
321
  this.fetch = options.fetch;
206
322
  this.hashBytes = options.hashBytes;
207
323
  this.sleep = options.sleep;
208
- this.fetchPolicy = options.fetchPolicy;
324
+ this.fetchPolicy = { ...options.fetchPolicy };
209
325
  this.headers = options.headers;
210
326
  this.blobStorageUrlResolver = options.blobStorageUrlResolver;
211
327
  }
212
328
  async resolve(reference, options = {}) {
213
- const requested = typeof reference === "string" ? { path: reference } : reference;
214
- const memoryKey = normalizePackagePath(requested.path);
215
- const memoryResource = this.memoryResources[memoryKey] ?? this.memoryResources[`./${memoryKey}`];
216
- if (memoryResource) {
217
- const bytes = await toBytes(memoryResource.bytes);
218
- await verifyResourceIntegrity(bytes, requested.integrity ?? memoryResource.integrity, this.hashBytes);
329
+ const inheritedLimit = byteLimit(this.fetchPolicy?.maxBytes);
330
+ const maxBytes = Math.min(inheritedLimit, byteLimit(options.maxBytes ?? inheritedLimit));
331
+ const controller = acquisitionController(options.signal, this.fetchPolicy?.timeoutMs, this.parentSignal);
332
+ try {
333
+ checkAbort(controller.signal);
334
+ const requested = typeof reference === "string" ? { path: reference } : reference;
335
+ const memoryKey = normalizePackagePath(requested.path);
336
+ const memoryResource = this.memoryResources[memoryKey] ?? this.memoryResources[`./${memoryKey}`];
337
+ if (memoryResource) {
338
+ const bytes = await boundedBytes(memoryResource.bytes, maxBytes, controller.signal);
339
+ await abortable(verifyResourceIntegrity(bytes, requested.integrity ?? memoryResource.integrity, this.hashBytes), controller.signal);
340
+ return {
341
+ path: memoryKey,
342
+ bytes,
343
+ contentType: requested.contentType ?? memoryResource.contentType,
344
+ contentHash: await abortable(this.hashBytes(bytes), controller.signal),
345
+ rangeSupported: false
346
+ };
347
+ }
348
+ const url = await abortable(this.resolveUrl(requested.path), controller.signal);
349
+ checkAbort(controller.signal);
350
+ const result = await fetchResource(url, {
351
+ fetch: this.fetch,
352
+ hashBytes: this.hashBytes,
353
+ sleep: this.sleep,
354
+ headers: this.headers,
355
+ integrity: requested.integrity,
356
+ policy: this.fetchPolicy,
357
+ resolveOptions: { ...options, maxBytes, signal: controller.signal }
358
+ });
219
359
  return {
220
360
  path: memoryKey,
221
- bytes,
222
- contentType: requested.contentType ?? memoryResource.contentType,
223
- contentHash: await this.hashBytes(bytes),
224
- rangeSupported: false
361
+ bytes: result.bytes,
362
+ contentType: requested.contentType ?? result.contentType,
363
+ contentHash: await abortable(this.hashBytes(result.bytes), controller.signal),
364
+ rangeSupported: result.rangeSupported
225
365
  };
366
+ } finally {
367
+ controller.dispose();
226
368
  }
227
- const url = await this.resolveUrl(requested.path);
228
- const result = await fetchResource(url, {
229
- fetch: this.fetch,
230
- hashBytes: this.hashBytes,
231
- sleep: this.sleep,
232
- headers: this.headers,
233
- integrity: requested.integrity,
234
- policy: this.fetchPolicy,
235
- resolveOptions: options
236
- });
237
- return {
238
- path: memoryKey,
239
- bytes: result.bytes,
240
- contentType: requested.contentType ?? result.contentType,
241
- contentHash: await this.hashBytes(result.bytes),
242
- rangeSupported: result.rangeSupported
243
- };
244
369
  }
245
370
  async resolveUrl(reference) {
246
371
  if (/^[a-z][a-z\d+.-]*:/iu.test(reference)) {
@@ -299,7 +424,7 @@ var ModelRuntime = class {
299
424
  fileName: input.fileName,
300
425
  sourceKind: source.kind
301
426
  }, options.formatId);
302
- const cacheKey = createModelCacheKey(input.contentHash, adapter.formatId, options.adapterOptions);
427
+ const cacheKey = createModelCacheKey(input.contentHash, adapter.formatId, { adapterOptions: options.adapterOptions ?? null, maxBytes: byteLimit(options.fetchPolicy?.maxBytes) });
303
428
  const useCache = options.useCache ?? true;
304
429
  if (useCache) {
305
430
  const cached = await this.cache.get(cacheKey);
@@ -345,54 +470,64 @@ var ModelRuntime = class {
345
470
  return adapter.load(input, context);
346
471
  }
347
472
  async resolveSource(source, options) {
348
- const fileName = sourceFileName(source);
349
- const baseUrl = "url" in source ? source.url : void 0;
350
- const headers = "headers" in source ? source.headers : void 0;
351
- let bytes;
352
- let contentType = "mimeTypeHint" in source ? source.mimeTypeHint : void 0;
353
- let rangeSupported = false;
354
- if (source.kind === "file-path") {
355
- if (!this.readFile) {
356
- throw new Error("file-path sources require a readFile dependency");
473
+ const maxBytes = byteLimit(options.fetchPolicy?.maxBytes);
474
+ const controller = acquisitionController(options.signal, options.fetchPolicy?.timeoutMs);
475
+ try {
476
+ checkAbort(controller.signal);
477
+ const fileName = sourceFileName(source);
478
+ const baseUrl = "url" in source ? source.url : void 0;
479
+ const headers = "headers" in source ? source.headers : void 0;
480
+ let bytes;
481
+ let contentType = "mimeTypeHint" in source ? source.mimeTypeHint : void 0;
482
+ let rangeSupported = false;
483
+ if (source.kind === "file-path") {
484
+ if (!this.readFile) {
485
+ throw new Error("file-path sources require a readFile dependency");
486
+ }
487
+ bytes = await abortable(this.readFile(source.path, controller.signal, { maxBytes }), controller.signal);
488
+ checkSize(bytes.byteLength, maxBytes);
489
+ } else if (source.kind === "url" || source.kind === "blob-storage-url") {
490
+ const result = await fetchResource(ensureHttpUrl(source.url), {
491
+ fetch: this.fetch,
492
+ hashBytes: this.hashBytes,
493
+ sleep: this.sleep,
494
+ headers,
495
+ integrity: options.integrity,
496
+ policy: options.fetchPolicy,
497
+ resolveOptions: { signal: controller.signal }
498
+ });
499
+ bytes = result.bytes;
500
+ contentType ??= result.contentType;
501
+ rangeSupported = result.rangeSupported;
502
+ } else {
503
+ bytes = await sourceToBytes(source, maxBytes, controller.signal);
357
504
  }
358
- bytes = await this.readFile(source.path, options.signal);
359
- } else if (source.kind === "url" || source.kind === "blob-storage-url") {
360
- const result = await fetchResource(ensureHttpUrl(source.url), {
505
+ checkAbort(controller.signal);
506
+ const contentHash = await abortable(this.hashBytes(bytes), controller.signal);
507
+ const resourceResolver = new PackageResourceResolver({
508
+ sourceKind: source.kind,
509
+ signal: options.signal,
510
+ baseUrl,
511
+ package: source.package,
361
512
  fetch: this.fetch,
362
513
  hashBytes: this.hashBytes,
363
514
  sleep: this.sleep,
515
+ fetchPolicy: options.fetchPolicy,
364
516
  headers,
365
- integrity: options.integrity,
366
- policy: options.fetchPolicy,
367
- resolveOptions: { signal: options.signal }
517
+ blobStorageUrlResolver: this.blobStorageUrlResolver
368
518
  });
369
- bytes = result.bytes;
370
- contentType ??= result.contentType;
371
- rangeSupported = result.rangeSupported;
372
- } else {
373
- bytes = await sourceToBytes(source);
519
+ return {
520
+ source,
521
+ bytes,
522
+ contentType,
523
+ fileName,
524
+ contentHash,
525
+ rangeSupported,
526
+ resourceResolver
527
+ };
528
+ } finally {
529
+ controller.dispose();
374
530
  }
375
- const contentHash = await this.hashBytes(bytes);
376
- const resourceResolver = new PackageResourceResolver({
377
- sourceKind: source.kind,
378
- baseUrl,
379
- package: source.package,
380
- fetch: this.fetch,
381
- hashBytes: this.hashBytes,
382
- sleep: this.sleep,
383
- fetchPolicy: options.fetchPolicy,
384
- headers,
385
- blobStorageUrlResolver: this.blobStorageUrlResolver
386
- });
387
- return {
388
- source,
389
- bytes,
390
- contentType,
391
- fileName,
392
- contentHash,
393
- rangeSupported,
394
- resourceResolver
395
- };
396
531
  }
397
532
  };
398
533
  function scoreRegistration(registration, input) {
@@ -424,50 +559,11 @@ function normalizePackagePath(path) {
424
559
  }
425
560
  return segments.join("/");
426
561
  }
427
- async function sourceToBytes(source) {
428
- if (source.kind === "blob") {
429
- return toBytes(source.blob);
430
- }
431
- if (source.kind === "array-buffer") {
432
- return new Uint8Array(source.bytes);
433
- }
434
- if (source.kind === "uint8-array") {
435
- return new Uint8Array(source.bytes);
436
- }
437
- const chunks = [];
438
- if (Symbol.asyncIterator in Object(source.stream)) {
439
- for await (const chunk of source.stream) {
440
- chunks.push(new Uint8Array(chunk));
441
- }
442
- } else {
443
- const reader = source.stream.getReader();
444
- try {
445
- while (true) {
446
- const next = await reader.read();
447
- if (next.done) break;
448
- chunks.push(new Uint8Array(next.value));
449
- }
450
- } finally {
451
- reader.releaseLock();
452
- }
453
- }
454
- const length = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
455
- const bytes = new Uint8Array(length);
456
- let offset = 0;
457
- for (const chunk of chunks) {
458
- bytes.set(chunk, offset);
459
- offset += chunk.byteLength;
460
- }
461
- return bytes;
462
- }
463
- async function toBytes(input) {
464
- if (input instanceof Blob) {
465
- return new Uint8Array(await input.arrayBuffer());
466
- }
467
- if (input instanceof Uint8Array) {
468
- return new Uint8Array(input);
469
- }
470
- return new Uint8Array(input);
562
+ async function sourceToBytes(source, maxBytes, signal) {
563
+ if (source.kind === "blob") return boundedBytes(source.blob, maxBytes, signal);
564
+ if (source.kind === "array-buffer" || source.kind === "uint8-array") return boundedBytes(source.bytes, maxBytes, signal);
565
+ if (source.byteLengthHint !== void 0) checkSize(source.byteLengthHint, maxBytes);
566
+ return readBoundedStream(source.stream, maxBytes, signal);
471
567
  }
472
568
  async function verifyResourceIntegrity(bytes, integrity, hashBytes) {
473
569
  if (!integrity) return;
@@ -1146,9 +1242,11 @@ var packageBootstrap = Object.freeze({
1146
1242
  });
1147
1243
  export {
1148
1244
  AdapterRegistry,
1245
+ DEFAULT_MODEL_MAX_BYTES,
1149
1246
  MemoryModelCache,
1150
1247
  ModelResidencyError,
1151
1248
  ModelResidencyManager,
1249
+ ModelResourceTooLargeError,
1152
1250
  ModelRuntime,
1153
1251
  PackageResourceResolver,
1154
1252
  createModelCacheKey,