react-native-litert-lm 0.1.0 โ†’ 0.1.1

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.
package/README.md CHANGED
@@ -9,8 +9,9 @@ High-performance LLM inference for React Native powered by [LiteRT-LM](https://g
9
9
  - โšก **GPU Acceleration** - GPU delegate (Android), Metal (iOS when available)
10
10
  - ๐Ÿ“ฆ **Bundled Tokenizer** - No separate tokenization library needed
11
11
  - ๐Ÿ”„ **Streaming Support** - Token-by-token generation callbacks
12
- - ๐Ÿ“ฑ **Cross-Platform** - Android API 26+ (iOS coming soon)
13
- - ๐Ÿšง **Multimodal** - Image and audio input (Coming Soon to Android)
12
+ - ๐Ÿ“ฑ **Cross-Platform** - Android API 26+
13
+ - ๐Ÿšง **Multimodal** - Image and audio input (Coming Soon)
14
+ - ๐Ÿงต **Async API** - Non-blocking inference to prevent UI freezes
14
15
 
15
16
  ## Status
16
17
 
@@ -86,15 +87,15 @@ import { createLLM } from "react-native-litert-lm";
86
87
 
87
88
  const llm = createLLM();
88
89
 
89
- // Load a Gemma 3n model
90
- llm.loadModel("/path/to/gemma-3n-e2b.litertlm", {
90
+ // Load a Gemma 3n model (async)
91
+ await llm.loadModel("/path/to/gemma-3n-e2b.litertlm", {
91
92
  backend: "gpu",
92
93
  temperature: 0.7,
93
94
  maxTokens: 512,
94
95
  });
95
96
 
96
- // Generate response
97
- const response = llm.sendMessage("What is the capital of France?");
97
+ // Generate response (async)
98
+ const response = await llm.sendMessage("What is the capital of France?");
98
99
  console.log(response);
99
100
 
100
101
  // Clean up
@@ -114,13 +115,15 @@ llm.sendMessageAsync("Tell me a story", (token, done) => {
114
115
 
115
116
  ```typescript
116
117
  // Image input (for vision models)
117
- const response = llm.sendMessageWithImage(
118
+ // Note: Currently throws error on Android (Coming Soon)
119
+ const response = await llm.sendMessageWithImage(
118
120
  "What's in this image?",
119
121
  "/path/to/image.jpg",
120
122
  );
121
123
 
122
124
  // Audio input (for audio models)
123
- const transcription = llm.sendMessageWithAudio(
125
+ // Note: Currently throws error on Android (Coming Soon)
126
+ const transcription = await llm.sendMessageWithAudio(
124
127
  "Transcribe this audio",
125
128
  "/path/to/audio.wav",
126
129
  );
@@ -152,7 +155,7 @@ Download `.litertlm` models from [HuggingFace](https://huggingface.co/litert-com
152
155
 
153
156
  Creates a new LLM inference engine instance.
154
157
 
155
- ### `loadModel(path, config?)`
158
+ ### `loadModel(path, config?): Promise<void>`
156
159
 
157
160
  - `path: string` - Absolute path to `.litertlm` file
158
161
  - `config.backend` - `'cpu'` | `'gpu'` | `'npu'` (default: `'gpu'`)
@@ -172,19 +175,19 @@ Creates a new LLM inference engine instance.
172
175
 
173
176
  > โš ๏ธ **NPU Note**: NPU acceleration requires compatible hardware (Qualcomm Hexagon, MediaTek APU, etc.). If unavailable, LiteRT-LM automatically falls back to GPU.
174
177
 
175
- ### `sendMessage(message): string`
178
+ ### `sendMessage(message): Promise<string>`
176
179
 
177
- Blocking generation. Returns complete response.
180
+ Blocking generation (executed on background thread). Returns complete response.
178
181
 
179
182
  ### `sendMessageAsync(message, callback)`
180
183
 
181
184
  Streaming generation. Callback receives `(token, isDone)`.
182
185
 
183
- ### `sendMessageWithImage(message, imagePath): string`
186
+ ### `sendMessageWithImage(message, imagePath): Promise<string>`
184
187
 
185
188
  Send a message with an image attachment (for vision models).
186
189
 
187
- ### `sendMessageWithAudio(message, audioPath): string`
190
+ ### `sendMessageWithAudio(message, audioPath): Promise<string>`
188
191
 
189
192
  Send a message with an audio attachment (for audio models).
190
193
 
@@ -19,6 +19,7 @@ import com.margelo.nitro.dev.litert.litertlm.HybridLiteRTLMSpec
19
19
  import com.margelo.nitro.dev.litert.litertlm.LLMConfig
20
20
  import com.margelo.nitro.dev.litert.litertlm.Message
21
21
  import com.margelo.nitro.dev.litert.litertlm.Role
22
+ import com.margelo.nitro.core.Promise
22
23
 
23
24
  // Alias to avoid confusion with our generated Message type
24
25
  typealias LiteRTMessage = com.google.ai.edge.litertlm.Message
@@ -35,6 +36,10 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
35
36
  private const val TAG = "HybridLiteRTLM"
36
37
  }
37
38
 
39
+ init {
40
+ LiteRTLMRegistry.register(this)
41
+ }
42
+
38
43
  // LiteRT-LM Engine and Conversation
39
44
  private var engine: Engine? = null
40
45
  private var conversation: Conversation? = null
@@ -60,116 +65,124 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
60
65
  private var maxTokens: Int = 1024
61
66
 
62
67
  override val memorySize: Long
63
- get() = 10L * 1024L * 1024L // ~10MB estimate
68
+ get() = 1024L * 1024L * 1024L // ~1GB (models are large)
64
69
 
65
70
  // -------------------------------------------------------------------------
66
71
  // loadModel - Initialize LiteRT-LM Engine and Conversation
67
72
  // -------------------------------------------------------------------------
68
- override fun loadModel(modelPath: String, config: LLMConfig?) {
69
- Log.i(TAG, "loadModel: $modelPath")
70
-
71
- // Clean up existing resources
72
- close()
73
-
74
- // Apply configuration
75
- config?.let { cfg ->
76
- cfg.backend?.let { backend = it }
77
- cfg.temperature?.let { temperature = it }
78
- cfg.topK?.let { topK = it.toInt() }
79
- cfg.topP?.let { topP = it }
80
- cfg.maxTokens?.let { maxTokens = it.toInt() }
81
- }
73
+ override fun loadModel(modelPath: String, config: LLMConfig?): Promise<Unit> {
74
+ return Promise.parallel {
75
+ Log.i(TAG, "loadModel: $modelPath")
76
+
77
+ // Clean up existing resources
78
+ close()
79
+
80
+ // Apply configuration
81
+ config?.let { cfg ->
82
+ cfg.backend?.let { backend = it }
83
+ cfg.temperature?.let { temperature = it }
84
+ cfg.topK?.let { topK = it.toInt() }
85
+ cfg.topP?.let { topP = it }
86
+ cfg.maxTokens?.let { maxTokens = it.toInt() }
87
+ }
82
88
 
83
- try {
84
- // Map our Backend enum to LiteRT-LM Backend enum
85
- val lmBackend = when (backend) {
86
- Backend.GPU -> com.google.ai.edge.litertlm.Backend.GPU
87
- Backend.NPU -> {
88
- Log.i(TAG, "NPU backend requested - requires hardware support")
89
- com.google.ai.edge.litertlm.Backend.NPU
89
+ try {
90
+ // Map our Backend enum to LiteRT-LM Backend enum
91
+ val lmBackend = when (backend) {
92
+ Backend.GPU -> com.google.ai.edge.litertlm.Backend.GPU
93
+ Backend.NPU -> {
94
+ Log.i(TAG, "NPU backend requested - requires hardware support")
95
+ com.google.ai.edge.litertlm.Backend.NPU
96
+ }
97
+ else -> com.google.ai.edge.litertlm.Backend.CPU
90
98
  }
91
- else -> com.google.ai.edge.litertlm.Backend.CPU
92
- }
93
-
94
- // Vision backend: hardcoded to GPU (required by Gemma 3n)
95
- val lmVisionBackend = com.google.ai.edge.litertlm.Backend.GPU
96
99
 
97
- // Audio backend: hardcoded to CPU (optimal for audio processing)
98
- val lmAudioBackend = com.google.ai.edge.litertlm.Backend.CPU
99
-
100
- Log.i(TAG, "Backend config: main=$lmBackend, vision=$lmVisionBackend (hardcoded), audio=$lmAudioBackend (hardcoded)")
101
-
102
- // Get cache directory from application context
103
- // LiteRT-LM needs this to store temporary compiled model files
104
- val cacheDirectory = LiteRTLMInitProvider.applicationContext?.cacheDir?.absolutePath
105
- Log.i(TAG, "Using cache directory: $cacheDirectory")
106
-
107
- // Create Engine configuration
108
- val engineConfig = EngineConfig(
109
- modelPath = modelPath,
110
- backend = lmBackend,
111
- visionBackend = lmVisionBackend,
112
- audioBackend = lmAudioBackend,
113
- maxNumTokens = maxTokens,
114
- cacheDir = cacheDirectory
115
- )
116
-
117
- // Create Engine (heavyweight - loads model)
118
- engine = Engine(engineConfig).also { it.initialize() }
119
- Log.i(TAG, "Engine created and initialized successfully")
120
-
121
- // Create Conversation (lightweight - holds KV cache)
122
- createNewConversation()
123
- Log.i(TAG, "Conversation created successfully")
124
-
125
- } catch (e: Exception) {
126
- Log.e(TAG, "Failed to load model: ${e.message}", e)
127
- throw RuntimeException("Failed to load model: ${e.message}", e)
100
+ // Vision backend: hardcoded to GPU (required by Gemma 3n)
101
+ val lmVisionBackend = com.google.ai.edge.litertlm.Backend.GPU
102
+
103
+ // Audio backend: hardcoded to CPU (optimal for audio processing)
104
+ val lmAudioBackend = com.google.ai.edge.litertlm.Backend.CPU
105
+
106
+ Log.i(TAG, "Backend config: main=$lmBackend, vision=$lmVisionBackend (hardcoded), audio=$lmAudioBackend (hardcoded)")
107
+
108
+ // Get cache directory from application context
109
+ val cacheDirectory = LiteRTLMInitProvider.applicationContext?.cacheDir?.absolutePath
110
+ Log.i(TAG, "Using cache directory: $cacheDirectory")
111
+
112
+ // Create Engine configuration
113
+ val engineConfig = EngineConfig(
114
+ modelPath = modelPath,
115
+ backend = lmBackend,
116
+ visionBackend = lmVisionBackend,
117
+ audioBackend = lmAudioBackend,
118
+ maxNumTokens = maxTokens,
119
+ cacheDir = cacheDirectory
120
+ )
121
+
122
+ // Initialize Engine
123
+ engine = Engine(engineConfig).also { it.initialize() }
124
+ Log.i(TAG, "Engine created and initialized successfully")
125
+
126
+ // Create Conversation
127
+ createNewConversation()
128
+ Log.i(TAG, "Conversation created successfully")
129
+
130
+ } catch (e: Exception) {
131
+ Log.e(TAG, "Failed to load model: ${e.message}", e)
132
+ throw RuntimeException("Failed to load model: ${e.message}", e)
133
+ }
128
134
  }
129
135
  }
130
136
 
131
137
  // -------------------------------------------------------------------------
132
- // sendMessage - Blocking text inference
138
+ // sendMessage - Helper for one-shot generation (internally uses Async)
133
139
  // -------------------------------------------------------------------------
134
- override fun sendMessage(message: String): String {
135
- ensureLoaded()
136
-
137
- // Add user message to history
138
- history.add(Message(Role.USER, message))
139
-
140
- // Pre-process message (chat template)
141
- Log.i(TAG, "sendMessage: $message")
142
-
143
- // Blocking inference
144
- // LiteRT-LM expects a Message object, not String
145
- val userMsg = LiteRTMessage.of(message)
146
- val responseMsg = conversation!!.sendMessage(userMsg)
147
-
148
- // Extract text from response Message
149
- val response = responseMsg.contents
150
- .filterIsInstance<com.google.ai.edge.litertlm.Content.Text>()
151
- .joinToString("") { it.text }
152
-
153
- // Add model response to history
154
- history.add(Message(Role.MODEL, response))
155
-
156
- // Update stats (mock/approximate for now as SDK doesn't return full stats for sync call)
157
- lastStats = GenerationStats(
158
- promptTokens = message.length / 4.0,
159
- completionTokens = response.length / 4.0,
160
- totalTokens = (message.length + response.length) / 4.0,
161
- timeToFirstToken = 0.0,
162
- totalTime = 0.0,
163
- tokensPerSecond = 0.0
164
- )
165
-
166
- return response
140
+ override fun sendMessage(message: String): Promise<String> {
141
+ // Implement Promise-based sendMessage using suspend coroutine logic wrapped in Promise
142
+ // Since Promise.parallel expects a blocking block returning T,
143
+ // and sendMessageAsync is callback-based, we need to bridge them.
144
+ // HOWEVER, we can just use the synchronous `sendMessage` API of the SDK
145
+ // inside the `Promise.parallel` block, which moves it off the main thread!
146
+ return Promise.parallel {
147
+ ensureLoaded()
148
+
149
+ // Add user message to history
150
+ history.add(Message(Role.USER, message))
151
+ Log.i(TAG, "sendMessage (Promise): $message")
152
+
153
+ // Blocking inference (safe here because we are in Promise.parallel worker thread)
154
+ val userMsg = LiteRTMessage.of(message)
155
+ val responseMsg = conversation!!.sendMessage(userMsg)
156
+
157
+ // Extract text
158
+ val response = responseMsg.contents
159
+ .filterIsInstance<com.google.ai.edge.litertlm.Content.Text>()
160
+ .joinToString("") { it.text }
161
+
162
+ // Add model response to history
163
+ history.add(Message(Role.MODEL, response))
164
+
165
+ // Update stats
166
+ lastStats = GenerationStats(
167
+ promptTokens = message.length / 4.0,
168
+ completionTokens = response.length / 4.0,
169
+ totalTokens = (message.length + response.length) / 4.0,
170
+ timeToFirstToken = 0.0,
171
+ totalTime = 0.0,
172
+ tokensPerSecond = 0.0
173
+ )
174
+
175
+ response // Return the string
176
+ }
167
177
  }
168
178
 
169
179
  // -------------------------------------------------------------------------
170
180
  // sendMessageAsync - Streaming inference
171
181
  // -------------------------------------------------------------------------
172
182
  override fun sendMessageAsync(message: String, onToken: (String, Boolean) -> Unit) {
183
+ // This is already async (void return), so we execute immediately on the calling thread
184
+ // (which is the Nitro specialized thread, not Main).
185
+ // The SDK's sendMessageAsync is non-blocking anyway.
173
186
  ensureLoaded()
174
187
 
175
188
  // Add user message to history
@@ -206,12 +219,8 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
206
219
  }
207
220
 
208
221
  try {
209
- // Construct Message object
210
222
  val userMsg = LiteRTMessage.of(message)
211
-
212
- // LiteRT-LM async call - SDK handles threading
213
223
  conversation!!.sendMessageAsync(userMsg, listener)
214
-
215
224
  } catch (e: Exception) {
216
225
  Log.e(TAG, "Failed into initiate async generation", e)
217
226
  onToken("Error: ${e.message}", true)
@@ -221,14 +230,18 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
221
230
  // -------------------------------------------------------------------------
222
231
  // Multimodal methods
223
232
  // -------------------------------------------------------------------------
224
- override fun sendMessageWithImage(message: String, imagePath: String): String {
225
- // TODO: Implement image loading from path
226
- throw RuntimeException("Multimodal (Image) not yet implemented in this wrapper")
233
+ override fun sendMessageWithImage(message: String, imagePath: String): Promise<String> {
234
+ return Promise.parallel {
235
+ // TODO: Implement image loading from path
236
+ throw RuntimeException("Multimodal (Image) not yet implemented in this wrapper")
237
+ }
227
238
  }
228
239
 
229
- override fun sendMessageWithAudio(message: String, audioPath: String): String {
230
- // TODO: Implement audio loading from path
231
- throw RuntimeException("Multimodal (Audio) not yet implemented in this wrapper")
240
+ override fun sendMessageWithAudio(message: String, audioPath: String): Promise<String> {
241
+ return Promise.parallel {
242
+ // TODO: Implement audio loading from path
243
+ throw RuntimeException("Multimodal (Audio) not yet implemented in this wrapper")
244
+ }
232
245
  }
233
246
 
234
247
  // -------------------------------------------------------------------------
@@ -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
+ }
@@ -17,6 +17,20 @@ class LiteRTLMInitProvider : ContentProvider() {
17
17
  override fun onCreate(): Boolean {
18
18
  applicationContext = context?.applicationContext
19
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
+
20
34
  return true
21
35
  }
22
36
 
@@ -108,31 +108,30 @@ export interface LiteRTLM extends HybridObject<{
108
108
  }> {
109
109
  /**
110
110
  * Load a .litertlm model file.
111
- * @param modelPath Absolute path to the .litertlm file.
112
111
  * @param config Optional configuration for backend and sampling.
113
112
  * @throws Error if the model cannot be loaded.
114
113
  */
115
- loadModel(modelPath: string, config?: LLMConfig): void;
114
+ loadModel(modelPath: string, config?: LLMConfig): Promise<void>;
116
115
  /**
117
116
  * Send a text message and get the complete response (blocking).
118
117
  * @param message User message text.
119
118
  * @returns The model's response text.
120
119
  */
121
- sendMessage(message: string): string;
120
+ sendMessage(message: string): Promise<string>;
122
121
  /**
123
122
  * Send a text message with an image (multimodal).
124
123
  * @param message User message text.
125
124
  * @param imagePath Absolute path to an image file.
126
125
  * @returns The model's response text.
127
126
  */
128
- sendMessageWithImage(message: string, imagePath: string): string;
127
+ sendMessageWithImage(message: string, imagePath: string): Promise<string>;
129
128
  /**
130
129
  * Send a text message with audio (multimodal).
131
130
  * @param message User message text.
132
131
  * @param audioPath Absolute path to an audio file (WAV).
133
132
  * @returns The model's response text.
134
133
  */
135
- sendMessageWithAudio(message: string, audioPath: string): string;
134
+ sendMessageWithAudio(message: string, audioPath: string): Promise<string>;
136
135
  /**
137
136
  * Send a message with streaming response.
138
137
  * Tokens are delivered via callback as they are generated.
@@ -18,6 +18,9 @@ namespace margelo::nitro::litertlm { struct LLMConfig; }
18
18
  // Forward declaration of `Backend` to properly resolve imports.
19
19
  namespace margelo::nitro::litertlm { enum class Backend; }
20
20
 
21
+ #include <NitroModules/Promise.hpp>
22
+ #include <NitroModules/JPromise.hpp>
23
+ #include <NitroModules/JUnit.hpp>
21
24
  #include <string>
22
25
  #include "Message.hpp"
23
26
  #include <vector>
@@ -74,24 +77,68 @@ namespace margelo::nitro::litertlm {
74
77
 
75
78
 
76
79
  // Methods
77
- void JHybridLiteRTLMSpec::loadModel(const std::string& modelPath, const std::optional<LLMConfig>& config) {
78
- static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* modelPath */, jni::alias_ref<JLLMConfig> /* config */)>("loadModel");
79
- method(_javaPart, jni::make_jstring(modelPath), config.has_value() ? JLLMConfig::fromCpp(config.value()) : nullptr);
80
+ std::shared_ptr<Promise<void>> JHybridLiteRTLMSpec::loadModel(const std::string& modelPath, const std::optional<LLMConfig>& config) {
81
+ static const auto method = javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>(jni::alias_ref<jni::JString> /* modelPath */, jni::alias_ref<JLLMConfig> /* config */)>("loadModel");
82
+ auto __result = method(_javaPart, jni::make_jstring(modelPath), config.has_value() ? JLLMConfig::fromCpp(config.value()) : nullptr);
83
+ return [&]() {
84
+ auto __promise = Promise<void>::create();
85
+ __result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& /* unit */) {
86
+ __promise->resolve();
87
+ });
88
+ __result->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) {
89
+ jni::JniException __jniError(__throwable);
90
+ __promise->reject(std::make_exception_ptr(__jniError));
91
+ });
92
+ return __promise;
93
+ }();
80
94
  }
81
- std::string JHybridLiteRTLMSpec::sendMessage(const std::string& message) {
82
- static const auto method = javaClassStatic()->getMethod<jni::local_ref<jni::JString>(jni::alias_ref<jni::JString> /* message */)>("sendMessage");
95
+ std::shared_ptr<Promise<std::string>> JHybridLiteRTLMSpec::sendMessage(const std::string& message) {
96
+ static const auto method = javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>(jni::alias_ref<jni::JString> /* message */)>("sendMessage");
83
97
  auto __result = method(_javaPart, jni::make_jstring(message));
84
- return __result->toStdString();
98
+ return [&]() {
99
+ auto __promise = Promise<std::string>::create();
100
+ __result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& __boxedResult) {
101
+ auto __result = jni::static_ref_cast<jni::JString>(__boxedResult);
102
+ __promise->resolve(__result->toStdString());
103
+ });
104
+ __result->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) {
105
+ jni::JniException __jniError(__throwable);
106
+ __promise->reject(std::make_exception_ptr(__jniError));
107
+ });
108
+ return __promise;
109
+ }();
85
110
  }
86
- std::string JHybridLiteRTLMSpec::sendMessageWithImage(const std::string& message, const std::string& imagePath) {
87
- static const auto method = javaClassStatic()->getMethod<jni::local_ref<jni::JString>(jni::alias_ref<jni::JString> /* message */, jni::alias_ref<jni::JString> /* imagePath */)>("sendMessageWithImage");
111
+ std::shared_ptr<Promise<std::string>> JHybridLiteRTLMSpec::sendMessageWithImage(const std::string& message, const std::string& imagePath) {
112
+ static const auto method = javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>(jni::alias_ref<jni::JString> /* message */, jni::alias_ref<jni::JString> /* imagePath */)>("sendMessageWithImage");
88
113
  auto __result = method(_javaPart, jni::make_jstring(message), jni::make_jstring(imagePath));
89
- return __result->toStdString();
114
+ return [&]() {
115
+ auto __promise = Promise<std::string>::create();
116
+ __result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& __boxedResult) {
117
+ auto __result = jni::static_ref_cast<jni::JString>(__boxedResult);
118
+ __promise->resolve(__result->toStdString());
119
+ });
120
+ __result->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) {
121
+ jni::JniException __jniError(__throwable);
122
+ __promise->reject(std::make_exception_ptr(__jniError));
123
+ });
124
+ return __promise;
125
+ }();
90
126
  }
91
- std::string JHybridLiteRTLMSpec::sendMessageWithAudio(const std::string& message, const std::string& audioPath) {
92
- static const auto method = javaClassStatic()->getMethod<jni::local_ref<jni::JString>(jni::alias_ref<jni::JString> /* message */, jni::alias_ref<jni::JString> /* audioPath */)>("sendMessageWithAudio");
127
+ std::shared_ptr<Promise<std::string>> JHybridLiteRTLMSpec::sendMessageWithAudio(const std::string& message, const std::string& audioPath) {
128
+ static const auto method = javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>(jni::alias_ref<jni::JString> /* message */, jni::alias_ref<jni::JString> /* audioPath */)>("sendMessageWithAudio");
93
129
  auto __result = method(_javaPart, jni::make_jstring(message), jni::make_jstring(audioPath));
94
- return __result->toStdString();
130
+ return [&]() {
131
+ auto __promise = Promise<std::string>::create();
132
+ __result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& __boxedResult) {
133
+ auto __result = jni::static_ref_cast<jni::JString>(__boxedResult);
134
+ __promise->resolve(__result->toStdString());
135
+ });
136
+ __result->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) {
137
+ jni::JniException __jniError(__throwable);
138
+ __promise->reject(std::make_exception_ptr(__jniError));
139
+ });
140
+ return __promise;
141
+ }();
95
142
  }
96
143
  void JHybridLiteRTLMSpec::sendMessageAsync(const std::string& message, const std::function<void(const std::string& /* token */, bool /* done */)>& onToken) {
97
144
  static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* message */, jni::alias_ref<JFunc_void_std__string_bool::javaobject> /* onToken */)>("sendMessageAsync_cxx");
@@ -55,10 +55,10 @@ namespace margelo::nitro::litertlm {
55
55
 
56
56
  public:
57
57
  // Methods
58
- void loadModel(const std::string& modelPath, const std::optional<LLMConfig>& config) override;
59
- std::string sendMessage(const std::string& message) override;
60
- std::string sendMessageWithImage(const std::string& message, const std::string& imagePath) override;
61
- std::string sendMessageWithAudio(const std::string& message, const std::string& audioPath) override;
58
+ std::shared_ptr<Promise<void>> loadModel(const std::string& modelPath, const std::optional<LLMConfig>& config) override;
59
+ std::shared_ptr<Promise<std::string>> sendMessage(const std::string& message) override;
60
+ std::shared_ptr<Promise<std::string>> sendMessageWithImage(const std::string& message, const std::string& imagePath) override;
61
+ std::shared_ptr<Promise<std::string>> sendMessageWithAudio(const std::string& message, const std::string& audioPath) override;
62
62
  void sendMessageAsync(const std::string& message, const std::function<void(const std::string& /* token */, bool /* done */)>& onToken) override;
63
63
  std::vector<Message> getHistory() override;
64
64
  void resetConversation() override;
@@ -10,6 +10,7 @@ package com.margelo.nitro.dev.litert.litertlm
10
10
  import androidx.annotation.Keep
11
11
  import com.facebook.jni.HybridData
12
12
  import com.facebook.proguard.annotations.DoNotStrip
13
+ import com.margelo.nitro.core.Promise
13
14
  import com.margelo.nitro.core.HybridObject
14
15
 
15
16
  /**
@@ -47,19 +48,19 @@ abstract class HybridLiteRTLMSpec: HybridObject() {
47
48
  // Methods
48
49
  @DoNotStrip
49
50
  @Keep
50
- abstract fun loadModel(modelPath: String, config: LLMConfig?): Unit
51
+ abstract fun loadModel(modelPath: String, config: LLMConfig?): Promise<Unit>
51
52
 
52
53
  @DoNotStrip
53
54
  @Keep
54
- abstract fun sendMessage(message: String): String
55
+ abstract fun sendMessage(message: String): Promise<String>
55
56
 
56
57
  @DoNotStrip
57
58
  @Keep
58
- abstract fun sendMessageWithImage(message: String, imagePath: String): String
59
+ abstract fun sendMessageWithImage(message: String, imagePath: String): Promise<String>
59
60
 
60
61
  @DoNotStrip
61
62
  @Keep
62
- abstract fun sendMessageWithAudio(message: String, audioPath: String): String
63
+ abstract fun sendMessageWithAudio(message: String, audioPath: String): Promise<String>
63
64
 
64
65
  abstract fun sendMessageAsync(message: String, onToken: (token: String, done: Boolean) -> Unit): Unit
65
66
 
@@ -20,6 +20,7 @@ namespace margelo::nitro::litertlm { struct Message; }
20
20
  // Forward declaration of `GenerationStats` to properly resolve imports.
21
21
  namespace margelo::nitro::litertlm { struct GenerationStats; }
22
22
 
23
+ #include <NitroModules/Promise.hpp>
23
24
  #include <string>
24
25
  #include "LLMConfig.hpp"
25
26
  #include <optional>
@@ -59,10 +60,10 @@ namespace margelo::nitro::litertlm {
59
60
 
60
61
  public:
61
62
  // Methods
62
- virtual void loadModel(const std::string& modelPath, const std::optional<LLMConfig>& config) = 0;
63
- virtual std::string sendMessage(const std::string& message) = 0;
64
- virtual std::string sendMessageWithImage(const std::string& message, const std::string& imagePath) = 0;
65
- virtual std::string sendMessageWithAudio(const std::string& message, const std::string& audioPath) = 0;
63
+ virtual std::shared_ptr<Promise<void>> loadModel(const std::string& modelPath, const std::optional<LLMConfig>& config) = 0;
64
+ virtual std::shared_ptr<Promise<std::string>> sendMessage(const std::string& message) = 0;
65
+ virtual std::shared_ptr<Promise<std::string>> sendMessageWithImage(const std::string& message, const std::string& imagePath) = 0;
66
+ virtual std::shared_ptr<Promise<std::string>> sendMessageWithAudio(const std::string& message, const std::string& audioPath) = 0;
66
67
  virtual void sendMessageAsync(const std::string& message, const std::function<void(const std::string& /* token */, bool /* done */)>& onToken) = 0;
67
68
  virtual std::vector<Message> getHistory() = 0;
68
69
  virtual void resetConversation() = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-litert-lm",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "High-performance LLM inference for React Native using LiteRT-LM. Optimized for Gemma 3n and other on-device language models.",
5
5
  "license": "MIT",
6
6
  "author": "Hugh Chen (https://github.com/hung-yueh)",
@@ -118,18 +118,17 @@ export interface LiteRTLM extends HybridObject<{
118
118
  }> {
119
119
  /**
120
120
  * Load a .litertlm model file.
121
- * @param modelPath Absolute path to the .litertlm file.
122
121
  * @param config Optional configuration for backend and sampling.
123
122
  * @throws Error if the model cannot be loaded.
124
123
  */
125
- loadModel(modelPath: string, config?: LLMConfig): void;
124
+ loadModel(modelPath: string, config?: LLMConfig): Promise<void>;
126
125
 
127
126
  /**
128
127
  * Send a text message and get the complete response (blocking).
129
128
  * @param message User message text.
130
129
  * @returns The model's response text.
131
130
  */
132
- sendMessage(message: string): string;
131
+ sendMessage(message: string): Promise<string>;
133
132
 
134
133
  /**
135
134
  * Send a text message with an image (multimodal).
@@ -137,7 +136,7 @@ export interface LiteRTLM extends HybridObject<{
137
136
  * @param imagePath Absolute path to an image file.
138
137
  * @returns The model's response text.
139
138
  */
140
- sendMessageWithImage(message: string, imagePath: string): string;
139
+ sendMessageWithImage(message: string, imagePath: string): Promise<string>;
141
140
 
142
141
  /**
143
142
  * Send a text message with audio (multimodal).
@@ -145,7 +144,7 @@ export interface LiteRTLM extends HybridObject<{
145
144
  * @param audioPath Absolute path to an audio file (WAV).
146
145
  * @returns The model's response text.
147
146
  */
148
- sendMessageWithAudio(message: string, audioPath: string): string;
147
+ sendMessageWithAudio(message: string, audioPath: string): Promise<string>;
149
148
 
150
149
  /**
151
150
  * Send a message with streaming response.