npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aparte/provider-transformers

v0.16.11

Published

Run LLMs 100% in the browser via Transformers.js (WebGPU/WASM) — a local, keyless AI provider for aparté. Streams tokens off the main thread in a Web Worker.

Readme

@aparte/provider-transformers

Run LLMs 100% in the browser via Transformers.js (WebGPU, with a WASM fallback) — no API, no key, no server. Inference runs off the main thread in a Web Worker, streaming tokens into aparté.

npm install @aparte/provider-transformers @huggingface/transformers

@huggingface/transformers is a peer dependency — you bring the version you want (it's heavy and ships its own onnxruntime). @aparte/core is a peer dependency.

import { aparteGlobalConfig, AparteDirectTransport } from '@aparte/core';
import { TransformersProvider, registerModel } from '@aparte/provider-transformers';

registerModel({
  id: 'onnx-community/Qwen2.5-0.5B-Instruct',
  name: 'Qwen2.5 0.5B',
  task: 'text-generation',
  capabilities: ['streaming'],
  dtype: 'q4',
});
aparteGlobalConfig.registerAIProvider(TransformersProvider);
aparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));

The provider owns its I/O (it runs inference locally), so AparteDirectTransport just delegates to it. Model weights download once and persist in the Cache API; prepareModel reports progress, and listCachedModels / deleteCachedModel manage the on-disk cache.

Runners — what loads the model

The worker runs a runner: the piece that loads a model and drives it. Two ship with the package, picked with task; a third option is a module of your own.

| task | loads | for | |---|---|---| | 'text-generation' (default) | pipeline('text-generation') | any chat model — Qwen, SmolLM, Llama, Phi… | | 'image-text-to-text' | AutoProcessor + AutoModelForImageTextToText | vision models — whatever AutoModelForImageTextToText resolves (SmolVLM, Qwen2-VL, LFM2-VL, Gemma 3…); measured on SmolVLM |

A vision model reads the image parts of a message ({ type: 'image', image: dataUrl }) exactly as the composer attaches them:

registerModel({
  id: 'HuggingFaceTB/SmolVLM-256M-Instruct',
  name: 'SmolVLM 256M',
  task: 'image-text-to-text',
  capabilities: ['streaming', 'vision'],
  // Three ONNX parts, three dtypes — the shape the model card recommends for WebGPU.
  dtype: { embed_tokens: 'fp16', vision_encoder: 'q4', decoder_model_merged: 'q4' },
});

Measured: SmolVLM-256M on WebGPU (Chromium, AMD Radeon 8060S): first load 7 s (download included), first token 3.7 s cold; Stop interrupts the model, not just the read.

Each runner is its own chunk, loaded only when a model asks for it. Both drop tool_call / tool_result turns with one warning (tool syntax is per model family), and the text runner says so when it drops an image — it never answers a photo it could not see as if it had.

A runner of your own

Point runner at an ES module that exports createRunner; it wins over task. The worker imports it and hands it the same Transformers.js instance the built-ins use, so a research-grade model — a custom vision tower, an adapter you swap at runtime — fits without giving up the provider's worker, queue, progress, cancel and cache:

registerModel({ id: 'my-org/my-model', name: 'Mine', capabilities: ['streaming'], runner: './runners/mine.js' });
// runners/mine.js — served by your app; the worker imports it by URL
export async function createRunner(ctx) {
  // ctx.transformers is the Transformers.js the page installed or mapped — never import your own
  const { AutoTokenizer, AutoModelForCausalLM, TextStreamer } = ctx.transformers;
  const tokenizer = await AutoTokenizer.from_pretrained(ctx.modelId);
  const model = await AutoModelForCausalLM.from_pretrained(ctx.modelId, { dtype: ctx.dtype, device: ctx.device });
  return {
    async generate({ messages, options, emit }) {
      // messages arrive WITH their content parts — render them the way this model wants;
      // this one reads text, so the parts are flattened and tool turns left out
      const turns = messages
        .filter((m) => m.role === 'user' || m.role === 'assistant' || m.role === 'system')
        .map((m) => ({ role: m.role, content: typeof m.content === 'string' ? m.content : m.content.filter((p) => p.type === 'text').map((p) => p.text).join('') }));
      const inputs = tokenizer.apply_chat_template(turns, { add_generation_prompt: true, return_dict: true });
      const streamer = new TextStreamer(tokenizer, {
        skip_prompt: true, skip_special_tokens: true,
        callback_function: (text) => emit({ type: 'text', delta: text }),
      });
      await model.generate({ ...inputs, max_new_tokens: options.maxTokens ?? 512, streamer });
      // `done` is optional — the provider closes the stream when generate() resolves
    },
    // anything that is not a generation: the provider passes it through, untouched
    async command(name, payload) { if (name === 'adapter') { /* swap a LoRA, warm a cache… */ } },
    dispose() { model.dispose(); },
  };
}

emit speaks aparté's stream vocabulary (text, thinking, tool_use, done, error), signal fires on Stop, ctx.progress (a RunnerProgress) / ctx.warn reach the page. The contract is exported: TransformersRunner, RunnerContext, RunnerGenerateInput, CreateRunner, RunnerModule (what the module exports), BuiltInRunner (the two task names) and TransformersModule (the type of ctx.transformers). runnerCommand(modelId, name, payload) reaches a runner's command() from the page, queued behind the generates in flight.

One pipeline per tab

Unlike every other aparté provider, this one's state is tab-scoped, not chat-scoped: one worker, one loaded model, one generate at a time. setComputeDevice, setMaxCachedModels and setHardwareTierModels set it for the whole page.

That is the resource talking, not a design preference — a local model is 1–2 GB of weights and one WebGPU pipeline, so a worker per chat would mean N copies resident in one tab. Two chats on the same model is the case this is for: they share the load, for free.

Two chats on different models is the case that costs. They serialize on the one pipeline, and at the default budget of one cached model each turn can evict and reload gigabytes. The provider warns once when it sees it:

import { setMaxCachedModels, getMaxCachedModels } from '@aparte/provider-transformers';

setMaxCachedModels(2);            // keep both resident, if the machine has the memory
setMaxCachedModels(0);            // no limit — you are managing memory yourself
getMaxCachedModels();             // the current budget

Scope: text and vision models through the built-in runners, anything else through a runner of your own; browser-only (unlike the other providers — it needs WebGPU/WASM, Workers and the Cache API, so it is the one adapter that does not run in Node). Tool-calling for local models is model-specific: the built-in runners drop tool_call / tool_result turns with one console warning; a custom runner may render them. Part of the aparté monorepo. ESM-only. See the Providers guide in the docs for the full usage.