In the browser (WebAssembly)
The TypeScript package also runs in the browser: the same Rust engine, compiled to WebAssembly, behind the same API. Agents, tools, approvals, typed output, threads, workflows and the gateway work as on a server; the agent loop runs in the visitor's tab (or a Web Worker), so a run costs your servers nothing. The playground runs its code samples this way.
Start#
// npm install criogenai, then in the browser (or a Web Worker):
// import { Agent, openai, ready, tool } from "criogenai/web";
await ready(); // loads the engine (WebAssembly) once; call it before anything else
const weather = tool({
name: "weather",
description: "Today's forecast for a city.",
input: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
run: ({ city }: { city: string }) => `${city}: 24 °C, sunny`,
});
// A key typed by the page's own user: it goes from their tab to the provider only.
const agent = new Agent({ model: openai("gpt-5-mini", { apiKey }), tools: [weather] });
const result = await agent.run("Picnic weather in Lisbon?");
console.log(result.text);- Import from
criogenai/web(an ES module; bundlers pick it up like any other). Callawait ready()once first: it fetches and compiles the engine, about 550 KB compressed, cached by the browser after. - The engine file (
criogenai_bg.wasm) is loaded from next to the script. Vite and webpack copy it for you; with a bundler that does not, copy it fromnode_modules/criogenai/dist-web/or say where it is:
// The engine file sits next to the script by default; elsewhere, say where:
await ready({ wasm: new URL("/assets/criogenai_bg.wasm", location.href) });- A page with a strict Content-Security-Policy needs
'wasm-unsafe-eval'inscript-srcto compile WebAssembly.
Keys and providers#
A browser has no environment variables: pass apiKey. A key in a page is
visible to whoever uses the page. That is fine for a person's own key in
their own tab (the playground does this: the key goes from the tab to the
provider and nowhere else). In a product, do not ship your key: point the
model at your own server, or at a CriogenAI gateway
with a virtual key per user and its limits.
The provider must accept calls from a browser page (CORS). OpenAI, Anthropic
(CriogenAI adds the header Anthropic asks browser callers for), Gemini's
OpenAI-compatible endpoint and OpenRouter do; a local Ollama needs
OLLAMA_ORIGINS. When a call is refused, the error says it may be CORS. The
usual rules still hold: redirects are never followed, a key never goes over
plain http to another machine, a response over 64 MiB is refused, and a
cancelled run stops its download.
Models written in JavaScript#
FunctionModel is a model whose every call goes to a function: a model
running in the page (WebGPU, a browser's built-in model), your own client, or
scripted replies chosen by what the request says. It works on servers too.
// A model written in JavaScript: a model running in the page, your own
// client, or scripted replies for tests and demos.
const model = new FunctionModel(async (call) => {
const result = call.messages.find((m) => m.role === "tool");
return result ? "It is sunny: go." : FakeReply.toolCall("weather", { city: "Lisbon" });
});The function gets the request (messages, tools, settings) and answers with
text, tool calls, a full message, or { error: { kind, message } }: a
rate_limited or overloaded error is retried like a provider's. A throw
fails the call with model_failed. To watch or change every request and
still use a real model, pass it on through the gateway:
// Pass every request on to a real model, tools and all, and watch it.
const gateway = new Gateway();
const upstream = openai("gpt-5-mini", { apiKey: upstreamKey });
const watched = new FunctionModel(async (call) => {
console.log("the model is asked", call.messages.length, "messages");
const reply = await gateway.chat(upstream, call.messages, { tools: call.tools });
return { message: reply.message };
});What needs a server#
These fail with the error code unsupported in the browser:
- Databases. Storage is in the tab's memory: a thread lasts as long as the page. SQLite and Postgres, encryption at rest and durable audit trails need a server.
- Telemetry export (Langfuse, OpenTelemetry) and the web tools (search, reading pages).
Good to know#
- One thread. Tasks are concurrent, not parallel: model and tool calls overlap (they mostly wait), heavy computation does not spread over cores.
- Background tabs. Browsers slow timers in background tabs, and phones may pause them: short, interactive runs fit best. Long or durable runs belong on a server.
- Speed. CriogenAI's own work is a fraction of a millisecond per run on a server, and a model call takes seconds; WebAssembly is somewhat slower than native, which matters little next to the model. What a visitor can feel is the network: each model call travels from their connection.
- Tested in Chromium, Firefox and WebKit (Safari's engine), and with the same test suite as the Node package.