{"slug": "a-browser-api-for-choosing-and-running-local-ai-models", "title": "A browser API for choosing and running local AI models", "summary": "A proposal called Web Models outlines `navigator.models`, a browser API that lets web apps request a specific open-weight model by model ID and run it on-device with user permission, with no quiet fallback to a different model. The API exposes `models.generate()`, `models.stream()` and `models.embed()`, and the browser shares model weights across sites so each site does not download and store another copy, while users can see and revoke access per site and model. Each model ID is tied to fixed weights, a tokenizer and default input and generation settings, and a changed release gets a new ID; shared catalogs, model retirement and cross-browser compatibility tests remain open questions.", "body_md": "### The model youtested against.\n\nThe best model for scanning a receipt, transcribing audio, and searching documents may differ. A model ID lets a developer ask for the same model and release in any browser.\n\nA PROPOSAL FOR THE WEB\n\nThe model your app was built for.\n\nWith user permission. In the browser.\n\n`navigator.models` is a proposed browser API for web apps that need a particular open-weight model on-device.\n\nA workflow built around one model may not work with another. Apps should be able to ask for the model they built and tested against, regardless of which browser someone uses.\n\nWeb Models makes that request explicit. The page names a model ID. The browser runs that model or tells the page it can’t. There is no quiet fallback.\n\nSome models take gigabytes to download and plenty of memory to run. The browser can share their weights across sites, so every site does not need to download and store another copy.\n\nAn app asks for a model, and the browser asks the user. They can see and revoke access for each site and model. The browser also keeps an eye on setup, execution, and resource limits.\n\nEvery top-level inference call on `navigator.models` names one model ID.\n\nYour app picks the model and calls the API. The user grants access. The browser runs it.\n\n```\n01// Request the model your app was built and tested against.\n02const model = '<model-id>';\n03\n04button.addEventListener('click', async () => {\n05  if (!('models' in navigator)) return showFallback();\n06\n07  try {\n08    const { status } = await navigator.models.requestAccess(model);\n09    if (status !== 'granted') return showFallback();\n10\n11    const result = await navigator.models.generate(model, {\n12      messages: [{ role: 'user', content: 'Explain the web in one sentence.' }],\n13    });\n14\n15    output.textContent = result.data ?? '';\n16  } catch {\n17    showFallback();\n18  }\n19});\n```\n\nThis is just illustrative JavaScript. `<model-id>` is a placeholder. Your app provides `button`, `output`, and `showFallback()`. The first access request has to start with a user gesture. This page does not run a model.\n\n`models.generate()`\nSend a prompt and get a complete response.\n\n`models.stream()`\nReceive the response as it is generated.\n\n`models.embed()`\nTurn text into vectors for search and similarity.\n\nFor a one-off task, make a direct call. If you will use the model again, keep it in a session. Your app still owns the conversation history.\n\nOnce access is granted, the browser tells you what that model supports. Vision, audio, tools, structured output, reasoning, and embeddings all depend on the model you chose.\n\nEach ID is tied to fixed weights, a tokenizer, and default input and generation settings. A changed release gets a new ID, so developers decide when to move to it. Shared catalogs, model retirement, and cross-browser compatibility tests are still open questions.\n\nOpen an example to see the code. The model IDs are placeholders; replace them with the specific model IDs your app uses.\n\n`generate()`\n\n```\n01// Placeholder: replace with the ID of your chosen LLM.\n02const model = '<llm-model-id>';\n03\n04button.addEventListener('click', async () => {\n05  if (!('models' in navigator)) return showFallback();\n06\n07  try {\n08    const { status } = await navigator.models.requestAccess(model);\n09    if (status !== 'granted') return showFallback();\n10\n11    const result = await navigator.models.generate(model, {\n12      messages: [\n13        { role: 'system', content: 'Summarize the key points in three bullets.' },\n14        { role: 'user', content: documentInput.value },\n15      ],\n16    });\n17\n18    if (result.finishReason !== 'stop') return showFallback();\n19    output.textContent = result.data ?? '';\n20  } catch {\n21    showFallback();\n22  }\n23});\n```\n\nHere, documentInput is a text area containing the article or document. `button`, `output`, and `showFallback()` are supplied by your app. These examples are illustrative and do not execute on this page.\n\n`output.schema`\n\n```\n01// Placeholder: a vision-capable model with structured output.\n02const model = '<vision-model-id>';\n03\n04button.addEventListener('click', async () => {\n05  if (!('models' in navigator)) return showFallback();\n06  const image = imageInput.files?.[0];\n07  if (!image) return showFallback();\n08\n09  try {\n10    const { status } = await navigator.models.requestAccess(model);\n11    if (status !== 'granted') return showFallback();\n12\n13    const result = await navigator.models.generate(model, {\n14      messages: [{\n15        role: 'user',\n16        content: [\n17          { type: 'text', text: 'Extract the merchant, date, and total.' },\n18          { type: 'image', image },\n19        ],\n20      }],\n21      output: {\n22        schema: {\n23          type: 'object',\n24          properties: {\n25            merchant: { type: 'string' },\n26            date: { type: 'string' },\n27            total: { type: 'number' },\n28          },\n29          required: ['merchant', 'date', 'total'],\n30        },\n31      },\n32    });\n33\n34    if (result.finishReason !== 'stop') return showFallback();\n35    output.textContent = JSON.stringify(result.data, null, 2);\n36  } catch {\n37    showFallback();\n38  }\n39});\n```\n\nHere, imageInput is a file input for a supported receipt image (up to 10 MiB). `button`, `output`, and `showFallback()` are supplied by your app. These examples are illustrative and do not execute on this page.\n\n`embed()`\n\n```\n01// Placeholder: replace with the ID of your chosen embedding model.\n02const model = '<embedding-model-id>';\n03\n04button.addEventListener('click', async () => {\n05  if (!('models' in navigator)) return showFallback();\n06\n07  try {\n08    const { status } = await navigator.models.requestAccess(model);\n09    if (status !== 'granted') return showFallback();\n10\n11    const index = await navigator.models.embed(model, notes);\n12    const query = await navigator.models.embed(index.model, queryInput.value);\n13    if (query.model !== index.model) return showFallback();\n14\n15    const matches = notes.map((text, i) => ({\n16      text,\n17      score: cosineSimilarity(query.vectors[0], index.vectors[i]),\n18    })).sort((a, b) => b.score - a.score);\n19\n20    output.textContent = JSON.stringify(matches.slice(0, 5), null, 2);\n21  } catch {\n22    showFallback();\n23  }\n24});\n```\n\nHere, notes is a small, nonempty array of text strings and queryInput is the search field. Split larger collections into batches that fit the API’s limits. `button`, `output`, and `showFallback()` are supplied by your app. These examples are illustrative and do not execute on this page.\n\nAn API like this needs more than a way to generate text. It needs permissions, privacy boundaries, and a sense of the device it runs on.\n\nThose are platform concerns.\n\nA site can detect the API, but it cannot list the models on a device. It gets details about a model only after access is granted.\n\nA grant applies to one top-level site, one requesting frame origin, and one model ID. Revoking it ends related work and destroys its sessions.\n\nThe browser sets quotas and resource policies. Access to a model is useful, but it is not an unlimited claim on compute.\n\nShared model weights do not mean shared conversations. Each site keeps its data private, and model execution stays isolated.\n\nThe proposal is still moving. Model ID governance, compatibility, and whether these capabilities should extend the Prompt API are open questions. [The WICG proposal is open for comments.](https://github.com/webmachinelearning/proposals/issues/22)", "url": "https://wpnews.pro/news/a-browser-api-for-choosing-and-running-local-ai-models", "canonical_source": "https://www.webmodels.dev/", "published_at": "2026-11-15 20:35:53+00:00", "updated_at": "2026-11-15 20:52:19.509272+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-tools", "developer-tools"], "entities": ["Web Models", "navigator.models", "models.generate()", "models.stream()", "models.embed()"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/a-browser-api-for-choosing-and-running-local-ai-models", "markdown": "https://wpnews.pro/news/a-browser-api-for-choosing-and-running-local-ai-models.md", "text": "https://wpnews.pro/news/a-browser-api-for-choosing-and-running-local-ai-models.txt", "jsonld": "https://wpnews.pro/news/a-browser-api-for-choosing-and-running-local-ai-models.jsonld"}}