@aparte/provider-transformers

中文文档
0.32.1发布于 2026/11/02MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 5475

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.

原始使用文档
文档目录3 节
  1. Runners — what loads the model
  2. A runner of your own
  3. One pipeline per tab

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

taskloadsfor
'text-generation' (default)pipeline('text-generation')any chat model — Qwen, SmolLM, Llama, Phi…
'image-text-to-text'AutoProcessor + AutoModelForImageTextToTextvision 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 the tool CALLS and their results — the calls on an assistant turn and the tool turns that answer them — with one warning (tool syntax is per model family); what the assistant said before calling stays in the prompt. The text runner says so when it drops an image: it never answers a photo it could not see as if it had, and the vision runner counts a content part it cannot carry the same way.

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, InterruptableStoppingCriteria } = 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, signal }) {
      if (signal.aborted) return;
      // 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 }),
      });
      // Stop: without this the model generates on to its ceiling, and the next reply waits behind it
      const stopping = new InterruptableStoppingCriteria();
      signal.addEventListener('abort', () => stopping.interrupt(), { once: true });
      await model.generate({ ...inputs, max_new_tokens: options.maxTokens ?? 8192, streamer, stopping_criteria: stopping });
      // `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(); },
  };
}

input.tools are the tools the request allows (a RunnerTool: name, description, inputSchema) and input.toolChoice its 'auto' / 'none' / { name } — absent when it has none. 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, RunnerTool, 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, so each turn unloads one model and loads the other. The cache budget adds to that: it is how many models stay downloaded in the browser's Cache API (disk, not memory: one model is loaded at a time whatever the budget), and at the default of one, a switch can delete the other model's weights and download them again. The provider warns once when it sees it:

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

setMaxCachedModels(2);            // keep both downloaded, if the browser's storage has room
setMaxCachedModels(0);            // no limit — the browser's storage quota is the only bound
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 the tool calls and their results — the calls on an assistant turn and the tool turns that answer them, not what the assistant said before them — 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.

版本与发布记录

@aparte/provider-transformers 共发布 65 个版本,首次发布于 2026/07/24;过去 12 个月发布了 65 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.32.1latest2026/11/025 天前
0.32.02026/11/025 天前
0.31.02026/11/026 天前
0.30.12026/11/016 天前
0.30.02026/11/016 天前
0.29.42026/11/016 天前

其他发布通道:alpha → 0.17.0。

关于 @aparte/provider-transformers 的常见问题

@aparte/provider-transformers 是什么?

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.

如何安装 @aparte/provider-transformers?

在项目目录运行 npm install @aparte/provider-transformers,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@aparte/core(>=0.32.1 <1.0.0)、@huggingface/transformers(^4.2.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@aparte/provider-transformers 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@aparte/provider-transformers 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@aparte/provider-transformers 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@aparte/provider-transformers 可以商用吗?

@aparte/provider-transformers 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@aparte/provider-transformers 的下载量有多少?

根据 npm 官方下载统计,@aparte/provider-transformers 在 2026/10/06 – 2026/11/04 这 30 天内被下载 5,475 次,日均约 183 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@aparte/provider-transformers 还在维护吗?

最新版本 0.32.1 发布于 2026/11/02(5 天前);至今共发布 65 个版本,其中过去 12 个月发布了 65 个;首次发布于 2026/07/24。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。