LLM Chat & Text Generation
The LLM extension lets you run generative Large Language Models (LLMs) and Vision-Language Models (VLMs) directly on user devices with real-time token streaming, complete privacy, and full offline support. Depending on what you are building, you can choose between two levels of control:
- Chat Sessions (
useLLMChatSession/createLLMChatSession): The recommended API for conversational apps and AI assistants. It manages multi-turn conversation history, applies Jinja2 chat templates, supports multimodal image inputs, and handles automated tool calling. - Low-Level Runner (
LLMRunner): A direct execution engine that operates on worklet threads. It processes raw text strings or media tensors without chat formatting, giving you manual control over KV cache prefilling, synchronous generation loops, and context rewinding.
| iOS | Android |
|---|---|
Quick Start
The useLLMChatSession hook handles remote model downloading, caching, tokenizer setup, and conversational state in a single React hook:
import { useState } from 'react';
import { models, useLLMChatSession } from 'react-native-executorch';
function MyChatComponent() {
const [streamingText, setStreamingText] = useState('');
const session = useLLMChatSession(models.llm.LFM2_5_1_2B.DEFAULT, {
initialMessages: [{ role: 'system', content: 'You are a helpful on-device assistant.' }],
generationConfig: {
temperature: 0.2,
maxNewTokens: 512,
},
});
// Hook state:
// session.isReady — true once model weights and tokenizer are loaded in memory
// session.downloadProgress — 0 to 100 download progress
// session.error — Error instance if download or load failed
// session.resource — resolved config with all URLs replaced by local file paths
const handleSend = async (userPrompt: string) => {
if (!session.isReady || !session.sendMessage) return;
setStreamingText('');
// Stream tokens as they are decoded
const turn = await session.sendMessage(userPrompt, (token) => {
setStreamingText((prev) => prev + token);
});
console.log('New messages added this turn:', turn.messages);
console.log('Turn generation statistics:', turn.stats);
};
// Trigger handleSend on submit from a prompt input or chat screen
}
See src/app/(screens)/llm-chat.tsx in the React Native ExecuTorch Gallery for a complete, runnable chat UI with token streaming, token/sec benchmarking, and message history.
Understanding the Output & Turn Result
When you call sendMessage(), the promise resolves to an LLMChatTurnResult describing what happened during that turn:
type LLMChatTurnResult = {
/**
* The new messages added to conversation history during this turn.
* Includes the user prompt, any assistant tool calls, tool responses,
* and the final assistant message.
*/
readonly messages: readonly ChatMessage[];
/**
* Performance statistics for each generation step in this turn.
* If the model executed tools, this array contains one entry per generation step.
*/
readonly stats: readonly LLMGenerationStats[];
/**
* The termination reason:
* - 'stop': The model generated an End-Of-Sequence (EOS) token or hit maxNewTokens.
* - 'maxToolTurns': The turn was terminated because tool execution reached maxToolTurns.
*/
readonly finishReason: 'stop' | 'maxToolTurns';
};
stats is an array of LLMGenerationStats because tool calling can trigger multiple consecutive generation steps in a single turn (e.g. stats[0] for the model generating the tool call, and stats[1] for generating the final answer after tool execution). Each entry provides numPromptTokens, numGeneratedTokens, prefillDurationMs, and start/end timestamps (inferenceStartMs / inferenceEndMs) to compute tokens per second (tok/s).
Chat Templates & Incremental KV Cache Diffing
Under the hood, createChatPreprocessor renders ChatMessage[] arrays using the model's official Jinja2 template from tokenizer_config.json (formatting special tokens, roles, and generation headers).
To keep multi-turn chat responsive without re-encoding past history on every message, the preprocessor uses incremental prompt diffing:
- It verifies that the rendered prefix of previously committed turns is an exact substring match of the newly updated conversation.
- It slices out only the newly appended tokens and passes them to
runner.prefill(). - The existing Key-Value (KV) cache in native memory is preserved, so generation starts immediately without recalculating prior turns.
If you use a custom model whose Jinja template dynamically rewires earlier turns when new messages arrive (breaking monotonicity), pass resetOnTurn: true in your session options to force full re-encoding each turn.
Multimodal Inputs
Vision-Language Models (such as Liquid AI's LFM2_5_VL_450M and LFM2_5_VL_1_6B) process interleaved text and visual payloads:
import { models, useLLMChatSession } from 'react-native-executorch';
import type { ImageBuffer } from 'react-native-executorch/cv';
function VisionChat() {
const session = useLLMChatSession(models.llm.LFM2_5_VL_450M.DEFAULT);
const handleAnalyzePhoto = async (image: ImageBuffer) => {
if (!session.isReady || !session.sendMessage) return;
// Send array of interleaved media and text
const turn = await session.sendMessage(
[
{ kind: 'image', image },
'What type of flower is this, and what care instructions should I follow?',
],
(token) => {
process.stdout.write(token);
}
);
console.log('Result:', turn.messages);
};
}
The session automatically embeds the model's sentinel vision tokens, resizes and normalizes the image buffer to the vision encoder's target shape, and feeds the resulting image tensors into the multimodal execution runner alongside text tokens.
Automated Tool Calling
The LLM chat session supports automated, multi-turn tool calling (function calling). When tool definitions and a parser are supplied, the session automatically invokes tool callbacks, feeds their results back into the conversation, and returns the final assistant answer.
1. Define Tools with execute
Declare tools matching standard JSON Schema specifications along with an asynchronous execute handler:
import { type ToolDefinition } from 'react-native-executorch/llm';
export const weatherTool: ToolDefinition<{ location: string; unit?: 'celsius' | 'fahrenheit' }> = {
type: 'function',
function: {
name: 'get_current_weather',
description: 'Get the current weather conditions for a given city.',
parameters: {
type: 'object',
properties: {
location: { type: 'string', description: 'City name' },
unit: { type: 'string', enum: ['celsius', 'fahrenheit'] },
},
required: ['location'],
},
},
execute: async ({ location, unit = 'celsius' }) => {
// Query local device sensors or web API
return JSON.stringify({ location, temperature: 22, unit, condition: 'Sunny' });
},
};
2. Supply a Tool Parser (parseToolCalls)
Because different open-source model families emit tool calls in varying syntax (e.g. XML <tool_call> tags, JSON markdown blocks, or special tokens), supply a parser function conforming to ToolParser:
import type { ToolParser, ToolParserResult } from 'react-native-executorch/llm';
// Example parser for XML tag syntax:
// <tool_call>{"name": "get_current_weather", "arguments": {"location": "San Francisco"}}</tool_call>
export const xmlToolParser: ToolParser = (text: string): ToolParserResult | undefined => {
const match = text.match(/<tool_call>([\s\S]*?)<\/tool_call>/);
if (!match) return undefined;
try {
const json = JSON.parse(match[1].trim());
return {
toolCalls: [
{
function: {
name: json.name,
arguments: json.arguments,
},
},
],
// Residual text outside the tool call tags
textContent: text.replace(match[0], '').trim(),
};
} catch {
return undefined;
}
};
3. Attach to Session
const session = useLLMChatSession(models.llm.LFM2_5_1_2B.DEFAULT, {
toolOpts: {
tools: [weatherTool],
parseToolCalls: xmlToolParser,
maxToolTurns: 5, // Maximum consecutive tool execution turns before halting
},
});
Imperative Session API
For headless background services or non-React architectures, create a full chat session imperatively using createLLMChatSession:
import { createLLMChatSession, download, models } from 'react-native-executorch';
// Download and cache LLM weights and tokenizer files
const model = await download(models.llm.LFM2_5_1_2B.DEFAULT);
const session = await createLLMChatSession(model, {
initialMessages: [{ role: 'system', content: 'You are an offline assistant.' }],
generationConfig: { temperature: 0.3, maxNewTokens: 256 },
});
try {
const result = await session.sendMessage("Summarize today's logs.");
console.log('Answer:', result.messages[result.messages.length - 1].content);
} finally {
session.dispose();
}
Low-Level Runner
While useLLMChatSession and createLLMChatSession handle chat formatting, message histories, and automated tool calling loops, you can drop down directly to the native LLMRunner via llm.createLLMRunner().
LLMRunner operates synchronously on a worklet runtime thread and provides low-level control:
- Raw Prompt Ingestion: Pass raw prompt strings or preprocessed image tensors directly to the runner without role formatting or Jinja chat template rendering.
- Manual Prefill: Execute
runner.prefill(prompt)to populate the Key-Value (KV) cache with large background contexts, system prompts, or document chunks before starting interactive generation. - Direct Synchronous Generation: Call
runner.generate(prompt, config, onToken)to generate text continuations with zero Promise scheduling overhead, executing theonTokencallback directly on each generated token. - KV Cache Inspection & Slicing: Query
runner.getKVCacheState()to check occupied tokens (pos), max context length (maxSeqLen), and context capacity ratio (usageRatio). - KV Cache Rewind & Branching: Call
runner.reset(targetPos)to rewind the KV cache back to an exact token position. This enables speculative branching, sampling multiple divergent continuations from a shared prompt prefix without re-encoding, or manual conversation tree management. - Cancellation: Call
runner.stop()from any thread to abort active autoregressive generation immediately.
Available Models
The library provides ready-to-use models from the Software Mansion HuggingFace LLM Collection, pre-packaged with their tokenizers and Jinja chat templates in models.llm:
| Model Family | Variants | Size Range | Supported Backends | Notes |
|---|---|---|---|---|
| Liquid LFM 2.5 | 350M, 1.2B, VL 450M, VL 1.6B | 265 MB – 2.43 GB | XNNPACK (CPU), MLX (Apple), Vulkan (Android) | Fast hybrid RNN/Transformer for low-latency chat & visual reasoning. |
| Meta Llama 3.2 | 1B, 3B | 1.06 GB – 5.99 GB | XNNPACK (CPU) | High-quality reasoning, summarization, and instruction following. |
| Google Gemma 4 | E2B | 2.45 GB – 2.70 GB | XNNPACK (CPU), MLX (Apple), Vulkan (Android) | High-fidelity instruction following from Google DeepMind research. |
| Alibaba Qwen 3 | 0.6B, 1.7B, 4B | 482 MB – 7.49 GB | XNNPACK (CPU) | Next-gen compact multilingual models supporting 29+ languages. |
| Alibaba Qwen 2.5 | 0.5B, 1.5B, 3B | 417 MB – 5.75 GB | XNNPACK (CPU) | Proven multilingual instruction models across code, math, and chat. |
| Hammer 2.1 | 0.5B, 1.5B, 3B | 398 MB – 5.75 GB | XNNPACK (CPU) | Fine-tuned function calling for automated tool execution & structured JSON. |
| Microsoft Phi-4 Mini | 3.8B | 2.62 GB – 7.15 GB | XNNPACK (CPU) | High-density reasoning model for STEM problem solving & coding. |
| SpeakLeash Bielik v3 | 1.5B | 923 MB – 2.97 GB | XNNPACK (CPU) | Bilingual Polish & English instruction model. |
To use your own fine-tuned LLM .pte model, pass an LLMModel configuration object to useLLMChatSession or createLLMChatSession:
const customSession = await createLLMChatSession({
modelPath: 'https://example.com/my-llm.pte',
tokenizerPath: 'https://example.com/tokenizer.json',
tokenizerConfigPath: 'https://example.com/tokenizer_config.json',
});
The pipeline automatically verifies that the model's exported methods and KV cache tensors match its requirements. To prepare and export your own .pte model to match this pipeline, see Exporting Custom Models.
API Reference
Hooks & Pipelines
useLLMChatSession()— React hook for managing LLM model downloading, KV cache, and conversational sessions.createLLMChatSession()— Imperative factory for multi-turn LLM chat sessions.llm.createLLMRunner()— Low-level factory for direct prompt execution and KV cache manipulation.llm.createChatPreprocessor()— Jinja2 template renderer, media processor, and prompt diffing engine.
Types & Options
LLMChatSession— Active chat session interface (sendMessage,stop,getHistory,getKVCacheState,dispose).LLMRunner— Low-level runner interface (prefill,generate,reset,getKVCacheState).ChatPreprocessor— Chat formatting and diffing preprocessor interface.ToolDefinition— Tool declaration with JSON Schema parameters andexecutecallback.ToolParser— Parser function type for extracting tool calls from model output.LLMChatTurnResult— Result of a chat turn with updated messages, finish reason, and performance statistics.LLMKVCacheState— KV cache metrics (pos,maxSeqLen,usageRatio).LLMChatSessionOptions— Session configuration options (generationConfig,initialMessages,toolOpts).LLMModel— Model configuration spec with model, tokenizer, and tokenizer config paths.LLMGenerationConfig— Sampling and decoding parameters (temperature,topP,maxNewTokens).ChatMessage— Standard chat message structure (role,content).
Model Presets
models.llm— Pre-configured LLM models registry.
View the implementation on GitHub: