●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
When Gemini Nano Won't Run — Chrome and Android Have Different Doors
Pasting a Gemini Nano sample often fails. Chrome moved from the ai.* namespace to LanguageModel, and Android's entry point is ML Kit GenAI. Here is what works today, plus the device requirements nobody mentions.
If summarization ran offline, it would work in places with no signal. That was the thought behind pasting a sample into the Chrome console. What came back was a single line:
Uncaught ReferenceError: ai is not defined
I checked the spelling. I checked the flags. I restarted Chrome. It was none of those. The ai namespace itself no longer existed.
Gemini Nano is usually described as "the lightweight Gemini," but the thing that actually blocks you isn't model capability. It's which door you walked through, and whether you assumed the model was already on the device. Miss either one and your prompt never executes, no matter how well written it is.
Here's where Chrome and Android each stand today.
ai.languageModel no longer exists
Early builds of Chrome's Built-in AI hung everything off a global called ai (or window.ai). Most samples still floating around date from that period.
Today each capability lives on its own global class.
// Old: throws "ai is not defined"const session = await ai.languageModel.create({ systemPrompt: "You are a helpful assistant."});// Current: call the LanguageModel global directlyconst session = await LanguageModel.create({ initialPrompts: [ { role: 'system', content: 'You are a helpful assistant. Keep answers brief.' } ]});
The system prompt moved too. Instead of a dedicated systemPrompt field, you put a role: 'system' message at the head of initialPrompts. Restoring a stored conversation uses the same array with user and assistant entries, which I find more honest about what's actually happening.
Notice type went from "tl;dr" to 'tldr'. Fix only the namespace and you'll get rejected on the option value instead — one error swapped for another.
These APIs also haven't landed as one bundle. As of July 2026 they're spread across different stages.
API
From a web page
From a Chrome extension
Translator / Language Detector
Stable in Chrome 138
Stable in Chrome 138
Summarizer
Stable in Chrome 138
Stable in Chrome 138
Prompt
Stable in Chrome 148
Stable in Chrome 138
Writer / Rewriter
Developer trial
Developer trial
Proofreader
Developer trial
Developer trial
Look at the Prompt row. Extensions got it as stable in 138; ordinary web pages had to wait until 148. Ten versions apart, for the same API. That gap is exactly why code that worked in your extension can die when you move it to a page.
I'd hold off on building a core feature on Writer or Rewriter. A developer trial is something you adopt knowing the shape may change. The Built-in AI API status table is the source of truth here.
Skip availability() and create() will fail
Fix the namespace and it still might not run. This is the next place to look.
Gemini Nano does not ship inside Chrome. The API is built into the browser, but the model downloads separately the first time an origin uses it. So at the moment your code runs, the model may simply not be there.
That's why the current API is shaped around asking before acting.
// 1. Check the state// Pass availability() the same options you'll pass prompt()const availability = await LanguageModel.availability({ expectedInputs: [{ type: 'text', languages: ['en'] }], expectedOutputs: [{ type: 'text', languages: ['en'] }],});if (availability === 'unavailable') { // Device doesn't meet the requirements — route to cloud, or hide the feature return fallbackToCloud();}// 2. Start the download from a user gesture// When availability is 'downloadable', create() needs user activationdownloadButton.addEventListener('click', async () => { const session = await LanguageModel.create({ monitor(m) { m.addEventListener('downloadprogress', (e) => { progressBar.value = e.loaded * 100; }); }, }); // 3. Only now can you run const result = await session.prompt('Summarize this article in three lines.'); console.log(result);});
Two things get skipped here more than anything else.
First, pass identical options to availability() and prompt(). Some language and modality combinations aren't supported, and if the options drift apart you get "available" back and then a NotSupportedError at execution time.
Second, user activation. Calling create() on page load will always fail on a device without the model. That's deliberate — a multi-gigabyte download shouldn't start without consent. Your dev machine already has the model, so this passes locally and surfaces for the first time in production.
Working solo as an indie developer, I nearly signed off on code that skipped both. It "worked" only because my own Chrome already had the model cached. Since then I treat the first-run experience on a model-less device as the real test, not the happy path on my laptop.
One more detail: availability() returns one of 'unavailable', 'downloadable', 'downloading', or 'available'. It's not a boolean, so if (availability) happily lets 'unavailable' through.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦The check to download to run pattern that actually works in current Chrome and Android
✦First-download and inference latency measured on one machine, next to a cloud round-trip
✦Exception branches that survive QuotaExceededError and model eviction, always falling back to cloud
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
"On-device" doesn't mean "no constraints." Chrome's Built-in AI has hard requirements.
Item
Requirement
OS
Windows 10 / 11, macOS 13+, Linux, ChromeOS (Chromebook Plus)
Storage
At least 22 GB free on the volume holding the Chrome profile
GPU
More than 4 GB VRAM
CPU (without GPU)
16 GB RAM or more, 4 cores or more
Network
Only for the initial download (metered connections excluded)
The 22 GB figure isn't the model's size. It's headroom for downloading and unpacking.
The operationally nasty part comes next. If free space drops below 10 GB after the download, the model is removed from the device. It re-downloads once requirements are met again, but in the meantime your on-device feature quietly disappears.
Which means "it worked once, so it works" doesn't hold. Your fallback path isn't just for first run — it's for a state the user can wander back into at any time, and you can't see their disk.
You can check the current model size at chrome://on-device-internals. When behavior looks strange, start there.
Two more constraints worth knowing before you design:
Web Workers aren't supported. Permission policy checks make this complicated, so for now it's top-level windows and same-origin iframes only. You can't push inference off the main thread this way yet.
Cross-origin iframes need explicit delegation — <iframe src="..." allow="language-model">.
Android doesn't go through Chrome's API — the door is ML Kit GenAI
This is the piece I see misunderstood most.
Chrome's Built-in AI APIs do not work in Chrome for Android, or on iOS. Desktop and Chromebook Plus only. "Gemini Nano on your phone" and "Gemini Nano in your browser" share a model name and almost nothing else from a developer's point of view.
The supported door for Android apps is ML Kit's GenAI APIs, built on top of AICore, an Android system service. One Gemini Nano lives on the device and every app shares it — nothing to bundle.
You get task-specific APIs (summarization, proofreading, rewriting, image description) plus a GenAI Prompt API for free-form requests.
// build.gradle.kts// Check the official docs for the current versiondependencies { implementation("com.google.mlkit:genai-summarization:<latest>")}
And the shape of the code will look familiar.
val options = SummarizerOptions.builder(context) .setInputType(InputType.ARTICLE) .setOutputType(OutputType.ONE_BULLET) .build()val summarizer = Summarization.getClient(options)// Ask before using — same role as Chrome's availability()when (summarizer.checkFeatureStatus().await()) { FeatureStatus.UNAVAILABLE -> useCloudFallback() FeatureStatus.DOWNLOADABLE -> summarizer.downloadFeature(downloadCallback) FeatureStatus.DOWNLOADING -> showProgress() FeatureStatus.AVAILABLE -> runSummarization(summarizer)}
The four states from checkFeatureStatus() line up almost exactly with the four from Chrome's availability(). Different platforms, same design philosophy: the model might be here, might not — ask first.
So if this article reduces to one sentence: remembering the door's name matters less than honoring the three steps — check, download, run. The real reason old samples fail isn't the renamed namespace. It's that those three steps were never there. The namespace change just happened to be the part you could see.
Device coverage varies too. GenAI APIs run on hardware with supported chipsets — MediaTek Dimensity, Qualcomm Snapdragon, Google Tensor. Not every Android device qualifies, so treat FeatureStatus.UNAVAILABLE as an ordinary branch rather than an exception. The ML Kit GenAI overview covers the details.
What to give Nano, and what to keep in the cloud
Once it runs, the question becomes what to hand it.
Nano is small, and its remit is correspondingly narrow. The Prompt API accepts text, image, and audio input, but output is text only. Language support currently covers en / ja / es / de / fr.
Here's where I landed on the split, in the context of my own indie development work.
Dimension
Give it to Nano
Keep it in the cloud (Gemini API)
Latency
Autocomplete, reply suggestions — anything you can't make users wait for
Batch work where seconds are fine
Sensitivity
Classifying notes and chats that shouldn't leave the device
Content meant to be published anyway
Output length
Short summaries, tagging, yes/no judgments
Long-form analysis and reports
Accuracy demands
Suggestions where being wrong is cheap
Extraction and formatting where it isn't
Cost
High-frequency calls you don't want metered
Low-frequency work that needs quality
Work with short, structurally constrained output — classification, tagging — is where Nano is genuinely good. And the Prompt API gives you responseConstraint to pin the output to a JSON Schema.
const session = await LanguageModel.create();const schema = { type: 'boolean' };const result = await session.prompt( `Is the following message a support request?\n\n${text}`, { responseConstraint: schema });console.log(JSON.parse(result)); // true / false
As a classifier, Nano is straightforward to use. Hand it long-form generation and it loses to the cloud on both quality and speed.
One more thing about session design: context fills up as conversations continue.
console.log(`${session.contextUsage} / ${session.contextWindow}`);session.addEventListener('contextoverflow', () => { // Older turns have started getting dropped});
On overflow, the oldest prompt/response pairs are removed first (the system prompt survives). If that still isn't enough, you get a QuotaExceededError. For long conversations, watching contextUsage and cutting the session yourself gives you far more predictable behavior.
Also worth noting: temperature and topK tuning is limited to the Prompt API for Chrome Extensions, or to origin trial participants. From an ordinary web page, those parameters currently do nothing. I spent a while passing values that were never being read.
What I measured on one machine — download and inference time
You can't make the call without numbers, so here are reference figures from a single machine in my own indie setup. Not a rigorous benchmark — just enough to feel the order of magnitude (macOS 14, M2, shared VRAM, Chrome 148).
Phase
Measured on my machine (reference)
First model download
~3–6 min (varies with connection and time of day)
create() init (model already fetched)
0.3–0.8 s
Short classification prompt (one boolean)
120–300 ms
Three-line summary (800-char input)
1.5–3 s
Same classification via Gemini Flash (cloud round-trip)
400–900 ms
Two things stood out. First, for a short judgment, Nano beats a cloud round-trip — with the network hop gone, short judgments felt roughly 2x faster and the wait simply disappeared. Second, as output grows the gap narrows and then flips: for summarization and other longer outputs, the larger cloud model was both faster and better.
So if speed is your reason for choosing Nano, keep it to short, high-frequency work. Push long-form generation on-device and the optimization turns into a liability.
Init cost matters too. create() runs per session, so rebuilding one for every classification stacks up the initialization overhead. Holding a single reusable session and repeating only prompt() proved far steadier.
Don't swallow the exceptions — turn three errors into branches
Write only the happy path and the worst production symptom is "it worked yesterday, today nothing happens, silently." With on-device AI that's an everyday possibility: drop below 10 GB and the model is evicted; overflow the context and an exception fires.
So shape the prompt() call to distinguish three errors, and the failure modes become legible.
async function runOnDevice(session, text) { try { return await session.prompt(text); } catch (err) { // 1. Context overflow — tear the session down and rebuild if (err.name === 'QuotaExceededError') { session.destroy(); return { retry: true, reason: 'context-full' }; } // 2. Model vanished at runtime — re-read the state if (err.name === 'NotSupportedError' || err.name === 'InvalidStateError') { const state = await LanguageModel.availability(); if (state !== 'available') return fallbackToCloud(text); } // 3. Anything else: don't swallow it, fall back to cloud return fallbackToCloud(text); }}
The point is that every error path ends back at the cloud. On-device is an optimization for speed and privacy, not the sole door to a feature. Narrow it to one entrance and the moment the model is evicted, the feature itself disappears.
And you can't predict the moment of eviction. That's why calling availability() not just on first run but again on failure pays off. Extend "ask before you use it" into "ask again when it breaks." That one extra step turns a silent defect into a visible branch.
Where to go next
If your code isn't running, check in this order.
Open chrome://on-device-internals and see whether the model is there
If it isn't, console.log the raw return value of availability()
If it says 'downloadable', move create() inside a button's click handler
Hunting down ai.-prefixed calls can wait. A renamed namespace announces itself; a missing availability() fails in silence.
And there's one thing worth deciding before you write any of it: what your app shows on a device that can't run the feature at all. Settle that first, and the 22 GB requirement and the below-10-GB eviction both stop being surprises and become branches you already handle.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.