murmur

murmur guide

Everything to add murmur to a site, choose how the AI is called, index your pages and use the npm package.

Quick start

Paste this line before </body> on every page of your site:

<script type="module" src="https://maxgfr.github.io/murmur/cdn/murmur.js"></script>

A bubble appears in the bottom right corner. Without any option, it answers from the text of the page the visitor is reading, with a small AI model that runs in the visitor's browser (after they agree to a one-time 1.1 GB download); visitors can connect their own AI key instead from the AI settings. To answer from the whole site, build an index (see Index your site); for a hosted model with nothing to set up for visitors, use a proxy (see AI models and keys).

The configurator on the home page writes the line for you.

Options

Every option is a data- attribute on the script tag. All are optional.

AttributeWhat it does
data-indexURL of the index file written by murmur index, for example /murmur-index.json. Without it, the widget reads the current page.
data-endpointURL of your OpenAI-compatible proxy (https). Visitors then chat without a key.
data-modeproxy, local, auto (default) or key: who answers (see AI models and keys).
data-preloadWhen the in-browser model downloads: question (default), open, idle or consent.
data-localfalse turns off the model that runs in the visitor's browser, which is otherwise the default when there is no proxy: visitors then need their own key.
data-providerProvider preselected for visitors who bring their own key: openai, anthropic, google, mistral, openrouter, groq…
data-modelModel id: the one preselected for visitors, or the one asked of your proxy.
data-instructionsYour instructions to the assistant: who it is, its tone, what it should not discuss.
data-title, data-greeting, data-placeholder, data-buttonThe panel title, the first message, the question field's placeholder and the bubble's label.
data-langLanguage of the interface and the answers: en or fr. By default, the page's lang, then the browser's.
data-positionbottom-right (default) or bottom-left.
data-accentAny CSS colour for the bubble, the header and the visitor's messages.
data-themeauto (default, follows the system), light or dark.
data-opentrue opens the panel on load.
data-storage-keyPrefix of what the widget stores, to keep two widgets on one domain apart.

Index your site

The index is a JSON file of your pages cut into passages under their headings. Build it from your site's output folder, after your static site generator:

npx murmur-rag index ./dist --out ./dist/murmur-index.json

It reads every .html, .md and .txt file. In HTML it keeps the content of main (or article, or body) and skips navigation, headers, footers, forms and scripts. Add data-murmur-ignore to any element you want left out.

OptionWhat it does
--outWhere to write the index (default murmur-index.json).
--baseURL prefix of the pages, for a site served under a sub-path such as /docs/.
--rootCSS selector of the content in each page.
--excludeCSS selectors to skip, on top of the defaults.
--max-chunkLongest passage in characters (default 2000).

The command also takes a live site: npx murmur-rag index https://example.com/sitemap.xml fetches every page of the sitemap.

In a GitHub Action

Add one step after your build, before uploading the Pages artifact:

- run: npx murmur-rag index ./_site --base /my-repo/ --out ./_site/murmur-index.json

AI models and keys

Pick how the bubble answers with data-mode. A key written in a web page can be read by anyone, so murmur never takes one: your key stays in your Worker, and a visitor's key stays in their own browser.

data-modeWho answers
proxyYour Worker, holding your key. Implied by data-endpoint. What most sites use.
localA model in the visitor's browser, nothing else: no key settings.
auto (default)Your proxy if set; else the browser model, with visitors free to connect their own key.
keyEach visitor's own key, nothing else.

Your Worker holds the key

Deploy the example Cloudflare Worker. It keeps your key as a secret, forwards chat requests to any OpenAI-compatible API (OpenRouter, OpenAI, Groq, Mistral…), accepts only your site's origin, fixes the model and caps the length of each answer. Then:

<script type="module" src="https://maxgfr.github.io/murmur/cdn/murmur.js"
  data-endpoint="https://murmur-proxy.you.workers.dev/v1" data-model="openai/gpt-5-mini"></script>

Set a spending limit at your provider: anyone who can open your site can use your proxy.

A model in the visitor's browser

With data-mode="local" (or by default when there is no proxy), the browser runs LFM3 1.7B by Liquid AI (October 2026), in its 4-bit WebGPU weights, through Transformers.js. Questions never leave the device.

data-preload says when the weights (about 1.1 GB) download:

data-preloadDownload starts
question (default)On the first question, with a discreet progress bar above the question field.
openWhen the visitor opens the bubble, so it is often ready by the time they finish typing.
idleAs soon as the page is idle, even if the visitor never opens the bubble.
consentAfter the visitor agrees on a prompt that names the model and its size.

The weights stay in the browser's cache, and murmur asks the browser to keep them (persistent storage) so a 1 GB download is not evicted. On the next visit, a model already in the cache is prepared in the background, without network, whatever data-preload says: the first answer then comes in a few seconds.

It needs WebGPU (recent Chrome, Edge or Safari) and a GPU with about 1.5 GB of memory to spare; without it, the bubble says so. A 1.7B model is less precise than a hosted one.

Each visitor brings their key

With data-mode="key", or from the AI settings in the default mode, the visitor picks a provider and a model, pastes a key, tests it and saves. llm-connect encrypts the key with AES-GCM under a key the page cannot export, keeps it in the browser (for the session, or on the device if the visitor asks) and sends it only to the chosen provider. The visitor can also point the widget at Ollama or LM Studio on their own machine.

The browser calls the provider directly, so the provider must accept calls from web pages (CORS): OpenAI, Anthropic, Google, Mistral, OpenRouter, Groq, Together, DeepInfra, xAI and local servers with CORS enabled do.

npm package

npm install murmur-rag

The package is split so you can take only what you need:

ImportContents
murmur-ragcreateMurmur, the headless engine, plus everything in /rag.
murmur-rag/ragbm25Retriever, buildIndex, splitText, formatSources, citedSources, the Retriever interface.
murmur-rag/indexerindexPages, indexDocument, extractSections.
murmur-rag/llmproxySettings, visitorStore, resolveSettings, on top of llm-connect.
murmur-rag/locallocalModel, an AI SDK model running in the page on WebGPU; hasWebGPU, DEFAULT_LOCAL_MODEL. Needs @huggingface/transformers.
murmur-rag/widgetmountMurmur, defineMurmurChat, MurmurChat, renderMarkdown.
murmur-rag/embedThe script-tag entry: reads data- attributes and mounts the bubble.

Headless

import { createMurmur } from "murmur-rag";
import { anthropic } from "@ai-sdk/anthropic";

const murmur = createMurmur({ index, model: anthropic("claude-sonnet-5-5"), language: "en" });
const answer = await murmur.ask("What does the Pro plan include?");
for await (const part of answer.textStream) process.stdout.write(part);
const { cited } = await answer.done; // numbers of the passages it cites

Your own retriever

Any object with search(query, { limit, budgetTokens }) returning passages replaces BM25: an embeddings index, a search API, a database.

const retriever = {
  async search(query) {
    const hits = await mySearch(query);
    return hits.map((h) => ({ doc: 0, url: h.url, title: h.title, heading: "", anchor: "", text: h.text }));
  },
};
mountMurmur({ retriever, llm: { endpoint: "https://proxy.example/v1" } });

Styling and events

The widget lives in a Shadow DOM, so your CSS cannot break it and its CSS cannot leak into your page. Restyle it through custom properties on murmur-chat:

murmur-chat {
  --murmur-accent: #0b5cad;
  --murmur-accent-ink: #ffffff;
  --murmur-font: "Inter", sans-serif;
  --murmur-radius: 12px;
  --murmur-z: 1000;
}

It sends a murmur:state event at each step (searching, streaming, done, error) with the passages found and the ones cited: the switchboard on the home page is drawn from it.

document.querySelector("murmur-chat").addEventListener("murmur:state", (e) => {
  console.log(e.detail.state, e.detail.passages, e.detail.cited);
});

Privacy and security

FAQ

Does it work on GitHub Pages?

Yes: murmur is static files. This site is on GitHub Pages and its bubble answers from its own index, built in its deployment workflow.

Which model should I use?

A fast, inexpensive model is enough: the work is reading a few passages and summarising them. The settings suggest the recommended model of each provider.

What if the answer is not on my site?

The assistant is told to say so and point to where to look, rather than invent. If no passage matches the question at all, it is told that nothing was found.

Can I use the search without AI?

Yes: bm25Retriever(index).search(query) from murmur-rag/rag returns the matching passages with their links, to build a plain site search.