diff --git a/README.md b/README.md
index d7f01395..0c51cc05 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,40 @@
-# RunAnywhere AI, Android example
+# RunAnywhere AI for Android
-An Android app built on the RunAnywhere Kotlin SDK. It runs chat, voice, vision, and
-document Q&A on-device, and exposes the SDK's other primitives (TTS, STT, VAD, OCR,
-segmentation, diarization, image generation, benchmarks) through an Advanced hub.
+
+
+
-The app resolves every SDK artifact from Maven Central. There is no monorepo checkout,
-no local AAR, and no native toolchain in the loop: a clean clone builds as soon as
-Gradle can reach the network.
+
+
+
+
+
-## Screenshots
+
+
+
+
+
+
+
-Captured on a physical arm64 device running LFM2 350M, quantised Q4_K_M, through the
-llama.cpp backend.
+The RunAnywhere consumer app for Android, written in Kotlin.
+
+Ask it questions, talk to it, or show it what your camera sees. The models run on your phone,
+so nothing you type or photograph leaves it, and it works with the network off. On Snapdragon
+hardware the inference runs on the Hexagon NPU.
+
+## Get it
+
+**[Google Play](https://play.google.com/store/apps/details?id=com.runanywhere.runanywhereai)**. Android 7.0 or newer, ARM64.
+
+
+
+## What it looks like
+
+Captured on a physical arm64 device, running a small GGUF chat model through the llama.cpp
+backend.
| | |
|---|---|
@@ -22,276 +45,102 @@ llama.cpp backend.
|  |  |
| Everything past chat lives here: OCR, segmentation, image generation, diarization, transcription, benchmarks. | Sampling, response length, the system prompt, and streaming. |
-The remaining captures, including the landscape layout, are in
-[`docs/screenshots/`](docs/screenshots).
+The rest, including the landscape layout, are in [`docs/screenshots/`](docs/screenshots).
-## Requirements
+## What you can do
-| Item | Minimum |
-|------|---------|
-| Android Studio | A release that supports AGP 9.2 and Gradle 9.6 |
-| Android SDK | API 24 (Android 7.0); compile and target SDK 37 |
-| JDK | 17 for the build, plus 21 for the Gradle daemon (see below) |
-| Disk space | Several GB, for downloaded models |
-| Device | arm64 physical device recommended; the debug variant also builds x86_64 for emulators |
+| | |
+| --- | --- |
+| **Ask** | Streaming chat with thinking mode, tool calling, and per-response analytics |
+| **Talk** | Hands-free voice assistant: it listens, transcribes, thinks, and speaks back |
+| **Images and live** | Ask about a photo, or about what the camera sees right now |
+| **Documents** | Add documents and ask questions, with sources cited |
+| **Advanced** | OCR, segmentation, diarization, image generation, read aloud, transcription, voice activity, tools, benchmarks |
-Two JDKs, because they serve different things. The app compiles against Java 17
-(`compileOptions` in `app/build.gradle.kts`), while `gradle/gradle-daemon-jvm.properties`
-pins `toolchainVersion=21` for the Gradle daemon itself. If no local JDK 21 is present,
-Gradle provisions one over the network on every run.
+Cloud providers exist but are opt-in and off by default.
-No NDK, CMake, or native toolchain is required. The SDK ships prebuilt native libraries
-inside its published AARs.
+## Models
-## Setup
+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, from a
+230M model that answers instantly to larger ones a recent phone can hold. Sizes shown are
+measured, not estimated, and the app checks each one against your device before recommending
+it.
-Clone the repo:
+The sheet is reachable from any screen that needs a model, and you can paste any GGUF repo
+from Hugging Face to add it alongside the curated set.
-```bash
-git clone https://github.com/RunanywhereAI/runanywhere-android.git
-cd runanywhere-android
-```
+## Snapdragon NPU
-Point Gradle at your Android SDK: export `ANDROID_HOME`, or copy
-`local.properties.example` to `local.properties` and set `sdk.dir`.
+On supported Qualcomm Hexagon hardware the app registers the QHexRT backend and inference
+runs on the NPU. Registration is rejected internally on parts outside the validated V75,
+V79, and V81 set, and the backend is ARM64 only, so it is unavailable on x86_64 emulators.
-Then build:
-
-```bash
-./scripts/verify.sh
-```
+Private `runanywhere/*_HNPU` model bundles need a Hugging Face token: Settings, then Private
+Downloads, paste the token, save. It is held in protected app storage, re-applied on each
+start, and never written to source, assets, or logs.
-Or open the project in Android Studio and run the `app` configuration, or install from
-the command line:
+## Build it yourself
```bash
+git clone https://github.com/RunanywhereAI/runanywhere-android.git
+cd runanywhere-android
./gradlew :app:installDebug
```
-The app runs without a control plane. `RUNANYWHERE_BASE_URL` and `RUNANYWHERE_API_KEY`
-are optional (settable via environment or `local.properties`); with both blank the SDK
-initializes in its development environment. `app/build.gradle.kts` fails the
-configuration phase if exactly one of the two is set.
-
-## SDK dependency
-
-All SDK artifacts come from Maven Central under the group `io.github.sanchitmonga22`,
-pinned by the single `runanywhere` version in `gradle/libs.versions.toml` (currently
-0.20.19). Nothing is declared as a local AAR or project path:
-
-```kotlin
-// gradle/libs.versions.toml
-runanywhere = "0.20.19"
-
-// app/build.gradle.kts
-implementation(libs.runanywhere.sdk) // io.github.sanchitmonga22:runanywhere-sdk
-implementation(libs.runanywhere.llamacpp) // io.github.sanchitmonga22:runanywhere-llamacpp
-implementation(libs.runanywhere.onnx) // io.github.sanchitmonga22:runanywhere-onnx
-implementation(libs.runanywhere.qhexrt) // io.github.sanchitmonga22:runanywhere-qhexrt-android
-```
-
-| Coordinate | Role |
-|---|---|
-| `runanywhere-sdk` | Core SDK and the commons native library |
-| `runanywhere-llamacpp` | llama.cpp backend (LLM, VLM) |
-| `runanywhere-onnx` | ONNX Runtime (embeddings) and Sherpa-ONNX (STT, TTS, VAD) in one AAR |
-| `runanywhere-qhexrt-android` | QHexRT backend (Qualcomm Hexagon NPU), arm64 only |
-
-The four move in lockstep; never mix versions across them. To move to a new SDK release,
-bump `runanywhere` in `gradle/libs.versions.toml`, then regenerate the two reproducibility
-files that pin the resolved graph, `app/gradle.lockfile` and
-`gradle/verification-metadata.xml`:
-
-```bash
-# 1. Dependency lock. Host-independent, so any OS will do.
-./gradlew :app:dependencies --write-locks
-
-# 2. Checksums. Gradle merges into the existing file (it adds entries and never
-# removes them) so run this on top of the committed file rather than deleting it.
-GRADLE_USER_HOME="$(mktemp -d)" ./gradlew --write-verification-metadata sha256 \
- :app:assembleDebug :app:testDebugUnitTest :app:lintRelease
-```
-
-Two traps make the checksum file easy to get subtly wrong:
-
-- Use a throwaway `GRADLE_USER_HOME`. Checksums are only recorded for artifacts Gradle
- actually downloads during the run. Against a warm `~/.gradle` the run looks successful
- but silently omits things already cached, in practice a handful of parent POMs and BOM
- metadata (`guava-parent`, `junit-bom`, `kotlin-gradle-plugins-bom`). The gap is
- invisible until someone builds from a genuinely cold cache, i.e. CI.
-- Cover Linux and macOS. A few build-time artifacts are OS-classified
- (`com.android.tools.build:aapt2:...-linux.jar` vs `...-osx.jar`) and Gradle records only
- the host's. The committed file carries both, so one file satisfies the Linux CI runner
- and a macOS developer. Because step 2 merges, the way to keep both is to run it on Linux
- (Docker is fine), commit that file, then run it again on macOS on top. If you can only
- reach one OS, hand-add the missing `` line to the
- `com.android.tools.build:aapt2` component. A Linux-only file breaks every macOS
- developer, and a macOS-only file breaks CI.
-
-Then confirm the result the same way CI will, with no bypass flags:
-
-```bash
-./gradlew :app:assembleDebug # dependency verification live
-CI=true ./gradlew :app:assembleDebug # + LockMode.STRICT
-```
-
-### Testing an unreleased SDK build
-
-To try a change from a [`runanywhere-sdks`](https://github.com/RunanywhereAI/runanywhere-sdks)
-checkout before it is on Maven Central, publish it to `~/.m2` and point this repo at it:
-
-```bash
-# In the monorepo. Publishes io.github.sanchitmonga22:*:
-(cd path/to/runanywhere-sdks/bindings/kotlin && ./gradlew publishToMavenLocal)
-
-# Here. Opt in per invocation, and relax verification for that one run
-./gradlew :app:assembleDebug \
- -Prunanywhere.useLocalSdkAars=true \
- --dependency-verification=lenient
-```
-
-`-Prunanywhere.useLocalSdkAars=true` adds `mavenLocal()` ahead of Google and Maven
-Central, scoped by `content { includeGroup("io.github.sanchitmonga22") }` so a stale
-`~/.m2` copy of any other dependency cannot shadow the verified one.
-
-`--dependency-verification=lenient` is required alongside it, and is not a bypass being
-smuggled in. A locally published AAR has the same coordinates as the released one but
-different bytes, so it can never match the sha256 in
-`gradle/verification-metadata.xml`. Without the flag the build stops with
-`artifacts failed verification`, which is the gate working correctly. Relax it per
-invocation like this; do not add a `` entry to
-the committed metadata, because that would permanently un-pin the four artifacts the gate
-exists to pin.
+You need Android Studio (latest stable), JDK 17, and a few GB of disk for models. No NDK,
+CMake, or native toolchain: the SDK ships prebuilt native libraries inside its published
+AARs. An ARM64 physical device is strongly preferred, since the NPU backend does not exist
+on emulators.
-Both flags are per-invocation only. Never commit `runanywhere.useLocalSdkAars` to
-`gradle.properties` and never set it in CI: `ci.yml` exists to prove a clean clone
-resolves the SDK from Maven Central, and a local AAR would make that proof vacuous.
+[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) covers the SDK pinning rules, dependency
+verification, testing an unreleased SDK build, CI, and troubleshooting.
-## Continuous integration
+## Architecture
-`.github/workflows/ci.yml` runs on every push to `main` and every pull request:
-`ubuntu-latest`, Temurin JDK 17 and 21, the Android SDK via `android-actions/setup-android`
-(`platform-tools`, `platforms;android-37.0`, `build-tools;37.0.0`), Gradle caching via
-`gradle/actions/setup-gradle`, then `./gradlew :app:assembleDebug --no-daemon --stacktrace`
-and an APK upload.
-
-CI runs the unmodified command, no bypass flags, so it exercises the same path a developer
-does. Both reproducibility gates are enforced there and locally:
-
-| Gate | What enforces it | What it pins |
-|---|---|---|
-| `gradle/verification-metadata.xml` | Auto-enabled by Gradle whenever the file exists; `./scripts/verify.sh` additionally passes `--dependency-verification strict` | sha256 of every resolved artifact, including the four `io.github.sanchitmonga22` AARs and both OS variants of `aapt2` |
-| `app/gradle.lockfile` | `app/build.gradle.kts` flips to `LockMode.STRICT` when `$CI` is set (or with `-Prunanywhere.strictLocks=true`); `LENIENT` otherwise, so Android Studio sync stays friction-free | the exact resolved version of every module on every configuration |
-
-If a dependency or SDK bump makes either gate fail, regenerate the files (see
-[SDK dependency](#sdk-dependency)). Do not add `--dependency-verification=off` or
-`env -u CI` to the workflow, because that hides the breakage from CI while every clean
-clone keeps failing.
-
-## What the app does
-
-The navigation drawer has two groups, defined in
-`ui/navigation/Destinations.kt`.
-
-Assistant:
-
-| Screen | What it does |
-|---|---|
-| Ask | Streaming chat, with thinking-mode display, tool calling, and per-response analytics |
-| Talk | Hands-free voice assistant over the VAD to STT to LLM to TTS pipeline |
-| Images & live | Image and live-camera understanding through a VLM |
-| Documents | Document ingestion and retrieval-augmented Q&A with sources |
-
-App:
-
-| Screen | What it does |
-|---|---|
-| Settings | App and assistant preferences, model downloads, storage usage and cache clearing, Hugging Face token |
-| Advanced | Hub for the remaining SDK surfaces |
-
-Advanced (`ui/screens/more/MoreScreen.kt`) links to Document OCR, segmentation,
-diarization, image generation, read-aloud (TTS), transcription (STT), voice activity
-(VAD), web and tools, solutions, cloud providers, and benchmarks.
-
-Model management is a sheet rather than a screen: `ui/screens/models/ModelSelectionSheet.kt`
-handles download, load, and delete, and is reachable from the surfaces that need a model.
-`data/ModelCatalog.kt` seeds the curated catalog, registered in the background after the
-first frame so cold start is not blocked by roughly a hundred `models.register()` JNI
-calls.
-
-Inference runs locally once models are downloaded. Cloud providers are opt-in and
-configured on their own screen.
-
-## NPU and QHexRT (Snapdragon devices)
-
-On supported Qualcomm Hexagon hardware the app registers the QHexRT backend for
-accelerated inference. `RunAnywhereApplication` registers it after
-`RunAnywhere.initialize()`, because the module extracts its DSP skels through the
-SDK-owned application `Context`. Registration is rejected internally on parts outside the
-validated V75/V79/V81 set, and the backend is arm64 only, so it is unavailable on x86_64
-emulators. `ADSP_LIBRARY_PATH` is set by the engine itself; the app supplies no glue.
-
-To use private `runanywhere/*_HNPU` model bundles:
-
-1. Open Settings, then the Private Downloads section.
-2. Paste a Hugging Face token and tap Save token.
-3. Download and load an HNPU model from the model picker. The SDK resolves the correct
- Hexagon architecture natively.
-4. Tap Clear to return to public, no-auth downloads.
-
-The token is held in protected app storage and re-applied to the SDK on each start. It is
-never embedded in source, assets, or logs.
-
-## Project layout
+Four AARs from Maven Central, no local project paths. Three share one version; QHexRT
+carries its own, because it stopped being published to Maven Central after `0.20.19`.
```
-app/src/main/java/com/runanywhere/runanywhereai/
- RunAnywhereApplication.kt SDK init, backend registration, catalog seeding
- MainActivity.kt Compose host
- ui/navigation/ Type-safe routes and the drawer destinations
- ui/screens/ One package per screen
- ui/theme/ Material 3 theming, brand orange #FF6900
- data/ Model catalog, settings, conversations, RAG, benchmarks
- tools/ Built-in tool-calling implementations
- download/ Model download service and progress state
-app/build.gradle.kts Variants, signing, dependency locking, SBOM, Play gate
-gradle/libs.versions.toml SDK coordinates and every dependency version
-gradle/verification-metadata.xml sha256 of every resolved artifact
-app/gradle.lockfile Resolved dependency graph
-scripts/verify.sh Strict debug APK build gate
-scripts/smoke.sh Fast static SDK API coverage check
-.github/workflows/ci.yml Clean-clone build gate
+ RunAnywhere AI (Jetpack Compose, MVVM)
+ │
+ ┌───────────────┴────────────────┐
+ │ io.github.sanchitmonga22:* │
+ └───────────────┬────────────────┘
+ │
+ ┌───────────────┬────┴─────────┬──────────────────┐
+ │ │ │ │
+runanywhere-sdk llamacpp onnx qhexrt-android
+core + commons LLM · VLM embeddings · Hexagon NPU
+ 0.20.24 0.20.24 STT·TTS·VAD arm64 only
+ 0.20.24 0.20.19
+ │
+ ▼
+ C++ commons, one core
+ shared with Swift, Web, and Electron
```
-## Troubleshooting
+Business logic lives in the SDK. The app is Compose UI, view models, and thin
+`RunAnywhere.*` calls. The catalog registers in the background after the first frame, so
+cold start is not blocked behind roughly a hundred `models.register()` JNI calls.
-| Symptom | Fix |
-|---------|-----|
-| `Could not find io.github.sanchitmonga22:runanywhere-*` | Check the `runanywhere` version in `gradle/libs.versions.toml` is published to Maven Central, and that `mavenCentral()` is reachable |
-| `N artifacts failed verification` | Regenerate `gradle/verification-metadata.xml`, following [SDK dependency](#sdk-dependency) step 2 exactly, including the throwaway `GRADLE_USER_HOME` and the Linux and macOS passes |
-| `... is not part of the dependency lock state` (usually only with `CI=true`) | Regenerate the lock: `./gradlew :app:dependencies --write-locks` |
-| Gradle downloads a JDK on every run | Install a local JDK 21 for the daemon toolchain |
-| `RUNANYWHERE_BASE_URL and RUNANYWHERE_API_KEY must either both be set or both be blank` | Set both, or clear both |
-| NPU models unavailable | Confirm the device has a supported Hexagon NPU and an arm64 build; HNPU bundles also need a saved HF token |
-
-For a quick static check without a full compile:
-
-```bash
-./scripts/smoke.sh
-```
+| Reference | |
+| --- | --- |
+| Building, pinning, tests, CI, troubleshooting | [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) |
+| Contributor conventions | [`AGENTS.md`](AGENTS.md) |
-## Related links
+## The other apps
-| Resource | Link |
-|----------|------|
-| Kotlin SDK | [runanywhere-sdks/bindings/kotlin](https://github.com/RunanywhereAI/runanywhere-sdks/tree/main/bindings/kotlin) |
-| Maven Central | [io.github.sanchitmonga22](https://central.sonatype.com/namespace/io.github.sanchitmonga22) |
-| Play Store | [com.runanywhere.runanywhereai](https://play.google.com/store/apps/details?id=com.runanywhere.runanywhereai) |
+| Platform | Repo |
+| --- | --- |
+| iOS and macOS, Swift | [runanywhere-ios](https://github.com/RunanywhereAI/runanywhere-ios) |
+| Windows, Electron | [runanywhere-electron](https://github.com/RunanywhereAI/runanywhere-electron) |
+| Web, TypeScript | [runanywhere-web](https://github.com/RunanywhereAI/runanywhere-web) |
+| 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) |
-| Issues | [GitHub Issues](https://github.com/RunanywhereAI/runanywhere-sdks/issues) |
-| Email | founders@runanywhere.ai |
## License
-RunAnywhere License, Version 1.0: Apache 2.0 based, with additional terms that require a
-separate commercial license for commercial use. See [LICENSE](LICENSE).
+RunAnywhere License, Apache 2.0 based with additional commercial-use terms. See
+[LICENSE](LICENSE).
diff --git a/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt b/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt
index b4d7d9fb..e11093f0 100644
--- a/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt
+++ b/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt
@@ -207,74 +207,14 @@ internal object ModelCatalog {
// --- LLM (llama.cpp) ------------------------------------------------------
private val llm = listOf(
- // SmolLM2
- SingleFileModel(
- "smollm2-360m-q8_0",
- "SmolLM2 360M Q8_0",
- "https://huggingface.co/prithivMLmods/SmolLM2-360M-GGUF/resolve/main/SmolLM2-360M.Q8_0.gguf",
- LLAMA,
- LANGUAGE,
- 386_404_416
- ),
- // Qwen (2.5, then 3, then 3.5)
- SingleFileModel(
- "qwen2.5-0.5b-instruct-q6_k",
- "Qwen 2.5 0.5B Instruct Q6_K",
- "https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q6_k.gguf",
- LLAMA,
- LANGUAGE,
- // downloadBytes defaults to memoryBytes, so a RAM-shaped round number here is also
- // published as the transfer total the progress bar divides by. 600_000_000 is 50 MB short
- // of the real asset (measured twice: 650_379_104 bytes on disk, and the HTTP layer logs
- // `bytes_written=650379104`), which drove the bar to 100% at 92% of the file and left the
- // line reading "596.2 MB of 572.2 MB" with the ETA gone while 50 MB was still arriving.
- memoryBytes = 600_000_000,
- downloadBytes = 650_379_104,
- supportsLora = true
- ),
- SingleFileModel(
- "qwen2.5-1.5b-instruct-q4_k_m",
- "Qwen 2.5 1.5B Instruct Q4_K_M",
- "https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf",
- LLAMA,
- LANGUAGE,
- 2_500_000_000
- ),
- SingleFileModel(
- "qwen3-0.6b-q4_k_m",
- "Qwen3 0.6B Q4_K_M",
- "https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_K_M.gguf",
- LLAMA,
- LANGUAGE,
- memoryBytes = 500_000_000,
- downloadBytes = 396_705_472,
- supportsThinking = true
- ),
- SingleFileModel(
- "qwen3-1.7b-q4_k_m",
- "Qwen3 1.7B Q4_K_M",
- "https://huggingface.co/unsloth/Qwen3-1.7B-GGUF/resolve/main/Qwen3-1.7B-Q4_K_M.gguf",
- LLAMA,
- LANGUAGE,
- 1_200_000_000,
- supportsThinking = true
- ),
- SingleFileModel(
- "qwen3-4b-q4_k_m",
- "Qwen3 4B Q4_K_M",
- "https://huggingface.co/unsloth/Qwen3-4B-GGUF/resolve/main/Qwen3-4B-Q4_K_M.gguf",
- LLAMA,
- LANGUAGE,
- 2_800_000_000,
- supportsThinking = true
- ),
SingleFileModel(
"qwen3.5-0.8b-q4_k_m",
"Qwen3.5 0.8B Q4_K_M",
- "https://huggingface.co/bartowski/Qwen_Qwen3.5-0.8B-GGUF/resolve/main/Qwen3.5-0.8B-Q4_K_M.gguf",
+ "https://huggingface.co/unsloth/Qwen3.5-0.8B-GGUF/resolve/main/Qwen3.5-0.8B-Q4_K_M.gguf",
LLAMA,
LANGUAGE,
- 620_000_000,
+ memoryBytes = 900_000_000,
+ downloadBytes = 532_517_120,
supportsThinking = true
),
// Qwen3.6 — MoE (35B total / 3B active), agentic-coding-focused release.
@@ -293,10 +233,11 @@ internal object ModelCatalog {
SingleFileModel(
"qwen3.8-27b-q4_k_m",
"Qwen3.8 27B Q4_K_M (heavy)",
- "https://huggingface.co/unsloth/Qwen3.8-27B-GGUF/resolve/main/Qwen3.8-27B-Q4_K_M.gguf",
+ "https://huggingface.co/unsloth/Qwen3.8-27B-GGUF/resolve/main/Qwen3.8-27B-UD-Q4_K_M.gguf",
LLAMA,
LANGUAGE,
- 17_106_775_008,
+ memoryBytes = 17_106_775_008,
+ downloadBytes = 16_464_440_224,
supportsThinking = true
),
// LFM2 / LFM2.5 (Liquid AI)
@@ -315,28 +256,6 @@ internal object ModelCatalog {
// 153,406,304 B of weights plus KV cache and runtime overhead.
190_000_000
),
- // ONE quantization per model. The Q8_0 sibling of this row was removed
- // deliberately: 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. The same
- // collapse was applied to LFM2 1.2B Tool and LFM2.5 2.6B below.
- SingleFileModel(
- "lfm2-350m-q4_k_m",
- "LiquidAI LFM2 350M Q4_K_M",
- "https://huggingface.co/LiquidAI/LFM2-350M-GGUF/resolve/main/LFM2-350M-Q4_K_M.gguf",
- LLAMA,
- LANGUAGE,
- 250_000_000
- ),
- // Q8_0 sibling removed — one quantization per model (see the 350M note above).
- SingleFileModel(
- "lfm2-1.2b-tool-q4_k_m",
- "LiquidAI LFM2 1.2B Tool Q4_K_M",
- "https://huggingface.co/LiquidAI/LFM2-1.2B-Tool-GGUF/resolve/main/LFM2-1.2B-Tool-Q4_K_M.gguf",
- LLAMA,
- LANGUAGE,
- 800_000_000
- ),
SingleFileModel(
"lfm2.5-1.2b-instruct-q4_k_m",
"LiquidAI LFM2.5 1.2B Instruct Q4_K_M",
@@ -355,24 +274,6 @@ internal object ModelCatalog {
1_674_000_000,
supportsThinking = true
),
- // Llama
- SingleFileModel(
- "llama-2-7b-chat-q4_k_m",
- "Llama 2 7B Chat Q4_K_M",
- "https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF/resolve/main/llama-2-7b-chat.Q4_K_M.gguf",
- LLAMA,
- LANGUAGE,
- 4_000_000_000
- ),
- // Mistral
- SingleFileModel(
- "mistral-7b-instruct-q4_k_m",
- "Mistral 7B Instruct Q4_K_M",
- "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.1-GGUF/resolve/main/mistral-7b-instruct-v0.1.Q4_K_M.gguf",
- LLAMA,
- LANGUAGE,
- 4_000_000_000
- ),
// Gemma
// Gemma 4 license: Google's Gemma Terms of Use (https://ai.google.dev/gemma/terms),
// not Apache — same license family as the gemma3n/gemma4 QHexRT rows in npuCatalog
@@ -429,14 +330,6 @@ internal object ModelCatalog {
LANGUAGE,
18_323_733_440
),
- SingleFileModel(
- "gemma-4-31b-it-ud-q2_k_xl",
- "Gemma 4 31B IT UD-Q2_K_XL (heavy)",
- "https://huggingface.co/unsloth/gemma-4-31B-it-GGUF/resolve/main/gemma-4-31B-it-UD-Q2_K_XL.gguf",
- LLAMA,
- LANGUAGE,
- 11_774_991_296
- ),
// Granite (IBM)
// Apache 2.0 (verified via HF cardData.license). Dense, three sizes.
SingleFileModel(
@@ -469,29 +362,6 @@ internal object ModelCatalog {
// Exact P0 NVIDIA checkpoint. The pinned llama.cpp fork has native
// `nemotron` support; this exact Q4_K_M artifact was load/inference
// checked through rcli on macOS before being exposed in the catalog.
- SingleFileModel(
- "nemotron-mini-4b-instruct-q4_k_m",
- "NVIDIA Nemotron Mini 4B Instruct Q4_K_M",
- "https://huggingface.co/bartowski/Nemotron-Mini-4B-Instruct-GGUF/resolve/fb49cde090c86092d89905bea2ffc41c23c2615e/Nemotron-Mini-4B-Instruct-Q4_K_M.gguf",
- LLAMA,
- LANGUAGE,
- 2_697_387_072,
- contextLength = 4_096
- ),
- // Exact P0 NVIDIA Nano checkpoint. The file itself is 4.92 GB, while
- // llama.cpp also needs KV/cache/compute headroom. Keep those two facts
- // separate so the download planner validates the exact transport size
- // and the mandatory SDK compatibility preflight requires 6 GiB of
- // currently available RAM before either download or lifecycle load.
- //
- // INTENTIONAL CROSS-SDK DIVERGENCE: this row is Android/rcli-only. The
- // pinned llama.cpp fork's `nemotron` path was load/inference checked for
- // this exact Q4_K_M artifact through rcli on macOS (not yet an on-device
- // Android smoke). iOS deliberately withholds it (ModelCatalogBootstrap:
- // pending an Apple-provider inference smoke) and the Web catalog omits it
- // (its ~4.92 GB single artifact exceeds the browser WASM32 4 GiB gate).
- // Hold or re-scope this row if the Android llama.cpp path has to clear the
- // same on-device bar iOS requires.
SingleFileModel(
"llama-3.1-nemotron-nano-4b-v1.1-q4_k_m",
"NVIDIA Llama 3.1 Nemotron Nano 4B v1.1 Q4_K_M",
@@ -512,6 +382,15 @@ internal object ModelCatalog {
downloadBytes = 4_920_736_864L,
contextLength = 4_096,
),
+ SingleFileModel(
+ "nemotron-mini-4b-instruct-q4_k_m",
+ "NVIDIA Nemotron Mini 4B Instruct Q4_K_M",
+ "https://huggingface.co/bartowski/Nemotron-Mini-4B-Instruct-GGUF/resolve/fb49cde090c86092d89905bea2ffc41c23c2615e/Nemotron-Mini-4B-Instruct-Q4_K_M.gguf",
+ LLAMA,
+ LANGUAGE,
+ 2_697_387_072,
+ contextLength = 4_096
+ ),
// Bonsai (PrismML)
// Bonsai family at TRUE 1-bit (Q1_0, ~1.125 bit/wt) on CPU via llama.cpp — the same GGUF
// that runs on the NPU (bonsai_{4b,8b,27b}_1bit, QHEXRT). Requires a llama.cpp build with
@@ -562,6 +441,58 @@ internal object ModelCatalog {
// "invalid ggml type 142" — it only added Q1_0 (plain Bonsai) support, not
// Ternary-Bonsai's tensor encoding. Re-enable once the fork adds it.
// Ternary-Bonsai MLX works fine (iOS/macOS only — no MLX on Android).
+
+ // Added from the verified model list.
+ SingleFileModel(
+ "lfm2.5-1.2b-thinking-q4_k_m",
+ "LFM2.5 1.2B Thinking Q4_K_M",
+ "https://huggingface.co/LiquidAI/LFM2.5-1.2B-Thinking-GGUF/resolve/main/LFM2.5-1.2B-Thinking-Q4_K_M.gguf",
+ LLAMA,
+ LANGUAGE,
+ memoryBytes = 900_000_000,
+ downloadBytes = 730_895_360,
+ supportsThinking = true
+ ),
+ SingleFileModel(
+ "qwen3.5-2b-q4_k_m",
+ "Qwen3.5 2B Q4_K_M",
+ "https://huggingface.co/unsloth/Qwen3.5-2B-GGUF/resolve/main/Qwen3.5-2B-Q4_K_M.gguf",
+ LLAMA,
+ LANGUAGE,
+ memoryBytes = 1_550_000_000,
+ downloadBytes = 1_280_835_840,
+ supportsThinking = true
+ ),
+ SingleFileModel(
+ "qwen3.5-4b-q4_k_m",
+ "Qwen3.5 4B Q4_K_M",
+ "https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf",
+ LLAMA,
+ LANGUAGE,
+ memoryBytes = 3_350_000_000,
+ downloadBytes = 2_740_937_888,
+ supportsThinking = true
+ ),
+ SingleFileModel(
+ "qwen3.5-9b-q4_k_m",
+ "Qwen3.5 9B Q4_K_M",
+ "https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf",
+ LLAMA,
+ LANGUAGE,
+ memoryBytes = 6_950_000_000,
+ downloadBytes = 5_680_522_464,
+ supportsThinking = true
+ ),
+ SingleFileModel(
+ "maple-preview-tq1_0",
+ "Maple Preview 20B-A1B TQ1_0 (1-bit)",
+ "https://huggingface.co/deepgrove/maple-preview-GGUF/resolve/main/maple-preview-TQ1_0-head-Q4_K.gguf",
+ LLAMA,
+ LANGUAGE,
+ memoryBytes = 6_100_000_000,
+ downloadBytes = 4_984_016_416,
+ supportsThinking = true
+ ),
)
// --- VLM (llama.cpp, multimodal) ------------------------------------------
@@ -603,47 +534,6 @@ internal object ModelCatalog {
TAR_GZ,
ArchiveStructure.ARCHIVE_STRUCTURE_DIRECTORY_BASED
),
- // Qwen (2-VL, then 2.5-VL)
- MultiFileModel(
- "qwen2-vl-2b-instruct-q4_k_m", "Qwen2-VL 2B Instruct", LLAMA, MULTIMODAL, 1_800_000_000,
- files = listOf(
- ModelFile(
- "https://huggingface.co/ggml-org/Qwen2-VL-2B-Instruct-GGUF/resolve/main/Qwen2-VL-2B-Instruct-Q4_K_M.gguf",
- "Qwen2-VL-2B-Instruct-Q4_K_M.gguf"
- ),
- ModelFile(
- "https://huggingface.co/ggml-org/Qwen2-VL-2B-Instruct-GGUF/resolve/main/mmproj-Qwen2-VL-2B-Instruct-Q8_0.gguf",
- "mmproj-Qwen2-VL-2B-Instruct-Q8_0.gguf"
- ),
- ),
- ),
- MultiFileModel(
- "qwen2.5-vl-3b-instruct-q4_k_m", "Qwen2.5-VL 3B Instruct Q4_K_M", LLAMA, MULTIMODAL, 2_800_000_000,
- files = listOf(
- ModelFile(
- "https://huggingface.co/ggml-org/Qwen2.5-VL-3B-Instruct-GGUF/resolve/main/Qwen2.5-VL-3B-Instruct-Q4_K_M.gguf",
- "Qwen2.5-VL-3B-Instruct-Q4_K_M.gguf"
- ),
- ModelFile(
- "https://huggingface.co/ggml-org/Qwen2.5-VL-3B-Instruct-GGUF/resolve/main/mmproj-Qwen2.5-VL-3B-Instruct-Q8_0.gguf",
- "mmproj-Qwen2.5-VL-3B-Instruct-Q8_0.gguf"
- ),
- ),
- ),
- // LFM2-VL / LFM2.5-VL (Liquid AI)
- MultiFileModel(
- "lfm2-vl-450m-q8_0", "LFM2-VL 450M", LLAMA, MULTIMODAL, 600_000_000,
- files = listOf(
- ModelFile(
- "https://huggingface.co/runanywhere/LFM2-VL-450M-GGUF/resolve/main/LFM2-VL-450M-Q8_0.gguf",
- "LFM2-VL-450M-Q8_0.gguf"
- ),
- ModelFile(
- "https://huggingface.co/runanywhere/LFM2-VL-450M-GGUF/resolve/main/mmproj-LFM2-VL-450M-Q8_0.gguf",
- "mmproj-LFM2-VL-450M-Q8_0.gguf"
- ),
- ),
- ),
// Q4_K_M, matching the Qwen2.5-VL 3B row beside it and the LFM2.5 2.6B LLM
// row this VLM is built on: one quantization per model, and Q4_K_M is what
// every other 3B-class GGUF row in this catalog ships. The mmproj is Q8_0
@@ -706,9 +596,10 @@ internal object ModelCatalog {
MultiFileModel(
"gemma-4-e4b-it-q4_k_m", "Gemma 4 E4B IT Q4_K_M (Experimental)", LLAMA, MULTIMODAL, 5_500_000_000,
files = listOf(
+ // ggml-org publishes no Q4_K_M for this repo — Q4_0 is its only 4-bit build.
ModelFile(
- "https://huggingface.co/ggml-org/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-Q4_K_M.gguf",
- "gemma-4-E4B-it-Q4_K_M.gguf"
+ "https://huggingface.co/ggml-org/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-Q4_0.gguf",
+ "gemma-4-E4B-it-Q4_0.gguf"
),
ModelFile(
"https://huggingface.co/ggml-org/gemma-4-E4B-it-GGUF/resolve/main/mmproj-gemma-4-E4B-it-Q8_0.gguf",
diff --git a/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/models/ModelRecommendation.kt b/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/models/ModelRecommendation.kt
index 75d9c3c8..74c6806c 100644
--- a/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/models/ModelRecommendation.kt
+++ b/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/models/ModelRecommendation.kt
@@ -38,12 +38,12 @@ object ModelRecommendation {
// Curated GGUF LLM ids, ordered best-first (product preference, not device fit).
private val preferredGgufLLMs: List = listOf(
"lfm2.5-1.2b-instruct-q4_k_m",
- "qwen3-1.7b-q4_k_m",
- "lfm2-1.2b-tool-q4_k_m",
- "qwen3-0.6b-q4_k_m",
- "qwen2.5-0.5b-instruct-q6_k",
- "lfm2-350m-q4_k_m",
+ "qwen3.5-2b-q4_k_m",
+ "lfm2.5-1.2b-thinking-q4_k_m",
"qwen3.5-0.8b-q4_k_m",
+ "granite-4.1-3b-q4_k_m",
+ "lfm2.5-230m-q4_k_m",
+ "bonsai-1.7b-q1_0",
)
// HNPU (QHexRT) LLMs surfaced first when the device reports a Hexagon NPU.
diff --git a/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/models/ModelTaxonomy.kt b/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/models/ModelTaxonomy.kt
index 06eff6f6..ed36bc62 100644
--- a/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/models/ModelTaxonomy.kt
+++ b/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/models/ModelTaxonomy.kt
@@ -15,11 +15,14 @@ enum class ModelOrg(val brand: Brand) {
ALIBABA(RACBrands.Alibaba),
GOOGLE(RACBrands.Google),
MICROSOFT(RACBrands.Microsoft),
+ IBM(RACBrands.Ibm),
DEEPSEEK(RACBrands.DeepSeek),
LIQUID(RACBrands.Liquid),
MISTRAL(RACBrands.Mistral),
PRISM(RACBrands.Prism),
+ DEEPGROVE(RACBrands.Deepgrove),
OPENAI(RACBrands.OpenAI),
+ ZHIPU(RACBrands.Zhipu),
HUGGING_FACE(RACBrands.HuggingFace),
APPLE(RACBrands.Apple),
OPEN_SOURCE(RACBrands.OpenSource),
@@ -55,14 +58,20 @@ private val orgSpecs: List = listOf(
),
org(ModelOrg.DEEPSEEK, "deepseek"),
org(ModelOrg.PRISM, "bonsai"),
- org(ModelOrg.MICROSOFT, "phi"),
+ org(ModelOrg.DEEPGROVE, "maple"),
+ org(ModelOrg.IBM, "granite"),
+ // "fara" sits with Microsoft's "phi" rather than on its own: Fara1.5 ships
+ // mirrored under our own HF org, so the catalog row names no publisher.
+ // Filing it by its own name beats guessing one into a UI label.
+ org(ModelOrg.MICROSOFT, "phi", "fara"),
org(ModelOrg.GOOGLE, "gemma", "embeddinggemma", "siglip"),
- org(ModelOrg.META, "llama"),
+ org(ModelOrg.META, "llama", "muse-glimmer", "muse_glimmer"),
org(ModelOrg.ALIBABA, "qwen"),
org(ModelOrg.LIQUID, "lfm2"),
- org(ModelOrg.MISTRAL, "mistral"),
+ org(ModelOrg.MISTRAL, "mistral", "ministral"),
org(ModelOrg.HUGGING_FACE, "smollm", "smolvlm"),
org(ModelOrg.OPENAI, "whisper"),
+ org(ModelOrg.ZHIPU, "glm"),
org(
ModelOrg.OPEN_SOURCE,
"internvl",
diff --git a/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.kt b/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.kt
index a9ec76c6..db23afc0 100644
--- a/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.kt
+++ b/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.kt
@@ -23,7 +23,7 @@ object SolutionsYaml {
# the model identifiers inline.
voice_agent:
- llm_model_id: "smollm2-360m-q8_0"
+ llm_model_id: "lfm2.5-230m-q4_k_m"
stt_model_id: "sherpa-onnx-whisper-tiny.en"
tts_model_id: "vits-piper-en_US-lessac-medium"
vad_model_id: "silero-vad"
@@ -60,7 +60,7 @@ voice_agent:
rag:
embed_model_id: "all-minilm-l6-v2"
- llm_model_id: "smollm2-360m-q8_0"
+ llm_model_id: "lfm2.5-230m-q4_k_m"
vector_store: "usearch"
vector_store_path: "/tmp/ra-rag.usearch"
diff --git a/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/icons/RACBrands.kt b/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/icons/RACBrands.kt
index b4bde1a9..e273ed4a 100644
--- a/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/icons/RACBrands.kt
+++ b/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/icons/RACBrands.kt
@@ -25,11 +25,14 @@ object RACBrands {
val Alibaba = Brand("Alibaba", RACIcons.Brands.Qwen, Color(0xFF615CED))
val Google = Brand("Google", RACIcons.Outline.Model, Color(0xFF4285F4))
val Microsoft = Brand("Microsoft", RACIcons.Outline.Model, Color(0xFF00A4EF))
+ val Ibm = Brand("IBM", RACIcons.Outline.Model, Color(0xFF0F62FE))
val DeepSeek = Brand("DeepSeek", RACIcons.Outline.Model, Color(0xFF4D6BFE))
val Liquid = Brand("Liquid AI", RACIcons.Brands.Liquid, Color(0xFF1E6FFF))
val Mistral = Brand("Mistral AI", RACIcons.Brands.Mistral, Color(0xFFFA520F))
val Prism = Brand("Prism", RACIcons.Outline.Model, Color(0xFF2FA98C))
+ val Deepgrove = Brand("Deepgrove", RACIcons.Outline.Model, Color(0xFF3F8F4F))
val OpenAI = Brand("OpenAI", RACIcons.Brands.Whisper, Color(0xFF10A37F))
+ val Zhipu = Brand("Zhipu AI", RACIcons.Outline.Model, Color(0xFF1F6FEB))
val HuggingFace = Brand("Hugging Face", RACIcons.Brands.HuggingFace, Color(0xFFFFD21E))
val Apple = Brand("Apple", RACIcons.Brands.Foundation, Color(0xFF00C2A8))
val OpenSource = Brand("Open source", RACIcons.Outline.Model, Color(0xFF9AA0A6))
diff --git a/app/src/test/java/com/runanywhere/runanywhereai/ModelCatalogTest.kt b/app/src/test/java/com/runanywhere/runanywhereai/ModelCatalogTest.kt
index 1bd8eb2a..d3d1d118 100644
--- a/app/src/test/java/com/runanywhere/runanywhereai/ModelCatalogTest.kt
+++ b/app/src/test/java/com/runanywhere/runanywhereai/ModelCatalogTest.kt
@@ -215,12 +215,22 @@ class ModelCatalogTest {
// The CPU 230M row and the HNPU 230M bundle are distinct models, not duplicates.
assertTrue(ModelCatalog.npuCatalog.any { it.id == "lfm2_5_230m" })
- listOf("lfm2-350m-q4_k_m", "lfm2-1.2b-tool-q4_k_m", "lfm2.5-2.6b-q4_k_m").forEach { id ->
- assertTrue("expected kept Q4_K_M row $id", byId.containsKey(id))
- }
- listOf("lfm2-350m-q8_0", "lfm2-1.2b-tool-q8_0", "lfm2.5-2.6b-q8_0").forEach { id ->
- assertFalse("removed Q8_0 sibling $id came back", byId.containsKey(id))
- }
+ // One quantization per model, asserted over the whole catalog rather than
+ // named rows so it keeps holding as models turn over.
+ val quant = Regex("-(ud-)?(q\\d[_a-z0-9]*|tq\\d_\\d|iq\\d[_a-z0-9]*)$")
+ // Keyed by category too: a family's text row and its vision row share a
+ // base id on purpose (the vision one carries an mmproj projector).
+ val bases = byId.values
+ .filter { quant.containsMatchIn(it.id) }
+ .map { model ->
+ val category = when (model) {
+ is SingleFileModel -> model.category
+ is MultiFileModel -> model.category
+ else -> null
+ }
+ model.id.replace(quant, "") to category
+ }
+ assertEquals("two quantizations of one model", bases.distinct().size, bases.size)
}
/**
diff --git a/app/src/test/java/com/runanywhere/runanywhereai/ui/screens/models/ModelTaxonomyTest.kt b/app/src/test/java/com/runanywhere/runanywhereai/ui/screens/models/ModelTaxonomyTest.kt
index ef8b175b..35b28201 100644
--- a/app/src/test/java/com/runanywhere/runanywhereai/ui/screens/models/ModelTaxonomyTest.kt
+++ b/app/src/test/java/com/runanywhere/runanywhereai/ui/screens/models/ModelTaxonomyTest.kt
@@ -71,7 +71,7 @@ class ModelTaxonomyTest {
catalogModel("nemotron_nano_8b"),
catalogModel("nemotron-mini-4b-instruct-q4_k_m"),
catalogModel("parakeet_ctc_1_1b"),
- catalogModel("qwen3-4b-q4_k_m"),
+ catalogModel("qwen3.5-4b-q4_k_m"),
).toOrgGroups()
assertEquals(listOf(ModelOrg.NVIDIA, ModelOrg.ALIBABA), groups.map { it.org })
@@ -84,7 +84,7 @@ class ModelTaxonomyTest {
fun orgGroupsOrderByOrgDeclaration() {
val groups = listOf(
catalogModel("all-minilm-l6-v2"),
- catalogModel("qwen3-4b-q4_k_m"),
+ catalogModel("qwen3.5-4b-q4_k_m"),
catalogModel("nemotron-mini-4b-instruct-q4_k_m"),
catalogModel("whisper_base"),
).toOrgGroups()
diff --git a/app/src/test/java/com/runanywhere/runanywhereai/ui/screens/solutions/AndroidSolutionsConfigTest.kt b/app/src/test/java/com/runanywhere/runanywhereai/ui/screens/solutions/AndroidSolutionsConfigTest.kt
index 814cd1fc..ff0408ba 100644
--- a/app/src/test/java/com/runanywhere/runanywhereai/ui/screens/solutions/AndroidSolutionsConfigTest.kt
+++ b/app/src/test/java/com/runanywhere/runanywhereai/ui/screens/solutions/AndroidSolutionsConfigTest.kt
@@ -25,7 +25,7 @@ class AndroidSolutionsConfigTest {
framework = InferenceFramework.INFERENCE_FRAMEWORK_ONNX,
),
model(
- id = "smollm2-360m-q8_0",
+ id = "lfm2.5-230m-q4_k_m",
category = ModelCategory.MODEL_CATEGORY_LANGUAGE,
framework = InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP,
),
@@ -169,7 +169,7 @@ class AndroidSolutionsConfigTest {
)
private val canonicalCpuIds = listOf(
- "smollm2-360m-q8_0",
+ "lfm2.5-230m-q4_k_m",
"sherpa-onnx-whisper-tiny.en",
"vits-piper-en_US-lessac-medium",
"all-minilm-l6-v2",
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
new file mode 100644
index 00000000..353abf77
--- /dev/null
+++ b/docs/DEVELOPMENT.md
@@ -0,0 +1,220 @@
+# Development reference
+
+Detail moved out of the root README so it stays a consumer-facing page. Everything here
+is about building, pinning, and testing the app, not about using it.
+
+## Requirements
+
+| Item | Minimum |
+|------|---------|
+| Android Studio | A release that supports AGP 9.2 and Gradle 9.6 |
+| Android SDK | API 24 (Android 7.0); compile and target SDK 37 |
+| JDK | 17 for the build, plus 21 for the Gradle daemon (see below) |
+| Disk space | Several GB, for downloaded models |
+| Device | arm64 physical device recommended; the debug variant also builds x86_64 for emulators |
+
+Two JDKs, because they serve different things. The app compiles against Java 17
+(`compileOptions` in `app/build.gradle.kts`), while `gradle/gradle-daemon-jvm.properties`
+pins `toolchainVersion=21` for the Gradle daemon itself. If no local JDK 21 is present,
+Gradle provisions one over the network on every run.
+
+No NDK, CMake, or native toolchain is required. The SDK ships prebuilt native libraries
+inside its published AARs.
+
+## Setup
+
+Clone the repo:
+
+```bash
+git clone https://github.com/RunanywhereAI/runanywhere-android.git
+cd runanywhere-android
+```
+
+Point Gradle at your Android SDK: export `ANDROID_HOME`, or copy
+`local.properties.example` to `local.properties` and set `sdk.dir`.
+
+Then build:
+
+```bash
+./scripts/verify.sh
+```
+
+Or open the project in Android Studio and run the `app` configuration, or install from
+the command line:
+
+```bash
+./gradlew :app:installDebug
+```
+
+The app runs without a control plane. `RUNANYWHERE_BASE_URL` and `RUNANYWHERE_API_KEY`
+are optional (settable via environment or `local.properties`); with both blank the SDK
+initializes in its development environment. `app/build.gradle.kts` fails the
+configuration phase if exactly one of the two is set.
+
+## SDK dependency
+
+All SDK artifacts come from Maven Central under the group `io.github.sanchitmonga22`,
+pinned in `gradle/libs.versions.toml`. Three of the four share one version; QHexRT carries
+its own, because it was excluded from Maven Central publishing after 0.20.19 and cannot
+advance past it there. Nothing is declared as a local AAR or project path:
+
+```kotlin
+// gradle/libs.versions.toml
+runanywhere = "0.20.24"
+runanywhereQhexrt = "0.20.19"
+
+// app/build.gradle.kts
+implementation(libs.runanywhere.sdk) // io.github.sanchitmonga22:runanywhere-sdk
+implementation(libs.runanywhere.llamacpp) // io.github.sanchitmonga22:runanywhere-llamacpp
+implementation(libs.runanywhere.onnx) // io.github.sanchitmonga22:runanywhere-onnx
+implementation(libs.runanywhere.qhexrt) // io.github.sanchitmonga22:runanywhere-qhexrt-android
+```
+
+| Coordinate | Role |
+|---|---|
+| `runanywhere-sdk` | Core SDK and the commons native library |
+| `runanywhere-llamacpp` | llama.cpp backend (LLM, VLM) |
+| `runanywhere-onnx` | ONNX Runtime (embeddings) and Sherpa-ONNX (STT, TTS, VAD) in one AAR |
+| `runanywhere-qhexrt-android` | QHexRT backend (Qualcomm Hexagon NPU), arm64 only |
+
+The four move in lockstep; never mix versions across them. To move to a new SDK release,
+bump `runanywhere` in `gradle/libs.versions.toml`, then regenerate the two reproducibility
+files that pin the resolved graph, `app/gradle.lockfile` and
+`gradle/verification-metadata.xml`:
+
+```bash
+# 1. Dependency lock. Host-independent, so any OS will do.
+./gradlew :app:dependencies --write-locks
+
+# 2. Checksums. Gradle merges into the existing file (it adds entries and never
+# removes them) so run this on top of the committed file rather than deleting it.
+GRADLE_USER_HOME="$(mktemp -d)" ./gradlew --write-verification-metadata sha256 \
+ :app:assembleDebug :app:testDebugUnitTest :app:lintRelease
+```
+
+Two traps make the checksum file easy to get subtly wrong:
+
+- Use a throwaway `GRADLE_USER_HOME`. Checksums are only recorded for artifacts Gradle
+ actually downloads during the run. Against a warm `~/.gradle` the run looks successful
+ but silently omits things already cached, in practice a handful of parent POMs and BOM
+ metadata (`guava-parent`, `junit-bom`, `kotlin-gradle-plugins-bom`). The gap is
+ invisible until someone builds from a genuinely cold cache, i.e. CI.
+- Cover Linux and macOS. A few build-time artifacts are OS-classified
+ (`com.android.tools.build:aapt2:...-linux.jar` vs `...-osx.jar`) and Gradle records only
+ the host's. The committed file carries both, so one file satisfies the Linux CI runner
+ and a macOS developer. Because step 2 merges, the way to keep both is to run it on Linux
+ (Docker is fine), commit that file, then run it again on macOS on top. If you can only
+ reach one OS, hand-add the missing `` line to the
+ `com.android.tools.build:aapt2` component. A Linux-only file breaks every macOS
+ developer, and a macOS-only file breaks CI.
+
+Then confirm the result the same way CI will, with no bypass flags:
+
+```bash
+./gradlew :app:assembleDebug # dependency verification live
+CI=true ./gradlew :app:assembleDebug # + LockMode.STRICT
+```
+
+### Testing an unreleased SDK build
+
+To try a change from a [`runanywhere-sdks`](https://github.com/RunanywhereAI/runanywhere-sdks)
+checkout before it is on Maven Central, publish it to `~/.m2` and point this repo at it:
+
+```bash
+# In the monorepo. Publishes io.github.sanchitmonga22:*:
+(cd path/to/runanywhere-sdks/bindings/kotlin && ./gradlew publishToMavenLocal)
+
+# Here. Opt in per invocation, and relax verification for that one run
+./gradlew :app:assembleDebug \
+ -Prunanywhere.useLocalSdkAars=true \
+ --dependency-verification=lenient
+```
+
+`-Prunanywhere.useLocalSdkAars=true` adds `mavenLocal()` ahead of Google and Maven
+Central, scoped by `content { includeGroup("io.github.sanchitmonga22") }` so a stale
+`~/.m2` copy of any other dependency cannot shadow the verified one.
+
+`--dependency-verification=lenient` is required alongside it, and is not a bypass being
+smuggled in. A locally published AAR has the same coordinates as the released one but
+different bytes, so it can never match the sha256 in
+`gradle/verification-metadata.xml`. Without the flag the build stops with
+`artifacts failed verification`, which is the gate working correctly. Relax it per
+invocation like this; do not add a `` entry to
+the committed metadata, because that would permanently un-pin the four artifacts the gate
+exists to pin.
+
+Both flags are per-invocation only. Never commit `runanywhere.useLocalSdkAars` to
+`gradle.properties` and never set it in CI: `ci.yml` exists to prove a clean clone
+resolves the SDK from Maven Central, and a local AAR would make that proof vacuous.
+
+## Continuous integration
+
+`.github/workflows/ci.yml` runs on every push to `main` and every pull request:
+`ubuntu-latest`, Temurin JDK 17 and 21, the Android SDK via `android-actions/setup-android`
+(`platform-tools`, `platforms;android-37.0`, `build-tools;37.0.0`), Gradle caching via
+`gradle/actions/setup-gradle`, then `./gradlew :app:assembleDebug --no-daemon --stacktrace`
+and an APK upload.
+
+CI runs the unmodified command, no bypass flags, so it exercises the same path a developer
+does. Both reproducibility gates are enforced there and locally:
+
+| Gate | What enforces it | What it pins |
+|---|---|---|
+| `gradle/verification-metadata.xml` | Auto-enabled by Gradle whenever the file exists; `./scripts/verify.sh` additionally passes `--dependency-verification strict` | sha256 of every resolved artifact, including the four `io.github.sanchitmonga22` AARs and both OS variants of `aapt2` |
+| `app/gradle.lockfile` | `app/build.gradle.kts` flips to `LockMode.STRICT` when `$CI` is set (or with `-Prunanywhere.strictLocks=true`); `LENIENT` otherwise, so Android Studio sync stays friction-free | the exact resolved version of every module on every configuration |
+
+If a dependency or SDK bump makes either gate fail, regenerate the files (see
+[SDK dependency](#sdk-dependency)). Do not add `--dependency-verification=off` or
+`env -u CI` to the workflow, because that hides the breakage from CI while every clean
+clone keeps failing.
+
+
+## Project layout
+
+```
+app/src/main/java/com/runanywhere/runanywhereai/
+ RunAnywhereApplication.kt SDK init, backend registration, catalog seeding
+ MainActivity.kt Compose host
+ ui/navigation/ Type-safe routes and the drawer destinations
+ ui/screens/ One package per screen
+ ui/theme/ Material 3 theming, brand orange #FF6900
+ data/ Model catalog, settings, conversations, RAG, benchmarks
+ tools/ Built-in tool-calling implementations
+ download/ Model download service and progress state
+app/build.gradle.kts Variants, signing, dependency locking, SBOM, Play gate
+gradle/libs.versions.toml SDK coordinates and every dependency version
+gradle/verification-metadata.xml sha256 of every resolved artifact
+app/gradle.lockfile Resolved dependency graph
+scripts/verify.sh Strict debug APK build gate
+scripts/smoke.sh Fast static SDK API coverage check
+.github/workflows/ci.yml Clean-clone build gate
+```
+
+## Troubleshooting
+
+| Symptom | Fix |
+|---------|-----|
+| `Could not find io.github.sanchitmonga22:runanywhere-*` | Check the `runanywhere` version in `gradle/libs.versions.toml` is published to Maven Central, and that `mavenCentral()` is reachable |
+| `N artifacts failed verification` | Regenerate `gradle/verification-metadata.xml`, following [SDK dependency](#sdk-dependency) step 2 exactly, including the throwaway `GRADLE_USER_HOME` and the Linux and macOS passes |
+| `... is not part of the dependency lock state` (usually only with `CI=true`) | Regenerate the lock: `./gradlew :app:dependencies --write-locks` |
+| Gradle downloads a JDK on every run | Install a local JDK 21 for the daemon toolchain |
+| `RUNANYWHERE_BASE_URL and RUNANYWHERE_API_KEY must either both be set or both be blank` | Set both, or clear both |
+| NPU models unavailable | Confirm the device has a supported Hexagon NPU and an arm64 build; HNPU bundles also need a saved HF token |
+
+For a quick static check without a full compile:
+
+```bash
+./scripts/smoke.sh
+```
+
+## Related links
+
+| Resource | Link |
+|----------|------|
+| Kotlin SDK | [runanywhere-sdks/bindings/kotlin](https://github.com/RunanywhereAI/runanywhere-sdks/tree/main/bindings/kotlin) |
+| Maven Central | [io.github.sanchitmonga22](https://central.sonatype.com/namespace/io.github.sanchitmonga22) |
+| Play Store | [com.runanywhere.runanywhereai](https://play.google.com/store/apps/details?id=com.runanywhere.runanywhereai) |
+| Discord | [discord.gg/N359FBbDVd](https://discord.gg/N359FBbDVd) |
+| Issues | [GitHub Issues](https://github.com/RunanywhereAI/runanywhere-sdks/issues) |
+| Email | founders@runanywhere.ai |
+