Skip to content

Repository files navigation

react-native-leap

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, sendWithTools runs 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 (forceLoad to override)
  • Vercel AI SDK provider: @react-native-leap/ai plugs LEAP models into streamText / generateObject from the ai package
  • Expo config plugin (prebuild / dev-client — Nitro does not run in Expo Go)

Installation

npm install react-native-leap react-native-nitro-modules

Expo (SDK 54+): add the plugin and rebuild —

{ "expo": { "plugins": ["react-native-leap"] } }
npx expo prebuild && npx expo run:ios --device

Bare 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)
end

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

Quick start

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);

Structured output (Zod / JSON Schema)

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.

Tool calling

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

Thinking models

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),
});

Vision

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) },
);

Voice (speech-to-speech)

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();

Vercel AI SDK

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

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.

Requirements

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)

Known upstream limitations

  • 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 app

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 --device

Licensing

MIT (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.

About

On-device LFM2 / LFM2.5 inference for React Native & Expo, powered by Liquid AI's LEAP SDK via Nitro Modules.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages