@dr33m/react-native-litert-lm 0.6.1 → 0.6.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.
@@ -728,6 +728,9 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
728
728
  * held and believed in.
729
729
  */
730
730
  fun releaseUnderMemoryPressure() {
731
+ // Nothing loaded is nothing to free, and saying otherwise turns one
732
+ // emergency into a page of identical warnings.
733
+ if (engine == null) return
731
734
  Log.w(TAG, "Releasing engine under memory pressure; instance stays reloadable")
732
735
  cleanupInternal()
733
736
  }
@@ -736,6 +739,9 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
736
739
  Log.d(TAG, "Closing resources")
737
740
  isClosed = true
738
741
  cleanupInternal()
742
+ // A closed instance can never load again, so it has no business being
743
+ // woken by the next memory emergency.
744
+ LiteRTLMRegistry.unregister(this)
739
745
  }
740
746
 
741
747
  private fun cleanupInternal() {
@@ -743,14 +749,36 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
743
749
  // for it while holding the lock would block loadModel behind it.
744
750
  cancelInFlightGeneration()
745
751
  synchronized(initLock) {
752
+ /*
753
+ * Dropped first, closed second.
754
+ *
755
+ * These used to be nulled after their `close()` calls, inside one
756
+ * try. A close that threw — and closing a conversation that has
757
+ * just been cancelled is exactly when one does — skipped the rest
758
+ * of the block and left a retired engine still referenced. Nothing
759
+ * downstream could tell: `isReady()` reads `engine != null`, so the
760
+ * instance went on reporting itself loaded, and the next
761
+ * `resetConversation()` called `createConversation` on a closed
762
+ * engine and threw "Engine is not initialized" from deep in the SDK.
763
+ *
764
+ * Letting go of the references first makes that impossible: after
765
+ * this block the instance is unloaded whatever the native side did.
766
+ */
767
+ val retiredConversation = conversation
768
+ val retiredEngine = engine
769
+ conversation = null
770
+ engine = null
771
+ loadedModelPath = null
772
+
773
+ try {
774
+ retiredConversation?.close()
775
+ } catch (e: Exception) {
776
+ Log.w(TAG, "Error closing conversation: ${e.message}")
777
+ }
746
778
  try {
747
- conversation?.close()
748
- conversation = null
749
- engine?.close() // Direct call
750
- engine = null
751
- loadedModelPath = null
779
+ retiredEngine?.close()
752
780
  } catch (e: Exception) {
753
- Log.e(TAG, "Error closing resources", e)
781
+ Log.w(TAG, "Error closing engine: ${e.message}")
754
782
  }
755
783
  }
756
784
  }
@@ -819,7 +847,12 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
819
847
  // - Kotlin: Not yet available — track at https://github.com/google-ai-edge/LiteRT-LM
820
848
  //
821
849
  // Once the Kotlin SDK exposes this, wire it via ConversationConfig here.
822
- conversation = engine!!.createConversation(convConfig)
850
+ // Checked rather than forced: this runs from `resetConversation`, which
851
+ // JS calls on paths that do not first ask whether anything is loaded.
852
+ // A named error beats a null-pointer dereference from inside the SDK.
853
+ val activeEngine = engine
854
+ ?: throw RuntimeException("Cannot create a conversation: no model is loaded.")
855
+ conversation = activeEngine.createConversation(convConfig)
823
856
  }
824
857
 
825
858
 
@@ -960,7 +993,17 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
960
993
 
961
994
  val userMsg = LiteRTMessage.user(Contents.of(contents))
962
995
 
963
- val extraContext: Map<String, String> = if (enableThinking) mapOf("enable_thinking" to "true") else emptyMap()
996
+ // Always stated, never omitted.
997
+ //
998
+ // `enable_thinking` is a chat-template variable, not a Gemma
999
+ // feature, and templates test it as "defined and false". Qwen3's
1000
+ // is exactly that shape, so leaving the key out is not the same
1001
+ // as setting it to false: undefined falls through to the
1002
+ // template's own default, which for a reasoning model is to
1003
+ // reason. Omitting it meant thinking could be turned on but
1004
+ // never off, and the reasoning arrived inline in the answer.
1005
+ val extraContext: Map<String, String> =
1006
+ mapOf("enable_thinking" to enableThinking.toString())
964
1007
 
965
1008
  if (onToken != null) {
966
1009
  // ── Streaming path ────────────────────────────────────────────────
@@ -21,6 +21,23 @@ object LiteRTLMRegistry {
21
21
  }
22
22
  }
23
23
 
24
+ /**
25
+ * Drop a retired instance.
26
+ *
27
+ * The set holds weak keys, so a dead instance does leave eventually — but
28
+ * only once GC gets to it, and until then it is still iterated and still
29
+ * logged against. Every `loadModel()` mints a fresh HybridLiteRTLM, so a
30
+ * few reloads were enough for one memory emergency to report five engines
31
+ * released when only the last of them held anything. Closing is a definite
32
+ * end, so it is a better moment to forget an instance than a collection
33
+ * that may not have happened yet.
34
+ */
35
+ fun unregister(instance: HybridLiteRTLM) {
36
+ synchronized(instances) {
37
+ instances.remove(instance)
38
+ }
39
+ }
40
+
24
41
  /**
25
42
  * Whether a trim level is a real emergency worth dropping engines for.
26
43
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dr33m/react-native-litert-lm",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "litertLm": {
5
5
  "version": "0.15.0",
6
6
  "androidMavenVersion": "0.15.0",