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 +

+ RunAnywhere +

-```bash -git clone https://github.com/RunanywhereAI/runanywhere-web.git -cd runanywhere-web +

+ + Try it in your browser + +

-npm ci -npm run dev # http://localhost:3000 -``` +

+ WebAssembly + WebGPU + TypeScript + RunAnywhere License +

-`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` | +| ![Chat](docs/screenshots/01-chat.jpg) | ![Choosing a model](docs/screenshots/02-model-picker.jpg) | +| 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](docs/screenshots/03-voice.jpg) | ![SDK utilities](docs/screenshots/04-advanced.jpg) | +| 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. | +| ![Downloads](docs/screenshots/05-downloads.jpg) | ![Settings](docs/screenshots/06-settings.jpg) | +| 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'];