Memory
On-device models are the largest allocation your app will make. Most crash reports from SDK integrations trace back to holding two of them at once.Load one model per modality
Model lifecycle is per category. Loading a second language model replaces the first; loading an STT model does not touch the LLM. That means a chat model and a recognizer can coexist, but a 3B and a 0.5B language model cannot.loadModel() is the single entry point for every modality. loadLLMModel(), loadSTTModel(),
loadTTSVoice(), and loadVLMModel() do not exist.
Unload before a large allocation
ModelUnloadRequest also accepts model_id, category,
and framework for narrower unloads.
Check compatibility before committing
ModelInfo.memory_required_bytes is the runtime headroom estimate and is deliberately separate
from download_size_bytes, which is transport size. Compare against available RAM, not against
free storage. downloadModel() already runs this preflight for registered models.
Performance
Quantization
Smaller quantizations trade output quality for memory and speed. Q4 variants are the usual starting point on phones; Q8 is worth trying when the model is small enough that its full size still fits comfortably.Stream anything the user reads
generateStream() emits LLMStreamEvent, not String, and is not a suspend function. See
Streaming.
Cap output tokens against the model, not against a constant
context_length. See VLM.
Keep blocking calls off the main thread
loadModel(), processImage(), and the first call into a freshly registered backend all block for
a long time in JNI. Dispatch them:
transcribeStream, synthesizeStream,
streamVoiceAgent) onto Dispatchers.IO, so collecting those from the main thread is fine.
Lifecycle
Initialize once, in Application
Registration order matters. CPU backends register beforeinitialize(); QHexRT registers after.
See Configuration.
Cancel streams when a screen goes away
A retained collector keeps feeding a shared model after the user has navigated on. Cancel the job and join it before tearing anything down:Bound live audio buffers
Live transcription reads from an upstream flow of microphone chunks. Bound it and drop the oldest under pressure, otherwise navigating away leaves minutes of audio queued behind one blocking recognizer call.Preload at launch
downloadModelStream(model: RAModelInfo) returns Flow<DownloadProgress>. The suspend form is
downloadModel(model, onProgress), which returns the terminal DownloadProgress. Both take a
ModelInfo, not a model id string.
Cancelling a download propagates to the native worker and preserves resume bytes for a later retry.
Errors
Branch on category
ErrorCode has roughly 280 values and grows with the proto. ErrorCategory has nine and is
stable. Full names are ERROR_CATEGORY_*, not bare MODEL or STORAGE. See
Error handling.
Rethrow CancellationException
CancellationException in a broad catch (e: Exception) breaks structured concurrency
and turns a normal user-driven stop into a spurious error banner.
Check result flags, not just exceptions
loadModel, unloadModel, listModels, queryModels, getModel, and getStorageInfo return a
result object with a success flag rather than throwing.
Show progress that reflects reality
total_bytes is 0 until the planner has sized the artifact, so a naive division shows nothing
during the plan stage.
Testing
Test on hardware. An emulator does not reproduce the memory ceiling, the CPU and NPU performance, or the thermal throttling that determine whether a model is usable. Measure with the fields the result actually carries:ttft_ms is set in streaming mode only.
Security
Keep the api key out of source:RunAnywhere.setHfToken(token); never embed it in the APK. The SDK attaches it only to
huggingface.co and hf.co over https and never logs it.
RunAnywhere.reset() is a suspend function that clears SDK state.
Android specifics
Runtime permissions
STT and the voice agent needRECORD_AUDIO granted at runtime through
ActivityResultContracts.RequestPermission(), not just declared in the manifest.
Audio formats
STT expects 16 kHz mono 16-bit PCM. TTS output sample rate varies by voice: readTTSOutput.sample_rate rather than assuming one. speak() handles the PCM-to-WAV conversion and
playback for you.
Content URIs
VLMImage with VLM_IMAGE_FORMAT_FILE_PATH needs a filesystem path. Images from the photo picker
arrive as content URIs and have to be copied to a cache file first, then deleted in a finally
block. See VLM.
Backends may be absent
WrapLlamaCPP.register() and ONNX.register() in runCatching. A build that ships only some
native libraries will throw on the missing one, and an unguarded call aborts SDK setup entirely.
Record which registrations succeeded so model pickers can hide rows whose backend cannot serve them.
JitPack repository
The SDK has transitive dependencies hosted on JitPack. Addmaven { url = uri("https://jitpack.io") } to settings.gradle.kts. See
Installation.
Qualcomm Hexagon NPU
The NPU has a single slot. Two NPU models cannot be resident at once, so loading one evicts the other. This has consequences that do not exist on CPU backends. The voice agent requires STT, LLM, and TTS to be co-resident, so it cannot run when both the recognizer and the chat model are QHexRT. Drive the turn manually, loading one model per phase. See Voice agent. A CPU or system TTS can play while an NPU LLM keeps generating, because they do not share the slot. An NPU TTS can only load after generation finishes. Branch on the framework, not on the model name:context_length (512 is common), which makes the VLM
output cap mandatory.
Gated NPU model repos need a HuggingFace token before they will download.
Checklist
Load one model per modality and unload before a large allocation. Capmax_tokens against the
model’s context, especially for VLM. Stream anything the user reads. Dispatch load and image calls
off the main thread. Cancel and join stream collectors on teardown. Branch on ErrorCategory and
rethrow CancellationException. Check success flags on result-returning calls. Test on hardware.