Caret is a Chrome extension I built over two evenings. Copy text, hit a hotkey, pick a transform, get the result on your clipboard. The whole thing runs against Chrome’s built-in Gemini Nano. Nothing leaves your machine.
The interesting part of the project isn’t the extension. It’s the Prompt API behind it.

What the API looks like in 2026
The global is LanguageModel. No namespace. It used to be window.ai.languageModel, then self.ai.assistant. Don’t trust training data on this; fetch the live docs.
const status = await LanguageModel.availability();
// 'available' | 'downloadable' | 'downloading' | 'unavailable'
const session = await LanguageModel.create({
temperature: 0.3,
topK: 3,
initialPrompts: [{ role: "system", content: "Rewrite this concisely." }],
});
let output = "";
const stream = session.promptStreaming(userText);
for await (const chunk of stream) {
output += chunk;
}
session.destroy();That’s it. The system prompt goes in initialPrompts as a role-tagged message, not a separate field (this changed in the spec). Streaming is a real ReadableStream<string>, consumable with for await. First token feels noticeably snappier than any cloud API because there’s no network hop.
The model download
Gemini Nano is roughly 4 GB on disk. Chrome refuses to download it unless you have 22 GB free.
It can stall. Mine sat at a few percent until I went to chrome://components, found “Optimization Guide On Device Model”, and clicked Check for Update. Once that re-kicked the chunk, the rest pulled through quickly.
Caret calls LanguageModel.create() with a monitor callback that fires downloadprogress events with e.loaded as a fraction between 0 and 1. That drives a progress bar in the setup page. There’s also a button that opens chrome://on-device-internals, since extension code can call browser.tabs.create({ url: 'chrome://...' }) even though regular pages can’t.
Surprises
topK and temperature are paired. If you pass temperature to create(), you must also pass topK. Otherwise:
Initializing a new session must either specify both topK
and temperature, or neither of them.Not documented anywhere I found. The fix is one line.
Service workers can’t read the clipboard. No navigator.clipboard in MV3 background scripts. You either set up an offscreen document or invoke through the popup (a normal extension document, where it works). I went with the popup.
chrome.action.openPopup() is gesture-bound. Chrome 127+ added it so background scripts can open the popup in response to a user gesture (hotkey, context menu click). Chrome’s gesture detection can refuse the call silently if it thinks the gesture has expired. Wrap it in try/catch.
Dev mode flag amnesia. WXT’s pnpm dev spawns a fresh Chrome with a clean profile. Every flag I enabled reset on the next reload. The fix is a web-ext.config.ts with defineRunnerConfig({ disabled: true }) so the dev server just builds to .output/chrome-mv3/ and you load it into your real Chrome instead.
What’s stable, what isn’t
Stable: the global name, the four availability literals, the create/promptStreaming surface, AbortSignal support, the monitor callback for downloads.
Still moving: the expectedInputs/expectedOutputs shape for multimodal inputs, the params discovery (LanguageModel.params() is marked deprecated but works in extensions), origin-trial scoping rules.
If you’re building against the Prompt API today, write a thin wrapper around the parts you actually use. When the API shifts, you fix one file.
Web Store
Submitted. The API is stable for extensions in Chrome 138+, but users still have to enable flags for the model to be available, so a reviewer’s vanilla Chrome will see “unavailable.” I’m submitting anyway to see how Google handles flag-gated experiences. Whichever way it lands, the GitHub install path stays.
Try it
The repo is at github.com/AhmedAlbarghouti/Caret. Clone, pnpm install && pnpm build, load .output/chrome-mv3/ unpacked, enable the two flags in the setup page, let the model download (poke chrome://components if it stalls), and you’re set.
The interesting file is src/lib/promptApi.ts. Everything Prompt-API-specific in one place. The rest is a small Preact app.
If you’re building against the Prompt API right now, send me what you found. The docs are still catching up to the implementation, and the only way through is comparing notes.