https://github.com/darksorrow/llamarn
React Native Turbo Module for llama.cpp integration, optimized for the New Architecture
https://github.com/darksorrow/llamarn
ai llamacpp react-native
Last synced: 3 months ago
JSON representation
React Native Turbo Module for llama.cpp integration, optimized for the New Architecture
- Host: GitHub
- URL: https://github.com/darksorrow/llamarn
- Owner: DarkSorrow
- License: apache-2.0
- Created: 2025-05-26T19:49:52.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-06-01T21:56:47.000Z (about 1 year ago)
- Last Synced: 2025-06-04T12:44:34.159Z (about 1 year ago)
- Topics: ai, llamacpp, react-native
- Language: C++
- Homepage:
- Size: 1.28 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# LlamaRN
A thin, reliable React Native Turbo Module wrapping [llama.cpp](https://github.com/ggml-org/llama.cpp) for on-device LLM inference on iOS and Android.
## Features
- Model loading, text completion, and chat completion
- Metal GPU on iOS; OpenCL / Vulkan / Hexagon NPU on Android
- Automatic CPU/GPU detection and optimal GPU layer estimation
- Chat completion with Jinja template support
- Multi-turn KV cache prefix reuse (pass a stable `id` per message)
- Embeddings generation
- Function / tool calling
- Thinking and reasoning model support (`reasoning_budget`, `reasoning_format`)
- **Multimodal / Vision** — image-in-prompt chat, CLIP-style embeddings, Whisper-style transcription, open-ended vision reasoning, and zero-copy camera frame pipeline
## Breaking changes
See **[BREAKING.md](./BREAKING.md)** for migration notes (thinking models / `reasoning_content`, KV cache behavior, GGUF + Jinja + tools pitfalls) and the **v0.7.0** release summary.
## Installation
```sh
npm install @novastera-oss/llamarn
```
## Developer Setup
### Prerequisites
1. Clone the repository
2. React Native development environment for your target platform(s)
### Initial Setup
```sh
npm install
npm run setup-llama-cpp # init llama.cpp submodule
```
### Android
```sh
./scripts/build_android_external.sh # build native libraries
cd example && npm run android
```
### iOS
```sh
cd example/ios && bundle exec pod install
cd .. && npm run ios
```
**Troubleshooting:**
- Android: `cd android && ./gradlew clean`
- iOS: `cd example/ios && rm -rf build Podfile.lock && pod install`
---
## Basic Usage
### Simple Completion
```js
import { initLlama } from '@novastera-oss/llamarn';
const context = await initLlama({
model: '/path/to/model.gguf',
n_ctx: 2048,
n_batch: 512,
});
const result = await context.completion({
prompt: 'What is artificial intelligence?',
temperature: 0.7,
top_p: 0.95,
});
console.log(result.text);
```
### Chat Completion
```js
const context = await initLlama({
model: '/path/to/model.gguf',
n_ctx: 4096,
use_jinja: true,
});
const result = await context.completion({
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'Tell me about quantum computing.' },
],
temperature: 0.7,
});
console.log(result.text);
// OpenAI-compatible: result.choices[0].message.content
```
### Multi-Turn Chat with KV Cache Reuse
Assign a stable `id` to each message. The native layer re-uses KV cache entries for messages whose ID matches the previous turn — only new tokens are encoded.
```js
const history = [
{ role: 'system', content: 'You are a helpful assistant.', id: 'sys-1' },
{ role: 'user', content: 'Hi!', id: 'turn-1-u' },
];
const r1 = await context.completion({ messages: history });
history.push({ role: 'assistant', content: r1.text, id: 'turn-1-a' });
history.push({ role: 'user', content: 'Tell me more.', id: 'turn-2-u' });
// Only [turn-2-u] is encoded; everything before hits the cache
const r2 = await context.completion({ messages: history });
```
**Rules:**
- Messages without an `id` are always fully re-encoded.
- If you edit a message's content, change its `id` so the cache is invalidated.
- `resetKvCache: true` forces a full clear.
```js
await context.completion({ messages: history, resetKvCache: true });
```
### Thinking and Reasoning Models
```js
const context = await initLlama({
model: '/path/to/reasoning-model.gguf',
n_ctx: 4096,
use_jinja: true,
reasoning_budget: -1, // -1 = unlimited, 0 = disabled, >0 = token limit
reasoning_format: 'deepseek', // 'none' | 'auto' | 'deepseek' | 'deepseek-legacy'
thinking_forced_open: true,
});
const result = await context.completion({
messages: [{ role: 'user', content: 'Solve: what is 15% of 240?' }],
});
// result.text may include … depending on the model
```
See **[BREAKING.md](./BREAKING.md)** for multi-turn thinking, KV cache, and tool-calling pitfalls.
### Tool Calling
```js
const context = await initLlama({
model: '/path/to/model.gguf',
n_ctx: 2048,
use_jinja: true, // parse_tool_calls enabled automatically
parallel_tool_calls: false,
});
const response = await context.completion({
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: "What's the weather in Paris?" },
],
tools: [{
type: 'function',
function: {
name: 'get_weather',
description: 'Get the current weather in a location',
parameters: {
type: 'object',
properties: {
location: { type: 'string' },
unit: { type: 'string', enum: ['celsius', 'fahrenheit'] },
},
required: ['location'],
},
},
}],
tool_choice: 'auto',
});
if (response.choices?.[0]?.finish_reason === 'tool_calls') {
const call = response.choices[0].message.tool_calls[0];
console.log(call.function.name, call.function.arguments);
}
```
### Embeddings
```js
const context = await initLlama({
model: '/path/to/embedding-model.gguf',
embedding: true,
n_ctx: 2048,
});
const { data } = await context.embedding({ input: 'Text to embed' });
console.log(data[0].embedding); // Float32 array
```
---
## Multimodal / Vision
llamarn supports image and audio input for compatible models (LLaVA, Qwen-VL, Whisper) via a separate **multimodal projection model** (`mmproj` `.gguf`).
### Init with capabilities
```typescript
const model = await initLlama({
model: '/path/to/model.gguf',
mmproj: '/path/to/mmproj.gguf',
capabilities: ['vision-chat'], // 'image-encode' | 'audio-transcribe' | 'vision-reasoning'
n_ctx: 4096,
use_jinja: true,
});
const mods = await model.getSupportedModalities();
// { vision: true, audio: false }
const enabled = await model.isMultimodalEnabled();
// true
```
**Supported capabilities:**
| Value | Description |
|-------|-------------|
| `vision-chat` | Image-in-prompt chat completions (LLaVA, Qwen-VL) |
| `image-encode` | CLIP-style image embeddings |
| `audio-transcribe` | Whisper-style audio → text |
| `vision-reasoning` | Open-ended image analysis returning raw model text |
### Vision Chat
```typescript
const result = await model.completion({
messages: [{
role: 'user',
content: [
{ type: 'text', text: 'What is in this image?' },
{ type: 'image_url', image_url: { url: 'file:///path/to/image.jpg' } },
],
}],
n_predict: 256,
});
console.log(result.text);
```
Multiple images per message are supported. `file://` URIs and `data:image/...;base64,...` strings are both accepted. KV cache prefix reuse is automatically disabled when messages contain images.
### Image Embeddings
```typescript
const { embedding, n_tokens, n_embd } = await model.embedImage(
'file:///path/to/image.jpg',
{ normalize: true },
);
// embedding: flat Float32 array, length = n_tokens * n_embd
```
### Audio Transcription
```typescript
const { text } = await model.transcribeAudio('file:///path/to/audio.wav');
```
### Open-ended Vision Reasoning
```typescript
const { raw_text } = await model.visionReasoning(
'file:///path/to/image.jpg',
{ prompt: 'List all objects you see.' },
);
```
### Camera Frame (zero-copy)
Works with [VisionCamera](https://github.com/mrousavy/react-native-vision-camera) NativeBuffer frames — no disk I/O required.
```typescript
import { useFrameProcessor, runAsync } from 'react-native-vision-camera';
const frameProcessor = useFrameProcessor((frame) => {
'worklet';
runAsync(frame, async () => {
// maxSize: 336 downsamples to ~330 KB during the pixel copy (recommended for LLaVA/CLIP)
// Omit maxSize (or 0) for full-resolution analysis (OCR, detailed scenes)
const result = await model.runOnFrame(
frame, frame.width, frame.height, 'image-encode', { maxSize: 336 });
if (result) {
// result.embedding — Float32 array (n_tokens × n_embd)
console.log('Tokens:', result.n_tokens);
}
// null means the encoder was busy with the previous frame — dropped automatically
});
}, [model]);
```
Frame dropping is handled automatically in C++ via an atomic flag — no JS-side throttle needed.
**Supported model families:**
| Model | Capability |
|-------|-----------|
| LLaVA 1.5 / 1.6 | vision-chat |
| Qwen2-VL / Qwen3-VL | vision-chat |
| LLaMA 4 Scout | vision-chat |
| MiniCPM-V | vision-chat |
| Whisper | audio-transcribe |
| CLIP | image-encode |
---
## Model Info
Query a model's properties without fully loading it:
```typescript
import { loadLlamaModelInfo } from '@novastera-oss/llamarn';
const info = await loadLlamaModelInfo('/path/to/model.gguf');
console.log(info.description); // human-readable model name + quant
console.log(info.n_params); // parameter count
console.log(info.n_layers); // total layer count
console.log(info.model_size_bytes); // quantized on-disk size
console.log(info.optimalGpuLayers); // recommended n_gpu_layers for this device
console.log(info.availableMemoryMB); // current free device RAM in MB
console.log(info.estimatedVramMB); // estimated VRAM needed for optimalGpuLayers
console.log(info.architecture); // e.g. "llama", "qwen2", "mistral"
console.log(info.gpuSupported); // true if GPU offload is available
```
---
## Advanced Configuration
### GPU and Memory Parameters
| Parameter | Default | Description |
|-----------|---------|-------------|
| `n_gpu_layers` | `0` | Layers to offload to GPU (use `optimalGpuLayers` from `loadLlamaModelInfo`) |
| `n_ctx` | `2048` | Context window size |
| `n_batch` | `512` | Prompt processing batch size |
| `use_mmap` | `true` | Memory-mapped loading (faster startup) |
| `use_mlock` | `false` | Lock model in RAM (prevents swapping) |
### Thinking and Reasoning Parameters
| Parameter | Default | Description |
|-----------|---------|-------------|
| `reasoning_budget` | `-1` | Token budget for thinking: `-1` unlimited, `0` disabled, `>0` limited |
| `reasoning_format` | `'auto'` | `'none'` \| `'auto'` \| `'deepseek'` \| `'deepseek-legacy'` |
| `thinking_forced_open` | `false` | Force thinking tags in every response |
| `parse_tool_calls` | `true` | Parse tool call JSON from output (auto-enabled with `use_jinja`) |
| `parallel_tool_calls` | `false` | Allow multiple tool calls per response |
---
## Model Path Handling
### iOS
- Bundle path (Xcode resource): `${RNFS.MainBundlePath}/model.gguf`
- Absolute path: `/var/mobile/…/model.gguf`
### Android
- Cache directory: `${RNFS.CachesDirectoryPath}/model.gguf`
- App assets (copied to cache first): `RNFS.copyFileAssets('model.gguf', dest)`
---
## Documentation
- [Interface Documentation](INTERFACE.md) — Detailed API interfaces
- [Breaking Changes](BREAKING.md) — Migration notes
- [Example App](example/) — Working example with text chat and vision demo
- [Contributing Guide](CONTRIBUTING.md)
---
## About Novastera
**LlamaRN** is part of the **Novastera** open-source ecosystem. This library powers on-device LLM inference in [Novastera's](https://novastera.com) mobile applications — no data leaves the user's device.
Learn more: [https://novastera.com/resources](https://novastera.com/resources)
## License
Apache 2.0
## Acknowledgments
- **[mybigday/llama.rn](https://github.com/mybigday/llama.rn)** — foundational React Native binding for llama.cpp
- **[ggml-org/llama.cpp](https://github.com/ggml-org/llama.cpp)** — the core C++ inference library
- **[react-native-pure-cpp-turbo-module-library](https://github.com/Zach-Dean-Attractions-io/react-native-pure-cpp-turbo-module-library)** — reference for the Android C++ Turbo Module pattern