feat: export supportedDevices and deviceToExecutionProviders from public API - #1753
Conversation
…lic API Resolves huggingface#1645. These helpers were only available from the internal backend module, so users could not query the list of supported devices or map a device to its execution providers without reaching into private paths. Export them from the package entry point so they become part of the public API. - backends/onnx.js: change 'const supportedDevices' to 'export const supportedDevices' - transformers.js: re-export supportedDevices and deviceToExecutionProviders
promiseeuler
left a comment
There was a problem hiding this comment.
While testing the new public export, I found that supportedDevices exposes the library's internal mutable array. A consumer can mutate it and corrupt later provider selection globally. On this branch I imported both exports, saved the first device ('coreml' here), ran supportedDevices.length = 0, and then deviceToExecutionProviders('coreml') threw: Unsupported device: "coreml". Should be one of: . Could the public API return an immutable snapshot (or freeze a separate exported copy) while keeping the internal array private, and add a regression proving consumer mutation cannot affect deviceToExecutionProviders? The focused import/reproduction is deterministic. Prettier passed for both changed files and tsc --build packages/transformers/tsconfig.json passed.
…internal array Per review feedback on huggingface#1753, the previous public export exposed the library's internal mutable array, so a consumer could mutate it (e.g. supportedDevices.length = 0) and corrupt provider selection globally (deviceToExecutionProviders would then throw for valid devices). - onnx.js: keep the internal array private (renamed to supportedDevicesInternal) and export a frozen copy (Object.freeze([...supportedDevicesInternal])) as the public supportedDevices. - exports.test.js: add regression tests proving the public export is frozen and that consumer mutation cannot affect deviceToExecutionProviders.
|
@promiseeuler Thanks for the thorough review and reproduction — you're right, the previous export leaked the library's internal mutable array. Fixed in the latest push:
I couldn't run the full \pnpm\ suite locally (no network for install here), so it'd be great if you could re-run \prettier\ and \ sc --build packages/transformers/tsconfig.json\ on your side. The change is otherwise minimal and confined to the two source files plus the new test. |
promiseeuler
left a comment
There was a problem hiding this comment.
Rechecked exact head 25f51a2. Prettier passes on all three changed files; tsc --build packages/transformers/tsconfig.json passes; and the focused exports.test.js suite passes 3/3, including both supportedDevices immutability regressions. The private internal array plus frozen public snapshot addresses the reported mutation path without changing provider selection. Thanks for the focused fix.
promiseeuler
left a comment
There was a problem hiding this comment.
Thanks for addressing the original supportedDevices export path. I tested exact head 25f51a2; the new focused test suite passes (3 tests), but the internal array is still exposed through deviceToExecutionProviders("auto").
const device = supportedDevices[0];
const auto = deviceToExecutionProviders("auto");
auto.length = 0;
deviceToExecutionProviders(device);On macOS, auto was not frozen, clearing it changed the next deviceToExecutionProviders("auto") result to [], and the final call threw Unsupported device: "coreml". Should be one of: .
Could the "auto" branch return a copy or immutable snapshot instead of supportedDevicesInternal, with a regression that mutates the value returned by deviceToExecutionProviders("auto")? The public export is now protected, but this second public path can still mutate the same internal state.
…l-array mutation
The 'auto' branch (and the default branch) returned the library's internal
mutable arrays (supportedDevicesInternal / defaultDevices). Even though the
public supportedDevices export is now frozen, a consumer could still corrupt
global provider selection by mutating the array returned by
deviceToExecutionProviders('auto') (e.g. auto.length = 0), causing later
lookups to throw 'Unsupported device'.
- Return copies ([...supportedDevicesInternal] / [...defaultDevices]) from the
'auto' and default branches. The 'gpu' branch already returns a new array via
.filter(), and device-specific branches return fresh array literals.
- Add a regression test that mutates the value returned by
deviceToExecutionProviders('auto') and asserts later calls are unaffected.
- Fix JSDoc return type to accurately reflect union of execution providers and device types - Document frozen supportedDevices export with @type ReadonlyArray<DeviceType> - Return copies from all branches in deviceToExecutionProviders to prevent internal state mutation - Add comprehensive regression tests: * default branch returns copy, mutation-safe * 'gpu' branch returns fresh array * specific device returns execution providers array * unsupported device throws descriptive error * defaultDevices and auto return copies are mutation-safe * all return paths verified to not expose internal mutable arrays
In environments that provide their own ONNX runtime via the
Symbol.for('onnxruntime') global, defaultDevices remains undefined.
The previous
eturn [...defaultDevices] change would throw a TypeError
instead of preserving the original (undefined) return value. Guard the
spread so the default branch stays non-throwing for those runtimes while
still returning a defensive copy when a list is present.
Co-Authored-By: opencode <opencode@anthropic.com>
nico-martin
left a comment
There was a problem hiding this comment.
Hi @Amarnath10i, thank you so much for looking into this! The latest version looks good. The public device list is immutable, provider results are defensive copies, and the mutation regressions are covered. No changes are requested; a small custom-runtime test would just be a non-blocking follow-up.
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
promiseeuler
left a comment
There was a problem hiding this comment.
Rechecked exact head 7dd9a53. The focused exports suite passes 9/9, Prettier passes on all three changed files, and tsc --build packages/transformers/tsconfig.json passes. The immutable public snapshot and defensive copies cover both mutation paths, while the custom-runtime guard preserves the existing undefined default. The requested changes are resolved.
|
Addressed your feedback! The array is now private and the public export is a frozen snapshot. |
|
Hi @Amarnath10i 👋 Thanks for the PR! One concern I do have with the shape of the PR in its current state is that this changes the top-level exports / API in a way that isn't consistent with the python transformers library. I think it would be better to attach it to the For example, you can access (and update) the ONNX env via // e.g., set location of .wasm files. Defaults to use a CDN.
env.backends.onnx.wasm.wasmPaths = '/path/to/files/';and since these properties/functions you mention are very specific to ONNX Runtime ("ExecutionProviders" and "devices"), it could be useful to expose this there. In fact, we have some precedence here with transformers.js/packages/transformers/src/backends/onnx.js Lines 386 to 390 in bf27627 Secondly, what use-case do you have for exposing the internal mapping for |
…nv.backends.onnx - Replace exported supportedDevices array with getSupportedDevices() function that returns a frozen snapshot (defense in depth) - Remove top-level exports from transformers.js - Both functions now accessible via env.backends.onnx, following setLogLevel precedent - Update regression tests to use new API path - Both return defensive copies to prevent internal state corruption
…ceToExecutionProviders under env.backends.onnx
Summary
Resolves #1645.
The supportedDevices list and the deviceToExecutionProviders() helper were only available from the internal ./backends/onnx.js module. Users who want to inspect supported devices or map a device to its ONNX execution providers had to reach into a private path. This PR surfaces both from the package entry point so they become part of the public API:
Both already carry JSDoc, so pnpm typegen will expose their types automatically.
Example
\\js
import { supportedDevices, deviceToExecutionProviders } from '@huggingface/transformers';
console.log(supportedDevices); // e.g. ['webgpu', 'cpu', ...]
console.log(deviceToExecutionProviders('auto')); // current device's execution providers
\\
Test plan
ode --check on the two edited files passes.