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.
One-shot: ask()
Section titled “One-shot: ask()”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().
Treat model output as untrusted
Section titled “Treat model output as untrusted”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.
Options
Section titled “Options”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;}Returns
Section titled “Returns”interface AskResult { output: string | null; cached: boolean;}Chat: createSession()
Section titled “Chat: createSession()”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 baseconst outputElement = document.querySelector<HTMLElement>("#output");// per task / run:const turn = await base.clone(); // awaits base readiness, then fresh historylet 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.
Prefill and message arrays
Section titled “Prefill and message arrays”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. UseomitResponseConstraintInput: trueto 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.
Multimodal input (text, image, audio)
Section titled “Multimodal input (text, image, audio)”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.
Context-window introspection
Section titled “Context-window introspection”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 hereconst turn = await base.clone(); // awaits readiness; clone is live hereconst quota = turn.contextWindow; // e.g. 4096 / 6144 tokensconst used = turn.contextUsage ?? 0; // ≈ system promptif (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});// laterstop();How it works
Section titled “How it works”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()returnfalse/nullon browsers without the API. The vanillaask()throwsPromptUnavailableError; the React hook surfacesstatus: "unavailable". - Warm base reuse for
ask()(ergonomic default, scoped toask). A bounded LRU caches baseLanguageModel.create()calls by stringified create options. Each prompt still runs on an isolated clone, or on a fresh one-shot instance whenclone()is unavailable.createSession()never touches this cache. - Optional result cache for
ask()(off by default). Every call hits the model unless you opt in. Passcache: "session"forsessionStorage,cache: "local"forlocalStorage, 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. PasscacheTtl(milliseconds) to change the TTL per call. PasscacheRefresh: trueto 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) andsendStreaming()(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.
System prompt + sampling
Section titled “System prompt + sampling”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",});Language hints
Section titled “Language hints”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.
Structured output
Section titled “Structured output”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.
Native tool calling (experimental)
Section titled “Native tool calling (experimental)”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.
Aborting
Section titled “Aborting”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;}Errors and unavailability
Section titled “Errors and unavailability”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.