react-native-litert-lm 0.3.2 → 0.3.3

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.
@@ -109,6 +109,34 @@ std::string HybridLiteRTLM::buildAudioMessageJson(const std::string& text, const
109
109
  "]}";
110
110
  }
111
111
 
112
+ /**
113
+ * Strip Gemma / LiteRT-LM control tokens from model output.
114
+ * The iOS C API returns raw model text including stop/turn markers
115
+ * that the Android Kotlin SDK strips automatically.
116
+ */
117
+ static std::string stripControlTokens(const std::string& text) {
118
+ static const char* tokens[] = {
119
+ "<end_of_turn>",
120
+ "<start_of_turn>model",
121
+ "<start_of_turn>user",
122
+ "<start_of_turn>",
123
+ "<eos>",
124
+ };
125
+ std::string result = text;
126
+ for (auto* tok : tokens) {
127
+ std::string t(tok);
128
+ size_t pos;
129
+ while ((pos = result.find(t)) != std::string::npos) {
130
+ result.erase(pos, t.length());
131
+ }
132
+ }
133
+ // Trim leading/trailing whitespace
134
+ size_t start = result.find_first_not_of(" \t\n\r");
135
+ if (start == std::string::npos) return "";
136
+ size_t end = result.find_last_not_of(" \t\n\r");
137
+ return result.substr(start, end - start + 1);
138
+ }
139
+
112
140
  std::string HybridLiteRTLM::extractTextFromResponse(const std::string& jsonResponse) {
113
141
  // The C API response JSON is structured as:
114
142
  // {"role":"model","content":[{"type":"text","text":"..."}]}
@@ -141,7 +169,7 @@ std::string HybridLiteRTLM::extractTextFromResponse(const std::string& jsonRespo
141
169
  result += jsonResponse[i];
142
170
  }
143
171
  }
144
- return result;
172
+ return stripControlTokens(result);
145
173
  }
146
174
  }
147
175
 
@@ -164,11 +192,11 @@ std::string HybridLiteRTLM::extractTextFromResponse(const std::string& jsonRespo
164
192
  result += jsonResponse[i];
165
193
  }
166
194
  }
167
- return result;
195
+ return stripControlTokens(result);
168
196
  }
169
197
 
170
- // Fallback: return full response
171
- return jsonResponse;
198
+ // Fallback: return full response (still strip control tokens)
199
+ return stripControlTokens(jsonResponse);
172
200
  }
173
201
 
174
202
  // =============================================================================
@@ -284,7 +312,7 @@ void HybridLiteRTLM::loadModelInternal(
284
312
  modelPath.c_str(),
285
313
  backend,
286
314
  visionBackend,
287
- nullptr // audio executor not supported on iOS yet
315
+ "cpu" // audio executor: iOS XCFramework lacks compiled audio ops (INTERNAL ERROR at Invoke)
288
316
  );
289
317
  if (!settings) {
290
318
  return false;
@@ -476,9 +504,13 @@ void HybridLiteRTLM::streamCallbackFn(void* callback_data, const char* chunk,
476
504
 
477
505
  if (chunk) {
478
506
  std::string token(chunk);
479
- ctx->fullResponse += token;
507
+ // Filter out Gemma control tokens from streamed chunks
508
+ std::string cleaned = stripControlTokens(token);
509
+ ctx->fullResponse += cleaned;
480
510
  ctx->tokenCount++;
481
- ctx->onToken(token, false);
511
+ if (!cleaned.empty()) {
512
+ ctx->onToken(cleaned, false);
513
+ }
482
514
  }
483
515
  }
484
516
 
@@ -635,7 +667,12 @@ std::string HybridLiteRTLM::sendMessageWithAudioInternal(
635
667
  conversation_, msgJson.c_str(), nullptr);
636
668
 
637
669
  if (!response) {
638
- throw std::runtime_error("LiteRT-LM: sendMessageWithAudio failed");
670
+ std::string errMsg = "LiteRT-LM: sendMessageWithAudio failed";
671
+ const char* nativeErr = litert_lm_get_last_error();
672
+ if (nativeErr && nativeErr[0] != '\0') {
673
+ errMsg += ": " + std::string(nativeErr);
674
+ }
675
+ throw std::runtime_error(errMsg);
639
676
  }
640
677
 
641
678
  const char* responseStr = litert_lm_json_response_get_string(response);
package/lib/hooks.js CHANGED
@@ -51,6 +51,10 @@ function useModel(pathOrUrl, config) {
51
51
  enableMemoryTracking,
52
52
  maxMemorySnapshots,
53
53
  });
54
+ // Reset ready state — the new instance has no model loaded yet.
55
+ // This prevents stale isReady=true after Fast Refresh (which
56
+ // preserves useState but re-runs useEffect).
57
+ setIsReady(false);
54
58
  // Cleanup on unmount
55
59
  return () => {
56
60
  try {
package/lib/index.js CHANGED
@@ -145,7 +145,7 @@ function checkBackendSupport(backend) {
145
145
  */
146
146
  function checkMultimodalSupport() {
147
147
  if (react_native_1.Platform.OS === "ios") {
148
- return "Multimodal (image/audio) is experimental on iOS. Vision and audio executors may not be available in the current build.";
148
+ return "Multimodal (image/audio) is not available on iOS. The XCFramework lacks compiled vision and audio executor ops.";
149
149
  }
150
150
  return undefined;
151
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-litert-lm",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "litertLm": {
5
5
  "version": "0.10.1",
6
6
  "androidMavenVersion": "0.10.0",
package/src/hooks.ts CHANGED
@@ -108,6 +108,11 @@ export function useModel(
108
108
  maxMemorySnapshots,
109
109
  });
110
110
 
111
+ // Reset ready state — the new instance has no model loaded yet.
112
+ // This prevents stale isReady=true after Fast Refresh (which
113
+ // preserves useState but re-runs useEffect).
114
+ setIsReady(false);
115
+
111
116
  // Cleanup on unmount
112
117
  return () => {
113
118
  try {
package/src/index.ts CHANGED
@@ -163,7 +163,7 @@ export function checkBackendSupport(backend: Backend): string | undefined {
163
163
  */
164
164
  export function checkMultimodalSupport(): string | undefined {
165
165
  if (Platform.OS === "ios") {
166
- return "Multimodal (image/audio) is experimental on iOS. Vision and audio executors may not be available in the current build.";
166
+ return "Multimodal (image/audio) is not available on iOS. The XCFramework lacks compiled vision and audio executor ops.";
167
167
  }
168
168
  return undefined;
169
169
  }