In this article

One ticket, three jobs

Chrome has been shipping AI into the browser for a year now, and the interesting question is not what the APIs can do. It is which of them you can put in front of customers, on hardware you have never seen. That is what this piece is about, and all of it stays inside the tab. One support ticket runs the whole way through, because these decisions only get concrete when there is a real string on the screen.

A support ticket arrives in German: a name, an order number, a complaint. The person on shift reads English. Helping them is three jobs: detect the language, translate the message, and once the thread grows long, summarize it. For years, one invisible cloud call did all three. It also did a fourth thing that nobody classified: it sent the customer’s name to an outside processor, and under European data-protection law that is a transfer of personal data. Nobody ever decided that fourth job; it simply came along with the feature.

Since Chrome 138, all three jobs can run inside the tab, on the machine that displays the ticket. Chrome did not ship a chatbot; it shipped a set of narrow task APIs with documented options and outputs. You no longer ask how to prompt a model; you ask which of these contracts fits the job, and what may leave the machine. Both answers begin with what Chrome actually shipped.

The shape is the contract

What Chrome shipped is a handful of constructors, so start there. Translator.create() gives you a translator for one language pair. Summarizer.create() gives you a summarizer with a fixed type and length. LanguageDetector tells you which language a text is in, with a confidence. None of them lets you talk to a model.

These are task-shaped APIs. A prompt describes a job in prose and hopes the model reads it the same way today as it did yesterday. A task-shaped API removes the prose: you pick a type and a length from a fixed list, and the documentation tells you what each combination produces; short plus key-points, for example, means three bullet points in the language you configured. That narrowing is the point: a request the options cannot express is a request the model cannot misread. The shape itself is not the new part; cloud translation APIs have taken fixed options and returned documented output for fifteen years. New is where the work runs and what a call costs: on the machine in front of the person, for nothing.

Seven verbs in three states of maturity: detect, translate and summarise are stable, the writing trio is still in trials, and Ask stands apart as the one verb without a contract.

Chrome ships seven of these APIs. Six are named by their verb, which makes the family easy to hold. Detect, translate and summarize are stable. Write, rewrite and proofread are gated behind trials and flags. The seventh is Ask; in code it is called LanguageModel, and ordinary web pages only got it in Chrome 148, while extensions had it since 138.

Ask breaks the pattern on purpose. It takes free text and media and returns whatever the current model makes of it; there are no fixed options and no documented output shape. It is the family’s escape hatch, and the last tool to reach for, not the first.

The machine decides first

All of this runs on hardware you do not control and have never seen, so you cannot know at ship time whether a feature works. You ask at run time, and every flow starts with the same call: availability(). The answer is specific to this website, this browser configuration and this machine, which is why it cannot be a table in the documentation.

One prerequisite decides whether any of this is worth your afternoon, so take it before the details. Summarizer and Prompt share one underlying model, Gemini Nano, the small model inside Chrome. Nothing but the language detector ships preinstalled, and the model needs a user gesture before it will even start. There are harder limits too, and if your product lives on phones they rule this out entirely; I have put them together at the end rather than scatter them. It downloads once, fills gigabytes, and Chrome deletes it again when disk space runs low. The Translator’s language packs are smaller. For those, availability() can answer downloadable even when another site downloaded that pair long ago: Chrome refuses to tell site B what site A did, a privacy decision, so the answer only ever describes what your own site has done. There is also no API yet to list the supported pairs at runtime; the gap is tracked publicly in translation-api#68.

When a call fails, it fails with a named DOMException, and the name tells you what to fix. Four names cover almost everything in practice:

The errorWhat it meansWhat to do
AbortErrorYour own AbortSignal fired.Nothing; the cancellation you requested happened.
NotAllowedErrorA required user action is missing, or a permissions policy blocks the API here.Move create() behind a click; check the frame’s allow attribute.
NotSupportedErrorThe options do not exist here: an unknown language pair, an output language the model cannot produce.Fall back, and say so in the interface.
QuotaExceededErrorThe input is larger than the session’s quota.Measure first, then chunk the input.

When nothing works at all, two internal pages tell you why. chrome://on-device-internals shows the model’s state on the machine, and chrome://components lists a component called Optimization Guide On Device Model; its version stays at 0.0.0.0 until the model has actually arrived. Ad blockers, VPNs and enterprise policies regularly switch the whole thing off, so rule those out before you debug your own code. One failure does not appear in the table: on my machine, a create() call once rejected with no error object at all, and afterwards every built-in AI call on the page failed until a reload. So the catch block also has to handle a rejection that carries no reason.

What I do here is treat unavailable as a state I design, not an error I catch.The feature that works on a developer laptop will meet the five-year-old machine at the support desk, the metered hotel Wi-Fi, and the phone, where none of this exists today. If the fallback is a cloud call, that fallback carries exactly the data-protection consequences the on-device path was chosen to avoid, and the interface should be honest about which path a given sentence took. If the fallback is that the feature is absent, the interface has to say that instead. The only wrong option is not deciding, because then the feature behaves differently on every machine, and whoever debugs it six months later will blame the API rather than the missing decision.

A named verb, or the escape hatch

The remaining decision has three outcomes, and you can write them down as a rule. If the job has a named API, use the named API. If the job has no named API yet, use Prompt, still on the machine. And if the machine cannot run the model, or the work is a batch over a million records instead of one string on a screen, then it was never a tab job, and it goes to the cloud.

The whole decision: a named verb if one exists, the escape hatch still on the machine if not, and the cloud only when it was never a tab job.
if (!("Translator" in self) || !("LanguageDetector" in self)) {
  return useCloudFallback();
}

const detector = await LanguageDetector.create();
const [{ detectedLanguage }] = await detector.detect(text);

const status = await Translator.availability({
  sourceLanguage: detectedLanguage, // BCP 47, e.g. "de"
  targetLanguage: "en",
});
if (status === "unavailable") return useCloudFallback();

// needs a user gesture when the language pack must download
const translator = await Translator.create({
  sourceLanguage: detectedLanguage,
  targetLanguage: "en",
  monitor(m) {
    m.addEventListener("downloadprogress", (e) => showProgress(e.loaded));
  },
});

const english = await translator.translate(text);

This exact flow runs on this page: if your browser exposes the APIs, the demo below uses them, and if not, you see the unavailable state:

Live on this page
  • Language Detector · checking…
  • Translator (de → en) · checking…
  • Summarizer · checking…
  • Prompt · checking…
  • Writer · checking…
  • Rewriter · checking…
  • Proofreader · checking…

The chips are live availability() answers from your browser, for this origin, on this machine, for everything Chrome offers today. Nothing downloads without your click, and every download button says what it fetches: the language pack is small, the shared model behind Summarise and Ask is a multi-gigabyte download. Write, rewrite and proofread are in trials; they appear as states here until their API shape settles.

Checking availability is one half of working with these APIs. The contract on the output is the other half, and the Summarizer shows it most clearly. Summarizer.create() takes a type, a format and a length. The types are key points, tl;dr, teaser and headline, and the documentation commits to the shapes: short key points means three bullets, a short headline stays under twelve words. Now compare that to a prompt. A prompt that asks for three bullets can get five, and nobody broke a promise, because there was none. You cannot ask a three-value length option for a fourth value. That is the difference between requesting a format and being able to promise one to the person reading the screen.

The Prompt API earns its place where no named API exists: classification, structured extraction, or questions about an image, since it also takes image and audio input. Its most useful pattern combines a response schema with a picture, here the damage photo the customer attached to the ticket:

const session = await LanguageModel.create({
  expectedInputs: [{ type: "image" }],
});

const raw = await session.prompt(
  [{
    role: "user",
    content: [
      { type: "text", value: "Extract the SKU and the damage from the photo." },
      { type: "image", value: attachedPhoto },
    ],
  }],
  {
    responseConstraint: {
      type: "object",
      properties: {
        sku: { type: "string" },
        damage: { type: "string" },
      },
      required: ["sku", "damage"],
    },
  },
);

const extracted = JSON.parse(raw);

The constraint is a JSON Schema, and the model’s output has to conform to it. So the parse on the last line needs no retry loop: the call can fail, with the named errors from earlier, but it cannot succeed with the wrong shape. The schema ends the parsing problem, not the verification problem: a conforming SKU can still be the wrong SKU, so the extracted value gets checked against the order system like any user input.

The session itself needs the same housekeeping as any expensive resource. Its context window fills up, and overflow silently drops the oldest exchanges. create() is the costly call, because a session usually starts with initial prompts, the instructions and examples you feed it once at creation; clone() copies a session with that prefix already processed, so prefer it to rebuilding. Destroy the session when the screen closes. Sessions do not survive a reload either; the workaround is replaying your initial prompts from storage you own, and a real fix is tracked in prompt-api#50.

The contract includes a budget

Every verb in the family also has a limit: how much it can read at once. The limit is not hidden. A session exposes it as inputQuota, and measureInputUsage() tells you what a given text would cost, both before anything runs. So the check comes first:

const summarizer = await Summarizer.create({
  type: "key-points",
  format: "plain-text",
  length: "short",
});

const usage = await summarizer.measureInputUsage(ticketText);
if (usage > summarizer.inputQuota) {
  return summarizeInChunks(ticketText);
}

for await (const chunk of summarizer.summarizeStreaming(ticketText)) {
  output.textContent += chunk; // append; never innerHTML
}

Skip the check and the call throws a QuotaExceededError after the person has already waited. The fallback is the map-reduce we have always used for oversized input: split the thread at its message boundaries, summarize each part, then summarize the summaries. Each level loses detail; that is the price of pushing a long document through a small window, and the person cannot see that price in three tidy bullets. So the interface has to say when a summary was built from summaries. And at some length the ladder bottoms out: a summary of summaries of summaries no longer answers to the thread, and the honest move is to admit the document outgrew the tab, not to chunk again.

The streaming loop in the code is there for latency: it shows the first words while the model is still writing the rest, which on a slow machine means the person reads while the model writes, instead of watching a spinner. Streaming has one rule: every chunk goes into textContent, never into innerHTML. A summary of the thread quotes the thread, and the thread contains whatever the customer typed. Written into the page as markup, that text gets parsed by the browser, and parsed markup can carry attributes that execute; the classic one is an onerror on an image.

What the contract buys

Back to the ticket, and to the fourth job. Translation looks like infrastructure. In practice it is often the first place where a product exports personal data, precisely because nobody thinks of it as a feature.

A harmless-looking sentence, with a name, an order number and a complaint in it, is already personal data.

The cloud call survives in most products for years, not because anyone decides to keep it but because it still compiles. The built-in APIs change what you can state about the same feature. After the one download, the sentence is translated on the machine that already displays it. No request leaves. There is no outside processor and no transfer, so there is nothing to add to the records of processing, the register of data flows that European data-protection law requires. That statement is true by construction, not by policy: it can go into a privacy notice as written, and a data-protection officer can verify it in the network tab.

That claim holds per path, not per product. As long as the cloud fallback exists, some sentences take it, and the outside processor stays in your records of processing. The transfer leaves the product entirely only with the other fallback from earlier, the feature that is absent where the model is absent. Which fallback you pick is a product decision with a price, and it is that decision, not the API, that the data-protection officer will ask about.

Machine translation was fluent years before Chrome 138. What blocked the feature in organizations with a data-protection officer was the transfer, and the contract removes it.

The browsers disagree about one verb

Everything above runs in Chrome, so the fair question is what happens beyond it. For the task APIs a second implementation already exists. They are on the standards track in the W3C’s Web Machine Learning group, and Edge already previews the same shapes, backed by Microsoft’s Phi-4-mini instead of Gemini Nano. Translation and summarization survive that swap because the code names the contract and never the model. The contract binds the shape of the output, not the words: Phi-4-mini writes a different summary than Gemini Nano, in the same three bullets. A feature that switches browsers keeps working; whether it still reads well is something you check on your own tickets.

The escape hatch has no such agreement. Mozilla’s standards position, argued in public on its own tracker, formally opposed the Prompt API as a web standard, the W3C’s technical architecture group raised concerns, and Microsoft keeps it behind a flag; Chrome shipped it to web pages anyway in 148. The technical core of the objection is easy to state: the same prompt gives different results on a different model, so a second browser has nothing to implement against. That is the contract argument again, made between browsers instead of inside one.

What you build on the named verbs is likely to stay portable across browsers and models. What you build on Prompt is a Chrome feature until further notice, and it deserves the evaluation suite you would give any model dependency, because even a Chrome update can change what it produces.

What this is not

One boundary is where the APIs run: desktop Chrome only, with no mobile path today, and absent from Web Workers, so a product that lives on phones cannot run any of this yet. Iframes add a wrinkle for vendor widgets. A cross-origin iframe sees none of the APIs until the embedding page grants them with the allow attribute, which means every customer embedding your widget has to change their markup. The usual replacement is a quiet cloud fallback, and the fourth job comes back with it.

The batch job is the other boundary. A million historical tickets, searchable in two languages, is not a tab job; it is a batch that runs next to the data, with a processor named in the records and a data-processing agreement. Deciding where that batch runs, and under which account it acts, is architecture beyond the tab, a question of its own. So is the WebMCP proposal, which lets a page declare its own actions for an agent to call. The tab’s verbs are for the ticket already on the screen, read by the person on shift.

Those boundaries also say who should ship this in 2026. If a data-protection officer has blocked one of your desktop features over the transfer, you have your reason now. The price is the dual path, plus one measurement: whether the small model’s output is good enough for real tickets, which no datasheet answers. A product that lives on phones, or a team unwilling to run two quality tiers, gains little yet; for them the right move is the availability check behind a flag, revisited when the boundary moves, because it is Chrome’s to move.

If you want to know whether any of this is worth your time, do not start with a feature. Open the console on the screen where a foreign-language message actually arrives, call availability() for the language pair your customers really write in, and read what your own users’ machines answer. That number decides what you can promise, and no datasheet will give it to you. If it comes back usable, the rest is short: take the one job that has a named verb, decide now which fallback you can live with, and say in the interface which path a given sentence took. What you ship at the end of that is smaller than the demo you could build this afternoon. It is also the version your data-protection officer will sign.

Measured · 16 August 2026
  • One real machine, first contact: Apple M4 Max, 128 GB RAM, Chrome 151, hardware far above every requirement. availability() answered: language detector available; the German–English translator pack, the Summarizer and the Prompt API all downloadable. Nothing beyond the detector ships preinstalled; the models arrive only after a page asks and the person agrees.
  • The detector, the one worker already present, created in about 1 ms and answered warm in 0.2 ms median across ten runs; the German support sentence came back de at confidence 1.0.
  • Documentation describes LanguageModel.params(). In this build the static members are availability() and create(), nothing else.
  • After the German–English pack was downloaded (a user click in the demo), create() returned in under 1 ms, the support sentence translated in 4 ms cold and 6 ms warm median across ten runs, and a four-sentence complaint paragraph took 18 ms. On this machine, that is fast enough to translate per keystroke.
  • By a later visit the same day, the shared model had arrived and Summarizer and Prompt answered available. Summarizer: create() in 1 ms; the first summary of a six-message support thread took 11.7 s, warm runs 1.0–1.5 s, and streaming delivered the first words after 40 ms. The session reported an input quota of 9,216 units; the thread measured 730. The three bullets were accurate, with one shift to expect from a small model: an offer in the thread, “pickup can be booked”, came back as a confirmed booking.
  • Prompt, same model: create() under 1 ms; a schema-constrained extraction, SKU and damage from one sentence, took 5.0 s cold and 0.5 s warm, and both runs conformed to the schema with the right values.
  • Failure without a name: one LanguageDetector.create() rejected with a literal undefined instead of a DOMException, and every subsequent built-in AI call on that page failed until the page was reloaded; a reload restored everything. A second specimen the same day: one Prompt call rejected with UnknownError(“kErrorUnknown”) and succeeded unchanged on retry.
Verified · August 2026
  • Stable on web pages: Translator, Language Detector, Summarizer since Chrome 138 (desktop only); Prompt API since Chrome 148 (extensions since 138). Writer and Rewriter run a joint origin trial, and Proofreader’s developer trial is documented for Chrome 141 to 145. None of the three exists as a global until you opt in: locally via chrome://flags/#writer-api-for-gemini-nano and #proofreader-api, on a deployed origin via a trial token.
  • Model requirements: Windows 10/11, macOS 13+, Linux or Chromebook Plus; roughly 22 GB free disk, with deletion below 10 GB; more than 4 GB of VRAM, or 16 GB RAM with at least four cores; audio input needs the GPU; first download on an unmetered connection.
  • Boundaries: not available in Web Workers; cross-origin iframes need allow="translator", allow="summarizer" or allow="language-model"; a create() that needs to download the model must follow a user action such as a click. Summarizer output languages today: English, German, French, Spanish, Japanese.
  • Quota: sessions expose inputQuota and measureInputUsage(); exceeding the quota throws QuotaExceededError. Streaming variants yield chunks to append, and a responseConstraint holds the Prompt API’s output to the supplied JSON Schema. Source: MDN Summarizer API guide.
  • Edge previews the Prompt API and the writing-assistance APIs backed by Phi-4-mini, behind flags. Source: Microsoft Edge documentation.
  • Standards positions: Mozilla’s pushback on the Prompt API is documented in The Register (April 2026); the task APIs are incubated in the W3C Web Machine Learning group.
  • Source: Chrome built-in AI documentation. These values move; check before you promise them.