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). Call await 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 from node_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' in script-src to 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.