Skip to content

feat: export supportedDevices and deviceToExecutionProviders from public API - #1753

Open
Amarnath10i wants to merge 7 commits into
huggingface:mainfrom
Amarnath10i:feat/export-supported-devices-and-device-to-ep
Open

feat: export supportedDevices and deviceToExecutionProviders from public API#1753
Amarnath10i wants to merge 7 commits into
huggingface:mainfrom
Amarnath10i:feat/export-supported-devices-and-device-to-ep

Conversation

@Amarnath10i

Copy link
Copy Markdown

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:

  • �ackends/onnx.js: change const supportedDevices = [] to export const supportedDevices = [].
  • ransformers.js: re-export supportedDevices and deviceToExecutionProviders from ./backends/onnx.js.

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.

  • Types are regenerated via pnpm typegen in CI.

…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 promiseeuler left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@Amarnath10i

Copy link
Copy Markdown
Author

@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:

  • The internal array is now private (renamed to \supportedDevicesInternal) and is no longer exported.
  • The public \supportedDevices\ is now a frozen snapshot: \Object.freeze([...supportedDevicesInternal]). Consumers can no longer mutate it, and even if they try (\length = 0, \push, index assignment) the internal state — and therefore \deviceToExecutionProviders\ — is unaffected.
  • Added regression tests in \exports.test.js\ that assert the export is frozen and that mutating it does not change \deviceToExecutionProviders('auto')\ or a specific device lookup.

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 promiseeuler left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 promiseeuler left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Amarnath10i and others added 3 commits August 23, 2026 23:45
…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 nico-martin self-assigned this Aug 24, 2026

@nico-martin nico-martin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

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 promiseeuler left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Amarnath10i

Copy link
Copy Markdown
Author

Addressed your feedback! The array is now private and the public export is a frozen snapshot.

@xenova

xenova commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

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 backends property in the env variable we export.

For example, you can access (and update) the ONNX env via env.backends.onnx

// 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 setLogLevel, and we could maybe do something like getSupportedDevices()

// Expose ONNX environment variables to `env.backends.onnx`
env.backends.onnx = {
...ONNX_ENV,
setLogLevel,
};

Secondly, what use-case do you have for exposing the internal mapping for deviceToExecutionProviders? I think that is somewhat internal (transformers.js uses a "device" abstraction, and we choose the "execution provider" based on this).

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Export supportedDevices / deviceToExecutionProviders from public API

5 participants