From 456f79cacddae2f61ded1d9ea10d6393cd2ce174 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Wed, 19 Aug 2026 03:37:39 +0530 Subject: [PATCH 1/7] docs: rewrite the README as a consumer page and move build detail to docs/DEVELOPMENT.md --- README.md | 347 ++++++++++++-------------------------------- docs/DEVELOPMENT.md | 220 ++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+), 254 deletions(-) create mode 100644 docs/DEVELOPMENT.md diff --git a/README.md b/README.md index d7f0139..624cb1a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,37 @@ -# 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. +

+ RunAnywhere +

-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. +

+ + Get it on Google Play + +

-## Screenshots +

+ Android 7.0+ + Kotlin 2.4 + Jetpack Compose + Snapdragon NPU + RunAnywhere License +

+ +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 LFM2 350M, quantised Q4_K_M, through the llama.cpp backend. @@ -22,276 +45,92 @@ llama.cpp backend. | ![The Advanced hub](docs/screenshots/08-advanced.png) | ![Settings](docs/screenshots/07-settings.png) | | 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). + +## What you can do -## Requirements +| | | +| --- | --- | +| **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 | -| 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 | +Models download from a curated catalog through a sheet you can reach from any screen that +needs one. Cloud providers exist but are opt-in and off by default. -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. +## Snapdragon NPU -No NDK, CMake, or native toolchain is required. The SDK ships prebuilt native libraries -inside its published AARs. +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. -## Setup +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. -Clone the repo: +## Build it yourself ```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. +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. -## SDK dependency +[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) covers the SDK pinning rules, dependency +verification, testing an unreleased SDK build, CI, and troubleshooting. -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: +## Architecture -```kotlin -// gradle/libs.versions.toml -runanywhere = "0.20.19" +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/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 + 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 ``` -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 -``` +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. -`-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. - -## 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 - -``` -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 -``` +| 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/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..353abf7 --- /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 | + From 590363d536dc42ff58002977f855c49395fc013c Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Thu, 20 Aug 2026 07:06:00 +0530 Subject: [PATCH 2/7] fix(catalog): point Qwen3.5 0.8B at a URL that resolves and correct its size --- .../java/com/runanywhere/runanywhereai/data/ModelCatalog.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 b4d7d9f..4bc5d1a 100644 --- a/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt +++ b/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt @@ -271,10 +271,11 @@ internal object ModelCatalog { 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. From 7dd4115fccbe1de782d6b720c20e2b7cfe0e9d33 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Thu, 20 Aug 2026 07:15:16 +0530 Subject: [PATCH 3/7] fix(catalog): repoint Qwen3.8 27B and Gemma 4 E4B vision at URLs that resolve --- .../com/runanywhere/runanywhereai/data/ModelCatalog.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 4bc5d1a..0f0126a 100644 --- a/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt +++ b/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt @@ -294,10 +294,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) @@ -707,9 +708,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", From 14195e10e10b0a37a31ddacf6338ca596f14ad95 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:46:06 +0530 Subject: [PATCH 4/7] feat(catalog): drop superseded families and add the verified current model set --- .../runanywhereai/data/ModelCatalog.kt | 234 +++++------------- .../ui/screens/models/ModelRecommendation.kt | 10 +- .../ui/screens/solutions/SolutionsYaml.kt | 4 +- .../runanywhereai/ModelCatalogTest.kt | 22 +- .../ui/screens/models/ModelTaxonomyTest.kt | 4 +- .../solutions/AndroidSolutionsConfigTest.kt | 4 +- 6 files changed, 88 insertions(+), 190 deletions(-) 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 0f0126a..e11093f 100644 --- a/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt +++ b/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt @@ -207,67 +207,6 @@ 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", @@ -317,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", @@ -357,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 @@ -431,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( @@ -471,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", @@ -514,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 @@ -564,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) ------------------------------------------ @@ -605,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 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 75d9c3c..74c6806 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/solutions/SolutionsYaml.kt b/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.kt index a9ec76c..db23afc 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/test/java/com/runanywhere/runanywhereai/ModelCatalogTest.kt b/app/src/test/java/com/runanywhere/runanywhereai/ModelCatalogTest.kt index 1bd8eb2..d3d1d11 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 ef8b175..35b2820 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 814cd1f..ff0408b 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", From 8c9bd5276b7b3d0eb659349b1bf699c4e0791633 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:42:07 +0530 Subject: [PATCH 5/7] feat(models): file Granite, Maple, Muse and Fara under their real publishers --- .../runanywhereai/ui/screens/models/ModelTaxonomy.kt | 11 +++++++++-- .../runanywhereai/ui/theme/icons/RACBrands.kt | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) 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 06eff6f..e234552 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,10 +15,12 @@ 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), HUGGING_FACE(RACBrands.HuggingFace), APPLE(RACBrands.Apple), @@ -55,9 +57,14 @@ 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"), 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 b4bde1a..7f92b21 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,10 +25,12 @@ 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 HuggingFace = Brand("Hugging Face", RACIcons.Brands.HuggingFace, Color(0xFFFFD21E)) val Apple = Brand("Apple", RACIcons.Brands.Foundation, Color(0xFF00C2A8)) From 004ae82124543bdbe4c9b853605345b5189ff691 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:15:55 +0530 Subject: [PATCH 6/7] feat(models): file Ministral under Mistral and GLM under Zhipu AI --- .../runanywhereai/ui/screens/models/ModelTaxonomy.kt | 4 +++- .../com/runanywhere/runanywhereai/ui/theme/icons/RACBrands.kt | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) 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 e234552..ed36bc6 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 @@ -22,6 +22,7 @@ enum class ModelOrg(val brand: Brand) { PRISM(RACBrands.Prism), DEEPGROVE(RACBrands.Deepgrove), OPENAI(RACBrands.OpenAI), + ZHIPU(RACBrands.Zhipu), HUGGING_FACE(RACBrands.HuggingFace), APPLE(RACBrands.Apple), OPEN_SOURCE(RACBrands.OpenSource), @@ -67,9 +68,10 @@ private val orgSpecs: List = listOf( 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/theme/icons/RACBrands.kt b/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/icons/RACBrands.kt index 7f92b21..e273ed4 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 @@ -32,6 +32,7 @@ object RACBrands { 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)) From b531012d31f3bf2431300696727090899e689916 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:33:45 +0530 Subject: [PATCH 7/7] docs: describe the current model picker and drop the model names that go stale --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 624cb1a..0c51cc0 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ hardware the inference runs on the Hexagon NPU. ## What it looks like -Captured on a physical arm64 device running LFM2 350M, quantised Q4_K_M, through the -llama.cpp backend. +Captured on a physical arm64 device, running a small GGUF chat model through the llama.cpp +backend. | | | |---|---| @@ -57,8 +57,18 @@ The rest, including the landscape layout, are in [`docs/screenshots/`](docs/scre | **Documents** | Add documents and ask questions, with sources cited | | **Advanced** | OCR, segmentation, diarization, image generation, read aloud, transcription, voice activity, tools, benchmarks | -Models download from a curated catalog through a sheet you can reach from any screen that -needs one. Cloud providers exist but are opt-in and off by default. +Cloud providers exist but are opt-in and off by default. + +## Models + +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. + +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. ## Snapdragon NPU