react-native-litert-lm 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +245 -29
  2. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +301 -58
  3. package/cpp/HybridLiteRTLM.cpp +109 -9
  4. package/cpp/HybridLiteRTLM.hpp +16 -0
  5. package/cpp/cpp-adapter.cpp +10 -2
  6. package/lib/hooks.d.ts +41 -0
  7. package/lib/hooks.js +131 -0
  8. package/lib/index.d.ts +30 -3
  9. package/lib/index.js +53 -6
  10. package/lib/memoryTracker.d.ts +128 -0
  11. package/lib/memoryTracker.js +155 -0
  12. package/lib/modelFactory.d.ts +18 -0
  13. package/lib/modelFactory.js +104 -0
  14. package/lib/specs/LiteRTLM.nitro.d.ts +38 -0
  15. package/lib/templates.d.ts +51 -0
  16. package/lib/templates.js +81 -0
  17. package/nitrogen/generated/android/LiteRTLMOnLoad.cpp +22 -17
  18. package/nitrogen/generated/android/LiteRTLMOnLoad.hpp +13 -4
  19. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  20. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +42 -1
  21. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +3 -0
  22. package/nitrogen/generated/android/c++/JLLMConfig.hpp +6 -1
  23. package/nitrogen/generated/android/c++/JMemoryUsage.hpp +69 -0
  24. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_double.kt +80 -0
  25. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +17 -0
  26. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +5 -2
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MemoryUsage.kt +47 -0
  28. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +3 -0
  29. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +6 -0
  30. package/nitrogen/generated/shared/c++/LLMConfig.hpp +7 -2
  31. package/nitrogen/generated/shared/c++/MemoryUsage.hpp +95 -0
  32. package/package.json +3 -3
  33. package/src/hooks.ts +195 -0
  34. package/src/index.ts +51 -3
  35. package/src/memoryTracker.ts +268 -0
  36. package/src/modelFactory.ts +120 -0
  37. package/src/specs/LiteRTLM.nitro.ts +47 -0
  38. package/src/templates.ts +105 -0
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Memory tracking utilities for LiteRT-LM using real native memory metrics.
3
+ *
4
+ * Records real memory usage from OS-level APIs via `getMemoryUsage()`,
5
+ * and stores snapshots in a native-backed ArrayBuffer allocated via
6
+ * `NitroModules.createNativeArrayBuffer()` (v0.34+) for zero-copy interop.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { createMemoryTracker } from 'react-native-litert-lm';
11
+ *
12
+ * const tracker = createMemoryTracker(100);
13
+ *
14
+ * // Record a real snapshot (typically called internally after inference)
15
+ * tracker.record({
16
+ * timestamp: Date.now(),
17
+ * nativeHeapBytes: usage.nativeHeapBytes,
18
+ * residentBytes: usage.residentBytes,
19
+ * availableMemoryBytes: usage.availableMemoryBytes,
20
+ * });
21
+ *
22
+ * console.log(`Peak RSS: ${tracker.getPeakMemory()} bytes`);
23
+ * ```
24
+ */
25
+
26
+ import { NitroModules } from "react-native-nitro-modules";
27
+
28
+ /**
29
+ * A single memory usage snapshot with real data from OS APIs.
30
+ */
31
+ export interface MemorySnapshot {
32
+ /** Unix timestamp in milliseconds */
33
+ timestamp: number;
34
+ /** Native heap allocated bytes (Debug.getNativeHeapAllocatedSize on Android, task_info on iOS) */
35
+ nativeHeapBytes: number;
36
+ /** Process resident set size (RSS) in bytes */
37
+ residentBytes: number;
38
+ /** Available system memory in bytes */
39
+ availableMemoryBytes: number;
40
+ }
41
+
42
+ /** Number of Float64 fields per snapshot */
43
+ const FIELDS_PER_SNAPSHOT = 4;
44
+ /** Bytes per Float64 value */
45
+ const BYTES_PER_FIELD = Float64Array.BYTES_PER_ELEMENT; // 8
46
+
47
+ /**
48
+ * Memory tracker that stores snapshots in a native-backed ArrayBuffer.
49
+ *
50
+ * Uses `NitroModules.createNativeArrayBuffer()` to allocate the backing
51
+ * buffer in native (C++) memory, ensuring zero-copy interop with native
52
+ * methods and keeping memory tracking data off the JS heap.
53
+ */
54
+ export interface MemoryTracker {
55
+ /**
56
+ * Record a new memory snapshot.
57
+ * @param snapshot The memory usage data to record
58
+ * @returns true if recorded, false if buffer is full
59
+ */
60
+ record(snapshot: MemorySnapshot): boolean;
61
+
62
+ /**
63
+ * Get all recorded snapshots as structured objects.
64
+ */
65
+ getSnapshots(): MemorySnapshot[];
66
+
67
+ /**
68
+ * Get the number of recorded snapshots.
69
+ */
70
+ getSnapshotCount(): number;
71
+
72
+ /**
73
+ * Get the maximum number of snapshots this tracker can hold.
74
+ */
75
+ getCapacity(): number;
76
+
77
+ /**
78
+ * Get the peak resident set size across all snapshots.
79
+ */
80
+ getPeakMemory(): number;
81
+
82
+ /**
83
+ * Get the latest memory snapshot, or undefined if none recorded.
84
+ */
85
+ getLatestSnapshot(): MemorySnapshot | undefined;
86
+
87
+ /**
88
+ * Get the underlying native ArrayBuffer.
89
+ * This buffer is allocated via `NitroModules.createNativeArrayBuffer()`
90
+ * and lives in native memory, enabling zero-copy transfer to native methods.
91
+ */
92
+ getNativeBuffer(): ArrayBuffer;
93
+
94
+ /**
95
+ * Get the Float64Array view over the native buffer.
96
+ */
97
+ getView(): Float64Array;
98
+
99
+ /**
100
+ * Reset the tracker, clearing all recorded snapshots.
101
+ * The native buffer is preserved (not reallocated).
102
+ */
103
+ reset(): void;
104
+
105
+ /**
106
+ * Get a summary of memory usage statistics.
107
+ */
108
+ getSummary(): MemoryTrackerSummary;
109
+ }
110
+
111
+ /**
112
+ * Summary statistics from the memory tracker.
113
+ */
114
+ export interface MemoryTrackerSummary {
115
+ /** Number of snapshots recorded */
116
+ snapshotCount: number;
117
+ /** Peak resident set size in bytes */
118
+ peakResidentBytes: number;
119
+ /** Average resident set size in bytes */
120
+ averageResidentBytes: number;
121
+ /** Latest resident set size in bytes */
122
+ currentResidentBytes: number;
123
+ /** Peak native heap allocated in bytes */
124
+ peakNativeHeapBytes: number;
125
+ /** Latest native heap allocated in bytes */
126
+ currentNativeHeapBytes: number;
127
+ /** RSS delta from first to last snapshot in bytes */
128
+ residentDeltaBytes: number;
129
+ /** Size of the native tracking buffer itself in bytes */
130
+ trackerBufferSizeBytes: number;
131
+ }
132
+
133
+ /**
134
+ * Create a new memory tracker backed by a native ArrayBuffer.
135
+ *
136
+ * @param maxSnapshots Maximum number of snapshots to store (default: 256)
137
+ * @returns A MemoryTracker instance
138
+ */
139
+ export function createMemoryTracker(maxSnapshots: number = 256): MemoryTracker {
140
+ const bufferSize = maxSnapshots * FIELDS_PER_SNAPSHOT * BYTES_PER_FIELD;
141
+
142
+ // Use NitroModules.createNativeArrayBuffer for native-backed allocation.
143
+ const nativeBuffer = NitroModules.createNativeArrayBuffer(bufferSize);
144
+ const view = new Float64Array(nativeBuffer);
145
+
146
+ let currentIndex = 0;
147
+
148
+ return {
149
+ record(snapshot: MemorySnapshot): boolean {
150
+ if (currentIndex >= maxSnapshots) {
151
+ return false;
152
+ }
153
+
154
+ const offset = currentIndex * FIELDS_PER_SNAPSHOT;
155
+ view[offset] = snapshot.timestamp;
156
+ view[offset + 1] = snapshot.nativeHeapBytes;
157
+ view[offset + 2] = snapshot.residentBytes;
158
+ view[offset + 3] = snapshot.availableMemoryBytes;
159
+ currentIndex++;
160
+
161
+ return true;
162
+ },
163
+
164
+ getSnapshots(): MemorySnapshot[] {
165
+ const snapshots: MemorySnapshot[] = [];
166
+ for (let i = 0; i < currentIndex; i++) {
167
+ const offset = i * FIELDS_PER_SNAPSHOT;
168
+ snapshots.push({
169
+ timestamp: view[offset]!,
170
+ nativeHeapBytes: view[offset + 1]!,
171
+ residentBytes: view[offset + 2]!,
172
+ availableMemoryBytes: view[offset + 3]!,
173
+ });
174
+ }
175
+ return snapshots;
176
+ },
177
+
178
+ getSnapshotCount(): number {
179
+ return currentIndex;
180
+ },
181
+
182
+ getCapacity(): number {
183
+ return maxSnapshots;
184
+ },
185
+
186
+ getPeakMemory(): number {
187
+ let peak = 0;
188
+ for (let i = 0; i < currentIndex; i++) {
189
+ const rss = view[i * FIELDS_PER_SNAPSHOT + 2]!;
190
+ if (rss > peak) {
191
+ peak = rss;
192
+ }
193
+ }
194
+ return peak;
195
+ },
196
+
197
+ getLatestSnapshot(): MemorySnapshot | undefined {
198
+ if (currentIndex === 0) return undefined;
199
+ const offset = (currentIndex - 1) * FIELDS_PER_SNAPSHOT;
200
+ return {
201
+ timestamp: view[offset]!,
202
+ nativeHeapBytes: view[offset + 1]!,
203
+ residentBytes: view[offset + 2]!,
204
+ availableMemoryBytes: view[offset + 3]!,
205
+ };
206
+ },
207
+
208
+ getNativeBuffer(): ArrayBuffer {
209
+ return nativeBuffer;
210
+ },
211
+
212
+ getView(): Float64Array {
213
+ return view;
214
+ },
215
+
216
+ reset(): void {
217
+ view.fill(0);
218
+ currentIndex = 0;
219
+ },
220
+
221
+ getSummary(): MemoryTrackerSummary {
222
+ let peakRss = 0;
223
+ let peakHeap = 0;
224
+ let sumRss = 0;
225
+ let firstRss = 0;
226
+ let lastRss = 0;
227
+ let lastHeap = 0;
228
+
229
+ for (let i = 0; i < currentIndex; i++) {
230
+ const offset = i * FIELDS_PER_SNAPSHOT;
231
+ const heap = view[offset + 1]!;
232
+ const rss = view[offset + 2]!;
233
+
234
+ if (rss > peakRss) peakRss = rss;
235
+ if (heap > peakHeap) peakHeap = heap;
236
+ sumRss += rss;
237
+ if (i === 0) firstRss = rss;
238
+ if (i === currentIndex - 1) {
239
+ lastRss = rss;
240
+ lastHeap = heap;
241
+ }
242
+ }
243
+
244
+ return {
245
+ snapshotCount: currentIndex,
246
+ peakResidentBytes: peakRss,
247
+ averageResidentBytes: currentIndex > 0 ? sumRss / currentIndex : 0,
248
+ currentResidentBytes: lastRss,
249
+ peakNativeHeapBytes: peakHeap,
250
+ currentNativeHeapBytes: lastHeap,
251
+ residentDeltaBytes: lastRss - firstRss,
252
+ trackerBufferSizeBytes: bufferSize,
253
+ };
254
+ },
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Create a native ArrayBuffer for efficient data transfer.
260
+ *
261
+ * A convenience wrapper around `NitroModules.createNativeArrayBuffer()`.
262
+ *
263
+ * @param size Size in bytes
264
+ * @returns A native-backed ArrayBuffer
265
+ */
266
+ export function createNativeBuffer(size: number): ArrayBuffer {
267
+ return NitroModules.createNativeArrayBuffer(size);
268
+ }
@@ -0,0 +1,120 @@
1
+ import { NitroModules } from "react-native-nitro-modules";
2
+ import { LiteRTLM, LLMConfig } from "./specs/LiteRTLM.nitro";
3
+ import { createMemoryTracker, MemoryTracker } from "./memoryTracker";
4
+
5
+ /**
6
+ * Creates a new LiteRT-LM inference engine instance.
7
+ *
8
+ * Optionally creates a native-backed memory tracker using
9
+ * `NitroModules.createNativeArrayBuffer()` (v0.34+) for efficient
10
+ * zero-copy memory usage tracking.
11
+ *
12
+ * @param options.enableMemoryTracking Enable automatic memory tracking (default: false)
13
+ * @param options.maxMemorySnapshots Maximum number of memory snapshots to store (default: 256)
14
+ */
15
+ export function createLLM(options?: {
16
+ enableMemoryTracking?: boolean;
17
+ maxMemorySnapshots?: number;
18
+ }): LiteRTLM & { memoryTracker?: MemoryTracker } {
19
+ const native = NitroModules.createHybridObject<LiteRTLM>("LiteRTLM");
20
+
21
+ const enableTracking = options?.enableMemoryTracking ?? false;
22
+ const tracker = enableTracking
23
+ ? createMemoryTracker(options?.maxMemorySnapshots ?? 256)
24
+ : undefined;
25
+
26
+ /**
27
+ * Record a real memory snapshot using OS-level APIs via getMemoryUsage().
28
+ */
29
+ const recordMemorySnapshot = () => {
30
+ if (!tracker) return;
31
+ try {
32
+ const usage = native.getMemoryUsage();
33
+ tracker.record({
34
+ timestamp: Date.now(),
35
+ nativeHeapBytes: usage.nativeHeapBytes,
36
+ residentBytes: usage.residentBytes,
37
+ availableMemoryBytes: usage.availableMemoryBytes,
38
+ });
39
+ } catch {
40
+ // Ignore errors during memory tracking - it's non-critical
41
+ }
42
+ };
43
+
44
+ return {
45
+ ...native,
46
+ memoryTracker: tracker,
47
+ loadModel: async (pathOrUrl: string, config?: LLMConfig) => {
48
+ let modelPath = pathOrUrl;
49
+
50
+ // Check if it's a URL
51
+ if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
52
+ // Extract filename from URL
53
+ const fileName = pathOrUrl.split("/").pop();
54
+ if (!fileName) {
55
+ throw new Error(`Invalid model URL: ${pathOrUrl}`);
56
+ }
57
+
58
+ console.log(`Checking model at ${pathOrUrl}...`);
59
+ modelPath = await native.downloadModel(
60
+ pathOrUrl,
61
+ fileName,
62
+ (progress) => {
63
+ console.log(`Download progress: ${progress}`);
64
+ },
65
+ );
66
+ console.log(`Model downloaded to: ${modelPath}`);
67
+ }
68
+
69
+ const result = await native.loadModel(modelPath, config);
70
+
71
+ // Record initial memory snapshot after model load
72
+ if (tracker) {
73
+ tracker.reset();
74
+ recordMemorySnapshot();
75
+ }
76
+
77
+ return result;
78
+ },
79
+ sendMessage: async (...args: Parameters<typeof native.sendMessage>) => {
80
+ const result = await native.sendMessage(...args);
81
+ recordMemorySnapshot();
82
+ return result;
83
+ },
84
+ sendMessageAsync: (...args: Parameters<typeof native.sendMessageAsync>) => {
85
+ const [message, onToken] = args;
86
+ native.sendMessageAsync(message, (token, done) => {
87
+ onToken(token, done);
88
+ if (done) {
89
+ recordMemorySnapshot();
90
+ }
91
+ });
92
+ },
93
+ sendMessageWithImage: async (
94
+ ...args: Parameters<typeof native.sendMessageWithImage>
95
+ ) => {
96
+ const result = await native.sendMessageWithImage(...args);
97
+ recordMemorySnapshot();
98
+ return result;
99
+ },
100
+ sendMessageWithAudio: async (
101
+ ...args: Parameters<typeof native.sendMessageWithAudio>
102
+ ) => {
103
+ const result = await native.sendMessageWithAudio(...args);
104
+ recordMemorySnapshot();
105
+ return result;
106
+ },
107
+ getHistory: native.getHistory.bind(native),
108
+ resetConversation: () => {
109
+ native.resetConversation();
110
+ // KV cache is cleared on reset, record the drop
111
+ recordMemorySnapshot();
112
+ },
113
+ isReady: native.isReady.bind(native),
114
+ getStats: native.getStats.bind(native),
115
+ getMemoryUsage: native.getMemoryUsage.bind(native),
116
+ close: native.close.bind(native),
117
+ downloadModel: native.downloadModel.bind(native),
118
+ deleteModel: native.deleteModel.bind(native),
119
+ };
120
+ }
@@ -21,6 +21,13 @@ export type Role = "user" | "model" | "system";
21
21
  * Configuration options for loading an LLM.
22
22
  */
23
23
  export interface LLMConfig {
24
+ /**
25
+ * System prompt to set the model's behavior.
26
+ * This is prepended to the conversation to guide model responses.
27
+ * @example "You are a helpful coding assistant."
28
+ */
29
+ systemPrompt?: string;
30
+
24
31
  /**
25
32
  * Primary compute backend for text generation.
26
33
  * - 'cpu': CPU inference (slower but always available)
@@ -92,6 +99,21 @@ export interface GenerationStats {
92
99
  tokensPerSecond: number;
93
100
  }
94
101
 
102
+ /**
103
+ * Real memory usage statistics from the native runtime.
104
+ * Measured from OS-level APIs, not estimated.
105
+ */
106
+ export interface MemoryUsage {
107
+ /** Native heap allocated bytes (Debug.getNativeHeapAllocatedSize on Android, malloc_size on iOS) */
108
+ nativeHeapBytes: number;
109
+ /** Total process resident set size (RSS) in bytes */
110
+ residentBytes: number;
111
+ /** Available system memory in bytes */
112
+ availableMemoryBytes: number;
113
+ /** Whether the system considers memory low */
114
+ isLowMemory: boolean;
115
+ }
116
+
95
117
  /**
96
118
  * LiteRT-LM: High-performance LLM inference engine.
97
119
  * Supports Gemma 3n, Phi-4, Qwen, and other .litertlm models.
@@ -138,6 +160,25 @@ export interface LiteRTLM extends HybridObject<{
138
160
  */
139
161
  sendMessageWithImage(message: string, imagePath: string): Promise<string>;
140
162
 
163
+ /**
164
+ * Download a model file from a URL.
165
+ * @param url URL to download from.
166
+ * @param fileName Filename to save as (in app's files directory).
167
+ * @param onProgress Callback for download progress (0.0 - 1.0).
168
+ * @returns Absolute path to the downloaded file.
169
+ */
170
+ downloadModel(
171
+ url: string,
172
+ fileName: string,
173
+ onProgress?: (progress: number) => void,
174
+ ): Promise<string>;
175
+
176
+ /**
177
+ * Delete a downloaded model file.
178
+ * @param fileName Filename to delete (in app's files directory).
179
+ */
180
+ deleteModel(fileName: string): Promise<void>;
181
+
141
182
  /**
142
183
  * Send a text message with audio (multimodal).
143
184
  * @param message User message text.
@@ -178,6 +219,12 @@ export interface LiteRTLM extends HybridObject<{
178
219
  */
179
220
  getStats(): GenerationStats;
180
221
 
222
+ /**
223
+ * Get real memory usage from the native runtime.
224
+ * Uses OS-level APIs to report actual memory consumption.
225
+ */
226
+ getMemoryUsage(): MemoryUsage;
227
+
181
228
  /**
182
229
  * Release all native resources.
183
230
  * Call this when done with the LLM instance.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Prompt template utilities for different LLM families.
3
+ *
4
+ * LiteRT-LM's Conversation API may handle templates internally for some models,
5
+ * but these utilities give developers explicit control for custom workflows
6
+ * or when using models with different template formats.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { applyGemmaTemplate, ChatMessage } from 'react-native-litert-lm';
11
+ *
12
+ * const history: ChatMessage[] = [
13
+ * { role: 'user', content: 'What is React Native?' },
14
+ * { role: 'model', content: 'React Native is a framework for building...' },
15
+ * { role: 'user', content: 'How do I use hooks?' }
16
+ * ];
17
+ *
18
+ * const prompt = applyGemmaTemplate(history, 'You are a helpful coding assistant.');
19
+ * ```
20
+ */
21
+
22
+ /**
23
+ * A message in a conversation.
24
+ */
25
+ export type ChatMessage = {
26
+ role: "user" | "model" | "system";
27
+ content: string;
28
+ };
29
+
30
+ /**
31
+ * Apply Gemma chat template (Gemma 2, Gemma 3, Gemma 3n).
32
+ *
33
+ * @param history Array of previous messages
34
+ * @param systemPrompt Optional system prompt
35
+ * @returns Formatted prompt string
36
+ */
37
+ export function applyGemmaTemplate(
38
+ history: ChatMessage[],
39
+ systemPrompt?: string,
40
+ ): string {
41
+ let result = "";
42
+
43
+ if (systemPrompt) {
44
+ result += `<start_of_turn>system\n${systemPrompt}<end_of_turn>\n`;
45
+ }
46
+
47
+ for (const m of history) {
48
+ result += `<start_of_turn>${m.role}\n${m.content}<end_of_turn>\n`;
49
+ }
50
+
51
+ result += "<start_of_turn>model\n";
52
+ return result;
53
+ }
54
+
55
+ /**
56
+ * Apply Phi chat template (Phi-3, Phi-4).
57
+ *
58
+ * @param history Array of previous messages
59
+ * @param systemPrompt Optional system prompt
60
+ * @returns Formatted prompt string
61
+ */
62
+ export function applyPhiTemplate(
63
+ history: ChatMessage[],
64
+ systemPrompt?: string,
65
+ ): string {
66
+ let result = "";
67
+
68
+ if (systemPrompt) {
69
+ result += `<|system|>\n${systemPrompt}<|end|>\n`;
70
+ }
71
+
72
+ for (const m of history) {
73
+ const role = m.role === "model" ? "assistant" : m.role;
74
+ result += `<|${role}|>\n${m.content}<|end|>\n`;
75
+ }
76
+
77
+ result += "<|assistant|>\n";
78
+ return result;
79
+ }
80
+
81
+ /**
82
+ * Apply Llama 3 chat template.
83
+ *
84
+ * @param history Array of previous messages
85
+ * @param systemPrompt Optional system prompt
86
+ * @returns Formatted prompt string
87
+ */
88
+ export function applyLlamaTemplate(
89
+ history: ChatMessage[],
90
+ systemPrompt?: string,
91
+ ): string {
92
+ let result = "<|begin_of_text|>";
93
+
94
+ if (systemPrompt) {
95
+ result += `<|start_header_id|>system<|end_header_id|>\n\n${systemPrompt}<|eot_id|>`;
96
+ }
97
+
98
+ for (const m of history) {
99
+ const role = m.role === "model" ? "assistant" : m.role;
100
+ result += `<|start_header_id|>${role}<|end_header_id|>\n\n${m.content}<|eot_id|>`;
101
+ }
102
+
103
+ result += "<|start_header_id|>assistant<|end_header_id|>\n\n";
104
+ return result;
105
+ }