@dr33m/react-native-litert-lm 0.5.0

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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +627 -0
  3. package/android/CMakeLists.txt +32 -0
  4. package/android/build.gradle +104 -0
  5. package/android/src/main/AndroidManifest.xml +19 -0
  6. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +888 -0
  7. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridModelStore.kt +189 -0
  8. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/LiteRTLMRegistry.kt +32 -0
  9. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/MultimodalPartFactories.kt +15 -0
  10. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/StreamingCallbackListener.kt +89 -0
  11. package/android/src/main/java/dev/litert/litertlm/LiteRTLMInitProvider.kt +57 -0
  12. package/android/src/main/java/dev/litert/litertlm/LiteRTLMPackage.kt +43 -0
  13. package/android/src/test/java/com/margelo/nitro/core/Promise.kt +46 -0
  14. package/android/src/test/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMTest.kt +122 -0
  15. package/app.plugin.js +58 -0
  16. package/cpp/cpp-adapter.cpp +21 -0
  17. package/cpp/include/README.md +32 -0
  18. package/cpp/include/litert_lm_engine.h +516 -0
  19. package/cpp/include/stb_image.h +7988 -0
  20. package/ios/HybridLiteRTLM+Execute.swift +287 -0
  21. package/ios/HybridLiteRTLM+Streaming.swift +153 -0
  22. package/ios/HybridLiteRTLM.swift +541 -0
  23. package/ios/HybridModelStore.swift +206 -0
  24. package/ios/MultimodalPart+Factories.swift +22 -0
  25. package/ios/Tests/HybridLiteRTLMTests.swift +187 -0
  26. package/lib/__mocks__/react-native-nitro-modules.d.ts +81 -0
  27. package/lib/__mocks__/react-native-nitro-modules.js +98 -0
  28. package/lib/__tests__/hooks.test.d.ts +1 -0
  29. package/lib/__tests__/hooks.test.js +175 -0
  30. package/lib/__tests__/inferenceRouting.test.d.ts +1 -0
  31. package/lib/__tests__/inferenceRouting.test.js +28 -0
  32. package/lib/__tests__/memoryTracker.test.d.ts +1 -0
  33. package/lib/__tests__/memoryTracker.test.js +74 -0
  34. package/lib/__tests__/modelFactory.test.d.ts +1 -0
  35. package/lib/__tests__/modelFactory.test.js +107 -0
  36. package/lib/__tests__/modelPath.test.d.ts +1 -0
  37. package/lib/__tests__/modelPath.test.js +12 -0
  38. package/lib/__tests__/modelRegistry.test.d.ts +1 -0
  39. package/lib/__tests__/modelRegistry.test.js +61 -0
  40. package/lib/hooks.d.ts +46 -0
  41. package/lib/hooks.js +159 -0
  42. package/lib/index.d.ts +142 -0
  43. package/lib/index.js +184 -0
  44. package/lib/inferenceRouting.d.ts +18 -0
  45. package/lib/inferenceRouting.js +63 -0
  46. package/lib/memoryTracker.d.ts +128 -0
  47. package/lib/memoryTracker.js +155 -0
  48. package/lib/modelFactory.d.ts +24 -0
  49. package/lib/modelFactory.js +128 -0
  50. package/lib/modelPath.d.ts +8 -0
  51. package/lib/modelPath.js +18 -0
  52. package/lib/modelRegistry.d.ts +55 -0
  53. package/lib/modelRegistry.js +82 -0
  54. package/lib/specs/LiteRTLM.nitro.d.ts +380 -0
  55. package/lib/specs/LiteRTLM.nitro.js +2 -0
  56. package/nitrogen/generated/.gitattributes +1 -0
  57. package/nitrogen/generated/android/LiteRTLM+autolinking.cmake +83 -0
  58. package/nitrogen/generated/android/LiteRTLM+autolinking.gradle +27 -0
  59. package/nitrogen/generated/android/LiteRTLMOnLoad.cpp +74 -0
  60. package/nitrogen/generated/android/LiteRTLMOnLoad.hpp +34 -0
  61. package/nitrogen/generated/android/c++/JBackend.hpp +61 -0
  62. package/nitrogen/generated/android/c++/JExecuteResult.hpp +86 -0
  63. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  64. package/nitrogen/generated/android/c++/JFunc_void_std__string_bool.hpp +76 -0
  65. package/nitrogen/generated/android/c++/JGenerationStats.hpp +77 -0
  66. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +371 -0
  67. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +83 -0
  68. package/nitrogen/generated/android/c++/JHybridModelStoreSpec.cpp +101 -0
  69. package/nitrogen/generated/android/c++/JHybridModelStoreSpec.hpp +67 -0
  70. package/nitrogen/generated/android/c++/JLLMConfig.hpp +125 -0
  71. package/nitrogen/generated/android/c++/JMemoryUsage.hpp +69 -0
  72. package/nitrogen/generated/android/c++/JMessage.hpp +63 -0
  73. package/nitrogen/generated/android/c++/JModelFile.hpp +69 -0
  74. package/nitrogen/generated/android/c++/JMultimodalPart.hpp +78 -0
  75. package/nitrogen/generated/android/c++/JPartType.hpp +61 -0
  76. package/nitrogen/generated/android/c++/JRole.hpp +61 -0
  77. package/nitrogen/generated/android/c++/JToolCall.hpp +61 -0
  78. package/nitrogen/generated/android/c++/JToolDefinition.hpp +65 -0
  79. package/nitrogen/generated/android/c++/JToolResponse.hpp +61 -0
  80. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Backend.kt +24 -0
  81. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ExecuteResult.kt +61 -0
  82. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_double.kt +80 -0
  83. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_std__string_bool.kt +80 -0
  84. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/GenerationStats.kt +76 -0
  85. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +165 -0
  86. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridModelStoreSpec.kt +76 -0
  87. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +106 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LiteRTLMOnLoad.kt +35 -0
  89. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MemoryUsage.kt +66 -0
  90. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Message.kt +56 -0
  91. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ModelFile.kt +66 -0
  92. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MultimodalPart.kt +71 -0
  93. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/PartType.kt +24 -0
  94. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Role.kt +24 -0
  95. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolCall.kt +56 -0
  96. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolDefinition.kt +61 -0
  97. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolResponse.kt +56 -0
  98. package/nitrogen/generated/ios/LiteRTLM+autolinking.rb +62 -0
  99. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.cpp +98 -0
  100. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.hpp +585 -0
  101. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Umbrella.hpp +93 -0
  102. package/nitrogen/generated/ios/LiteRTLMAutolinking.mm +41 -0
  103. package/nitrogen/generated/ios/LiteRTLMAutolinking.swift +38 -0
  104. package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.cpp +11 -0
  105. package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +279 -0
  106. package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.cpp +11 -0
  107. package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.hpp +117 -0
  108. package/nitrogen/generated/ios/swift/Backend.swift +44 -0
  109. package/nitrogen/generated/ios/swift/ExecuteResult.swift +45 -0
  110. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  111. package/nitrogen/generated/ios/swift/Func_void_ExecuteResult.swift +46 -0
  112. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  113. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  114. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  115. package/nitrogen/generated/ios/swift/Func_void_std__string_bool.swift +46 -0
  116. package/nitrogen/generated/ios/swift/GenerationStats.swift +54 -0
  117. package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +75 -0
  118. package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +516 -0
  119. package/nitrogen/generated/ios/swift/HybridModelStoreSpec.swift +59 -0
  120. package/nitrogen/generated/ios/swift/HybridModelStoreSpec_cxx.swift +203 -0
  121. package/nitrogen/generated/ios/swift/LLMConfig.swift +239 -0
  122. package/nitrogen/generated/ios/swift/MemoryUsage.swift +44 -0
  123. package/nitrogen/generated/ios/swift/Message.swift +34 -0
  124. package/nitrogen/generated/ios/swift/ModelFile.swift +44 -0
  125. package/nitrogen/generated/ios/swift/MultimodalPart.swift +101 -0
  126. package/nitrogen/generated/ios/swift/PartType.swift +44 -0
  127. package/nitrogen/generated/ios/swift/Role.swift +44 -0
  128. package/nitrogen/generated/ios/swift/ToolCall.swift +34 -0
  129. package/nitrogen/generated/ios/swift/ToolDefinition.swift +39 -0
  130. package/nitrogen/generated/ios/swift/ToolResponse.swift +34 -0
  131. package/nitrogen/generated/shared/c++/Backend.hpp +80 -0
  132. package/nitrogen/generated/shared/c++/ExecuteResult.hpp +94 -0
  133. package/nitrogen/generated/shared/c++/GenerationStats.hpp +103 -0
  134. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +41 -0
  135. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +109 -0
  136. package/nitrogen/generated/shared/c++/HybridModelStoreSpec.cpp +25 -0
  137. package/nitrogen/generated/shared/c++/HybridModelStoreSpec.hpp +71 -0
  138. package/nitrogen/generated/shared/c++/LLMConfig.hpp +134 -0
  139. package/nitrogen/generated/shared/c++/MemoryUsage.hpp +95 -0
  140. package/nitrogen/generated/shared/c++/Message.hpp +89 -0
  141. package/nitrogen/generated/shared/c++/ModelFile.hpp +95 -0
  142. package/nitrogen/generated/shared/c++/MultimodalPart.hpp +103 -0
  143. package/nitrogen/generated/shared/c++/PartType.hpp +80 -0
  144. package/nitrogen/generated/shared/c++/Role.hpp +80 -0
  145. package/nitrogen/generated/shared/c++/ToolCall.hpp +87 -0
  146. package/nitrogen/generated/shared/c++/ToolDefinition.hpp +91 -0
  147. package/nitrogen/generated/shared/c++/ToolResponse.hpp +87 -0
  148. package/package.json +115 -0
  149. package/react-native-litert-lm.podspec +62 -0
  150. package/react-native.config.js +16 -0
  151. package/scripts/download-ios-frameworks.sh +39 -0
  152. package/scripts/framework-source.js +46 -0
  153. package/scripts/postinstall.js +145 -0
  154. package/src/__mocks__/react-native-nitro-modules.ts +132 -0
  155. package/src/__tests__/hooks.test.ts +216 -0
  156. package/src/__tests__/inferenceRouting.test.ts +36 -0
  157. package/src/__tests__/memoryTracker.test.ts +87 -0
  158. package/src/__tests__/modelFactory.test.ts +153 -0
  159. package/src/__tests__/modelPath.test.ts +14 -0
  160. package/src/__tests__/modelRegistry.test.ts +73 -0
  161. package/src/hooks.ts +234 -0
  162. package/src/index.ts +214 -0
  163. package/src/inferenceRouting.ts +80 -0
  164. package/src/memoryTracker.ts +268 -0
  165. package/src/modelFactory.ts +163 -0
  166. package/src/modelPath.ts +16 -0
  167. package/src/modelRegistry.ts +104 -0
  168. package/src/specs/LiteRTLM.nitro.ts +451 -0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall.js
4
+ *
5
+ * Downloads prebuilt LiteRT-LM iOS frameworks from this package's GitHub
6
+ * releases when consumers run `npm install react-native-litert-lm`.
7
+ *
8
+ * The framework is intentionally NOT shipped inside the npm tarball (it is
9
+ * ~40MB and irrelevant to Android-only consumers). The asset name, tag, and
10
+ * URL come from scripts/framework-source.js so they stay in lockstep with the
11
+ * release-time guardrail (scripts/check-framework-release.js).
12
+ *
13
+ * Skips download if:
14
+ * - Not on macOS (iOS builds require macOS)
15
+ * - Frameworks already exist
16
+ * - SKIP_IOS_FRAMEWORK_DOWNLOAD=1 (e.g. Android-only / CI builds)
17
+ */
18
+
19
+ const { execSync } = require('child_process');
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const https = require('https');
23
+
24
+ const { ASSET_URL, FRAMEWORKS_DIR, FRAMEWORK_TAG } = require('./framework-source');
25
+
26
+ /** ZIP local-file-header magic ("PK\x03\x04"). Guards against truncated downloads / HTML error pages. */
27
+ const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
28
+ /** A valid framework zip is well over this; anything smaller is certainly an error page. */
29
+ const MIN_VALID_BYTES = 1024 * 1024; // 1 MB
30
+
31
+ function log(msg) {
32
+ console.log(`[react-native-litert-lm] ${msg}`);
33
+ }
34
+
35
+ function shouldSkip() {
36
+ // Skip if not macOS
37
+ if (process.platform !== 'darwin') {
38
+ log('Skipping iOS framework download (not macOS).');
39
+ return true;
40
+ }
41
+
42
+ // Skip if explicitly disabled
43
+ if (process.env.SKIP_IOS_FRAMEWORK_DOWNLOAD === '1') {
44
+ log('Skipping iOS framework download (SKIP_IOS_FRAMEWORK_DOWNLOAD=1).');
45
+ return true;
46
+ }
47
+
48
+ // Skip if frameworks already exist
49
+ if (fs.existsSync(FRAMEWORKS_DIR) && fs.readdirSync(FRAMEWORKS_DIR).length > 0) {
50
+ log('iOS frameworks already present, skipping download.');
51
+ return true;
52
+ }
53
+
54
+ return false;
55
+ }
56
+
57
+ function downloadFile(url, destPath, maxRedirects = 5) {
58
+ return new Promise((resolve, reject) => {
59
+ if (maxRedirects <= 0) {
60
+ return reject(new Error('Too many redirects'));
61
+ }
62
+
63
+ const protocol = url.startsWith('https') ? https : require('http');
64
+
65
+ protocol.get(url, { headers: { 'User-Agent': 'react-native-litert-lm' } }, (res) => {
66
+ // Follow redirects
67
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
68
+ return downloadFile(res.headers.location, destPath, maxRedirects - 1)
69
+ .then(resolve)
70
+ .catch(reject);
71
+ }
72
+
73
+ if (res.statusCode !== 200) {
74
+ return reject(new Error(`HTTP ${res.statusCode} downloading ${url}`));
75
+ }
76
+
77
+ const file = fs.createWriteStream(destPath);
78
+ res.pipe(file);
79
+ file.on('finish', () => {
80
+ file.close(resolve);
81
+ });
82
+ file.on('error', reject);
83
+ }).on('error', reject);
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Verify the downloaded file is a non-trivial ZIP before we trust it.
89
+ * Catches GitHub HTML error pages and truncated downloads that would
90
+ * otherwise fail cryptically at `unzip` or, worse, link a corrupt framework.
91
+ */
92
+ function assertValidZip(zipPath) {
93
+ const { size } = fs.statSync(zipPath);
94
+ if (size < MIN_VALID_BYTES) {
95
+ throw new Error(`downloaded asset is only ${size} bytes — expected a multi-MB framework zip (likely an error page or truncated download)`);
96
+ }
97
+
98
+ const header = Buffer.alloc(4);
99
+ const fd = fs.openSync(zipPath, 'r');
100
+ try {
101
+ fs.readSync(fd, header, 0, 4, 0);
102
+ } finally {
103
+ fs.closeSync(fd);
104
+ }
105
+ if (!header.equals(ZIP_MAGIC)) {
106
+ throw new Error('downloaded asset is not a valid ZIP (bad magic bytes)');
107
+ }
108
+ }
109
+
110
+ async function main() {
111
+ if (shouldSkip()) return;
112
+
113
+ log(`Downloading iOS frameworks (engine ${FRAMEWORK_TAG}) from: ${ASSET_URL}`);
114
+
115
+ const tmpZip = path.join(path.dirname(FRAMEWORKS_DIR), '.ios-frameworks-tmp.zip');
116
+
117
+ try {
118
+ await downloadFile(ASSET_URL, tmpZip);
119
+ assertValidZip(tmpZip);
120
+
121
+ // Extract
122
+ fs.mkdirSync(FRAMEWORKS_DIR, { recursive: true });
123
+ execSync(`unzip -o -q "${tmpZip}" -d "${FRAMEWORKS_DIR}"`, { stdio: 'inherit' });
124
+
125
+ // Cleanup
126
+ fs.unlinkSync(tmpZip);
127
+
128
+ log('iOS frameworks installed successfully.');
129
+ } catch (err) {
130
+ // Cleanup partial download
131
+ try { fs.unlinkSync(tmpZip); } catch {}
132
+
133
+ log(`Error: Could not download iOS frameworks: ${err.message}`);
134
+ log('iOS builds will not work until frameworks are available.');
135
+ log('Run: ./scripts/download-ios-frameworks.sh to download manually.');
136
+
137
+ // Fail fast on macOS so users discover the problem now, not at Xcode link time.
138
+ if (process.platform === 'darwin') {
139
+ log('Set SKIP_IOS_FRAMEWORK_DOWNLOAD=1 to suppress this error (e.g. Android-only builds).');
140
+ process.exit(1);
141
+ }
142
+ }
143
+ }
144
+
145
+ main();
@@ -0,0 +1,132 @@
1
+ import type { MultimodalPart } from "../specs/LiteRTLM.nitro";
2
+
3
+ function streamMockTokens(
4
+ parts: MultimodalPart[],
5
+ onToken: (token: string, done: boolean) => void,
6
+ ): void {
7
+ const hasImage = parts.some((p) => p.type === "image");
8
+ const hasAudio = parts.some((p) => p.type === "audio");
9
+ if (hasImage) {
10
+ onToken("Mock vision ", false);
11
+ onToken("token", true);
12
+ } else if (hasAudio) {
13
+ onToken("Mock audio ", false);
14
+ onToken("token", true);
15
+ } else {
16
+ onToken("Mock ", false);
17
+ onToken("token", true);
18
+ }
19
+ }
20
+
21
+ function mockExecuteResponse(parts: MultimodalPart[]): string {
22
+ const hasImage = parts.some((p) => p.type === "image");
23
+ const hasAudio = parts.some((p) => p.type === "audio");
24
+ return hasImage
25
+ ? "Mock vision token"
26
+ : hasAudio
27
+ ? "Mock audio token"
28
+ : "Mock token";
29
+ }
30
+
31
+ const mockExecute = jest.fn(
32
+ (parts: MultimodalPart[], onToken?: (token: string, done: boolean) => void): Promise<string> => {
33
+ if (onToken) {
34
+ streamMockTokens(parts, onToken);
35
+ }
36
+ return Promise.resolve(mockExecuteResponse(parts));
37
+ },
38
+ );
39
+
40
+ export const mockLiteRTLM = {
41
+ isReady: jest.fn(() => false),
42
+ loadModel: jest.fn().mockResolvedValue(undefined),
43
+ execute: mockExecute,
44
+ sendMessage: jest.fn((message: string) =>
45
+ mockExecute([{ type: "text", text: message }]),
46
+ ),
47
+ sendMessageWithImage: jest.fn((message: string, imagePath: string) =>
48
+ mockExecute([
49
+ { type: "text", text: message },
50
+ { type: "image", path: imagePath },
51
+ ]),
52
+ ),
53
+ downloadModel: jest.fn(async (url: string, fileName: string, onProgress?: (progress: number) => void) => {
54
+ onProgress?.(1.0);
55
+ return "/mock/path/model.litertlm";
56
+ }),
57
+ deleteModel: jest.fn().mockResolvedValue(undefined),
58
+ sendMessageWithAudio: jest.fn((message: string, audioPath: string) =>
59
+ mockExecute([
60
+ { type: "text", text: message },
61
+ { type: "audio", path: audioPath },
62
+ ]),
63
+ ),
64
+ sendMultimodalMessage: jest.fn((parts: MultimodalPart[]) => mockExecute(parts)),
65
+ sendMessageAsync: jest.fn((msg: string, onToken: (token: string, done: boolean) => void) =>
66
+ mockExecute([{ type: "text", text: msg }], onToken).then(() => {}),
67
+ ),
68
+ sendMessageWithImageAsync: jest.fn(
69
+ (msg: string, imagePath: string, onToken: (token: string, done: boolean) => void) =>
70
+ mockExecute(
71
+ [{ type: "text", text: msg }, { type: "image", path: imagePath }],
72
+ onToken,
73
+ ).then(() => {}),
74
+ ),
75
+ sendMessageWithAudioAsync: jest.fn(
76
+ (msg: string, audioPath: string, onToken: (token: string, done: boolean) => void) =>
77
+ mockExecute(
78
+ [{ type: "text", text: msg }, { type: "audio", path: audioPath }],
79
+ onToken,
80
+ ).then(() => {}),
81
+ ),
82
+ getHistory: jest.fn(() => []),
83
+ resetConversation: jest.fn(),
84
+ getStats: jest.fn(() => ({
85
+ promptTokens: 10,
86
+ completionTokens: 20,
87
+ totalTokens: 30,
88
+ timeToFirstToken: 5,
89
+ totalTime: 50,
90
+ tokensPerSecond: 400,
91
+ })),
92
+ countTokens: jest.fn(() => -1),
93
+ getMemoryUsage: jest.fn(() => ({
94
+ nativeHeapBytes: 1000000,
95
+ residentBytes: 2000000,
96
+ availableMemoryBytes: 4000000,
97
+ isLowMemory: false,
98
+ })),
99
+ close: jest.fn(),
100
+ };
101
+
102
+ export const mockModelStore = {
103
+ isCached: jest.fn((fileName: string) => false),
104
+ getFilePath: jest.fn((fileName: string) => `/mock/path/${fileName}`),
105
+ listCachedFiles: jest.fn(() => []),
106
+ deleteFile: jest.fn((fileName: string) => {
107
+ return mockLiteRTLM.deleteModel(fileName);
108
+ }),
109
+ downloadFile: jest.fn(
110
+ async (
111
+ url: string,
112
+ fileName: string,
113
+ headersJson: string,
114
+ onProgress: (progress: number) => void,
115
+ ) => mockLiteRTLM.downloadModel(url, fileName, onProgress),
116
+ ),
117
+ };
118
+
119
+ export const NitroModules = {
120
+ createHybridObject: jest.fn((name: string) => {
121
+ if (name === "LiteRTLM") {
122
+ return mockLiteRTLM;
123
+ }
124
+ if (name === "ModelStore") {
125
+ return mockModelStore;
126
+ }
127
+ throw new Error(`Mock not implemented for hybrid object: ${name}`);
128
+ }),
129
+ createNativeArrayBuffer: jest.fn((size: number) => {
130
+ return new ArrayBuffer(size);
131
+ }),
132
+ };
@@ -0,0 +1,216 @@
1
+ // Configure React act environment
2
+ (global as any).IS_REACT_ACT_ENVIRONMENT = true;
3
+
4
+ import { useModel } from '../hooks';
5
+ import { mockLiteRTLM } from '../__mocks__/react-native-nitro-modules';
6
+ import TestRenderer from 'react-test-renderer';
7
+ import React from 'react';
8
+
9
+ // Mock react-native
10
+ jest.mock('react-native', () => ({
11
+ Platform: {
12
+ OS: 'ios',
13
+ select: jest.fn((dict) => dict.ios),
14
+ },
15
+ }));
16
+
17
+ // Helper to render and test hooks using react-test-renderer
18
+ function renderHook<P, R>(callback: (props: P) => R, initialProps?: P) {
19
+ let result = { current: null as unknown as R };
20
+
21
+ const TestComponent = ({ props }: { props: P }) => {
22
+ result.current = callback(props);
23
+ return null;
24
+ };
25
+
26
+ let renderer: TestRenderer.ReactTestRenderer;
27
+ TestRenderer.act(() => {
28
+ renderer = TestRenderer.create(React.createElement(TestComponent, { props: initialProps as P }));
29
+ });
30
+
31
+ const rerender = (newProps: P) => {
32
+ TestRenderer.act(() => {
33
+ renderer.update(React.createElement(TestComponent, { props: newProps }));
34
+ });
35
+ };
36
+
37
+ const unmount = () => {
38
+ TestRenderer.act(() => {
39
+ renderer.unmount();
40
+ });
41
+ };
42
+
43
+ return { result, rerender, unmount };
44
+ }
45
+
46
+ describe('useModel React Hook Unit Tests', () => {
47
+ beforeEach(() => {
48
+ jest.clearAllMocks();
49
+ });
50
+
51
+ it('should initialize with correct default state and call loadModel automatically when autoLoad is true', async () => {
52
+ let hookResult: any;
53
+
54
+ await TestRenderer.act(async () => {
55
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
56
+ });
57
+
58
+ expect(hookResult.result.current.isReady).toBe(true);
59
+ expect(hookResult.result.current.isGenerating).toBe(false);
60
+ expect(hookResult.result.current.downloadProgress).toBe(1); // loadModel completed
61
+ expect(hookResult.result.current.error).toBeNull();
62
+ expect(mockLiteRTLM.loadModel).toHaveBeenCalled();
63
+ });
64
+
65
+ it('should not call loadModel automatically when autoLoad is false', async () => {
66
+ let hookResult: any;
67
+
68
+ await TestRenderer.act(async () => {
69
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: false }));
70
+ });
71
+
72
+ expect(hookResult.result.current.isReady).toBe(false);
73
+ expect(mockLiteRTLM.loadModel).not.toHaveBeenCalled();
74
+
75
+ // Call load manually
76
+ await TestRenderer.act(async () => {
77
+ await hookResult.result.current.load();
78
+ });
79
+
80
+ expect(hookResult.result.current.isReady).toBe(true);
81
+ expect(mockLiteRTLM.loadModel).toHaveBeenCalled();
82
+ });
83
+
84
+ it('should handle model load failure gracefully', async () => {
85
+ mockLiteRTLM.loadModel.mockRejectedValueOnce(new Error("Model load failed"));
86
+ let hookResult: any;
87
+
88
+ await TestRenderer.act(async () => {
89
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
90
+ });
91
+
92
+ expect(hookResult.result.current.isReady).toBe(false);
93
+ expect(hookResult.result.current.error).toBe("Model load failed");
94
+ });
95
+
96
+ it('should generate text successfully and trigger memory summary update', async () => {
97
+ let hookResult: any;
98
+
99
+ await TestRenderer.act(async () => {
100
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', {
101
+ autoLoad: true,
102
+ enableMemoryTracking: true
103
+ }));
104
+ });
105
+
106
+ let response = "";
107
+ await TestRenderer.act(async () => {
108
+ response = await hookResult.result.current.generate("Test prompt");
109
+ });
110
+
111
+ expect(response).toBe("Mock token");
112
+ expect(mockLiteRTLM.execute).toHaveBeenCalledWith(
113
+ [{ type: "text", text: "Test prompt" }],
114
+ undefined,
115
+ );
116
+ expect(hookResult.result.current.memorySummary).toBeDefined();
117
+ });
118
+
119
+ it('should reset conversation correctly', async () => {
120
+ let hookResult: any;
121
+
122
+ await TestRenderer.act(async () => {
123
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
124
+ });
125
+
126
+ hookResult.result.current.reset();
127
+ expect(mockLiteRTLM.resetConversation).toHaveBeenCalled();
128
+ });
129
+
130
+ it('should delete model file correctly', async () => {
131
+ let hookResult: any;
132
+
133
+ await TestRenderer.act(async () => {
134
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
135
+ });
136
+
137
+ await TestRenderer.act(async () => {
138
+ await hookResult.result.current.deleteModel();
139
+ });
140
+
141
+ expect(mockLiteRTLM.deleteModel).toHaveBeenCalledWith('model.litertlm');
142
+ expect(hookResult.result.current.isReady).toBe(false);
143
+ expect(hookResult.result.current.downloadProgress).toBe(0);
144
+ });
145
+
146
+ it('should call close on unmount', async () => {
147
+ let hookResult: any;
148
+
149
+ await TestRenderer.act(async () => {
150
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: false }));
151
+ });
152
+
153
+ hookResult.unmount();
154
+ expect(mockLiteRTLM.close).toHaveBeenCalled();
155
+ });
156
+
157
+ it('should pass maxContextTokens and maxOutputTokens through to loadModel', async () => {
158
+ let hookResult: any;
159
+
160
+ await TestRenderer.act(async () => {
161
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', {
162
+ autoLoad: true,
163
+ maxContextTokens: 8192,
164
+ maxOutputTokens: 2048,
165
+ }));
166
+ });
167
+
168
+ expect(mockLiteRTLM.loadModel).toHaveBeenCalled();
169
+ const callArgs = mockLiteRTLM.loadModel.mock.calls[0];
170
+ const config = callArgs[1];
171
+ expect(config).toMatchObject({
172
+ maxContextTokens: 8192,
173
+ maxOutputTokens: 2048,
174
+ });
175
+ });
176
+
177
+ it('should pass legacy maxTokens through when new fields are not set', async () => {
178
+ let hookResult: any;
179
+
180
+ await TestRenderer.act(async () => {
181
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', {
182
+ autoLoad: true,
183
+ maxTokens: 512,
184
+ }));
185
+ });
186
+
187
+ expect(mockLiteRTLM.loadModel).toHaveBeenCalled();
188
+ const callArgs = mockLiteRTLM.loadModel.mock.calls[0];
189
+ const config = callArgs[1];
190
+ expect(config).toMatchObject({ maxTokens: 512 });
191
+ expect(config.maxContextTokens).toBeUndefined();
192
+ expect(config.maxOutputTokens).toBeUndefined();
193
+ });
194
+
195
+ it('should pass both new fields alongside legacy maxTokens when all are set', async () => {
196
+ let hookResult: any;
197
+
198
+ await TestRenderer.act(async () => {
199
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', {
200
+ autoLoad: true,
201
+ maxTokens: 512,
202
+ maxContextTokens: 4096,
203
+ maxOutputTokens: 1024,
204
+ }));
205
+ });
206
+
207
+ expect(mockLiteRTLM.loadModel).toHaveBeenCalled();
208
+ const callArgs = mockLiteRTLM.loadModel.mock.calls[0];
209
+ const config = callArgs[1];
210
+ expect(config).toMatchObject({
211
+ maxTokens: 512,
212
+ maxContextTokens: 4096,
213
+ maxOutputTokens: 1024,
214
+ });
215
+ });
216
+ });
@@ -0,0 +1,36 @@
1
+ import {
2
+ routeLegacyInference,
3
+ isLegacyInferenceMethod,
4
+ textPart,
5
+ imagePart,
6
+ audioPart,
7
+ } from "../inferenceRouting";
8
+
9
+ describe("inferenceRouting", () => {
10
+ it("routes sendMessage to a single text part", () => {
11
+ const route = routeLegacyInference("sendMessage", ["hello"]);
12
+ expect(route).toEqual({ parts: [textPart("hello")] });
13
+ });
14
+
15
+ it("routes sendMessageWithImageAsync with stream callback", () => {
16
+ const onToken = jest.fn();
17
+ const route = routeLegacyInference("sendMessageWithImageAsync", [
18
+ "describe",
19
+ "/img.jpg",
20
+ onToken,
21
+ ]);
22
+ expect(route).toEqual({
23
+ parts: [textPart("describe"), imagePart("/img.jpg")],
24
+ onToken,
25
+ });
26
+ });
27
+
28
+ it("returns null for unknown methods", () => {
29
+ expect(routeLegacyInference("downloadModel", ["url", "file"])).toBeNull();
30
+ });
31
+
32
+ it("isLegacyInferenceMethod narrows known methods", () => {
33
+ expect(isLegacyInferenceMethod("sendMessage")).toBe(true);
34
+ expect(isLegacyInferenceMethod("close")).toBe(false);
35
+ });
36
+ });
@@ -0,0 +1,87 @@
1
+ import { createMemoryTracker, createNativeBuffer } from '../memoryTracker';
2
+ import { NitroModules } from 'react-native-nitro-modules';
3
+
4
+ describe('MemoryTracker Unit Tests', () => {
5
+ beforeEach(() => {
6
+ jest.clearAllMocks();
7
+ });
8
+
9
+ it('should allocate correct native-backed ArrayBuffer size on initialization', () => {
10
+ const tracker = createMemoryTracker(10);
11
+ expect(NitroModules.createNativeArrayBuffer).toHaveBeenCalledWith(10 * 4 * 8); // 10 snapshots * 4 fields * 8 bytes/Float64
12
+ expect(tracker.getCapacity()).toBe(10);
13
+ expect(tracker.getSnapshotCount()).toBe(0);
14
+ });
15
+
16
+ it('should record snapshots correctly and retrieve them', () => {
17
+ const tracker = createMemoryTracker(5);
18
+ const snapshot1 = {
19
+ timestamp: 1000,
20
+ nativeHeapBytes: 100,
21
+ residentBytes: 200,
22
+ availableMemoryBytes: 500,
23
+ };
24
+
25
+ expect(tracker.record(snapshot1)).toBe(true);
26
+ expect(tracker.getSnapshotCount()).toBe(1);
27
+ expect(tracker.getLatestSnapshot()).toEqual(snapshot1);
28
+
29
+ const snapshots = tracker.getSnapshots();
30
+ expect(snapshots).toHaveLength(1);
31
+ expect(snapshots[0]).toEqual(snapshot1);
32
+ });
33
+
34
+ it('should reject new snapshots and return false when capacity is reached', () => {
35
+ const tracker = createMemoryTracker(2);
36
+
37
+ expect(tracker.record({ timestamp: 1, nativeHeapBytes: 10, residentBytes: 20, availableMemoryBytes: 50 })).toBe(true);
38
+ expect(tracker.record({ timestamp: 2, nativeHeapBytes: 20, residentBytes: 30, availableMemoryBytes: 40 })).toBe(true);
39
+ expect(tracker.record({ timestamp: 3, nativeHeapBytes: 30, residentBytes: 40, availableMemoryBytes: 30 })).toBe(false);
40
+
41
+ expect(tracker.getSnapshotCount()).toBe(2);
42
+ });
43
+
44
+ it('should calculate correct peak resident memory size', () => {
45
+ const tracker = createMemoryTracker(5);
46
+ tracker.record({ timestamp: 1, nativeHeapBytes: 100, residentBytes: 150, availableMemoryBytes: 1000 });
47
+ tracker.record({ timestamp: 2, nativeHeapBytes: 120, residentBytes: 300, availableMemoryBytes: 1000 });
48
+ tracker.record({ timestamp: 3, nativeHeapBytes: 110, residentBytes: 200, availableMemoryBytes: 1000 });
49
+
50
+ expect(tracker.getPeakMemory()).toBe(300);
51
+ });
52
+
53
+ it('should calculate accurate memory summary statistics', () => {
54
+ const tracker = createMemoryTracker(5);
55
+ tracker.record({ timestamp: 1, nativeHeapBytes: 50, residentBytes: 100, availableMemoryBytes: 1000 });
56
+ tracker.record({ timestamp: 2, nativeHeapBytes: 150, residentBytes: 300, availableMemoryBytes: 800 });
57
+ tracker.record({ timestamp: 3, nativeHeapBytes: 100, residentBytes: 200, availableMemoryBytes: 900 });
58
+
59
+ const summary = tracker.getSummary();
60
+ expect(summary.snapshotCount).toBe(3);
61
+ expect(summary.peakResidentBytes).toBe(300);
62
+ expect(summary.averageResidentBytes).toBe(200); // (100 + 300 + 200) / 3
63
+ expect(summary.currentResidentBytes).toBe(200);
64
+ expect(summary.peakNativeHeapBytes).toBe(150);
65
+ expect(summary.currentNativeHeapBytes).toBe(100);
66
+ expect(summary.residentDeltaBytes).toBe(100); // currentRss(200) - firstRss(100)
67
+ expect(summary.trackerBufferSizeBytes).toBe(5 * 4 * 8);
68
+ });
69
+
70
+ it('should preserve buffer but reset internal state when reset() is called', () => {
71
+ const tracker = createMemoryTracker(5);
72
+ tracker.record({ timestamp: 1, nativeHeapBytes: 50, residentBytes: 100, availableMemoryBytes: 1000 });
73
+
74
+ expect(tracker.getSnapshotCount()).toBe(1);
75
+ tracker.reset();
76
+ expect(tracker.getSnapshotCount()).toBe(0);
77
+ expect(tracker.getLatestSnapshot()).toBeUndefined();
78
+ expect(tracker.getSnapshots()).toEqual([]);
79
+ });
80
+
81
+ it('should allow standalone native ArrayBuffer allocation via createNativeBuffer', () => {
82
+ const size = 128;
83
+ const buffer = createNativeBuffer(size);
84
+ expect(NitroModules.createNativeArrayBuffer).toHaveBeenCalledWith(size);
85
+ expect(buffer.byteLength).toBe(size);
86
+ });
87
+ });