diff --git a/packages/transformers/package.json b/packages/transformers/package.json index 8ea694b11..f7f092e68 100644 --- a/packages/transformers/package.json +++ b/packages/transformers/package.json @@ -58,7 +58,7 @@ "@huggingface/jinja": "^0.5.6", "@huggingface/tokenizers": "^0.1.3", "onnxruntime-node": "1.24.3", - "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", + "onnxruntime-web": "1.29.0-dev.20260811-e415ef9afd", "sharp": "^0.34.5" }, "devDependencies": { diff --git a/packages/transformers/src/utils/hub.js b/packages/transformers/src/utils/hub.js index 09f42e168..3f8ba5109 100755 --- a/packages/transformers/src/utils/hub.js +++ b/packages/transformers/src/utils/hub.js @@ -245,9 +245,14 @@ export async function storeCachedResource(path_or_repo_id, filename, cache, cach * @param {PretrainedOptions} [options] An object containing optional parameters. * @param {boolean} [return_path=false] Whether to return the path of the file instead of the file content. * @param {import('./cache.js').CacheInterface | null} [cache] The cache instance to use. + * @param {boolean} [as_blob=false] Whether to return a `Blob` rather than reading the file into a + * `Uint8Array`. Honoured on two paths: a cache HIT, which reads the cached `Response` directly, and a COLD + * file that is headed for the cache anyway, which is streamed into it and read straight back. Ignored — and a + * `Uint8Array` returned — when neither applies, which covers a cold file with no cache available and the + * fallback taken when the cache refuses the write. * * @throws Will throw an error if the file is not found and `fatal` is true. - * @returns {Promise} A Promise that resolves with the file content as a Uint8Array if `return_path` is false, or the file path as a string if `return_path` is true. + * @returns {Promise} A Promise that resolves with the file path as a string if `return_path` is true; otherwise a `Blob` when `as_blob` was honoured, and the file content as a `Uint8Array` in every other case. */ export async function loadResourceFile( path_or_repo_id, @@ -256,6 +261,7 @@ export async function loadResourceFile( options = {}, return_path = false, cache = null, + as_blob = false, ) { const { requestURL, localPath, remoteURL, proposedCacheKey, validModelId } = buildResourcePaths( path_or_repo_id, @@ -362,11 +368,97 @@ export async function loadResourceFile( // loads from disk directly). A completion progress event is emitted // after the caching block below to ensure progress_total reaches 100%. } else { - /** @type {Uint8Array} */ + // A `Blob` on the `as_blob` branches, bytes everywhere else. Widened rather than cast away at each + // use, so a future branch that forgets which it is fails here instead of at run time. + /** @type {Uint8Array|Blob} */ let buffer; if (typeof response !== 'string') { - if (!options.progress_callback) { + if (as_blob && cacheHit) { + // The whole point of `as_blob`, and it is one call: a Cache Storage `Response` hands back a + // Blob that is a FILE REFERENCE rather than a copy, so reading the file does not put it on + // the JS heap. What the RUNTIME then does with that Blob is a separate question — see the + // note on the cold branch below. + // + // Gated on `cacheHit`, which is a deliberate trade rather than caution. Reading the stream to + // report progress would defeat the whole thing — the chunks would be resident twice, once as + // buffers and once in Blob storage — and on a cold download progress is worth more than peak + // memory, because the user is watching several gigabytes arrive. Every load AFTER the first + // takes this branch, is instantaneous, and is where the peak actually matters. + buffer = await response.blob(); + + dispatchCallback(options.progress_callback, { + status: 'progress', + name: path_or_repo_id, + file: filename, + progress: 100, + loaded: buffer.size, + total: buffer.size, + }); + } else if (as_blob && toCacheResponse && response.body) { + // COLD, and headed for the cache anyway. Stream the body straight into Cache Storage and then + // read it back as a Blob, so the DOWNLOAD goes network -> disk without the file ever sitting + // on the JS heap. + // + // ⚠️ TWO SEPARATE SAVINGS, AND ONLY ONE OF THEM IS UNCONDITIONAL. This branch is about the + // download, happens before onnxruntime-web sees anything, and holds whichever build is in + // use — it is what stops a cold multi-gigabyte load dying in `getModelDataFiles` below. What + // the runtime does with the Blob afterwards is the other saving and is NOT unconditional: the + // JSPI build reads external data from a Blob without materialising it, while the default + // asyncify build copies it in. So on the default build this buys the download and not the + // session, and the end-to-end peak is unchanged at session creation. + // + // This is the case that actually fails. `getModelDataFiles` starts every external-data chunk + // concurrently, and reading each one into a `Uint8Array` to report progress means a cold load + // peaks at the SUM of the chunks: a 17 GB model raises `Array buffer allocation failed` at + // ~16 GB and only completes on a later attempt, once enough files are cached to take the + // branch above. + // + // Progress survives, which is the reason the buffer existed. A pass-through `TransformStream` + // counts bytes as they go by; it holds one chunk, not the file, and backpressure keeps it + // that way. + let loaded = 0; + const total = parseInt(response.headers.get('content-length'), 10) || 0; + const counting = new TransformStream({ + transform(chunk, controller) { + loaded += chunk.byteLength; + dispatchCallback(options.progress_callback, { + status: 'progress', + name: path_or_repo_id, + file: filename, + progress: total ? (loaded / total) * 100 : 0, + loaded, + total, + }); + controller.enqueue(chunk); + }, + }); + + // `content-length` explicitly, because the Cache API may strip it — same reason the buffered + // store below sets it. + const headers = new Headers(response.headers); + if (total) headers.set('content-length', String(total)); + + try { + await cache.put(cacheKey, new Response(response.body.pipeThrough(counting), { headers })); + const stored = await cache.match(cacheKey); + if (!stored) throw new Error('cache.match missed the entry just written'); + // `cache.match` is typed for every backend, including the Node file cache; what was just + // written here is a `Response`, and only a `Response` has `.blob()`. + buffer = await /** @type {Response} */ (stored).blob(); + // Already stored, so the block at the end of this function must not store it again. + toCacheResponse = false; + } catch (err) { + // The buffered path keeps working when the cache refuses the write (QuotaExceededError is + // the expected one). It cannot reuse `response` — the body is consumed — so it re-fetches. + logger.warn(`Unable to stream response into the cache, falling back to a buffer: ${err}.`); + // `getFile`, not bare `fetch`: it routes through `env.fetch` and applies + // `getFetchHeaders`, so a gated repo keeps its Authorization header on the way back. + const retry = await getFile(remoteURL); + if (retry.status !== 200) return handleError(retry.status, remoteURL, fatal); + buffer = new Uint8Array(await retry.arrayBuffer()); + } + } else if (!options.progress_callback) { // If no progress callback is specified, we can use the `.arrayBuffer()` // method to read the response. buffer = new Uint8Array(await response.arrayBuffer()); @@ -431,7 +523,12 @@ export async function loadResourceFile( cacheKey && typeof response !== 'string' ) { - await storeCachedResource(path_or_repo_id, filename, cache, cacheKey, response, result, options); + // ⛔ NEVER A BLOB HERE, and the two `as_blob` branches are why. A warm read is a cache HIT, which + // never sets `toCacheResponse`; a cold stream writes the entry itself and then clears the flag. So + // anything reaching this line was buffered, and the narrowing is a statement of that rather than a + // convenience — if a third branch ever produces a Blob without storing it, this is where it lands. + const buffered = /** @type {Uint8Array} */ (result); + await storeCachedResource(path_or_repo_id, filename, cache, cacheKey, response, buffered, options); } // In Node.js with return_path, the buffer read is skipped so no progress @@ -480,7 +577,7 @@ export async function loadResourceFile( throw new Error('Unable to get model file path or buffer.'); } -/** @type {Map>} Pending file loads keyed by resource identity. */ +/** @type {Map>} Pending file loads keyed by resource identity. */ const INFLIGHT_LOADS = new Map(); /** @@ -494,11 +591,21 @@ const INFLIGHT_LOADS = new Map(); * @param {boolean} [fatal=true] Whether to throw an error if the file is not found. * @param {PretrainedOptions} [options] An object containing optional parameters. * @param {boolean} [return_path=false] Whether to return the path of the file instead of the file content. + * @param {boolean} [as_blob=false] Whether to accept a `Blob` instead of a copy of the file's bytes on the JS + * heap. Intended for external data, which is handed straight to the runtime. Honoured for a cached file and + * for a cold one being written to the cache; a `Uint8Array` comes back otherwise, so callers must handle both. * * @throws Will throw an error if the file is not found and `fatal` is true. - * @returns {Promise} A Promise that resolves with the file content as a Uint8Array if `return_path` is false, or the file path as a string if `return_path` is true. + * @returns {Promise} A Promise that resolves with the file path as a string if `return_path` is true; otherwise a `Blob` when `as_blob` was honoured, and the file content as a `Uint8Array` in every other case. */ -export async function getModelFile(path_or_repo_id, filename, fatal = true, options = {}, return_path = false) { +export async function getModelFile( + path_or_repo_id, + filename, + fatal = true, + options = {}, + return_path = false, + as_blob = false, +) { if (!env.allowLocalModels) { // User has disabled local models, so we just make sure other settings are correct. @@ -518,7 +625,12 @@ export async function getModelFile(path_or_repo_id, filename, fatal = true, opti // sibling calls within one pipeline() share a single download (and a // single `initiate` event). Otherwise fall back to the global in-flight // map for concurrent dedup, with entries cleared on settle. - const key = makePretrainedOptionsKey(path_or_repo_id, options, filename, fatal, return_path); + // + // `as_blob` is part of the key because it changes the RESOLVED TYPE, not just how the bytes are fetched. + // Two callers asking for the same file with different values are not asking the same question: without + // it, whichever arrives first decides, and the other gets a `Blob` where it expects a `Uint8Array` or the + // reverse. `return_path` is in the key for exactly this reason already. + const key = makePretrainedOptionsKey(path_or_repo_id, options, filename, fatal, return_path, as_blob); const { progress_callback } = options; const loads = progress_callback instanceof DefaultProgressCallback ? progress_callback.loads : INFLIGHT_LOADS; let pending = loads.get(key); @@ -529,7 +641,7 @@ export async function getModelFile(path_or_repo_id, filename, fatal = true, opti file: filename, }); pending = getCache(options.cache_dir).then((cache) => - loadResourceFile(path_or_repo_id, filename, fatal, options, return_path, cache), + loadResourceFile(path_or_repo_id, filename, fatal, options, return_path, cache, as_blob), ); if (loads === INFLIGHT_LOADS) { pending = pending.finally(() => INFLIGHT_LOADS.delete(key)); diff --git a/packages/transformers/src/utils/model-loader.js b/packages/transformers/src/utils/model-loader.js index 599aefe9d..d9f0c858a 100644 --- a/packages/transformers/src/utils/model-loader.js +++ b/packages/transformers/src/utils/model-loader.js @@ -45,7 +45,11 @@ export async function getCoreModelFile(pretrained_model_name_or_path, fileName, const baseName = `${fileName}${suffix}.onnx`; const fullPath = `${options.subfolder ?? ''}/${baseName}`; - return await getModelFile(pretrained_model_name_or_path, fullPath, true, options, apis.IS_NODE_ENV); + // Not `as_blob`, so a `Blob` cannot come back — narrowed here rather than widening every caller of a + // function that only ever asks for bytes or a path. + return /** @type {string|Uint8Array} */ ( + await getModelFile(pretrained_model_name_or_path, fullPath, true, options, apis.IS_NODE_ENV) + ); } /** @@ -57,7 +61,7 @@ export async function getCoreModelFile(pretrained_model_name_or_path, fileName, * @param {import('./hub.js').PretrainedModelOptions} options Additional options for loading the model. * @param {import('./hub.js').ExternalData|Record|undefined} use_external_data_format External data format configuration. * @param {any} [session_options] Optional session options that may contain externalData configuration. - * @returns {Promise>} A Promise that resolves to an array of external data files. + * @returns {Promise>} A Promise that resolves to an array of external data files. */ export async function getModelDataFiles( pretrained_model_name_or_path, @@ -70,7 +74,7 @@ export async function getModelDataFiles( const baseName = `${fileName}${suffix}.onnx`; const return_path = apis.IS_NODE_ENV; - /** @type {Promise[]} */ + /** @type {Promise[]} */ let externalDataPromises = []; const num_chunks = resolveExternalDataFormat(use_external_data_format, baseName, fileName); @@ -91,8 +95,16 @@ export async function getModelDataFiles( true, options, return_path, + // In the browser, hand onnxruntime-web a Blob rather than a materialised buffer. + // Node keeps returning a path, which is cheaper still. + // + // Accepting a Blob is safe on either web build; only the JSPI one avoids copying it + // into the heap at session creation. The unconditional win is upstream of here — the + // download no longer buffers each chunk, which is what a cold load of a model with + // several multi-gigabyte chunks runs out of memory doing. + !return_path, ); - resolve(data instanceof Uint8Array ? { path, data } : path); + resolve(data instanceof Uint8Array || data instanceof Blob ? { path, data } : path); }), ); } diff --git a/packages/transformers/tests/utils/hub_external_data.test.js b/packages/transformers/tests/utils/hub_external_data.test.js new file mode 100644 index 000000000..99c5e141a --- /dev/null +++ b/packages/transformers/tests/utils/hub_external_data.test.js @@ -0,0 +1,166 @@ +import { env } from "../../src/env.js"; +import { getModelFile } from "../../src/utils/hub.js"; + +/** + * The two things `as_blob` has to get right are a COLD file and a WARM one, and they take different branches: + * cold streams the body into the cache and reads it back, warm reads the cached `Response` directly. Both are + * exercised here against an in-memory cache and a stubbed `env.fetch`, so nothing touches the network and the + * assertions can be about which branch ran rather than about wall-clock behaviour. + */ +class MemoryCache { + constructor() { + /** @type {Map} */ + this.entries = new Map(); + this.puts = 0; + /** @type {Error|null} Set to make `put` reject, standing in for QuotaExceededError. */ + this.putError = null; + } + + async match(key) { + const hit = this.entries.get(key); + return hit ? hit.clone() : undefined; + } + + async put(key, response) { + this.puts++; + if (this.putError) throw this.putError; + // Read the body here rather than storing the streaming `Response`: a real Cache Storage entry is settled + // by the time `match` can see it, and holding the live stream would let a test pass on a tee that a real + // cache would never hand back. + this.entries.set(key, new Response(await response.arrayBuffer(), { headers: response.headers })); + } +} + +const BYTES = new Uint8Array(4096).fill(7); + +describe("External data as a Blob", () => { + const saved = {}; + let cache; + let fetches; + + beforeEach(() => { + for (const k of ["useCustomCache", "customCache", "useBrowserCache", "useFSCache", "allowLocalModels", "fetch"]) { + saved[k] = env[k]; + } + cache = new MemoryCache(); + fetches = 0; + env.useCustomCache = true; + env.customCache = cache; + env.useBrowserCache = false; + env.useFSCache = false; + env.allowLocalModels = false; + env.fetch = async () => { + ++fetches; + return new Response(BYTES, { + status: 200, + headers: { "content-length": String(BYTES.length), "content-type": "application/octet-stream" }, + }); + }; + }); + + afterEach(() => { + for (const [k, v] of Object.entries(saved)) env[k] = v; + }); + + const load = (as_blob, options = {}, filename = "onnx/model.onnx_data") => getModelFile("hf-internal-testing/blob-external-data", filename, true, options, false, as_blob); + + it("streams a cold file into the cache and resolves a Blob, storing it once", async () => { + const progress = []; + const out = await load(true, { progress_callback: (e) => e.status === "progress" && progress.push(e) }); + + expect(out).toBeInstanceOf(Blob); + expect(out.size).toBe(BYTES.length); + expect(cache.puts).toBe(1); // and NOT stored a second time by the block at the end of loadResourceFile + expect(fetches).toBe(1); + + // Progress survives the streaming path — it is the reason the buffer existed, so losing it silently would + // be the regression that matters most to a user watching gigabytes arrive. + expect(progress.length).toBeGreaterThan(0); + expect(progress.at(-1).loaded).toBe(BYTES.length); + expect(progress.at(-1).total).toBe(BYTES.length); + }); + + it("reads a warm file straight out of the cache without fetching again", async () => { + await load(true); + expect(fetches).toBe(1); + + const progress = []; + const out = await load(true, { progress_callback: (e) => e.status === "progress" && progress.push(e) }); + + expect(out).toBeInstanceOf(Blob); + expect(out.size).toBe(BYTES.length); + expect(fetches).toBe(1); // served from the cache + expect(cache.puts).toBe(1); // and not rewritten + // The warm branch reports one completed event rather than a stream of them. + expect(progress.at(-1)).toMatchObject({ progress: 100, loaded: BYTES.length, total: BYTES.length }); + }); + + it("falls back to a buffer when the cache refuses the write", async () => { + cache.putError = Object.assign(new Error("quota"), { name: "QuotaExceededError" }); + + const out = await load(true); + + // A cache that cannot accept the file must not fail the load — it costs the memory saving, nothing else. + expect(out).toBeInstanceOf(Uint8Array); + expect(out.length).toBe(BYTES.length); + // Re-fetched, because the streaming attempt consumed the first body. + expect(fetches).toBe(2); + }); + + it("still resolves a Uint8Array when as_blob is not asked for", async () => { + const out = await load(false); + expect(out).toBeInstanceOf(Uint8Array); + expect(out.length).toBe(BYTES.length); + }); + + // ⛔ THE HALF OF THE MEMORY CLAIM THIS LAYER CAN ACTUALLY TEST. Whether onnxruntime-web copies the Blob at + // session creation depends on its build — JSPI does not, the default asyncify one does — and that is not + // observable from here. What IS observable, and is the saving this file is responsible for, is that the + // DOWNLOAD never materialises the body: no `arrayBuffer()` on the fetched response, on any build. + // + // Paired with the negative case on purpose. Asserting "never called" alone would pass just as happily if + // the spy were wired to the wrong object, so the same spy has to SEE the call on the buffered path. + it("streams a cold file without materialising the response, and buffers it when not asked for a Blob", async () => { + let materialised = 0; + const inner = env.fetch; + env.fetch = async (...args) => { + const response = await inner(...args); + const original = response.arrayBuffer.bind(response); + response.arrayBuffer = async () => { + ++materialised; + return original(); + }; + return response; + }; + + expect(await load(true)).toBeInstanceOf(Blob); + expect(materialised).toBe(0); + + // Same spy, same fetch, `as_blob` off: the buffered path reads the body onto the heap, which is what the + // streaming branch is avoiding — and proves the assertion above is watching the right object. + expect(await load(false, {}, "onnx/other.onnx_data")).toBeInstanceOf(Uint8Array); + expect(materialised).toBe(1); + }); + + // ⛔ THE REGRESSION THIS EXISTS FOR. `as_blob` changes the RESOLVED TYPE, so it has to be part of the + // in-flight dedup key. Without it whichever caller arrives first decides for both, and the other gets a + // `Blob` where it expects a `Uint8Array` or the reverse — silently, and only under concurrency. + it("does not hand a Blob to a concurrent caller that asked for bytes", async () => { + await load(true); // warm, so both calls below take the same branch and race deterministically + + const [asBlob, asBytes] = await Promise.all([load(true), load(false)]); + + expect(asBlob).toBeInstanceOf(Blob); + expect(asBytes).toBeInstanceOf(Uint8Array); + }); + + it("does not hand bytes to a concurrent caller that asked for a Blob", async () => { + await load(true); + + // The reverse order, because a key bug is only visible in whichever direction loses the race. + const [asBytes, asBlob] = await Promise.all([load(false), load(true)]); + + expect(asBytes).toBeInstanceOf(Uint8Array); + expect(asBlob).toBeInstanceOf(Blob); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bea34792e..cd1eca67c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: 1.24.3 version: 1.24.3 onnxruntime-web: - specifier: 1.26.0-dev.20260416-b7804b056c - version: 1.26.0-dev.20260416-b7804b056c + specifier: 1.29.0-dev.20260811-e415ef9afd + version: 1.29.0-dev.20260811-e415ef9afd sharp: specifier: ^0.34.5 version: 0.34.5 @@ -760,6 +760,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -1253,11 +1254,12 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-agent@3.0.0: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} @@ -1685,18 +1687,18 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} - onnxruntime-common@1.24.0-dev.20251116-b39e144322: - resolution: {integrity: sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==} - onnxruntime-common@1.24.3: resolution: {integrity: sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==} + onnxruntime-common@1.29.0-dev.20260723-1b1e1db7bc: + resolution: {integrity: sha512-MtH74K5iL3PZZrhVwElASZKy23yBlBwDi/J1Z+KrcwiVrOUDQ/rB3OcL944/t/40b9kHHFQQ/67FOetzQChvhQ==} + onnxruntime-node@1.24.3: resolution: {integrity: sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==} os: [win32, darwin, linux] - onnxruntime-web@1.26.0-dev.20260416-b7804b056c: - resolution: {integrity: sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==} + onnxruntime-web@1.29.0-dev.20260811-e415ef9afd: + resolution: {integrity: sha512-HYm3DQkPEGeCI83Zl1thEdSIthaH00Ew1VR+SrEuQU61KDa9d4UwmnXpFvdDBmL31LE5i/6TnNgPaGecBd9sWA==} p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} @@ -3823,22 +3825,22 @@ snapshots: dependencies: mimic-fn: 2.1.0 - onnxruntime-common@1.24.0-dev.20251116-b39e144322: {} - onnxruntime-common@1.24.3: {} + onnxruntime-common@1.29.0-dev.20260723-1b1e1db7bc: {} + onnxruntime-node@1.24.3: dependencies: adm-zip: 0.5.16 global-agent: 3.0.0 onnxruntime-common: 1.24.3 - onnxruntime-web@1.26.0-dev.20260416-b7804b056c: + onnxruntime-web@1.29.0-dev.20260811-e415ef9afd: dependencies: flatbuffers: 25.9.23 guid-typescript: 1.0.9 long: 5.3.2 - onnxruntime-common: 1.24.0-dev.20251116-b39e144322 + onnxruntime-common: 1.29.0-dev.20260723-1b1e1db7bc platform: 1.3.6 protobufjs: 7.5.4