diff --git a/README.md b/README.md
index fdc842d1..f010bf56 100644
--- a/README.md
+++ b/README.md
@@ -1,182 +1,151 @@
-# RunAnywhere web example
-
-A browser app built on the RunAnywhere Web SDK. Models download to the browser
-and run there via WebAssembly, with a WebGPU path where the browser and the
-model support it. Prompts, audio, images, and documents stay on the device.
-
-## What works
-
-| Surface | What it does | SDK entry point |
-|---|---|---|
-| Assistant | Streaming chat, tool calling, saved conversations | `RunAnywhere.llm.generateStream`, `RunAnywhere.llm.tools` |
-| Talk | Full voice session (VAD, STT, LLM, TTS) | `RunAnywhere.voice.createSession` |
-| Image & Live | Describe a photo or a live camera frame | `RunAnywhere.vlm.generateStream` |
-| Transcribe | Batch and streaming speech to text | `RunAnywhere.stt.transcribe`, `.transcribeStream` |
-| Read Aloud | Speak arbitrary text | `RunAnywhere.tts.speak` |
-| Voice Activity | Streaming speech detection | `RunAnywhere.vad.detectStream` |
-| Documents | RAG over `.txt`, `.md`, and `.json` files you drop in | `RunAnywhere.rag.open` |
-| Solutions | Two packaged YAML pipelines: voice agent and document Q&A | `RunAnywhere.solutions.run` |
-| Benchmarks | One prompt at three token budgets (50, 256, 512), charted | `RunAnywhere.llm.generateStream` |
-| Downloads | Model registry, disk usage, storage folder | `RunAnywhere.storage`, `RunAnywhere.models` |
-| Settings | Generation preferences, API credentials, Hugging Face token | `RunAnywhere.setHuggingFaceToken` |
-
-Segmentation and Diarization have views and SDK calls wired, but no browser
-engine registers those capabilities, so the catalog is empty for both and each
-tab renders an unavailable placeholder.
-
-## Requirements
-
-| Item | Minimum |
-|---|---|
-| Node.js | 22.12 (CI runs 24) |
-| Browser | Chrome or Edge 86, Safari, Firefox |
-| Cross-origin isolation | Required for `SharedArrayBuffer`. The dev server and `vercel.json` send COOP `same-origin` and COEP `require-corp`; `public/coi-serviceworker.js` covers hosts that cannot |
-| Disk space | Hundreds of megabytes to a few gigabytes for downloaded models, held in OPFS |
+# RunAnywhere AI for the web
-## Run it
+
+
+
-```bash
-git clone https://github.com/RunanywhereAI/runanywhere-web.git
-cd runanywhere-web
+
+
+
+
+
-npm ci
-npm run dev # http://localhost:3000
-```
+
+
+
+
+
+
-`npm ci` pulls the SDK and its WASM artifacts. There is no separate WASM build
-step and no Emscripten toolchain to install.
+The RunAnywhere consumer app for the browser, written in TypeScript.
-| Script | What it does |
-|---|---|
-| `npm run dev` | Vite dev server on `localhost:3000` (strict port) with COOP/COEP headers |
-| `npm run build` | Production bundle into `dist/`, including the Emscripten `.js`/`.wasm` pairs |
-| `npm run preview` | Serve the built `dist/` on `localhost:3000` |
-| `npm run typecheck` | `tsc --noEmit` |
-| `npm run lint` | ESLint over `src`, zero warnings tolerated |
-| `npm run test` | Vitest over `src/**/*.test.ts` |
-| `npm run release:build` | `build` followed by `release:verify` |
-| `npm run release:verify` | Assert `dist/` holds every required runtime file |
-| `npm run release:deploy` | Build, verify, and deploy a prebuilt static bundle to Vercel |
-
-## SDK packages
-
-Everything comes from the npm registry. There are no `file:` links, no `paths`
-aliases, and no monorepo checkout, so `npm install` is the only thing that
-decides which SDK version the app runs against. Both the TypeScript modules and
-every WASM artifact come out of `node_modules/@runanywhere/*`.
-
-```jsonc
-"dependencies": {
- "@runanywhere/proto-ts": "^0.20.19",
- "@runanywhere/web": "^0.20.19",
- "@runanywhere/web-llamacpp": "^0.20.19",
- "@runanywhere/web-onnx": "^0.20.19"
-}
-```
+There is nothing to install. Open the page, pick a model, and it downloads into your browser
+and runs there through WebAssembly, with a WebGPU path where your browser and the model both
+support it. Whatever you type, say, or upload stays in the tab.
-| Package | Role |
-|---|---|
-| `@runanywhere/web` | SDK lifecycle and the inference facades. `@runanywhere/web/browser` adds `AudioCapture`, `AudioFileLoader`, and `VideoCapture` |
-| `@runanywhere/web-llamacpp` | LLM and VLM backend registration, CPU and WebGPU builds |
-| `@runanywhere/web-onnx` | Sherpa-ONNX backend registration for STT, TTS, VAD, and embeddings |
-| `@runanywhere/proto-ts` | Generated protobuf types for models, events, errors, and modalities |
+## Try it
-To try an unreleased SDK build, `npm install` a packed tarball or use
-`npm link`. Do not reintroduce a source alias.
+**[runanywhere-web-demo.vercel.app](https://runanywhere-web-demo.vercel.app)**
-## WASM artifacts
+Works in Chrome or Edge 86 and newer, Safari, and Firefox. Models are held in OPFS on your
+own disk, so the second visit starts instantly.
-Five JS/WASM pairs ship across the three SDK packages. `vite.config.ts` copies
-each canonical pair into `dist/assets/` next to Vite's hashed copy, because
-Emscripten's pthread glue starts its workers from the original filename.
+
-| Pair | Package |
+## What it looks like
+
+Captured in a Chromium browser with WebGPU active, running a small GGUF chat model through
+the llama.cpp WASM build.
+
+| | |
|---|---|
-| `racommons.{js,wasm}` | `@runanywhere/web` |
-| `racommons-llamacpp.{js,wasm}` | `@runanywhere/web-llamacpp` |
-| `racommons-llamacpp-webgpu.{js,wasm}` | `@runanywhere/web-llamacpp` |
-| `racommons-onnx-sherpa.{js,wasm}` | `@runanywhere/web-onnx` |
-| `racommons-onnx-sherpa-webgpu.{js,wasm}` | `@runanywhere/web-onnx` |
+|  |  |
+| An answer generated in the browser tab. The badge in the corner shows WebGPU is in use. | Models are grouped by who published them. The picker recommends one for the machine and reports what the browser can do. |
+|  |  |
+| Talk assembles a speech-to-text, chat, text-to-speech and voice-detection model, then runs the conversation in the tab. | The lower-level surfaces: transcribe, read aloud, voice activity, diarization, segmentation, benchmarks, solutions. |
+|  |  |
+| What is on disk and what it costs. Browser storage is private to the site unless you pick a folder. | Sampling, the system prompt, and optional API credentials. |
-A production build fails naming the missing files rather than shipping a bundle
-that only breaks after deployment.
+The image files are in [`docs/screenshots/`](docs/screenshots).
-## Project layout
+## What you can do
-```
-runanywhere-web/
- index.html Vite entry, plus the pre-paint theme script and boot screen
- src/
- main.ts Boot: cross-origin isolation, SDK init, backend registration, catalog
- app.ts Shell, drawer navigation, hash routing, the Advanced hub
- views/ One file per surface: chat, vision, voice, transcribe, speak, vad,
- segmentation, diarization, documents, storage, solutions,
- benchmarks, settings
- services/ Model catalog, engine availability, conversation store (IndexedDB),
- Hugging Face client, markdown, formatting helpers
- components/ Model selection sheet, dialogs, file drop, icons, shared notices
- styles/ design-system.css is the only token layer
- public/coi-serviceworker.js Cross-origin-isolation fallback
- scripts/release.sh Static release verify, stage, and deploy
- tests/ Manual browser test plan
- vite.config.ts Dev/preview COOP-COEP headers, WASM copy plugin, chrome86 target
- vercel.json COOP/COEP headers and SPA rewrites
-```
+| | |
+| --- | --- |
+| **Assistant** | Streaming chat with tool calling and saved conversations |
+| **Talk** | A full voice session: it listens, thinks, and answers out loud |
+| **Image and Live** | Describe a photo, or a live camera frame |
+| **Transcribe** | Batch and streaming speech to text |
+| **Read aloud** | Speak any text you give it |
+| **Documents** | Drop in `.txt`, `.md`, or `.json` and ask questions about them |
+| **Solutions** | Two packaged pipelines: a voice agent and document Q&A |
+| **Benchmarks** | One prompt at three token budgets, charted |
+| **Downloads** | Model registry, disk usage, storage folder |
-Every surface has a URL fragment (`#/vision`, `#/benchmarks`), so a tab survives
-a refresh, a pasted link, and the reload the isolation service worker performs
-on Safari.
+Segmentation and Diarization have views wired up, but no browser engine registers those
+capabilities yet, so both tabs show an unavailable placeholder.
-Views may import `@runanywhere/web` and `@runanywhere/web/browser`. They must
-not reach into `@runanywhere/web/internal` or `@runanywhere/web/backend`, and
-must not reimplement SDK routing, storage, or inference rules in UI code. See
-`AGENTS.md`.
+## Models
-## Configuration
+The picker groups models by publisher, so you pick a name you recognise and then a size. It
+carries current-generation open models across chat, vision, speech, and embedding. Sizes
+shown are measured, not estimated.
-Settings holds an API key and base URL for the session only; neither is written
-to storage. For a hosted deployment, Vite reads two build-time variables and
-boots the SDK straight into the production environment:
+A browser tab is the tightest of the four runtimes: WASM32 gives a 4 GiB address space and
+the runtime needs headroom inside it, so the picker reports what your browser can actually
+hold and marks the rest as out of reach rather than letting a download fail late.
-| Variable | Meaning |
-|---|---|
-| `VITE_RUNANYWHERE_API_KEY` | Publishable browser key. Never a server-side secret, since Vite inlines it into the bundle |
-| `VITE_RUNANYWHERE_BASE_URL` | Production API origin |
+## Build it yourself
-Both must be set, or the app boots in the development environment.
+```bash
+git clone https://github.com/RunanywhereAI/runanywhere-web.git
+cd runanywhere-web
-## Continuous integration
+npm ci
+npm run dev # http://localhost:3000
+```
-`.github/workflows/ci.yml` runs on every push to `main` and every pull request:
-`ubuntu-latest` and Node 24, then `npm ci`, `typecheck`, `lint`, `test`, `build`.
+`npm ci` pulls the SDK and every WASM artifact with it. There is no separate WASM build step
+and no Emscripten toolchain to install. You need Node 22.12 or newer.
-CI installs with `npm ci`, the same command `vercel.json` uses, so a
-`package-lock.json` out of sync with `package.json` fails the gate instead of
-breaking production. Commit the regenerated lock with any dependency change:
-`npm install` would quietly repair the lock locally and hide the breakage.
+The one thing that trips people up: the page has to be cross-origin isolated for
+`SharedArrayBuffer`. The dev server and `vercel.json` both send COOP `same-origin` and COEP
+`require-corp`, and `public/coi-serviceworker.js` covers hosts that cannot.
-## Troubleshooting
+[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) has the scripts table, the WASM artifact list,
+deployment configuration, and troubleshooting.
-| Symptom | Fix |
-|---|---|
-| `npm ci` fails with `Missing: @runanywhere/… from lock file` | `package.json` and `package-lock.json` drifted. Run `npm install` and commit the refreshed lock |
-| `SharedArrayBuffer is not defined` | The page is not cross-origin isolated. Serve with COOP `same-origin` and COEP `require-corp` |
-| Build fails naming missing `racommons-*` files | The SDK packages did not install completely. Re-run `npm ci` |
-| Model download stalls or workers hang | Hard-reload to clear a stale service worker, then recheck the COOP/COEP headers |
-| A WebGPU model produces garbage | Switch that model to the CPU variant |
+## Architecture
-## Related
+Four npm packages, no monorepo checkout, no source aliases. What `npm install` resolves is
+what the app runs against, currently `0.20.24`.
-| Resource | Link |
-|---|---|
-| iOS example | [github.com/RunanywhereAI/runanywhere-ios](https://github.com/RunanywhereAI/runanywhere-ios) |
-| Android example | [github.com/RunanywhereAI/runanywhere-android](https://github.com/RunanywhereAI/runanywhere-android) |
-| Electron example | [github.com/RunanywhereAI/runanywhere-electron](https://github.com/RunanywhereAI/runanywhere-electron) |
-| SDK monorepo | [github.com/RunanywhereAI/runanywhere-sdks](https://github.com/RunanywhereAI/runanywhere-sdks) |
+```
+ index.html → main.ts → app.ts
+ boot screen SDK init shell, routing, views
+ │
+ ┌─────────────────┴──────────────────┐
+ │ @runanywhere/web │ lifecycle + facades
+ └─────────────────┬──────────────────┘
+ │
+ ┌─────────────────┼──────────────────┐
+ │ │ │
+ web-llamacpp web-onnx proto-ts
+ LLM · VLM STT·TTS·VAD·embed generated types
+ CPU + WebGPU CPU + WebGPU
+ │
+ ▼
+ racommons.wasm, the same C++ core
+ that ships in the Swift, Kotlin, and Electron apps
+```
+
+Views call `RunAnywhere.*` and nothing deeper. They may import `@runanywhere/web` and
+`@runanywhere/web/browser`, never `/internal` or `/backend`, and they do not reimplement
+routing, storage, or inference rules in UI code.
+
+Every surface has a URL fragment (`#/vision`, `#/benchmarks`), so a tab survives a refresh, a
+pasted link, and the reload the isolation service worker performs on Safari.
+
+| Reference | |
+| --- | --- |
+| Which SDK call each view makes | [`docs/reference/sdk-surface-by-view.md`](docs/reference/sdk-surface-by-view.md) |
+| WASM artifacts and how they are copied | [`docs/reference/wasm-artifacts.md`](docs/reference/wasm-artifacts.md) |
+| Building, deploying, troubleshooting | [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) |
+| Contributor conventions | [`AGENTS.md`](AGENTS.md) |
+
+## The other apps
+
+| Platform | Repo |
+| --- | --- |
+| iOS and macOS, Swift | [runanywhere-ios](https://github.com/RunanywhereAI/runanywhere-ios) |
+| Android, Kotlin | [runanywhere-android](https://github.com/RunanywhereAI/runanywhere-android) |
+| Windows, Electron | [runanywhere-electron](https://github.com/RunanywhereAI/runanywhere-electron) |
+| SDK monorepo | [runanywhere-sdks](https://github.com/RunanywhereAI/runanywhere-sdks) |
+| Documentation | [docs.runanywhere.ai](https://docs.runanywhere.ai) |
| Discord | [discord.gg/N359FBbDVd](https://discord.gg/N359FBbDVd) |
-| Email | founders@runanywhere.ai |
## License
-RunAnywhere License, based on Apache 2.0 with additional commercial-use terms.
-See [LICENSE](LICENSE).
+RunAnywhere License, Apache 2.0 based with additional commercial-use terms. See
+[LICENSE](LICENSE).
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
new file mode 100644
index 00000000..5cb1d3dd
--- /dev/null
+++ b/docs/DEVELOPMENT.md
@@ -0,0 +1,146 @@
+# Development reference
+
+Detail moved out of the root README so it stays a consumer-facing page. Everything here
+is about building and shipping the app, not about using it.
+
+## Requirements
+
+| Item | Minimum |
+|---|---|
+| Node.js | 22.12 (CI runs 24) |
+| Browser | Chrome or Edge 86, Safari, Firefox |
+| Cross-origin isolation | Required for `SharedArrayBuffer`. The dev server and `vercel.json` send COOP `same-origin` and COEP `require-corp`; `public/coi-serviceworker.js` covers hosts that cannot |
+| Disk space | Hundreds of megabytes to a few gigabytes for downloaded models, held in OPFS |
+
+## Run it
+
+```bash
+git clone https://github.com/RunanywhereAI/runanywhere-web.git
+cd runanywhere-web
+
+npm ci
+npm run dev # http://localhost:3000
+```
+
+`npm ci` pulls the SDK and its WASM artifacts. There is no separate WASM build
+step and no Emscripten toolchain to install.
+
+| Script | What it does |
+|---|---|
+| `npm run dev` | Vite dev server on `localhost:3000` (strict port) with COOP/COEP headers |
+| `npm run build` | Production bundle into `dist/`, including the Emscripten `.js`/`.wasm` pairs |
+| `npm run preview` | Serve the built `dist/` on `localhost:3000` |
+| `npm run typecheck` | `tsc --noEmit` |
+| `npm run lint` | ESLint over `src`, zero warnings tolerated |
+| `npm run test` | Vitest over `src/**/*.test.ts` |
+| `npm run release:build` | `build` followed by `release:verify` |
+| `npm run release:verify` | Assert `dist/` holds every required runtime file |
+| `npm run release:deploy` | Build, verify, and deploy a prebuilt static bundle to Vercel |
+
+## SDK packages
+
+Everything comes from the npm registry. There are no `file:` links, no `paths`
+aliases, and no monorepo checkout, so `npm install` is the only thing that
+decides which SDK version the app runs against. Both the TypeScript modules and
+every WASM artifact come out of `node_modules/@runanywhere/*`.
+
+```jsonc
+"dependencies": {
+ "@runanywhere/proto-ts": "^0.20.24",
+ "@runanywhere/web": "^0.20.24",
+ "@runanywhere/web-llamacpp": "^0.20.24",
+ "@runanywhere/web-onnx": "^0.20.24"
+}
+```
+
+| Package | Role |
+|---|---|
+| `@runanywhere/web` | SDK lifecycle and the inference facades. `@runanywhere/web/browser` adds `AudioCapture`, `AudioFileLoader`, and `VideoCapture` |
+| `@runanywhere/web-llamacpp` | LLM and VLM backend registration, CPU and WebGPU builds |
+| `@runanywhere/web-onnx` | Sherpa-ONNX backend registration for STT, TTS, VAD, and embeddings |
+| `@runanywhere/proto-ts` | Generated protobuf types for models, events, errors, and modalities |
+
+To try an unreleased SDK build, `npm install` a packed tarball or use
+`npm link`. Do not reintroduce a source alias.
+
+## WASM artifacts
+
+Five JS/WASM pairs ship across the three SDK packages. `vite.config.ts` copies
+each canonical pair into `dist/assets/` next to Vite's hashed copy, because
+Emscripten's pthread glue starts its workers from the original filename.
+
+| Pair | Package |
+|---|---|
+| `racommons.{js,wasm}` | `@runanywhere/web` |
+| `racommons-llamacpp.{js,wasm}` | `@runanywhere/web-llamacpp` |
+| `racommons-llamacpp-webgpu.{js,wasm}` | `@runanywhere/web-llamacpp` |
+| `racommons-onnx-sherpa.{js,wasm}` | `@runanywhere/web-onnx` |
+| `racommons-onnx-sherpa-webgpu.{js,wasm}` | `@runanywhere/web-onnx` |
+
+A production build fails naming the missing files rather than shipping a bundle
+that only breaks after deployment.
+
+## Project layout
+
+```
+runanywhere-web/
+ index.html Vite entry, plus the pre-paint theme script and boot screen
+ src/
+ main.ts Boot: cross-origin isolation, SDK init, backend registration, catalog
+ app.ts Shell, drawer navigation, hash routing, the Advanced hub
+ views/ One file per surface: chat, vision, voice, transcribe, speak, vad,
+ segmentation, diarization, documents, storage, solutions,
+ benchmarks, settings
+ services/ Model catalog, engine availability, conversation store (IndexedDB),
+ Hugging Face client, markdown, formatting helpers
+ components/ Model selection sheet, dialogs, file drop, icons, shared notices
+ styles/ design-system.css is the only token layer
+ public/coi-serviceworker.js Cross-origin-isolation fallback
+ scripts/release.sh Static release verify, stage, and deploy
+ tests/ Manual browser test plan
+ vite.config.ts Dev/preview COOP-COEP headers, WASM copy plugin, chrome86 target
+ vercel.json COOP/COEP headers and SPA rewrites
+```
+
+Every surface has a URL fragment (`#/vision`, `#/benchmarks`), so a tab survives
+a refresh, a pasted link, and the reload the isolation service worker performs
+on Safari.
+
+Views may import `@runanywhere/web` and `@runanywhere/web/browser`. They must
+not reach into `@runanywhere/web/internal` or `@runanywhere/web/backend`, and
+must not reimplement SDK routing, storage, or inference rules in UI code. See
+`AGENTS.md`.
+
+## Configuration
+
+Settings holds an API key and base URL for the session only; neither is written
+to storage. For a hosted deployment, Vite reads two build-time variables and
+boots the SDK straight into the production environment:
+
+| Variable | Meaning |
+|---|---|
+| `VITE_RUNANYWHERE_API_KEY` | Publishable browser key. Never a server-side secret, since Vite inlines it into the bundle |
+| `VITE_RUNANYWHERE_BASE_URL` | Production API origin |
+
+Both must be set, or the app boots in the development environment.
+
+## Continuous integration
+
+`.github/workflows/ci.yml` runs on every push to `main` and every pull request:
+`ubuntu-latest` and Node 24, then `npm ci`, `typecheck`, `lint`, `test`, `build`.
+
+CI installs with `npm ci`, the same command `vercel.json` uses, so a
+`package-lock.json` out of sync with `package.json` fails the gate instead of
+breaking production. Commit the regenerated lock with any dependency change:
+`npm install` would quietly repair the lock locally and hide the breakage.
+
+## Troubleshooting
+
+| Symptom | Fix |
+|---|---|
+| `npm ci` fails with `Missing: @runanywhere/… from lock file` | `package.json` and `package-lock.json` drifted. Run `npm install` and commit the refreshed lock |
+| `SharedArrayBuffer is not defined` | The page is not cross-origin isolated. Serve with COOP `same-origin` and COEP `require-corp` |
+| Build fails naming missing `racommons-*` files | The SDK packages did not install completely. Re-run `npm ci` |
+| Model download stalls or workers hang | Hard-reload to clear a stale service worker, then recheck the COOP/COEP headers |
+| A WebGPU model produces garbage | Switch that model to the CPU variant |
+
diff --git a/docs/screenshots/01-chat.jpg b/docs/screenshots/01-chat.jpg
new file mode 100644
index 00000000..5d1566a3
Binary files /dev/null and b/docs/screenshots/01-chat.jpg differ
diff --git a/docs/screenshots/02-model-picker.jpg b/docs/screenshots/02-model-picker.jpg
new file mode 100644
index 00000000..046a88d3
Binary files /dev/null and b/docs/screenshots/02-model-picker.jpg differ
diff --git a/docs/screenshots/03-voice.jpg b/docs/screenshots/03-voice.jpg
new file mode 100644
index 00000000..06969e3a
Binary files /dev/null and b/docs/screenshots/03-voice.jpg differ
diff --git a/docs/screenshots/04-advanced.jpg b/docs/screenshots/04-advanced.jpg
new file mode 100644
index 00000000..49f2e63b
Binary files /dev/null and b/docs/screenshots/04-advanced.jpg differ
diff --git a/docs/screenshots/05-downloads.jpg b/docs/screenshots/05-downloads.jpg
new file mode 100644
index 00000000..36cdd68b
Binary files /dev/null and b/docs/screenshots/05-downloads.jpg differ
diff --git a/docs/screenshots/06-settings.jpg b/docs/screenshots/06-settings.jpg
new file mode 100644
index 00000000..78cf518b
Binary files /dev/null and b/docs/screenshots/06-settings.jpg differ
diff --git a/src/services/model-catalog.test.ts b/src/services/model-catalog.test.ts
index b8b8624a..ef795552 100644
--- a/src/services/model-catalog.test.ts
+++ b/src/services/model-catalog.test.ts
@@ -26,10 +26,14 @@ describe('Web catalog integrity', () => {
it('keeps one quantization per llama.cpp model', () => {
// Two quants of the same model cost a catalog slot and a "which one?"
- // decision without adding a capability. lfm2-350m-q8_0 is the row this
- // rule drops (the file header documents the omission).
- expect(getCatalog().some(({ id }) => id === 'lfm2-350m-q8_0')).toBe(false);
- expect(getCatalog().some(({ id }) => id === 'lfm2-350m-q4_k_m')).toBe(true);
+ // decision without adding a capability. Asserted over the whole catalog
+ // rather than one example row, so it keeps holding as models turn over.
+ const quant = /-(q\d[_a-z0-9]*|tq\d_\d|iq\d[_a-z0-9]*)$/;
+ const bases = getCatalog()
+ .filter(({ format }) => format === ModelFormat.MODEL_FORMAT_GGUF)
+ .map(({ id }) => id.replace(quant, ''));
+
+ expect(bases).toEqual([...new Set(bases)]);
});
it('registers the LFM2.5 230M Q4_K_M row and clears the WASM32 gate', () => {
diff --git a/src/services/model-catalog.ts b/src/services/model-catalog.ts
index aa377915..53f4bda2 100644
--- a/src/services/model-catalog.ts
+++ b/src/services/model-catalog.ts
@@ -124,72 +124,79 @@ const WASM32_ADDRESS_SPACE_BYTES = 2 ** 32;
const MINIMUM_WASM_RUNTIME_HEADROOM_BYTES = 512 * 1024 * 1024;
const CATALOG: readonly CatalogEntry[] = [
- // ---------- LLM (llama.cpp) ----------
-
- // --- SmolLM2 (HuggingFaceTB) ---
{
- // Preserve the cross-SDK model ID while using the model author's official
- // instruction-tuned artifact; the historical base-model URL cannot
- // reliably follow chat instructions.
- id: 'smollm2-360m-q8_0',
- name: 'SmolLM2 360M Q8_0',
- description: 'Small instruction-tuned LLM that runs in the WASM build.',
+ id: 'qwen3.5-0.8b-q4_k_m',
+ name: 'Qwen3.5 0.8B Q4_K_M',
+ description: 'Latest-generation compact Qwen; smallest chat model that still follows tool syntax.',
category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
format: ModelFormat.MODEL_FORMAT_GGUF,
downloadUrl:
- 'https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct-GGUF/resolve/main/smollm2-360m-instruct-q8_0.gguf',
- downloadSizeBytes: 386_404_992,
- memoryRequiredBytes: 500_000_000,
- contextLength: 2048,
+ 'https://huggingface.co/unsloth/Qwen3.5-0.8B-GGUF/resolve/main/Qwen3.5-0.8B-Q4_K_M.gguf',
+ downloadSizeBytes: 532_517_120,
+ memoryRequiredBytes: 900_000_000,
+ contextLength: 4096,
+ supportsThinking: true,
},
-
- // --- Qwen (Alibaba) ---
{
- // iOS parity: ModelCatalogBootstrap.swift:50-59 — Q6_K quant, base model
- // of the seeded abliterated LoRA adapter, hence supportsLora.
- id: 'qwen2.5-0.5b-instruct-q6_k',
- name: 'Qwen 2.5 0.5B Instruct Q6_K',
- description: 'Compact instruction LLM; base model of the abliterated LoRA adapter.',
+ id: 'qwen3.5-2b-q4_k_m',
+ name: 'Qwen3.5 2B Q4_K_M',
+ description: 'Mid-size Qwen3.5; the default chat model on the desktop builds.',
category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
format: ModelFormat.MODEL_FORMAT_GGUF,
downloadUrl:
- 'https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q6_k.gguf',
- downloadSizeBytes: 650_379_104,
- memoryRequiredBytes: 750_000_000,
+ 'https://huggingface.co/unsloth/Qwen3.5-2B-GGUF/resolve/main/Qwen3.5-2B-Q4_K_M.gguf',
+ downloadSizeBytes: 1_280_835_840,
+ memoryRequiredBytes: 1_800_000_000,
contextLength: 4096,
- supportsLora: true,
+ supportsThinking: true,
},
{
- // iOS parity: ModelCatalogBootstrap.swift:102-109
- id: 'qwen3-0.6b-q4_k_m',
- name: 'Qwen3 0.6B Q4_K_M',
- description: 'Qwen3 series compact LLM with thinking-mode support.',
+ id: 'qwen3.5-4b-q4_k_m',
+ name: 'Qwen3.5 4B Q4_K_M',
+ description: 'Largest Qwen3.5 that still clears the 4 GiB WASM32 address space.',
category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
format: ModelFormat.MODEL_FORMAT_GGUF,
downloadUrl:
- 'https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_K_M.gguf',
- downloadSizeBytes: 396_705_472,
- memoryRequiredBytes: 500_000_000,
+ 'https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf',
+ downloadSizeBytes: 2_740_937_888,
+ memoryRequiredBytes: 3_200_000_000,
contextLength: 4096,
supportsThinking: true,
},
+
+ // --- Granite 4.1 (IBM) ---
{
- // iOS parity: ModelCatalogBootstrap.swift:118-125
- id: 'qwen3-4b-q4_k_m',
- name: 'Qwen3 4B Q4_K_M',
- description: 'Qwen3 series larger LLM with thinking-mode support.',
+ id: 'granite-4.1-3b-q4_k_m',
+ name: 'IBM Granite 4.1 3B Q4_K_M',
+ description: 'Apache-2.0 instruction model tuned for retrieval and tool use.',
category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
format: ModelFormat.MODEL_FORMAT_GGUF,
downloadUrl:
- 'https://huggingface.co/unsloth/Qwen3-4B-GGUF/resolve/main/Qwen3-4B-Q4_K_M.gguf',
- downloadSizeBytes: 2_497_281_312,
- memoryRequiredBytes: 3_000_000_000,
+ 'https://huggingface.co/unsloth/granite-4.1-3b-GGUF/resolve/main/granite-4.1-3b-Q4_K_M.gguf',
+ downloadSizeBytes: 2_099_502_400,
+ memoryRequiredBytes: 2_600_000_000,
+ contextLength: 4096,
+ },
+
+ // --- Gemma 4 (Google DeepMind) ---
+ // E2B only. The E4B weights are 4.98 GB, which cannot fit the WASM32 address
+ // space no matter how much RAM the machine has.
+ {
+ id: 'gemma-4-e2b-it-q4_k_m',
+ name: 'Gemma 4 E2B IT Q4_K_M',
+ description: 'Multimodal-capable Gemma 4 in its smallest instruction-tuned size.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/unsloth/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q4_K_M.gguf',
+ downloadSizeBytes: 3_106_738_272,
+ memoryRequiredBytes: 3_400_000_000,
contextLength: 4096,
- supportsThinking: true,
},
// --- LFM2 / LFM2.5 (Liquid AI) ---
@@ -211,25 +218,6 @@ const CATALOG: readonly CatalogEntry[] = [
memoryRequiredBytes: 190_000_000,
contextLength: 2048,
},
- {
- // iOS parity: ModelCatalogBootstrap.swift:67-73. ONE quantization per
- // model: the Q8_0 sibling of this row is deliberately absent (see the
- // omission list in the file header) — two quants of the same 350M model
- // differ only in bytes (229 MB vs 379 MB), so the second row costs a
- // catalog slot and a "which one do I pick?" decision without adding a
- // capability.
- id: 'lfm2-350m-q4_k_m',
- name: 'LiquidAI LFM2 350M Q4_K_M',
- description: 'LiquidAI compact LLM tuned for fast on-device chat.',
- category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
- framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
- format: ModelFormat.MODEL_FORMAT_GGUF,
- downloadUrl:
- 'https://huggingface.co/LiquidAI/LFM2-350M-GGUF/resolve/main/LFM2-350M-Q4_K_M.gguf',
- downloadSizeBytes: 229_309_376,
- memoryRequiredBytes: 300_000_000,
- contextLength: 2048,
- },
// --- Nemotron (NVIDIA) ---
{
@@ -396,36 +384,6 @@ const CATALOG: readonly CatalogEntry[] = [
},
],
},
-
- // --- LFM2-VL (Liquid AI) ---
- {
- // iOS parity: ModelCatalogBootstrap.swift:159-171 (multi-file)
- id: 'lfm2-vl-450m-q8_0',
- name: 'LFM2-VL 450M',
- description: 'LiquidAI LFM2-VL vision-language model (primary GGUF + mmproj sidecar).',
- category: ModelCategory.MODEL_CATEGORY_MULTIMODAL,
- framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
- format: ModelFormat.MODEL_FORMAT_GGUF,
- downloadUrl:
- 'https://huggingface.co/runanywhere/LFM2-VL-450M-GGUF/resolve/main/LFM2-VL-450M-Q8_0.gguf',
- downloadSizeBytes: 483_105_280,
- memoryRequiredBytes: 650_000_000,
- contextLength: 2048,
- files: [
- {
- url: 'https://huggingface.co/runanywhere/LFM2-VL-450M-GGUF/resolve/main/LFM2-VL-450M-Q8_0.gguf',
- filename: 'LFM2-VL-450M-Q8_0.gguf',
- role: ModelFileRole.MODEL_FILE_ROLE_PRIMARY_MODEL,
- sizeBytes: 379_215_264,
- },
- {
- url: 'https://huggingface.co/runanywhere/LFM2-VL-450M-GGUF/resolve/main/mmproj-LFM2-VL-450M-Q8_0.gguf',
- filename: 'mmproj-LFM2-VL-450M-Q8_0.gguf',
- role: ModelFileRole.MODEL_FILE_ROLE_VISION_PROJECTOR,
- sizeBytes: 103_890_016,
- },
- ],
- },
{
// LiquidAI's LFM2.5-VL 3B — the vision-language sibling of the LFM2.5 text
// rows above, and the largest VLM Liquid publishes as GGUF. Listed for
@@ -469,6 +427,278 @@ const CATALOG: readonly CatalogEntry[] = [
],
},
+ // --- Added from the verified model list; oversized rows stay catalogued and
+ // are gated by webModelCompatibility rather than hidden. ---
+ {
+ id: 'lfm2.5-1.2b-instruct-q4_k_m',
+ name: 'LFM2.5 1.2B Instruct Q4_K_M',
+ description: 'Liquid AI LFM2.5, 1.2B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct-GGUF/resolve/main/LFM2.5-1.2B-Instruct-Q4_K_M.gguf',
+ downloadSizeBytes: 730_895_168,
+ memoryRequiredBytes: 891_692_104,
+ contextLength: 4096,
+ },
+ {
+ id: 'lfm2.5-1.2b-thinking-q4_k_m',
+ name: 'LFM2.5 1.2B Thinking Q4_K_M',
+ description: 'Liquid AI LFM2.5, 1.2B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/LiquidAI/LFM2.5-1.2B-Thinking-GGUF/resolve/main/LFM2.5-1.2B-Thinking-Q4_K_M.gguf',
+ downloadSizeBytes: 730_895_360,
+ memoryRequiredBytes: 891_692_339,
+ contextLength: 4096,
+ supportsThinking: true,
+ },
+ {
+ id: 'lfm2.5-2.6b-q4_k_m',
+ name: 'LFM2.5 2.6B Q4_K_M',
+ description: 'Liquid AI LFM2.5, 2.6B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/LiquidAI/LFM2.5-2.6B-GGUF/resolve/main/LFM2.5-2.6B-Q4_K_M.gguf',
+ downloadSizeBytes: 1_674_455_040,
+ memoryRequiredBytes: 2_042_835_148,
+ contextLength: 4096,
+ supportsThinking: true,
+ },
+ {
+ id: 'gemma-4-e4b-it-q4_k_m',
+ name: 'Gemma 4 E4B IT Q4_K_M',
+ description: 'Google Gemma 4, E4B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-Q4_K_M.gguf',
+ downloadSizeBytes: 4_977_171_584,
+ memoryRequiredBytes: 6_072_149_332,
+ contextLength: 4096,
+ },
+ {
+ id: 'granite-4.1-8b-q4_k_m',
+ name: 'IBM Granite 4.1 8B Q4_K_M',
+ description: 'IBM Granite 4.1, 8B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/unsloth/granite-4.1-8b-GGUF/resolve/main/granite-4.1-8b-Q4_K_M.gguf',
+ downloadSizeBytes: 5_347_915_136,
+ memoryRequiredBytes: 6_524_456_465,
+ contextLength: 4096,
+ },
+ {
+ id: 'qwen3.5-9b-q4_k_m',
+ name: 'Qwen3.5 9B Q4_K_M',
+ description: 'Alibaba Qwen3.5, 9B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf',
+ downloadSizeBytes: 5_680_522_464,
+ memoryRequiredBytes: 6_930_237_406,
+ contextLength: 4096,
+ supportsThinking: true,
+ },
+ {
+ id: 'gemma-4-12b-it-q4_k_m',
+ name: 'Gemma 4 12B IT Q4_K_M',
+ description: 'Google Gemma 4, 12B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/unsloth/gemma-4-12b-it-GGUF/resolve/main/gemma-4-12b-it-Q4_K_M.gguf',
+ downloadSizeBytes: 7_121_861_440,
+ memoryRequiredBytes: 8_688_670_956,
+ contextLength: 4096,
+ },
+ {
+ id: 'maple-preview-tq1_0',
+ name: 'Maple Preview 20B-A1B TQ1_0 (1-bit)',
+ description: 'Deepgrove Maple, 20B-A1B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/deepgrove/maple-preview-GGUF/resolve/main/maple-preview-TQ1_0-head-Q4_K.gguf',
+ downloadSizeBytes: 4_984_016_416,
+ memoryRequiredBytes: 6_080_500_027,
+ contextLength: 4096,
+ supportsThinking: true,
+ },
+ {
+ id: 'qwen3.8-27b-q4_k_m',
+ name: 'Qwen3.8 27B UD-Q4_K_M',
+ description: 'Alibaba Qwen3.8, 27B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/unsloth/Qwen3.8-27B-GGUF/resolve/main/Qwen3.8-27B-UD-Q4_K_M.gguf',
+ downloadSizeBytes: 16_464_440_224,
+ memoryRequiredBytes: 20_086_617_073,
+ contextLength: 4096,
+ supportsThinking: true,
+ },
+ {
+ id: 'gemma-4-26b-a4b-it-q4_k_m',
+ name: 'Gemma 4 26B-A4B IT Q4_K_M',
+ description: 'Google Gemma 4, 26B-A4B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/unsloth/gemma-4-26B-A4B-it-GGUF/resolve/main/gemma-4-26B-A4B-it-UD-Q4_K_M.gguf',
+ downloadSizeBytes: 16_947_541_728,
+ memoryRequiredBytes: 20_676_000_908,
+ contextLength: 4096,
+ },
+ {
+ id: 'granite-4.1-30b-q4_k_m',
+ name: 'IBM Granite 4.1 30B Q4_K_M',
+ description: 'IBM Granite 4.1, 30B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/unsloth/granite-4.1-30b-GGUF/resolve/main/granite-4.1-30b-Q4_K_M.gguf',
+ downloadSizeBytes: 17_490_241_472,
+ memoryRequiredBytes: 21_338_094_595,
+ contextLength: 4096,
+ },
+ {
+ id: 'gemma-4-31b-it-q4_k_m',
+ name: 'Gemma 4 31B IT Q4_K_M',
+ description: 'Google Gemma 4, 31B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_LANGUAGE,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/unsloth/gemma-4-31b-it-GGUF/resolve/main/gemma-4-31B-it-Q4_K_M.gguf',
+ downloadSizeBytes: 18_323_733_440,
+ memoryRequiredBytes: 22_354_954_796,
+ contextLength: 4096,
+ },
+ {
+ id: 'lfm2.5-vl-1.6b-q4_k_m',
+ name: 'LFM2.5 VL 1.6B Q4_K_M',
+ description: 'Liquid AI LFM2.5, 1.6B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_MULTIMODAL,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/LiquidAI/LFM2.5-VL-1.6B-GGUF/resolve/main/LFM2.5-VL-1.6B-Q4_K_M.gguf',
+ downloadSizeBytes: 730_896_256,
+ memoryRequiredBytes: 891_693_432,
+ contextLength: 4096,
+ files: [
+ {
+ url: 'https://huggingface.co/LiquidAI/LFM2.5-VL-1.6B-GGUF/resolve/main/LFM2.5-VL-1.6B-Q4_K_M.gguf',
+ filename: 'LFM2.5-VL-1.6B-Q4_K_M.gguf',
+ role: ModelFileRole.MODEL_FILE_ROLE_PRIMARY_MODEL,
+ sizeBytes: 730_896_256,
+ },
+ {
+ url: 'https://huggingface.co/LiquidAI/LFM2.5-VL-1.6B-GGUF/resolve/main/mmproj-LFM2.5-VL-1.6b-BF16.gguf',
+ filename: 'mmproj-LFM2.5-VL-1.6b-BF16.gguf',
+ role: ModelFileRole.MODEL_FILE_ROLE_VISION_PROJECTOR,
+ sizeBytes: 855_763_328,
+ },
+ ],
+ },
+ {
+ id: 'gemma-4-e2b-it-vision-q4_0',
+ name: 'Gemma 4 E2B Vision Q4_0',
+ description: 'Google Gemma 4, E2B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_MULTIMODAL,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/ggml-org/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q4_0.gguf',
+ downloadSizeBytes: 2_841_481_184,
+ memoryRequiredBytes: 3_466_607_044,
+ contextLength: 4096,
+ files: [
+ {
+ url: 'https://huggingface.co/ggml-org/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q4_0.gguf',
+ filename: 'gemma-4-E2B-it-Q4_0.gguf',
+ role: ModelFileRole.MODEL_FILE_ROLE_PRIMARY_MODEL,
+ sizeBytes: 2_841_481_184,
+ },
+ {
+ url: 'https://huggingface.co/ggml-org/gemma-4-E2B-it-GGUF/resolve/main/mmproj-gemma-4-E2B-it-BF16.gguf',
+ filename: 'mmproj-gemma-4-E2B-it-BF16.gguf',
+ role: ModelFileRole.MODEL_FILE_ROLE_VISION_PROJECTOR,
+ sizeBytes: 986_833_664,
+ },
+ ],
+ },
+ {
+ id: 'gemma-4-e4b-it-vision-q4_0',
+ name: 'Gemma 4 E4B Vision Q4_0',
+ description: 'Google Gemma 4, E4B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_MULTIMODAL,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/ggml-org/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-Q4_0.gguf',
+ downloadSizeBytes: 4_590_807_392,
+ memoryRequiredBytes: 5_600_785_018,
+ contextLength: 4096,
+ files: [
+ {
+ url: 'https://huggingface.co/ggml-org/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-Q4_0.gguf',
+ filename: 'gemma-4-E4B-it-Q4_0.gguf',
+ role: ModelFileRole.MODEL_FILE_ROLE_PRIMARY_MODEL,
+ sizeBytes: 4_590_807_392,
+ },
+ {
+ url: 'https://huggingface.co/ggml-org/gemma-4-E4B-it-GGUF/resolve/main/mmproj-gemma-4-E4B-it-BF16.gguf',
+ filename: 'mmproj-gemma-4-E4B-it-BF16.gguf',
+ role: ModelFileRole.MODEL_FILE_ROLE_VISION_PROJECTOR,
+ sizeBytes: 991_552_256,
+ },
+ ],
+ },
+ {
+ id: 'muse-glimmer-30b-q4_k_xl',
+ name: 'Muse Glimmer 30B UD-Q4_K_XL',
+ description: 'Meta Muse Glimmer, 30B parameters.',
+ category: ModelCategory.MODEL_CATEGORY_MULTIMODAL,
+ framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
+ format: ModelFormat.MODEL_FORMAT_GGUF,
+ downloadUrl:
+ 'https://huggingface.co/unsloth/Muse-Glimmer-30B-GGUF/resolve/main/Muse-Glimmer-30B-UD-Q4_K_XL.gguf',
+ downloadSizeBytes: 15_878_222_368,
+ memoryRequiredBytes: 19_371_431_288,
+ contextLength: 4096,
+ files: [
+ {
+ url: 'https://huggingface.co/unsloth/Muse-Glimmer-30B-GGUF/resolve/main/Muse-Glimmer-30B-UD-Q4_K_XL.gguf',
+ filename: 'Muse-Glimmer-30B-UD-Q4_K_XL.gguf',
+ role: ModelFileRole.MODEL_FILE_ROLE_PRIMARY_MODEL,
+ sizeBytes: 15_878_222_368,
+ },
+ {
+ url: 'https://huggingface.co/unsloth/Muse-Glimmer-30B-GGUF/resolve/main/mmproj-Muse-Glimmer-30B-BF16.gguf',
+ filename: 'mmproj-Muse-Glimmer-30B-BF16.gguf',
+ role: ModelFileRole.MODEL_FILE_ROLE_VISION_PROJECTOR,
+ sizeBytes: 3_849_173_728,
+ },
+ ],
+ },
+
// ---------- STT (Sherpa-ONNX) ----------
// --- Nemotron (NVIDIA) ---
@@ -748,18 +978,9 @@ interface LoraCatalogEntry {
defaultScale: number;
}
-const LORA_ADAPTERS: readonly LoraCatalogEntry[] = [
- {
- id: 'abliterated-lora',
- name: 'Abliterated LoRA (F16)',
- description: 'Removes refusal behavior — model answers directly without disclaimers',
- url: 'https://huggingface.co/Void2377/qwen-lora-gguf/resolve/main/qwen2.5-0.5b-abliterated-lora-f16.gguf',
- filename: 'qwen2.5-0.5b-abliterated-lora-f16.gguf',
- compatibleModels: ['qwen2.5-0.5b-instruct-q6_k'],
- sizeBytes: 17_620_224,
- defaultScale: 1.0,
- },
-];
+// The only adapter we shipped was trained for qwen2.5-0.5b, which this catalog
+// no longer carries. Re-add an adapter and its base together.
+const LORA_ADAPTERS: readonly LoraCatalogEntry[] = [];
// ---------------------------------------------------------------------------
// Registration — delegated to `RunAnywhere.models.register`.
diff --git a/src/services/model-display.ts b/src/services/model-display.ts
index 66743c7f..9196545c 100644
--- a/src/services/model-display.ts
+++ b/src/services/model-display.ts
@@ -325,18 +325,26 @@ const ORG_MATCHERS: ReadonlyArray<{
},
{ key: 'deepseek', name: 'DeepSeek', test: /deepseek/ },
{ key: 'prism', name: 'Prism', test: /bonsai|prismml|prism-?ml/ },
- { key: 'microsoft', name: 'Microsoft', test: /\bphi\b/ },
+ { key: 'deepgrove', name: 'Deepgrove', test: /maple/ },
+ { key: 'ibm', name: 'IBM', test: /granite/ },
+ // `fara` rides with Microsoft's `phi`: Fara1.5 ships mirrored under our own HF
+ // org, so the catalog row names no upstream publisher. Filing it by its own
+ // name beats guessing one into a UI label.
+ { key: 'microsoft', name: 'Microsoft', test: /\bphi\b|fara/ },
{ key: 'google', name: 'Google', test: /gemma|embeddinggemma|siglip/ },
- { key: 'meta', name: 'Meta', test: /llama/ },
+ // Muse Glimmer is Meta's, per the catalog row's own name, but this rule
+ // matched only the Llama family.
+ { key: 'meta', name: 'Meta', test: /llama|muse-glimmer|muse_glimmer/ },
{ key: 'alibaba', name: 'Alibaba', test: /qwen/ },
{ key: 'liquid', name: 'Liquid AI', test: /lfm2/ },
- { key: 'mistral', name: 'Mistral AI', test: /mistral/ },
+ { key: 'mistral', name: 'Mistral AI', test: /mistral|ministral/ },
{ key: 'hugging-face', name: 'Hugging Face', test: /smollm|smolvlm/ },
{ key: 'openai', name: 'OpenAI', test: /whisper/ },
+ { key: 'zhipu', name: 'Zhipu AI', test: /\bglm\b|glm-/ },
{
key: 'open-source',
name: 'Open source',
- test: /internvl|lama_dilated|moonshine|melo|kokoro|kitten|piper|vits|silero|vad|minilm|soprano|pocket-tts|glm-asr/,
+ test: /internvl|lama_dilated|moonshine|melo|kokoro|kitten|piper|vits|silero|vad|minilm|soprano|pocket-tts|supertonic/,
},
];
diff --git a/src/services/model-org.test.ts b/src/services/model-org.test.ts
index ba14ae8a..93c8f554 100644
--- a/src/services/model-org.test.ts
+++ b/src/services/model-org.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
+import { getCatalog } from './model-catalog';
import { modelOrg } from './model-display';
import type { CatalogEntry } from './model-catalog';
import { InferenceFramework, ModelCategory } from '@runanywhere/web';
@@ -29,4 +30,23 @@ describe('modelOrg', () => {
expect(modelOrg(entry('deepseek_r1_distill_qwen_1_5b', 'DeepSeek R1 Distill Qwen')).name).toBe('DeepSeek');
expect(modelOrg(entry('qwen3-4b', 'Qwen3 4B')).name).toBe('Alibaba');
});
+
+ it('files the publishers the catalog rebuild introduced', () => {
+ expect(modelOrg(entry('granite-4.1-3b-q4_k_m', 'IBM Granite 4.1 3B')).name).toBe('IBM');
+ expect(modelOrg(entry('maple-preview-tq1_0', 'Maple Preview 20B-A1B')).name).toBe('Deepgrove');
+ expect(modelOrg(entry('muse-glimmer-30b-q4_k_xl', 'Meta Muse Glimmer 30B')).name).toBe('Meta');
+ expect(modelOrg(entry('fara1.5-4b-q4_k_m', 'Fara1.5 4B Computer-Use Agent')).name).toBe('Microsoft');
+ });
+
+ // The rules are hand-kept and the catalog is edited in its own PR, so the
+ // thing worth locking is coverage rather than any single mapping: nothing
+ // should reach the picker as an unnamed publisher.
+ it('leaves nothing in the catalog without a named publisher', () => {
+ const community = /piper|silero|minilm|supertonic|vits/;
+ const unnamed = getCatalog()
+ .filter((e) => !community.test(e.id))
+ .filter((e) => modelOrg(e).key === 'open-source')
+ .map((e) => e.id);
+ expect(unnamed).toEqual([]);
+ });
});
diff --git a/src/services/model-recommendation.ts b/src/services/model-recommendation.ts
index 82de7c12..e00f6a3f 100644
--- a/src/services/model-recommendation.ts
+++ b/src/services/model-recommendation.ts
@@ -45,20 +45,28 @@ export interface RecommendedSelection {
* Curated LLM id preference order (presentation). Fit is decided by can_run,
* not by these lists.
*/
+// Ordered by how good the model is, not how small it is. `isRecommendable`
+// already refuses anything the WASM32 address space cannot hold, so this list
+// states a preference and lets the fit gate do the excluding.
+//
+// Five of the previous eight ids no longer existed in the catalog, so the
+// back-fill below was doing all the work and the "recommended" section was
+// really just catalog order.
const LLM_PREFERENCE: readonly string[] = [
+ 'qwen3.5-2b-q4_k_m',
+ 'lfm2.5-1.2b-instruct-q4_k_m',
+ 'qwen3.5-0.8b-q4_k_m',
+ 'granite-4.1-3b-q4_k_m',
+ 'lfm2.5-1.2b-thinking-q4_k_m',
+ 'gemma-4-e2b-it-q4_k_m',
+ 'lfm2.5-230m-q4_k_m',
'bonsai-1.7b-q1_0',
- 'bonsai-4b-q1_0',
- 'bonsai-8b-q1_0',
- 'qwen2.5-0.5b-instruct-q6_k',
- 'qwen3-0.6b-q4_k_m',
- 'qwen3-4b-q4_k_m',
- 'lfm2-350m-q4_k_m',
- 'smollm2-360m-q8_0',
];
const VLM_PREFERENCE: readonly string[] = [
- 'lfm2-vl-450m-q8_0',
'smolvlm2-256m-video-instruct-q8_0',
+ 'smolvlm-500m-instruct-q8_0',
+ 'lfm2.5-vl-1.6b-q4_k_m',
];
const ASR_PREFERENCE: readonly string[] = ['sherpa-onnx-whisper-tiny.en'];