Skip to content

Prompt API

This package wraps the built-in Prompt API (LanguageModel). Use ask() for one-shot prompts. Use createSession() for multi-turn conversations.

The package README covers installation and the API reference. This page explains the vanilla API. For React, see usePrompt and useSession.

Before shipping, review the Production checklist for intent-driven preparation, session isolation, input selection, safe rendering, progress, reversible edits, and cache freshness.

Use prepareLanguageModel to warm a session on user intent and release it when the feature closes. See the Session lifecycle guide.

import { ask } from "@web-ai-sdk/prompt";
const outputElement = document.querySelector<HTMLElement>("#prompt-output");
const result = await ask({
input: "Summarize this in one sentence: WebMCP lets pages expose tools to agents.",
systemPrompt: "You are concise. Reply with a single sentence.",
samplingMode: "predictable",
onUpdate: (text) => {
if (outputElement) outputElement.textContent = text;
},
});
console.log(result.output, result.cached);

Pass onUpdate to render partial text. result.output contains the final cleaned text. result.cached reports whether the result came from cache.

onUpdate receives the cumulative buffer, not deltas. For delta-shaped streaming use createSession().sendStreaming().

The wrapper removes selected non-printing control characters. It does not sanitize HTML or Markdown. Treat all model output as untrusted.

Use React interpolation or DOM textContent for plain text. If you render HTML, disable raw HTML and sanitize the complete buffer before insertion. Do not sanitize deltas separately; unsafe input can span updates.

interface AskOptions {
input: string;
systemPrompt?: string;
samplingMode?: "most-predictable" | "predictable" | "balanced" | "creative" | "most-creative";
temperature?: number;
topK?: number;
language?: string;
supportedLanguages?: readonly string[];
expectedInputs?: LanguageModelExpectedInput[];
expectedOutputs?: LanguageModelExpectedOutput[];
tools?: LanguageModelTool[];
monitor?: (m: CreateMonitor) => void;
responseConstraint?: object;
cache?: "session" | "local" | { get, set };
cacheKey?: string;
cacheTtl?: number; // built-in shortcut TTL in ms; default 1 hour
cacheRefresh?: boolean; // skip the cache read, write the fresh result
onUpdate?: (text: string) => void;
signal?: AbortSignal;
}
Name Type Description
input required string The user-facing prompt / question. Empty / whitespace resolves to { output: null }.
systemPrompt string Optional system prompt (folded into initialPrompts as a system role).
samplingMode "most-predictable" | "predictable" | "balanced" | "creative" | "most-creative" Semantic sampling preset. Mutually exclusive with temperature / topK.
temperature number Legacy raw sampling temperature. Web page contexts are moving to samplingMode.
topK number Legacy raw sampling top-k. Web page contexts are moving to samplingMode.
language string BCP-47 language hint. Folded into expectedInputs / expectedOutputs when supported.
supportedLanguages readonly string[] Languages the model supports for the language hint. Defaults to ["en"].
expectedInputs LanguageModelExpectedInput[] Advanced: full passthrough. Overrides the language hint.
expectedOutputs LanguageModelExpectedOutput[] Advanced: full passthrough. Overrides the language hint.
tools LanguageModelTool[] Experimental. Native function-calling passthrough (see Native tool calling).
monitor (m) => void Observe the first-call model download.
responseConstraint object JSON Schema for structured output.
cache "session" | "local" | { get, set } Opt-in result cache.
cacheKey string Override the default cache key (JSON string of the prompt and output-shaping options).
cacheTtl number TTL in milliseconds for entries written by the built-in "session" / "local" shortcuts. Defaults to one hour. Ignored by custom { get, set } caches.
cacheRefresh boolean Skip the cache read and replace the cached value after a successful run. The refreshed run reports cached: false.
onUpdate (text: string) => void Streaming update callback. Cumulative buffer, not deltas.
signal AbortSignal Abort signal.
interface AskResult {
output: string | null;
cached: boolean;
}

ask() isolates each call. It can reuse a base model, but each prompt runs on a clone or fresh instance. Use createSession() when a conversation needs multi-turn history:

import { createSession } from "@web-ai-sdk/prompt";
const session = createSession({
systemPrompt: "You are a helpful assistant.",
samplingMode: "balanced",
});
// Streaming yields DELTA chunks (not cumulative buffers):
for await (const delta of session.sendStreaming("Tell me about WebMCP.")) {
process.stdout.write(delta);
}
// Or one-shot per turn:
const text = await session.send("And what about the Prompt API?");
// Tear down explicitly when the conversation ends.
session.destroy();

The wrapper normalizes stream chunks, removes selected control characters, handles aborts, and reports typed unavailability. It forwards other behavior to the native instance.

It does not store UI history or queue sends. Your application owns both. Use clone() to start an isolated task from a prepared base session.

createSession() does not use the ask() session cache. Each call creates an independent instance with its own history and lifecycle.

Calls on the same session are not queued. Overlapping calls fail with InvalidStateError. Await the current send or call session.abort() first.

createSession() starts native creation immediately. Create the base when the workflow is known and the initial instructions are final. The first send or clone waits for creation.

Concurrency note. Each session has independent history, system prompts, sampling, and lifecycle. Scheduling across different native sessions is browser-defined, so do not depend on token-level interleaving or a particular parallelism policy.

Session resilience: base + per-task clone()

Section titled “Session resilience: base + per-task clone()”

Do not reuse one session across unrelated tasks. Its history will continue to grow. Creating a new session for every task repeats setup work.

Chrome’s session-management guidance recommends one prepared base session with only the system prompt. Clone it for each task. Each clone has independent history and lifecycle.

// As soon as the workflow is chosen, begin native creation with final instructions.
const base = createSession({ systemPrompt }); // eager prewarm; keep this base
const outputElement = document.querySelector<HTMLElement>("#output");
// per task / run:
const turn = await base.clone(); // awaits base readiness, then fresh history
let response = "";
try {
for await (const delta of turn.sendStreaming(input)) {
response += delta;
if (outputElement) outputElement.textContent = response;
}
} finally {
turn.destroy(); // free the clone, keep the base warm
}

clone() throws SessionDestroyedError if the base has been destroyed, and PromptUnavailableError if the underlying browser instance doesn’t support cloning. The clone is fully independent: destroying it never affects the base, and vice versa. The React useSession hook returns the Session directly, so session.clone() is available there too.

Injecting context without a turn — Session.append()

Section titled “Injecting context without a turn — Session.append()”

Agent loops often need to push tool results or other context into history without triggering a model turn. Faking this with an extra send() wastes tokens and latency on an empty intermediate response. session.append() forwards to the native LanguageModel.append(): the messages land in history, and the next send / sendStreaming sees them as prior turns.

const session = createSession({ systemPrompt });
await session.send("What's the weather in Tokyo?");
// The model asked to call a tool; run it yourself, then inject the result:
await session.append([
{ role: "assistant", content: "I'll check the weather." },
{ role: "user", content: "tool result: 24°C, clear" },
]);
// The next turn sees the tool result as history — no wasted intermediate turn.
const plan = await session.send("Based on that, suggest an outfit.");

append() throws SessionDestroyedError if the session is destroyed and PromptUnavailableError if the browser instance doesn’t support append(). Aborts reject with PromptAbortError.

Session.send / sendStreaming accept either a single string turn or a full LanguageModelMessage[]. Passing an array lets you supply multi-message context, control roles per turn, and, most usefully, prefill the assistant’s reply: set prefix: true on the trailing assistant message and the model treats its content as the start of its own answer rather than a turn to respond to.

const session = createSession({ systemPrompt });
// Multi-message turn: full conversation context, roles per message.
const reply = await session.send([
{ role: "user", content: "What is RAG?" },
{ role: "assistant", content: "Retrieval-Augmented Generation." },
{ role: "user", content: "Give me the three-step recipe." },
]);
// Prefill: bias the model toward JSON without a full schema.
const json = await session.send([
{ role: "user", content: "Describe a cat in one word of JSON." },
{ role: "assistant", content: '{"thought":"', prefix: true },
]);
// The model completes the prefix with `feline"`; parse the JSON as {"thought":"feline"}.

Prefill vs responseConstraint: both shape output, different trade-offs:

  • Prefill (prefix: true): cheaper per turn (no schema inlined into context), weaker guarantee; the model may drift off the prefixed format. Good for cheap nudges and structured-output hints that you parse defensively.
  • responseConstraint: enforced JSON Schema (the runtime validates against it), higher per-turn token cost when the schema is large. Use omitResponseConstraintInput: true to drop the inlined schema and keep only the enforced constraint.

They compose: prefill the opening brace, set responseConstraint for the full shape.

Spec rule: prefix: true is valid only on the final assistant message. Other uses cause a "SyntaxError" DOMException. The SDK passes this error to the caller.

LanguageModelMessage.content also accepts an array of multimodal content parts. See Multimodal input.

Chrome 148+ documents multimodal sessions with text, image, and audio expected inputs. LanguageModelMessage.content accepts a plain string or an ordered array of content parts:

type LanguageModelMessageContent =
| { type: "text"; value: string }
| { type: "image"; value: ImageBitmapSource | BufferSource }
| { type: "audio"; value: AudioBuffer | Blob | BufferSource };

Image parts accept browser-native image values: Blob, ImageData, ImageBitmap, VideoFrame, OffscreenCanvas, canvas / image / video elements, and BufferSource. Audio parts accept AudioBuffer, Blob, and BufferSource.

Declare non-text modalities at creation with expectedInputs. Probe support first with checkAvailability() and pass the same expectedInputs and expectedOutputs you will use for creation:

import { checkAvailability, createSession } from "@web-ai-sdk/prompt";
const expectedInputs = [
{ type: "text" as const },
{ type: "image" as const },
{ type: "audio" as const },
];
const availability = await checkAvailability({ expectedInputs });
if (availability === null || availability === "unavailable") {
// The browser cannot serve these modalities; fall back.
}
const session = createSession({ expectedInputs });
const description = await session.send([
{
role: "user",
content: [
{ type: "text", value: "Describe this image." },
{ type: "image", value: imageBlob },
],
},
]);

Content parts work everywhere messages flow: initialPrompts (through createOptions.initialPrompts), send(), sendStreaming(), and append(). The SDK forwards media values losslessly to the browser. It never serializes, clones, transcodes, inspects, or reorders them.

A media-only message is never treated as empty. Empty strings, empty message arrays, and messages with only blank text parts still resolve to null without a model call.

Chrome requires a GPU for audio input. The browser throws a "NotSupportedError" DOMException for undeclared or unsupported modalities. The SDK passes that error through unchanged; it does not convert it into PromptUnavailableError.

On browsers without multimodal support, checkAvailability({ expectedInputs }) reports "unavailable" (or null without the API). Session creation then fails, and the first send() surfaces the error.

ask() stays text-only: AskOptions.input is a string, and its result cache keys assume text. Use createSession() for multimodal prompts; it owns an explicit session lifecycle and bypasses the one-shot result cache. In React, useSession returns the same multimodal-capable Session. Try the live demo on the useSession page.

Session exposes the token budget reported by the native instance. Use it to size input for the current context window:

  • session.contextWindow — max input tokens for the session (the context window).
  • session.contextUsage — input tokens used so far. On a fresh base-clone this reflects the inherited history (≈ the system prompt), the right baseline to budget a turn against.

These values mirror the Prompt API’s contextWindow and contextUsage. The wrapper also reads the deprecated names for older Chrome builds.

Both getters are undefined until native creation finishes. A resolved clone is ready immediately. React’s useSession stays "loading" until creation finishes.

const base = createSession({ systemPrompt }); // native creation starts here
const turn = await base.clone(); // awaits readiness; clone is live here
const quota = turn.contextWindow; // e.g. 4096 / 6144 tokens
const used = turn.contextUsage ?? 0; // ≈ system prompt
if (quota) {
const available = quota - used - ANSWER_RESERVE_TOKENS;
const budgetChars = Math.max(0, available) * 4; // ~4 chars/token
// truncate fetched content to budgetChars so it fits in one turn
}
// Fall back to a fixed char cap when contextWindow is undefined
// (older browsers / pre-creation).

session.onContextOverflow(listener) subscribes to the native contextoverflow event, which fires when a turn pushes usage past the window and the oldest history is dropped. Use it to compact or fork a fresh clone() before hitting QuotaExceededError. It returns an idempotent cleanup function, and is a no-op (returns a no-op cleanup) when the underlying instance doesn’t expose the event.

const stop = session.onContextOverflow(() => {
// compact, summarize, or start a fresh clone before QuotaExceededError
});
// later
stop();

Chrome’s LanguageModel exposes LanguageModel.create({...}) to spin up a session and session.prompt(input) / session.promptStreaming(input) to run it. The wrapper does the following on top:

  • Feature detection. isAvailable() / checkAvailability() return false / null on browsers without the API. The vanilla ask() throws PromptUnavailableError; the React hook surfaces status: "unavailable".
  • Warm base reuse for ask() (ergonomic default, scoped to ask). A bounded LRU caches base LanguageModel.create() calls by stringified create options. Each prompt still runs on an isolated clone, or on a fresh one-shot instance when clone() is unavailable. createSession() never touches this cache.
  • Optional result cache for ask() (off by default). Every call hits the model unless you opt in. Pass cache: "session" for sessionStorage, cache: "local" for localStorage, or any { get, set }-shaped object to memoize responses by (input, systemPrompt, samplingMode / temperature / topK). Built-in "session" / "local" entries expire after one hour by default. Pass cacheTtl (milliseconds) to change the TTL per call. Pass cacheRefresh: true to skip the read and replace the cached value after a successful run. Custom { get, set } caches own their expiry policy.
  • Delta-vs-cumulative chunk detection. Browser implementations may expose delta or cumulative chunks. The wrapper normalizes per chunk so onUpdate (cumulative) and sendStreaming() (deltas) have stable SDK semantics.

The two cache-shaped items above are ergonomic defaults scoped to ask(), not opinions about how to use a language model. Multi-package compositions (agent loops, conversation history, tool dispatch) are not part of this package; see Architecture for the split.

systemPrompt is folded into the session’s initialPrompts as a system role. Prefer samplingMode for output variety: the browser maps the semantic mode to model-appropriate sampling parameters. Legacy temperature and topK are still passed through where browsers expose them, but they are mutually exclusive with samplingMode.

For createSession, use either the systemPrompt shorthand or createOptions.initialPrompts for restored multi-turn context. If both are provided, the advanced initialPrompts value is authoritative and the SDK warns once per loaded module instance that systemPrompt was ignored.

ask({
input: "Tell me a joke about web standards.",
systemPrompt: "You are a stand-up comedian. Be punchy.",
samplingMode: "creative",
});

Chrome’s Prompt API accepts expectedInputs / expectedOutputs with optional language arrays. The wrapper sets these automatically when you pass language and the language is in supportedLanguages (default: ["en"]):

ask({
input: "Explain CORS in two sentences.",
language: "en-US",
});

For unsupported languages the hints are silently omitted; the model still runs, just without the hint.

Pass a JSON Schema via responseConstraint to constrain the model’s output:

const result = await ask({
input: "Extract the city and country from: 'I live in Belo Horizonte, Brazil.'",
responseConstraint: {
type: "object",
properties: {
city: { type: "string" },
country: { type: "string" },
},
required: ["city", "country"],
},
});
const parsed = JSON.parse(result.output ?? "{}");

The wrapper passes responseConstraint straight through to session.prompt() / session.promptStreaming(). Support depends on the browser implementation.

By default the schema is inlined into the prompt context, which costs tokens. Pass omitResponseConstraintInput: true (on ask() or SessionSendOptions) to drop it; the constraint still shapes the output, but you should then include format guidance in the prompt text itself. The flag is only forwarded when responseConstraint is also set — the native API throws a TypeError otherwise, so the wrapper ignores it on its own.

The Prompt API spec defines native function calling: register tools on the session, and the runtime invokes each tool’s execute on the model’s behalf, then feeds the result back into the conversation. ask() and createSession() forward a tools array straight through to LanguageModel.create():

import { createSession, type LanguageModelTool } from "@web-ai-sdk/prompt";
const tools: LanguageModelTool[] = [
{
name: "fetch_url",
description: "Fetch a URL and return its text.",
inputSchema: {
type: "object",
properties: { url: { type: "string" } },
required: ["url"],
},
async execute(args) {
const { url } = args as { url: string };
return await (await fetch(url)).text();
},
},
];
const session = createSession({ systemPrompt, tools });

The SDK only forwards tools; it does not call execute. Native execution depends on the browser. Treat it as experimental and provide a fallback.

Your application must parse model-emitted tool calls and run any manual execution loop. To declare native tool modalities, use expectedInputs and expectedOutputs.

tools also works with ask({ input, tools }). Its base-session cache key excludes functions, so execute does not affect that key.

Each ask() still uses a clone or fresh instance. Prefer createSession() for tool-based sessions because it bypasses this cache.

AbortSignal is supported on every surface. Aborting mid-stream resolves cleanly; an opt-in result cache is not written for aborted runs:

const controller = new AbortController();
const promise = ask({ input: "Long story…", signal: controller.signal });
setTimeout(() => controller.abort(), 1000);

For sessions, call session.abort() to stop the most recent in-flight send / sendStreaming, or session.destroy() to tear down the underlying instance.

Aborted runs reject with PromptAbortError (exported from the package). Both ask() and sessions throw the same class, so err instanceof PromptAbortError works; its name is "AbortError" for compatibility with standard abort handling.

import { ask, PromptAbortError } from "@web-ai-sdk/prompt";
try {
await ask({ input: "", signal });
} catch (err) {
if (err instanceof PromptAbortError) return; // user cancelled
throw err;
}

The vanilla ask() throws PromptUnavailableError when the API is missing or reports availability: "unavailable". Callers branch explicitly:

import { ask, PromptUnavailableError } from "@web-ai-sdk/prompt";
try {
const result = await ask({ input: "hi" });
} catch (err) {
if (err instanceof PromptUnavailableError) {
// Native API unavailable or model preparation blocked; fall back.
return;
}
throw err;
}

createSession() starts native creation immediately and returns a Session wrapper synchronously. If creation fails, the error surfaces when the first send, sendStreaming, or clone awaits that already-started work.