Skip to main content

Shipping it: the compatibility matrix

Ok. This is the page you keep pinned in a tab while you ship — every built-in AI API against the exact Chrome that runs it, the flag that unlocks it, and the state it's actually in. Bookmark it, because the matrix moves: Gemini Nano updates itself, origin trials lapse, and a flag that gated a global last month is a stable global this month. Everything below is reconciled against the live capabilities page, so when the table and the browser disagree, believe the browser.

Prerequisites

Desktop Chrome with built-in AI switched on — if the globals aren't on self yet, start with Setup & the availability lifecycle. This is the reference you keep open while shipping; it assumes you already know the four availability states.

The compatibility matrix

One model sits behind almost every row — Gemini Nano, a few billion parameters sized to your hardware — behind one download. (Embeddings is the exception: its SemanticEmbedder row runs a separate small model, embeddinggemma-300m.) Read the status column before you promise a PM anything. Stable means it's on the open web with no flag. Behind a flag means localhost or Canary only. Origin trial means you need a token to run it on a deployed origin.

APIGlobalChromeStatusFlagLesson
Prompt APILanguageModel148+ (web)Stablenone on 148+ · older #prompt-api-for-gemini-nanoPrompt API
SummarizerSummarizer138+Stablenone · older #summarization-api-for-gemini-nanoSummarizer
TranslatorTranslator138+Stablenone · older #translation-apiTranslator + Language Detector
Language DetectorLanguageDetector138+Stablenone · older #language-detection-apiTranslator + Language Detector
WriterWriter137–148 (lapsed)Behind a flag#writer-api-for-gemini-nanoWriter & Rewriter
RewriterRewriter137–148 (lapsed)Behind a flag#writer-api-for-gemini-nano (shared with Writer)Writer & Rewriter
ProofreaderProofreader141–145 (lapsed)Behind a flag#proofreader-api or #proofreader-api-for-gemini-nanoProofreader
EmbeddingsSemanticEmbedderCanary onlyBehind a flag · Intent to Prototype#semantic-embedder-apiEmbeddings
WebMCPdocument.modelContext149+Origin trial#enable-webmcp-testingWebMCP
Generative UIPrompt API + sandboxed iframeexperimentalNot productionrides the Prompt APIGenerative UI
MCP clientfetch + Streamable HTTPanyNo new APInoneMCP client

One more flag sits under every row: #optimization-guide-on-device-model, set to Enabled BypassPerfRequirement — the model download gate you flip on dev machines that sit under the hardware bar. Inspect the download at chrome://on-device-internals/, and remember users have a kill-switch of their own at Settings → System → "Turn on-device AI on or off".

Every row also needs the same host. Desktop only — Windows 10/11, macOS 13+, Linux, or ChromeOS (Platform 16389.0.0+) on Chromebook Plus devices — no Android or iOS. About 22 GB of free disk to pull the model down; Chrome purges it if free space later falls under 10 GB, or if the device fails the eligibility criteria for 30 days (that clock is unmet criteria, not plain idle time). A GPU with more than 4 GB of VRAM, or the CPU path — 16 GB of RAM and 4-plus cores, together. A non-metered network for the first download, and a secure context. Miss any one of those and availability() comes back unavailable with no flag you can flip to fix it — the hardware bar is covered in Setup & the availability lifecycle.

A few footnotes the table can't hold. 148+ (web) is the open-web stable line for the Prompt API; it landed in Chrome Extensions and behind the origin trial earlier, and the tuning knobs — temperature, topK, params() — still only apply in extensions or with the trial enabled, never on a plain website. The Prompt API's image input is that same LanguageModel global — you opt in with expectedInputs, there's no separate global — but pre-148 Canary needed #prompt-api-for-gemini-nano-multimodal-input, since retired. The Proofreader flag has drifted: current docs read #proofreader-api, though some builds still expose #proofreader-api-for-gemini-nano — enable whichever your Chrome shows, they gate the same API. Embeddings is Intent to Prototype — Canary-only behind #semantic-embedder-api, not an Early Preview Program (EPP) feature. And WebMCP's entry point is document.modelContext (navigator.modelContext is deprecated).

Feature-detect everything

The globals are simply undefined on browsers that don't ship them and on any page served over plain http://. So the first move is always the same: check typeof X !== 'undefined' in a secure context, then ask availability(). Never touch a global you didn't detect, and never assume the answer.

Here's the one probe worth keeping — it walks every API and buckets each into available, needs-download, or unavailable. downloadable and downloading both collapse to needs-download: the model is coming, but a multi-gigabyte download stands between you and a working call.

capabilities.js
// Built-in AI globals exist only in a secure context (https or localhost).
// Feature-detect first, then ask availability() — never the other way around.
const APIS = {
prompt: 'LanguageModel',
summarizer: 'Summarizer',
translator: 'Translator',
detector: 'LanguageDetector',
writer: 'Writer',
rewriter: 'Rewriter',
proofreader: 'Proofreader',
embeddings: 'SemanticEmbedder',
};

// A few APIs want an option to answer precisely; the rest read bare.
const OPTS = {
LanguageModel: { expectedInputs: [{ type: 'text', languages: ['en'] }], expectedOutputs: [{ type: 'text', languages: ['en'] }] },
Translator: { sourceLanguage: 'en', targetLanguage: 'es' },
Proofreader: { expectedInputLanguages: ['en'] },
};

// downloadable / downloading both mean "there, once a download finishes".
const bucket = (state) =>
state === 'available' ? 'available'
: state === 'downloadable' || state === 'downloading' ? 'needs-download'
: 'unavailable';

async function probe() {
const report = {};
for (const [key, name] of Object.entries(APIS)) {
const api = globalThis[name];
if (typeof api === 'undefined') { report[key] = 'unavailable'; continue; }
try {
report[key] = bucket(await api.availability(OPTS[name]));
} catch {
report[key] = 'unavailable'; // older builds throw on options they don't know
}
}
return report;
}
// → { prompt: 'available', translator: 'needs-download', embeddings: 'unavailable', … }

Fall back on purpose

Built-in AI is an enhancement, not a dependency. Treat it like one you can lose. Half your traffic — mobile, old browsers, machines under the bar — will never see the global, and the honest response to that is a second path, not a spinner that never resolves.

So plan for all four answers up front. No global, or unavailable, and you hand off to a cloud API or a reduced feature. downloadable or downloading, and create() blocks on a multi-gigabyte download — wire a monitor and show a bar, because a frozen button reads as a broken page. Only available lets you go straight through. Never assume you're on that last branch.

fallback.js
// One entry point, every branch handled. Built-in AI wins when it's there;
// the cloud (or a smaller feature) covers everyone else.
async function enhance(prompt, { onProgress, cloud }) {
// No global: old browser, http://, or mobile. Fall back and move on.
if (typeof LanguageModel === 'undefined') return cloud(prompt);

// Ask before you create. Never assume 'available'.
const state = await LanguageModel.availability({ expectedInputs: [{ type: 'text', languages: ['en'] }], expectedOutputs: [{ type: 'text', languages: ['en'] }] });
if (state === 'unavailable') return cloud(prompt);

// 'downloadable' / 'downloading': create() blocks on the model download.
const session = await LanguageModel.create({
expectedInputs: [{ type: 'text', languages: ['en'] }],
expectedOutputs: [{ type: 'text', languages: ['en'] }],
monitor(m) {
// e.loaded is a 0..1 fraction; e.total exists and is always 1.
m.addEventListener('downloadprogress', (e) => onProgress(e.loaded * 100));
},
});
try {
return await session.prompt(prompt);
} finally {
session.destroy();
}
}

No exception for Embeddings, either: SemanticEmbedder.create({ monitor(m) { … } }) takes the same monitor callback, so draw the same download bar off its downloadprogress events. And availability() is read-only — it never kicks off a download — so the worst a poll loop costs you is wasted cycles, not a surprise fetch. There's just no reason to spin on it; wire the monitor instead.

Free the memory

Every session, every task instance, every clone is a live claim on GPU memory, and none of them die on their own. You open them, you close them.

destroy() when you're done. destroy() each clone separately — a clone is a second session that costs exactly what the first one did, and it does not go down with its parent. And wire a teardown for the moment the user leaves, because an SPA route change or a closed tab that skips destroy() leaves the model pinned until the process dies.

teardown.js
const session = await LanguageModel.create({ expectedInputs: [{ type: 'text', languages: ['en'] }], expectedOutputs: [{ type: 'text', languages: ['en'] }] });
const branch = await session.clone(); // a second live session — same GPU cost

// … use them …

session.destroy();
branch.destroy(); // clones don't die with the parent — kill each one

// Route change or tab close: free anything still open.
window.addEventListener('beforeunload', () => session.destroy());

Leak enough of these and the next create() fails on a machine that had plenty of room ten sessions ago. It won't look like your bug. It is.

Gotcha: create() suddenly throws NotSupportedError

Symptom: create() starts throwing NotSupportedError for calls that worked a minute ago, and availability() flips to unavailable — even for bare, option-less calls. Cause: the on-device model process crashed enough times to trip an undocumented cooldown, and a tight retry loop is what trips it. Fix: stop retrying, check chrome://crashes, wait. It clears itself after a few minutes.

The pre-ship checklist

Run this before it goes anywhere near a user. Not a vibe — a list you tick.

  • Feature-detect every global with typeof LanguageModel !== 'undefined' (and the same for Summarizer, Translator, LanguageDetector, the rest) inside a secure context — https or localhost — before you touch it.
  • Call availability() and handle all four states: unavailable, downloadable, downloading, available.
  • For downloadable / downloading, create with a monitor and render e.loaded * 100 as a progress bar. (e.loaded is a 0..1 fraction; e.total exists and is always 1.)
  • Have a real fallback for unavailable — a cloud API or a reduced feature. Never a blank page.
  • destroy() every session, task instance, and clone, and wire a beforeunload teardown.
  • Declare expectedInputs/expectedOutputs (with the languages you use) on every Prompt API create().
  • Message the limits: desktop-only, secure-context-only, so the visitors who can't run it get a reason instead of a dead button.
  • Don't ship a flag-only or lapsed-trial API — Writer, Rewriter, Proofreader, Embeddings, WebMCP — to the open web without a fallback or a registered origin-trial token.
  • Instrument it with observability & tracing and grade it with evaluation before you call it done.
  • Pin your eval suite and re-run it on every Chrome release — Gemini Nano auto-updates silently, with no version you can query from JS.
  • Don't log raw prompts or responses. On-device means the data never left the machine; your own logging is the one way to leak it anyway.
Check your browser

Don't trust a table — trust the browser in front of you. The live capabilities page at windowai.danduh.me/status probes every API in your current Chrome and prints the real state, flag by flag. And every pattern in this course has a runnable version: the chrome-ai-course repo ships all 16 demos, one folder each, no build step.

Recap

  • One model, Gemini Nano, sits behind almost every API — embeddings is the exception, on its own small model — and they land at different Chrome versions and stability tiers, so the status column is the promise you're actually allowed to make.
  • Feature-detect typeof X !== 'undefined' in a secure context, then availability(), before you create anything.
  • Treat built-in AI as an enhancement: handle all four states, wire a monitor for the download, and always keep a fallback for unavailable.
  • destroy() every session and clone, or orphaned sessions pin GPU memory until the tab dies.
  • Pin your evals and re-run them on every Chrome release; Nano moves under you with no version to query.

Sixteen lessons ago, the introduction handed you a model that already lives on your users' machines, free. This last page is the tax on that gift: detect it, fall back when it's missing, free what you open, and re-check the matrix every time Chrome ships. Do the boring parts and the free model stays free.

Everyone else ships a blank page and calls it on-device.