@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,189 @@
1
+ package com.margelo.nitro.dev.litert.litertlm
2
+
3
+ import android.util.Log
4
+ import androidx.annotation.Keep
5
+ import com.facebook.proguard.annotations.DoNotStrip
6
+ import com.margelo.nitro.core.Promise
7
+ import dev.litert.litertlm.LiteRTLMInitProvider
8
+ import java.io.File
9
+ import java.io.FileOutputStream
10
+ import java.net.HttpURLConnection
11
+ import java.net.URL
12
+ import org.json.JSONObject
13
+
14
+ @DoNotStrip
15
+ @Keep
16
+ class HybridModelStore : HybridModelStoreSpec() {
17
+
18
+ private val tag = "HybridModelStore"
19
+
20
+ private val modelsDirectory: File
21
+ get() {
22
+ val context = LiteRTLMInitProvider.applicationContext
23
+ ?: throw RuntimeException("Android Application Context is not available")
24
+ val modelsDir = File(context.filesDir, "models")
25
+ if (!modelsDir.exists()) {
26
+ modelsDir.mkdirs()
27
+ }
28
+ return modelsDir
29
+ }
30
+
31
+ private fun sanitizeFileName(fileName: String) {
32
+ if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
33
+ throw IllegalArgumentException("Invalid filename: path traversal or directory separators are not allowed.")
34
+ }
35
+ }
36
+
37
+ override fun isCached(fileName: String): Boolean {
38
+ sanitizeFileName(fileName)
39
+ val file = File(modelsDirectory, fileName)
40
+ return file.exists() && file.length() > 0
41
+ }
42
+
43
+ override fun getFilePath(fileName: String): String {
44
+ sanitizeFileName(fileName)
45
+ return File(modelsDirectory, fileName).absolutePath
46
+ }
47
+
48
+ override fun listCachedFiles(): Array<ModelFile> {
49
+ return try {
50
+ val dir = modelsDirectory
51
+ val files = dir.listFiles() ?: return emptyArray()
52
+ val result = mutableListOf<ModelFile>()
53
+ for (f in files) {
54
+ if (f.isFile && !f.name.endsWith(".tmp")) {
55
+ result.add(
56
+ ModelFile(
57
+ fileName = f.name,
58
+ absolutePath = f.absolutePath,
59
+ sizeBytes = f.length().toDouble(),
60
+ lastModifiedMs = f.lastModified().toDouble()
61
+ )
62
+ )
63
+ }
64
+ }
65
+ result.toTypedArray()
66
+ } catch (e: Exception) {
67
+ Log.e(tag, "Failed to list cached files", e)
68
+ emptyArray()
69
+ }
70
+ }
71
+
72
+ override fun deleteFile(fileName: String) {
73
+ sanitizeFileName(fileName)
74
+ val file = File(modelsDirectory, fileName)
75
+ if (file.exists()) {
76
+ val deleted = file.delete()
77
+ if (!deleted) {
78
+ throw RuntimeException("Failed to delete model file: ${file.absolutePath}")
79
+ }
80
+ }
81
+ }
82
+
83
+ override fun downloadFile(
84
+ url: String,
85
+ fileName: String,
86
+ headersJson: String,
87
+ onProgress: (progress: Double) -> Unit
88
+ ): Promise<String> {
89
+ return Promise.parallel {
90
+ Log.i(tag, "downloadFile: $url -> $fileName")
91
+ sanitizeFileName(fileName)
92
+
93
+ if (!url.startsWith("https://", ignoreCase = true)) {
94
+ throw IllegalArgumentException("Invalid download URL: HTTPS is required for security.")
95
+ }
96
+
97
+ val dir = modelsDirectory
98
+ val modelFile = File(dir, fileName)
99
+ val tempFile = File(dir, "$fileName.tmp")
100
+
101
+ // Fast cache check
102
+ if (modelFile.exists() && modelFile.length() > 0) {
103
+ Log.i(tag, "Model already exists: ${modelFile.absolutePath}")
104
+ onProgress(1.0)
105
+ return@parallel modelFile.absolutePath
106
+ }
107
+
108
+ Log.i(tag, "Downloading model to temp file: ${tempFile.absolutePath}")
109
+ onProgress(0.0)
110
+
111
+ // Parse headers
112
+ val headersMap = mutableMapOf<String, String>()
113
+ if (headersJson.isNotEmpty()) {
114
+ try {
115
+ val json = JSONObject(headersJson)
116
+ val keys = json.keys()
117
+ while (keys.hasNext()) {
118
+ val key = keys.next()
119
+ headersMap[key] = json.getString(key)
120
+ }
121
+ } catch (e: Exception) {
122
+ Log.e(tag, "Failed to parse custom headers JSON", e)
123
+ }
124
+ }
125
+
126
+ try {
127
+ val connection = URL(url).openConnection() as HttpURLConnection
128
+ connection.connectTimeout = 15000 // 15s
129
+ connection.readTimeout = 0 // Infinite read timeout for large files
130
+ connection.doInput = true
131
+
132
+ // Apply headers
133
+ for ((key, value) in headersMap) {
134
+ connection.setRequestProperty(key, value)
135
+ }
136
+
137
+ connection.connect()
138
+
139
+ if (connection.responseCode != HttpURLConnection.HTTP_OK) {
140
+ throw RuntimeException("Failed to download model: HTTP ${connection.responseCode}")
141
+ }
142
+
143
+ val contentLength = connection.contentLengthLong
144
+ val input = connection.inputStream
145
+ val output = FileOutputStream(tempFile)
146
+
147
+ val buffer = ByteArray(8 * 1024)
148
+ var bytesRead: Int
149
+ var totalBytesRead = 0L
150
+ var lastProgressUpdate = 0L
151
+
152
+ while (input.read(buffer).also { bytesRead = it } != -1) {
153
+ output.write(buffer, 0, bytesRead)
154
+ totalBytesRead += bytesRead
155
+
156
+ if (contentLength > 0) {
157
+ val currentTime = System.currentTimeMillis()
158
+ // Update progress roughly every 100ms to prevent bridge flooding
159
+ if (currentTime - lastProgressUpdate > 100) {
160
+ val progress = totalBytesRead.toDouble() / contentLength.toDouble()
161
+ onProgress(progress)
162
+ lastProgressUpdate = currentTime
163
+ }
164
+ }
165
+ }
166
+
167
+ output.flush()
168
+ output.close()
169
+ input.close()
170
+ connection.disconnect()
171
+
172
+ // Atomic rename
173
+ if (tempFile.renameTo(modelFile)) {
174
+ Log.i(tag, "Download complete and verified at: ${modelFile.absolutePath}")
175
+ onProgress(1.0)
176
+ modelFile.absolutePath
177
+ } else {
178
+ throw RuntimeException("Failed to rename temporary file to model file")
179
+ }
180
+ } catch (e: Exception) {
181
+ Log.e(tag, "Download failed", e)
182
+ if (tempFile.exists()) {
183
+ tempFile.delete()
184
+ }
185
+ throw RuntimeException("Download failed: ${e.message}", e)
186
+ }
187
+ }
188
+ }
189
+ }
@@ -0,0 +1,32 @@
1
+ package com.margelo.nitro.dev.litert.litertlm
2
+
3
+ import java.util.Collections
4
+ import java.util.WeakHashMap
5
+ import android.util.Log
6
+
7
+ /**
8
+ * Global registry to track active LiteRTLM instances.
9
+ * Used for memory trimming and cleanup.
10
+ */
11
+ object LiteRTLMRegistry {
12
+ private const val TAG = "LiteRTLMRegistry"
13
+
14
+ // Use WeakSet-like structure to prevent leaks
15
+ private val instances = Collections.newSetFromMap(WeakHashMap<HybridLiteRTLM, Boolean>())
16
+
17
+ fun register(instance: HybridLiteRTLM) {
18
+ synchronized(instances) {
19
+ instances.add(instance)
20
+ }
21
+ }
22
+
23
+ fun onTrimMemory(level: Int) {
24
+ Log.w(TAG, "Received memory warning (level=$level). Releasing resources...")
25
+ synchronized(instances) {
26
+ instances.forEach { it.close() }
27
+ // Note: We don't clear the set here, as close() should be idempotent
28
+ // and the instance might still be ref-counted by JS.
29
+ // We just ensure the HEAVY native resources are gone.
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,15 @@
1
+ package com.margelo.nitro.dev.litert.litertlm
2
+
3
+ /**
4
+ * Mirrors [src/inferenceRouting.ts] — keep shapes in sync for native direct hybrid access.
5
+ */
6
+ object MultimodalPartFactories {
7
+ fun textPart(text: String): MultimodalPart =
8
+ MultimodalPart(type = PartType.TEXT, text = text, path = null, imageBuffer = null, audioBuffer = null)
9
+
10
+ fun imagePart(path: String): MultimodalPart =
11
+ MultimodalPart(type = PartType.IMAGE, text = null, path = path, imageBuffer = null, audioBuffer = null)
12
+
13
+ fun audioPart(path: String): MultimodalPart =
14
+ MultimodalPart(type = PartType.AUDIO, text = null, path = path, imageBuffer = null, audioBuffer = null)
15
+ }
@@ -0,0 +1,89 @@
1
+ package com.margelo.nitro.dev.litert.litertlm
2
+
3
+ import android.util.Log
4
+ import com.google.ai.edge.litertlm.Content
5
+
6
+ /**
7
+ * Named implementation of the LiteRT-LM MessageCallback for streaming inference.
8
+ *
9
+ * Accumulates response chunks, forwards tokens to JS, and appends the final
10
+ * response to the conversation history.
11
+ */
12
+ internal class StreamingCallbackListener(
13
+ private val onToken: (String, Boolean) -> Unit,
14
+ private val responseBuilder: StringBuilder,
15
+ private val thinkingBuilder: StringBuilder = StringBuilder(),
16
+ private val history: MutableList<Message>,
17
+ private val userMessage: String,
18
+ private val onStatsReady: (GenerationStats) -> Unit,
19
+ private val onFailure: ((Throwable) -> Unit)? = null,
20
+ private val onThinkingToken: ((String) -> Unit)? = null,
21
+ ) : com.google.ai.edge.litertlm.MessageCallback {
22
+
23
+ private val startTime = System.nanoTime()
24
+ private var firstTokenTime = 0L
25
+ private var tokenCount = 0
26
+
27
+ override fun onMessage(message: com.google.ai.edge.litertlm.Message) {
28
+ val chunk = message.contents.contents
29
+ .filterIsInstance<Content.Text>()
30
+ .joinToString("") { it.text }
31
+
32
+ // Capture thinking from the "thought" channel
33
+ val thinkingChunk = message.channels["thought"]
34
+ if (!thinkingChunk.isNullOrEmpty()) {
35
+ thinkingBuilder.append(thinkingChunk)
36
+ onThinkingToken?.invoke(thinkingChunk)
37
+ }
38
+
39
+ if (firstTokenTime == 0L && chunk.isNotEmpty()) {
40
+ firstTokenTime = System.nanoTime()
41
+ }
42
+ if (chunk.isNotEmpty()) {
43
+ tokenCount++
44
+ }
45
+
46
+ onToken(chunk, false)
47
+
48
+ if (chunk.isNotEmpty()) {
49
+ responseBuilder.append(chunk)
50
+ }
51
+ }
52
+
53
+ override fun onDone() {
54
+ val fullResponse = responseBuilder.toString()
55
+ history.add(Message(Role.MODEL, fullResponse))
56
+
57
+ // Compute stats using heuristic token counts (~4 chars/token)
58
+ val elapsedMs = (System.nanoTime() - startTime) / 1_000_000.0
59
+ val ttftMs = if (firstTokenTime > 0) (firstTokenTime - startTime) / 1_000_000.0 else 0.0
60
+ val promptTokens = userMessage.length / 4.0
61
+ val completionTokens = fullResponse.length / 4.0
62
+ onStatsReady(GenerationStats(
63
+ promptTokens = promptTokens,
64
+ completionTokens = completionTokens,
65
+ totalTokens = promptTokens + completionTokens,
66
+ timeToFirstToken = ttftMs,
67
+ totalTime = elapsedMs,
68
+ tokensPerSecond = if (elapsedMs > 0) completionTokens / (elapsedMs / 1000.0) else 0.0
69
+ ))
70
+
71
+ Log.d("StreamingCallbackListener", "Streaming done. Length: ${fullResponse.length}, TTFT: ${ttftMs.toLong()}ms, Total: ${elapsedMs.toLong()}ms")
72
+
73
+ // Notify JS that streaming is done AFTER updating history and stats
74
+ onToken("", true)
75
+ }
76
+
77
+ override fun onError(throwable: Throwable) {
78
+ if (throwable is kotlinx.coroutines.CancellationException ||
79
+ throwable.message?.contains("cancel", ignoreCase = true) == true) {
80
+ // Graceful cancellation via stopGeneration() — treat as done with partial content
81
+ Log.i("StreamingCallbackListener", "Inference cancelled, emitting partial result")
82
+ onDone()
83
+ return
84
+ }
85
+ Log.e("StreamingCallbackListener", "Async generation failed", throwable)
86
+ onToken("Error: ${throwable.message}", true)
87
+ onFailure?.invoke(throwable)
88
+ }
89
+ }
@@ -0,0 +1,57 @@
1
+ package dev.litert.litertlm
2
+
3
+ import android.content.ContentProvider
4
+ import android.content.ContentValues
5
+ import android.content.Context
6
+ import android.database.Cursor
7
+ import android.net.Uri
8
+ import android.util.Log
9
+
10
+ class LiteRTLMInitProvider : ContentProvider() {
11
+ companion object {
12
+ private const val TAG = "LiteRTLMInitProvider"
13
+ var applicationContext: Context? = null
14
+ private set
15
+ }
16
+
17
+ override fun onCreate(): Boolean {
18
+ applicationContext = context?.applicationContext
19
+ Log.i(TAG, "LiteRTLMInitProvider initialized with context: $applicationContext")
20
+
21
+ applicationContext?.registerComponentCallbacks(object : android.content.ComponentCallbacks2 {
22
+ override fun onTrimMemory(level: Int) {
23
+ if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
24
+ com.margelo.nitro.dev.litert.litertlm.LiteRTLMRegistry.onTrimMemory(level)
25
+ }
26
+ }
27
+
28
+ override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {}
29
+ override fun onLowMemory() {
30
+ com.margelo.nitro.dev.litert.litertlm.LiteRTLMRegistry.onTrimMemory(android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE)
31
+ }
32
+ })
33
+
34
+ return true
35
+ }
36
+
37
+ override fun query(
38
+ uri: Uri,
39
+ projection: Array<out String>?,
40
+ selection: String?,
41
+ selectionArgs: Array<out String>?,
42
+ sortOrder: String?
43
+ ): Cursor? = null
44
+
45
+ override fun getType(uri: Uri): String? = null
46
+
47
+ override fun insert(uri: Uri, values: ContentValues?): Uri? = null
48
+
49
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
50
+
51
+ override fun update(
52
+ uri: Uri,
53
+ values: ContentValues?,
54
+ selection: String?,
55
+ selectionArgs: Array<out String>?
56
+ ): Int = 0
57
+ }
@@ -0,0 +1,43 @@
1
+ package dev.litert.litertlm
2
+
3
+ import android.os.Build
4
+ import android.util.Log
5
+ import com.facebook.react.TurboReactPackage
6
+ import com.facebook.react.bridge.NativeModule
7
+ import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.facebook.react.module.model.ReactModuleInfo
9
+ import com.facebook.react.module.model.ReactModuleInfoProvider
10
+
11
+
12
+ import com.margelo.nitro.dev.litert.litertlm.LiteRTLMOnLoad
13
+
14
+ class LiteRTLMPackage : TurboReactPackage() {
15
+ companion object {
16
+ private const val TAG = "LiteRTLMPackage"
17
+
18
+ private fun isSupportedPrimaryAbi(): Boolean {
19
+ val primaryAbi = Build.SUPPORTED_64_BIT_ABIS.firstOrNull() ?: return false
20
+ return primaryAbi == "arm64-v8a"
21
+ }
22
+ }
23
+ init {
24
+ if (!isSupportedPrimaryAbi()) {
25
+ Log.w(TAG, "Skipping LiteRTLM native init on unsupported primary ABI: ${Build.SUPPORTED_64_BIT_ABIS.firstOrNull()}")
26
+ } else {
27
+ try {
28
+ LiteRTLMOnLoad.initializeNative()
29
+ } catch (e: UnsatisfiedLinkError) {
30
+ Log.e(TAG, "LiteRTLM native init failed; disabling LiteRTLM for this process.", e)
31
+ }
32
+ }
33
+ }
34
+
35
+
36
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
37
+ return null
38
+ }
39
+
40
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
41
+ return ReactModuleInfoProvider { emptyMap<String, ReactModuleInfo>() }
42
+ }
43
+ }
@@ -0,0 +1,46 @@
1
+ package com.margelo.nitro.core
2
+
3
+ import androidx.annotation.Keep
4
+ import com.facebook.proguard.annotations.DoNotStrip
5
+
6
+ @Keep
7
+ @DoNotStrip
8
+ class Promise<T> {
9
+ companion object {
10
+ @JvmStatic
11
+ fun <T> parallel(block: () -> T): Promise<T> {
12
+ val promise = Promise<T>()
13
+ try {
14
+ val result = block()
15
+ promise.resolve(result)
16
+ } catch (e: Throwable) {
17
+ promise.reject(e)
18
+ }
19
+ return promise
20
+ }
21
+ }
22
+
23
+ var result: T? = null
24
+ private set
25
+ var error: Throwable? = null
26
+ private set
27
+ var isCompleted = false
28
+ private set
29
+ private val callbacks = mutableListOf<(T?, Throwable?) -> Unit>()
30
+
31
+ fun resolve(value: T) {
32
+ synchronized(this) {
33
+ result = value
34
+ isCompleted = true
35
+ callbacks.forEach { it(value, null) }
36
+ }
37
+ }
38
+
39
+ fun reject(exception: Throwable) {
40
+ synchronized(this) {
41
+ error = exception
42
+ isCompleted = true
43
+ callbacks.forEach { it(null, exception) }
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,122 @@
1
+ package com.margelo.nitro.dev.litert.litertlm
2
+
3
+ import org.junit.Assert.*
4
+ import org.junit.Before
5
+ import org.junit.After
6
+ import org.junit.Test
7
+ import org.junit.runner.RunWith
8
+ import org.robolectric.RobolectricTestRunner
9
+ import org.robolectric.RuntimeEnvironment
10
+ import dev.litert.litertlm.LiteRTLMInitProvider
11
+ import java.lang.IllegalArgumentException
12
+
13
+ @RunWith(RobolectricTestRunner::class)
14
+ class HybridLiteRTLMTest {
15
+ private lateinit var bridge: HybridLiteRTLM
16
+
17
+ @Before
18
+ fun setUp() {
19
+ // Initialize the static applicationContext inside LiteRTLMInitProvider via reflection
20
+ try {
21
+ val field = LiteRTLMInitProvider::class.java.getDeclaredField("applicationContext")
22
+ field.isAccessible = true
23
+ field.set(null, RuntimeEnvironment.getApplication())
24
+ } catch (e: Exception) {
25
+ e.printStackTrace()
26
+ }
27
+
28
+ bridge = HybridLiteRTLM()
29
+ }
30
+
31
+ @After
32
+ fun tearDown() {
33
+ bridge.close()
34
+ }
35
+
36
+ @Test
37
+ fun testAndroidPathTraversalPrevention() {
38
+ val traversals = arrayOf("../secret", "/etc/hosts", "nested\\..\\file", "..", "../", "..\\")
39
+ for (traversal in traversals) {
40
+ val promise = bridge.deleteModel(traversal)
41
+ assertNotNull("Promise should not be null", promise)
42
+ assertTrue("Promise should be completed", promise.isCompleted)
43
+ assertNotNull("Promise should have rejected with an error for filename: $traversal", promise.error)
44
+ val error = promise.error!!
45
+ val errMsg = error.message ?: error.cause?.message ?: ""
46
+ assertTrue("Expected message to contain traversal warning, got: $errMsg",
47
+ errMsg.contains("path traversal or directory separators are not allowed"))
48
+ }
49
+ }
50
+
51
+ @Test
52
+ fun testAndroidHTTPSDownloadEnforcement() {
53
+ val promise = bridge.downloadModel("http://insecure.site/model.bin", "model.bin", null)
54
+ assertNotNull("Promise should not be null", promise)
55
+ assertTrue("Promise should be completed", promise.isCompleted)
56
+ assertNotNull("Promise should have rejected with an error", promise.error)
57
+ val error = promise.error!!
58
+ val errMsg = error.message ?: error.cause?.message ?: ""
59
+ assertTrue("Expected message to contain HTTPS warning, got: $errMsg",
60
+ errMsg.contains("HTTPS is required for security"))
61
+ }
62
+
63
+ @Test
64
+ fun testAndroidMemoryTelemetry() {
65
+ val mem = bridge.getMemoryUsage()
66
+ assertNotNull(mem)
67
+ assertTrue(mem.nativeHeapBytes >= 0.0)
68
+ assertTrue(mem.residentBytes >= 0.0)
69
+ assertTrue(mem.availableMemoryBytes >= 0.0)
70
+ }
71
+
72
+ @Test
73
+ fun testSendMessageWithImageAsyncRejectsWithoutModel() {
74
+ val promise = bridge.sendMessageWithImageAsync("hello", "/tmp/image.jpg") { _, _ -> }
75
+ assertNotNull("Promise should not be null", promise)
76
+ assertTrue("Promise should be completed", promise.isCompleted)
77
+ assertNotNull("Promise should have rejected without model", promise.error)
78
+ val errMsg = promise.error!!.message ?: promise.error!!.cause?.message ?: ""
79
+ assertTrue("Expected no-model error, got: $errMsg",
80
+ errMsg.contains("No model loaded"))
81
+ }
82
+
83
+ @Test
84
+ fun testSendMessageWithAudioAsyncRejectsWithoutModel() {
85
+ val promise = bridge.sendMessageWithAudioAsync("hello", "/tmp/audio.wav") { _, _ -> }
86
+ assertNotNull("Promise should not be null", promise)
87
+ assertTrue("Promise should be completed", promise.isCompleted)
88
+ assertNotNull("Promise should have rejected without model", promise.error)
89
+ val errMsg = promise.error!!.message ?: promise.error!!.cause?.message ?: ""
90
+ assertTrue("Expected no-model error, got: $errMsg",
91
+ errMsg.contains("No model loaded"))
92
+ }
93
+
94
+ @Test
95
+ fun testAndroidInitialStats() {
96
+ val stats = bridge.getStats()
97
+ assertNotNull(stats)
98
+ assertEquals(0.0, stats.promptTokens, 0.0)
99
+ assertEquals(0.0, stats.completionTokens, 0.0)
100
+ assertEquals(0.0, stats.totalTokens, 0.0)
101
+ assertEquals(0.0, stats.timeToFirstToken, 0.0)
102
+ assertEquals(0.0, stats.totalTime, 0.0)
103
+ assertEquals(0.0, stats.tokensPerSecond, 0.0)
104
+ }
105
+
106
+ @Test
107
+ fun testDeleteModelCleanupLogic() {
108
+ val loadedPathField = HybridLiteRTLM::class.java.getDeclaredField("loadedModelPath")
109
+ loadedPathField.isAccessible = true
110
+ loadedPathField.set(bridge, "/path/to/my_loaded_model.litertlm")
111
+
112
+ val promise1 = bridge.deleteModel("other_model.litertlm")
113
+ assertNotNull(promise1)
114
+ while (!promise1.isCompleted) { Thread.sleep(10) }
115
+ assertEquals("/path/to/my_loaded_model.litertlm", loadedPathField.get(bridge))
116
+
117
+ val promise2 = bridge.deleteModel("my_loaded_model.litertlm")
118
+ assertNotNull(promise2)
119
+ while (!promise2.isCompleted) { Thread.sleep(10) }
120
+ assertNull(loadedPathField.get(bridge))
121
+ }
122
+ }
package/app.plugin.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Expo config plugin for react-native-litert-lm.
3
+ *
4
+ * Ensures correct build settings for the LiteRT-LM native module:
5
+ * - Android: minSdkVersion 26, Kotlin 2.3.0 (required by litertlm-android AAR)
6
+ */
7
+ const {
8
+ withGradleProperties,
9
+ withProjectBuildGradle,
10
+ } = require('@expo/config-plugins');
11
+
12
+ function withLiteRTLM(config) {
13
+ // Android: Ensure minSdkVersion is at least 26
14
+ config = withGradleProperties(config, (config) => {
15
+ const props = config.modResults;
16
+
17
+ // Set minSdkVersion if not already high enough
18
+ const minSdkProp = props.find((p) => p.key === 'android.minSdkVersion');
19
+ if (!minSdkProp) {
20
+ props.push({
21
+ type: 'property',
22
+ key: 'android.minSdkVersion',
23
+ value: '26',
24
+ });
25
+ } else if (parseInt(minSdkProp.value, 10) < 26) {
26
+ minSdkProp.value = '26';
27
+ }
28
+
29
+ return config;
30
+ });
31
+
32
+ // Android: Pin Kotlin Gradle plugin to 2.3.0
33
+ // The litertlm-android AAR uses Kotlin 2.3.0 metadata (version defined in
34
+ // package.json → litertLm.androidMavenVersion).
35
+ // React Native's default Kotlin version (2.1.0) cannot read this metadata,
36
+ // so we must force the Kotlin Gradle plugin to 2.3.0 in the project-level
37
+ // build.gradle. This ensures the fix survives `expo prebuild --clean`.
38
+ config = withProjectBuildGradle(config, (config) => {
39
+ if (config.modResults.language === 'groovy') {
40
+ const contents = config.modResults.contents;
41
+
42
+ // Only add if not already pinned
43
+ if (!contents.includes("kotlin-gradle-plugin:2.3.0")) {
44
+ // Replace the unversioned kotlin-gradle-plugin classpath with a pinned one
45
+ config.modResults.contents = contents.replace(
46
+ "classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')",
47
+ "classpath('org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.0')"
48
+ );
49
+ }
50
+ }
51
+
52
+ return config;
53
+ });
54
+
55
+ return config;
56
+ }
57
+
58
+ module.exports = withLiteRTLM;
@@ -0,0 +1,21 @@
1
+ ///
2
+ /// cpp-adapter.cpp
3
+ /// JNI Entry Point - Required by Nitrogen to register Kotlin HybridObjects
4
+ ///
5
+ /// Updated for react-native-nitro-modules v0.35+:
6
+ /// Uses facebook::jni::initialize() directly with registerAllNatives().
7
+ ///
8
+
9
+ #include <jni.h>
10
+ #include <fbjni/fbjni.h>
11
+ #include "LiteRTLMOnLoad.hpp"
12
+
13
+ // JNI_OnLoad is called when the native library is loaded via System.loadLibrary()
14
+ // This is where we initialize the Nitrogen bridge and register all Kotlin HybridObjects.
15
+ // The new v0.35 API allows registering custom C++ native JNI classes/functions
16
+ // alongside Nitrogen's auto-generated registrations.
17
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
18
+ return facebook::jni::initialize(vm, []() {
19
+ margelo::nitro::litertlm::registerAllNatives();
20
+ });
21
+ }