# API — every way to call it The complete call surface of `everything-webgpu`, one page. [README.md](README.md) is the pitch and the migration story; this is the catalogue. Asserted against the code by [test/api-doc.test.mjs](test/api-doc.test.mjs) — every engine method named here exists, every package export appears, and the error table equals `ERROR`, so this page cannot drift from the source without failing `npm test`. The page is in three tiers. **[Start here](#start-here)** — `load`, `ask`, `conversation`, `environment` — is the dead-simple path, and most apps need nothing else. **[Native passthrough](#native-passthrough)** is `chat.completions.create()`, the WebLLM/OpenAI compatibility layer, which **never changes** (see [Stability](README.md#stability)). **[When you need more](#when-you-need-more)** is the rest of the surface: the general calls and their scheduling fields, embeddings, residency, device inspection, configuration, lifecycle. Everything outside the passthrough is pre-1.0 and may move; the ergonomic verbs are being consolidated in [ROADMAP.md](ROADMAP.md). --- ## The four lines ```js import { CreateScheduledEngine } from "everything-webgpu"; // 1. import const engine = await CreateScheduledEngine("Llama-3.2-1B-Instruct-q4f16_1-MLC"); // 2. load a model const reply = await engine.ask("Name three primary colours."); // 3. ask console.log(reply); // 4. the answer ``` `reply` is a plain string. Line 2 downloads ~0.8 GB the first time and prints throttled progress to the console unless you pass `initProgressCallback` (render it yourself) or `initProgressCallback: null` (silence). After that first run it is a cache read and needs no network. On Vite, add one plugin — see [Bundlers](#bundlers). --- ## Start here Four calls. Get an engine with `CreateScheduledEngine` (below, under [Getting an engine](#getting-an-engine)), then `load` a model, `ask` it things or hold a `conversation`, and `environment` tells you whether the machine is up to it. ### Load a model — `engine.load(src, opts?)` **One call, four source shapes.** `engine.load(src, opts?)` works out what you handed it, registers whatever needs registering, and brings the model up. | `src` | route | | --- | --- | | `"Llama-3.2-1B-Instruct-q4f16_1-MLC"` | a prebuilt id, or anything you registered earlier. A typo is answered with near matches. | | `"https://huggingface.co/mlc-ai/Foo-MLC"` | an HF repo. `/resolve/main/` is **not** derived — WebLLM appends it. | | `"https://cdn.example/models/foo/"` + `{ modelLib }` | any base URL you host. `modelLib` is **required** and never guessed (0 of 163 prebuilt models have a derivable lib name or same-origin lib). | | `{ model, modelLib }` | the explicit remote spec. | | `input.files` \| `dropEvent.dataTransfer` \| `{ files }` | a folder off disk. **No network at any point.** | **`opts`** — `keepResident`, `signal`, `modelType`, `contextWindow`, `vramRequiredMB`, `id`, `onProgress`, `defer`. - `id` overrides the id derived from the URL's last segment. - `defer: true` registers the source **without building a pool** — the drop-now-load-later flow. It returns the registry record. `defer` on a bare prebuilt id is an error, not a silent load. - `keepResident: true` holds this model in VRAM alongside whatever is already up. The default unloads everything else first — the safe choice on a 16 GB machine. ```js await engine.load("Llama-3.2-1B-Instruct-q4f16_1-MLC"); await engine.load("https://cdn.example/models/my-model/", { modelLib: "https://cdn.example/models/my-model/lib.wasm" }); await engine.load(dropEvent.dataTransfer); await engine.load(input.files, { defer: true }); // register now, build the pool on first use ``` `load()` composes the lower-level `registerModel`, `ingestModelFolder` and the download primitives; `prefetch()` warms the cache with no GPU. All of that is under [More on loading](#more-on-loading). ### Ask one question — `engine.ask(input, opts?)` One question, its own task, **no session** — two `ask()`s never supersede each other. Returns the reply string. `opts.onDelta` to stream. Goes through the same scheduler as everything else: priority bands, one-task-one-engine, opt-in preemption. ### Hold a conversation — `engine.conversation(opts)` A multi-turn chat that keeps its own history. `engine.conversation({ system?, keep?, ...defaults })` — one stable task for every turn, turns serialised, history bounded at `keep: 12` exchanges (`Infinity` opts out). ```js const chat = engine.conversation({ system: "You are terse." }); await chat.say("capital of France?"); // → { text, finishReason } await chat.say("and its population?", onDelta); // remembers chat.messages; chat.length; chat.reset(); chat.restore(messages); ``` ### Inspect the machine — `engine.environment(opts?)` | call | answers | | --- | --- | | `await engine.environment(opts?)` | **the preflight.** A report; every line has `severity` · `affects` · `cause` · `fix` · `operable`. `{ scope: "local" }` never touches the model layer (poll freely); `{ scope: "device" }` is hardware only. | | `await engine.environment.measure()` | one calibration generation → measured tok/s for the current model | **`environment()` only reports.** Writes go through `configure()`; passing a setting to `environment()` is an error that names `configure()`. Per-model "will it run" is [`canRun(modelId)`](#inspecting-the-machine); the fuller device surface is there too. ## Native passthrough The one call that **never changes**. `@mlc-ai/web-llm` is OpenAI-shaped, and so is this: the migration off it is a one-line import swap, and `chat.completions.create()` then takes and returns exactly the same shapes — same streamed chunk objects, same finish reasons, same non-streaming envelope. See [Stability](README.md#stability). | call | shape | | --- | --- | | `engine.chat.completions.create(params)` | the **WebLLM/OpenAI** shape, unchanged. Streams the same chunks, same finish reasons. `session`/`priority`/`task`/`preemptible` are additive. | **`params`** = the OpenAI generation fields WebLLM already speaks (`messages`, `temperature`, `max_tokens`, `response_format`, `extra_body`, …) **plus** the scheduling fields that are the only thing this adds over calling WebLLM directly: | field | meaning | | --- | --- | | `modelId` | load/route to this model instead of the current one | | `id` | job id; also what `cancel(id)` takes | | `task` | the unit that owns an engine; a whole batch shares one | | `session` | a later job with this key supersedes the earlier one | | `priority` | `"interactive"` \| `"normal"` \| `"background"` | | `preemptible` | may be interrupted by an `interactive` job (set it on the work that can afford to lose) | `complete()`, `completeRaw()` and `batch()` take the same scheduling fields and expose `cancelled` / `preempted` as first-class outcomes the OpenAI shape has no room for — see [The general calls](#the-general-calls). ## When you need more ### Getting an engine | call | when | | --- | --- | | `await CreateScheduledEngine(modelId?, opts?)` | the common case. Loads `modelId` before returning, like WebLLM's `CreateMLCEngine`. Omit it for an engine that loads later. | | `new ScheduledEngine({ store, workerUrl?, loadWebLLM?, prebuilt? })` | when you must pass a store explicitly — a worker, a test, an extension. Does **not** load anything. | **`opts` for `CreateScheduledEngine`** — `store`, `initProgressCallback`, `workerUrl`, `loadWebLLM`, `prebuilt`. Anything not `store`/`initProgressCallback` is forwarded to the constructor. | constructor field | default | meaning | | --- | --- | --- | | `store` | IndexedDB (`CreateScheduledEngine` only; the constructor requires it) | a `ModelStore`, or a bare `StorageAdapter` it wraps | | `prebuilt` | `true` | expose WebLLM's 163 HuggingFace models. `false` = offline-only: `load()` resolves registered models and nothing else, and an unknown id fails before the WebLLM bundle is fetched | | `workerUrl` | `new URL("./engine-worker.js", import.meta.url)` | the decode worker's module URL | | `loadWebLLM` | `() => import("../../vendor/web-llm.js")` | override the bundle source (tests) | **Stores** — `import { indexedDBStorage } from "everything-webgpu/adapters/idb"` (pages, plus `ensurePersistent()`), `everything-webgpu/adapters/memory` (`memoryStorage()`, tests), `everything-webgpu/adapters/webext` (`webExtensionStorage()` + `attachWebExtensionTransport()`). ```js import { ScheduledEngine, ModelStore } from "everything-webgpu"; import { memoryStorage } from "everything-webgpu/adapters/memory"; const engine = new ScheduledEngine({ store: new ModelStore(memoryStorage()) }); ``` ### More on loading **Warming the cache first** — `await engine.prefetch(modelId, { onProgress, signal })`. Downloads the weights **without building an engine and without WebGPU**, so an app can warm the cache before it knows whether the machine can run the model. Interrupted downloads resume; a second call is free. WebLLM cannot express this — `reload()` needs a GPU before it fetches a shard. **Low-level, still exported** — `load()` composes these rather than replacing them: | call | does | | --- | --- | | `engine.registerModel(spec)` | add a `{ modelId, model, modelLib }` or `{ modelId, files }` record, no pool | | `ingestModelFolder(entries, { store })` | folder → populated Cache Storage, returns the record | | `filesFromInput(input.files)` / `filesFromDataTransfer(dt)` | either browser shape → flat `{ path, file }[]` (the latter is async) | | `prefetchModel({ modelId, record, ... })` / `resolveModelUrl(...)` | the engine-free download primitives | ### The general calls Every call here goes through the **same scheduler** as `ask` / `conversation` / `chat.completions`: priority bands, `session` supersession, one-task-one-engine, opt-in preemption. | call | shape | | --- | --- | | `await engine.complete(payload, onChunk?)` | `{ text, usage, finishReason, cancelled?, preempted? }`. `onChunk(delta)` streams plain text. | | `await engine.completeRaw(payload, onRawChunk?)` | same, but `onRawChunk` gets WebLLM's chunk object verbatim. | | `await engine.batch({ requests, task?, ...sched }, onItem?)` | `requests` fanned across the pool as **one task**. Returns `BatchItem[]` — each with `index`, `engineIndex`, `startedAt`, `finishedAt`, and `text`/`usage` or `error`. | **`payload`** is the OpenAI generation fields plus the scheduling fields — the same table as [Native passthrough](#native-passthrough) (`modelId`, `id`, `task`, `session`, `priority`, `preemptible`). Result flags: `cancelled: true` (superseded or `cancel()`ed), `preempted: true` (`text` is partial). ### Ghost text — `engine.ghostText(opts)` `engine.ghostText({ prompt, debounceMs?, maxTokens?, session?, ...defaults })` — debounce + one session key + `interactive` priority + **resolves `null` when stale**. `prompt` is **required**, no default: prompts are model-specific and belong to whoever owns the feature. ```js const ghost = engine.ghostText({ prompt: (before) => `Continue:\n${before}` }); const hint = await ghost.suggest(editor.textBefore()); // string | null ghost.cancel(); // on blur / accept ``` ### Embeddings Needs an **embedding model** (`snowflake-arctic-embed-*`, from 239 MB), usually held resident alongside a chat model. | call | returns | | --- | --- | | `await engine.embed(input, opts?)` | `number[][]` — one vector per input, in order | | `await engine.embedRaw(input, opts?)` | WebLLM's OpenAI envelope (`data[].embedding`) | `opts`: `modelId`, `task`, `session`, `priority`, `preemptible`, `id`. **A running embedding cannot be interrupted** — one forward pass has no decode loop to break out of; queued embeddings supersede normally. ### Residency and cache A resident model is a full copy of its weights in VRAM, and nothing reports free VRAM to a page — so residency is explicit. | call | frees | keeps | | --- | --- | --- | | `await engine.unload()` | current model's VRAM | cache + registry | | `await engine.unload(id)` | that model's VRAM | cache + registry | | `await engine.unload(id, "cache")` | VRAM + cached bytes | registry entry | | `await engine.unloadAll()` | every resident model's VRAM | cache + registry | | `await engine.remove(id)` | bytes + registry entry | nothing — for an injected model, means re-supplying the folder | | `engine.evict(id)` | low-level primitive `unload(id, "cache")` is built on | registry entry | **Routing without loading** — `engine.use(id)` points unaddressed requests at an already-resident model (free and instant; `load()` is what costs). `engine.resident` lists model ids with a live pool. `await engine.cacheState(id)` says what is on disk (`"complete"` / `"partial"` / absent). ### Inspecting the machine The device surface behind the [Start here](#start-here) preflight. | call | answers | | --- | --- | | `await engine.canRun(modelId)` | per-**model**: `{ ok, blockers, warnings }`, before anything downloads | | `await engine.recommendModels(opts?)` | which models this device should be asked to run, best first. `opts`: `maxVramMB`, `needsVision`, `needsToolCalling`, `prefer` | | `await engine.estimateSpeed(modelId?)` | projected decode tok/s (uses the measured rate once one generation has happened) | | `await engine.probe()` | raw device probe: WebGPU, adapter, `shader-f16`, the five limits, storage quota. Cached. | | `await engine.features()` | what is switched **on** now, vs what the device could support. `multiStepOff` is non-null when decode fell back to one GPU sync per token — the silent halving `environment()` reports as `degraded` | | `engine.hasWebGPU` | `Boolean(navigator.gpu)` | | `await engine.listAvailableModels()` | registered + prebuilt, normalised. Costs one bundle fetch. | | `engine.listModels()` | registered only — cheap, no bundle load | ### Configuration `await engine.configure(patch)` — applies a runtime knob and persists it as the default. | knob | effect | | --- | --- | | `decodeSteps` | forward steps per GPU sync. Hot, no reload. `1`–`32` (`DEFAULT_DECODE_STEPS` = 15). | | `engineCount` | pool size. Persisted; live pools keep the size they came up with. | | `temperature`, `maxTokens`, `systemPrompt` | generation defaults (`DEFAULT_SETTINGS`) | Not operable from JS, report-only via `environment()`: KV reuse (derived from the 9-storage-buffer cap), compute-pass batching (build-time `NO_PASS_MERGE`), `shader-f16`, GPU, `about:config` flags. ### Lifecycle and cancellation | call | | | --- | --- | | `const stop = engine.subscribe(listener)` | `listener(state)` fires immediately, then on every change. Returns unsubscribe. | | `engine.state` | snapshot: `status`, `modelId`, `progress`, `error`, `pool {size,busy,queued,maxSize,growthBlocked}`, `resident`, `decode` | | `engine.store` | the `ModelStore`, so a host can drive the registry without a second handle | | `engine.cancel(idOrSession)` | cancel by job id or by session key | | `engine.load(id, { signal })` | an `AbortController` signal tears down an in-flight download | `state.status` is one of `ENGINE_STATE`: `"idle"` · `"loading"` · `"ready"` · `"error"`. ## Errors Every failure is an `EngineError` with a `.code`, a human-readable `.message` (the thing you print), and structured `.detail`. `import { isEngineError, ERROR } from "everything-webgpu"`. | code | what to do | | --- | --- | | `NO_WEBGPU` | tell the user to check flags/hardware; retrying is futile | | `NO_MODEL` | nothing registered — send them to your setup flow | | `UNKNOWN_MODEL` | that id is not resolvable; `listAvailableModels()` says what is | | `CACHE_INCOMPLETE` | a locally-registered model was evicted; re-register the folder | | `INVALID_MODEL_FOLDER` | not a compiled MLC model; `detail` says what is missing | | `BAD_REQUEST` | the caller's arguments are wrong — a bug in the caller | | `ABORTED` | the caller cancelled it. Not a failure; do not report it as one | | `GENERATION_FAILED` | the model failed mid-generation | | `PACKAGE_INCOMPLETE` | your **build** is wrong, not your code — missing `vendor/` bundle, or a decode worker the bundler did not emit. `message` names the fix; `detail.cause` says which | ```js try { await engine.load(id); } catch (err) { if (isEngineError(err, ERROR.CACHE_INCOMPLETE)) return reRegisterFolder(); throw err; } ``` ## Bundlers The engine spawns its decode worker with `new Worker(new URL("./engine-worker.js", import.meta.url), { type: "module" })`. On **Vite**, its dependency pre-bundler rewrites that URL to a path that 404s — in `vite dev`, on a real (non-linked) install only. Add the plugin: ```js import { everythingWebGPU } from "everything-webgpu/vite"; export default defineConfig({ plugins: [everythingWebGPU()] }); ``` Equivalent by hand: `optimizeDeps: { exclude: ["everything-webgpu"] }`. Skip both and `load()` throws `PACKAGE_INCOMPLETE` naming the fix rather than hanging. `vite build` is unaffected either way. Other bundlers that honour `new URL(..., import.meta.url)` for workers (Webpack 5, Rollup, Parcel 2) need nothing. ## Every export `import { … } from "everything-webgpu"` — 43 names. **Engine & entry** — `ScheduledEngine`, `CreateScheduledEngine`, `EnginePool` **Model sources** — `ModelStore`, `ingestModelFolder`, `filesFromInput`, `filesFromDataTransfer`, `prefetchModel`, `resolveModelUrl`, `isInjected`, `baseUrlFor`, `groupKeysByScope`, `toAppConfig` **Recipes** (also methods on the engine) — `ask`, `conversation`, `ghostText` **Device** — `probeDevice`, `canRun`, `projectSpeed`, `rankModels`, `REFERENCE_DECODE_BYTES_PER_SECOND` **Multi-step decoding** — `installMultiStepDecoding`, `burstSize`, `clampSteps`, `DEFAULT_DECODE_STEPS`, `MAX_DECODE_STEPS` **Errors** — `EngineError`, `ERROR`, `isEngineError`, `asEngineError` **Formatting** — `formatBytes` **Enums / constants** — `PRIORITY`, `PRIORITY_ORDER`, `ENGINE_STATE`, `UNLOAD_LEVEL`, `SEVERITY`, `MODEL_TYPE`, `SOURCE`, `DEFAULT_SETTINGS`, `WORKER_CONFIGURE`, `CACHE_CONFIG`, `CACHE_MODEL`, `CACHE_WASM` ### Enum values | enum | values | | --- | --- | | `PRIORITY` | `interactive` · `normal` · `background` | | `ENGINE_STATE` | `idle` · `loading` · `ready` · `error` | | `UNLOAD_LEVEL` | `vram` · `cache` | | `SEVERITY` | `blocked` · `degraded` · `tune` · `info` · `ok` | | `SOURCE` | `prebuilt` · `remote` · `injected` | | `MODEL_TYPE` | `llm` = 0 · `embedding` = 1 · `vlm` = 2 | | `CACHE_*` | `webllm/config` · `webllm/model` · `webllm/wasm` | | `DEFAULT_SETTINGS` | `engineCount: 2`, `decodeSteps: 15`, `temperature: 0.6`, `maxTokens: 1024`, `systemPrompt: ""` | ## Subpath exports | specifier | | | --- | --- | | `everything-webgpu` | everything above | | `everything-webgpu/vite` | `everythingWebGPU()` Vite plugin | | `everything-webgpu/worker` | the decode worker entry (for a custom `workerUrl`) | | `everything-webgpu/adapters/idb` | `indexedDBStorage()` | | `everything-webgpu/adapters/memory` | `memoryStorage()` | | `everything-webgpu/adapters/webext` | `webExtensionStorage()`, `attachWebExtensionTransport()` | | `everything-webgpu/adapters/protocol` | the wire-protocol constants |