On-device LFM2 / LFM2.5 inference for React Native & Expo, powered by Liquid AI's LEAP SDK via Nitro Modules.
Status: every headline feature validated on real targets — streaming chat, constrained JSON, tool loop, vision (VL-450M describing a screenshot), and voice (Audio-1.5B: speech in → streamed 24 kHz speech + captions out) on a physical iPhone and the Android emulator.
- Streaming chat with typed events (
chunk/reasoningChunk/functionCalls/audioSample/error/complete) and per-generation stats (tok/s, KV-cache hits) - Structured output: grammar-constrained JSON at the token level — pass a Zod v4 schema or raw JSON Schema to
generateObject - Tool calling: register JS tools once,
sendWithToolsruns the model↔tool loop automatically - Thinking models (LFM2.5-*-Thinking): reasoning streamed separately via
onReasoning - Vision: send image parts to LFM2.5-VL models; Voice: LFM2.5-Audio speech-to-speech with streamed PCM + captions
- Built-in model management: download from the LEAP model library with progress, query/cancel/delete, or sideload any GGUF — plus a memory pre-flight that refuses loads that would OOM (
forceLoadto override) - Vercel AI SDK provider:
@react-native-leap/aiplugs LEAP models intostreamText/generateObjectfrom theaipackage - Expo config plugin (prebuild / dev-client — Nitro does not run in Expo Go)
npm install react-native-leap react-native-nitro-modulesExpo (SDK 54+): add the plugin and rebuild —
{ "expo": { "plugins": ["react-native-leap"] } }npx expo prebuild && npx expo run:ios --deviceBare React Native: add two lines to ios/Podfile (physical-device builds need the nested-dylib signing hook; the Expo plugin does this automatically):
require_relative '../node_modules/react-native-leap/scripts/leap_codesign'
post_integrate do |installer|
leap_codesign_post_integrate(installer)
endThe LEAP SDK binaries are not in the npm package: iOS downloads Liquid's official XCFramework release at install time (SHA-256-verified); Android pulls ai.liquid.leap:* from Maven Central at build time.
import { useLeapModel, Models } from "react-native-leap";
function Chat() {
const { model, isReady, downloadProgress, error } = useLeapModel(
Models.LFM25_1_2B_INSTRUCT.name,
{ contextSize: 4096 },
);
if (!isReady) return <Text>{error ?? `Loading ${Math.round(downloadProgress * 100)}%`}</Text>;
const chat = model.createConversation({ system: "You are terse." });
const reply = await chat.send("Hello!", {
onChunk: (t) => console.log(t),
});
}Without the hook:
import { Leap, Models } from "react-native-leap";
const model = await Leap.load(Models.LFM25_350M, {
onProgress: (p) => console.log(`${Math.round(p.fraction * 100)}%`),
});
const chat = model.createConversation();
const text = await chat.send("Say hello in five words.");
console.log(chat.lastStats.tokensPerSecond);Decoding is constrained to the schema at the token level by the LEAP engine; Zod results are also runtime-validated and fully typed:
import { z } from "zod"; // optional peer dependency (v4+)
const Receipt = z.object({ vendor: z.string(), total: z.number(), date: z.string() });
const receipt = await chat.generateObject("Extract: " + ocrText, { schema: Receipt });
// ^? { vendor: string; total: number; date: string }Tip: the task-tuned Models.LFM2_350M_EXTRACT (~230 MB) is built for exactly this.
chat.registerTool({
name: "get_weather",
description: "Current weather for a city",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => fetchWeather(city),
});
const answer = await chat.sendWithTools("What's the weather in Tokyo?");Qwen models from the LEAP library use the Hermes dialect — pass functionCallParser: "hermes".
const model = await Leap.load(Models.LFM25_1_2B_THINKING);
const chat = model.createConversation();
await chat.send("Prove there are infinitely many primes.", {
enableThinking: true,
onReasoning: (r) => setThinking((t) => t + r),
onChunk: (t) => setAnswer((a) => a + t),
});VL models accept image parts alongside text (LEAP ships VL weights at Q8_0):
const vl = await Leap.load(Models.LFM25_VL_450M);
const chat = vl.createConversation();
const description = await chat.send(
[
{ type: "image", path: photoUri },
{ type: "text", text: "Describe this image briefly." },
],
{ onChunk: (t) => console.log(t) },
);LFM2.5-Audio takes speech (or text) in and streams interleaved speech + captions out. createVoiceSession applies the activation system prompt the engine requires and cleans special tokens from captions — feed the PCM chunks straight into your audio player:
const audio = await Leap.load(Models.LFM25_AUDIO_1_5B);
const voice = audio.createVoiceSession({
onText: (caption) => append(caption),
onAudioChunk: (pcm, sampleRate) => player.enqueue(pcm, sampleRate), // 24 kHz float PCM
});
await voice.sendAudio({ path: recordingUri });
voice.close();import { streamText } from "ai";
import { leap } from "@react-native-leap/ai";
const { textStream } = streamText({
model: leap("LFM2.5-1.2B-Instruct"),
prompt: "Write a haiku about running offline.",
});Models.* covers the LEAP model library (LFM2.5 chat/thinking/JP, LFM2 + task-specific Extract/RAG/Tool "Nanos", VL vision, Audio speech, and Qwen3). Any GGUF can be sideloaded:
const model = await Leap.loadFromSource({ modelPath: "file:///path/model.gguf" });Management: Leap.download, Leap.queryStatus, Leap.cancelDownload, Leap.deleteModel, Leap.getModelSizeBytes.
| React Native | ≥ 0.76 (New Architecture; no Expo Go) |
| react-native-nitro-modules | ^0.36 |
| iOS | 17.0+, Apple Silicon simulators only |
| Android | minSdk 31, arm64-v8a, Kotlin 2.3.x (the Expo plugin pins this) |
| Device | ≥ 3 GB RAM (350M-class models run on much less) |
- iOS simulator generation: LEAP SDK v0.10.9's simulator slice produces degenerate output (and can crash the process) — an upstream engine bug. Model download/load work on the simulator; run generations on a physical device (or macOS) until Liquid ships a fix. Android emulators are unaffected.
- No coexistence with other llama.cpp runtimes: linking a second llama.cpp-based library in the same app (e.g. llama.rn, whisper.rn, llama.swift) breaks LEAP model loading (leap-sdk#7) — the runtimes' symbols/resources collide. Pick one per app until Liquid namespaces their engine.
example/ is a ChatGPT-style on-device chat (dark UI after MargeloChat): streaming chat with tool calls, a conversations drawer persisted with react-native-mmkv, and suggestion chips that exercise structured output, tool calling, vision, and voice end-to-end.
cd example && npm install
npx expo run:android # or: npx expo run:ios --deviceMIT (this wrapper). The LEAP SDK is proprietary (Leap Terms of Use) and is fetched from Liquid's official channels at build time. LFM model weights use the LFM Open License v1.0 — free commercial use below $10M annual revenue. Unofficial community project — not affiliated with or endorsed by Liquid AI.