@dr33m/react-native-litert-lm 0.5.4 → 0.5.6

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.
@@ -575,7 +575,10 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
575
575
  return lastStats
576
576
  }
577
577
 
578
- override fun getMemoryUsage(): MemoryUsage {
578
+ /** Shared by the public [getMemoryUsage] override and the pre-flight guard
579
+ * in [execute] — both need the same real, OS-level reading, not an
580
+ * estimate. */
581
+ private fun currentMemoryUsage(): MemoryUsage {
579
582
  // Native heap: allocated bytes from Debug APIs (most accurate for native allocations)
580
583
  val nativeHeapBytes = Debug.getNativeHeapAllocatedSize().toDouble()
581
584
 
@@ -619,6 +622,8 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
619
622
  )
620
623
  }
621
624
 
625
+ override fun getMemoryUsage(): MemoryUsage = currentMemoryUsage()
626
+
622
627
  override fun checkModelCapabilities(modelPath: String): ModelCapabilities {
623
628
  var supportsSpeculativeDecoding = false
624
629
  try {
@@ -784,6 +789,16 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
784
789
 
785
790
  return Promise.parallel {
786
791
  ensureLoaded()
792
+
793
+ // Refuse rather than risk it: growing the KV-cache under real memory
794
+ // pressure can fail at the native engine level in a way that never
795
+ // surfaces as a catchable Kotlin exception — it takes the whole
796
+ // process down. A plain RuntimeException here is a normal rejected
797
+ // Promise for callers, same contract as any other execute() failure.
798
+ if (currentMemoryUsage().isLowMemory) {
799
+ throw RuntimeException("LiteRTLM: Device memory is critically low; refusing to continue generation.")
800
+ }
801
+
787
802
  // Clear any previous tool calls before new inference
788
803
  pendingToolCalls.clear()
789
804
 
@@ -80,6 +80,18 @@ extension HybridLiteRTLM {
80
80
  return
81
81
  }
82
82
 
83
+ // Refuse rather than risk it: growing the KV-cache under real memory
84
+ // pressure can fail inside the underlying C engine in a way that
85
+ // never surfaces as a catchable Swift error — it takes the whole
86
+ // process down (Jetsam or a hard native failure). An NSError here
87
+ // is a normal rejected Promise for callers, same contract as any
88
+ // other execute() failure.
89
+ if Self.currentMemoryUsage().isLowMemory {
90
+ promise.reject(withError: NSError(domain: "LiteRTLM", code: 507,
91
+ userInfo: [NSLocalizedDescriptionKey: "LiteRTLM: Device memory is critically low; refusing to continue generation."]))
92
+ return
93
+ }
94
+
83
95
  let payload: (json: String, tempFiles: [String])
84
96
  do { payload = try self.buildExecutePayload(preprocessed) }
85
97
  catch { promise.reject(withError: error); return }
@@ -111,9 +111,18 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
111
111
  }
112
112
 
113
113
  public func getMemoryUsage() throws -> MemoryUsage {
114
+ return Self.currentMemoryUsage()
115
+ }
116
+
117
+ /// Shared by the public `getMemoryUsage()` override and the pre-flight
118
+ /// guard in `HybridLiteRTLM+Execute.swift` — both need the same real,
119
+ /// OS-level reading, not an estimate. `static` since it needs no
120
+ /// instance state, which also makes it trivially callable from the
121
+ /// extension file.
122
+ static func currentMemoryUsage() -> MemoryUsage {
114
123
  var residentBytes: Double = 0.0
115
124
  var nativeHeapBytes: Double = 0.0
116
-
125
+
117
126
  // Retrieve process resident set size (RSS) via Mach basic task info
118
127
  var info = mach_task_basic_info()
119
128
  var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size / MemoryLayout<integer_t>.size)
@@ -122,18 +131,18 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
122
131
  task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
123
132
  }
124
133
  }
125
-
134
+
126
135
  if kerr == KERN_SUCCESS {
127
136
  residentBytes = Double(info.resident_size)
128
137
  nativeHeapBytes = Double(info.resident_size)
129
138
  }
130
-
139
+
131
140
  // os_proc_available_memory reports actual headroom available before Jetsam termination (iOS 13+)
132
141
  let availableBytes = Double(os_proc_available_memory())
133
-
142
+
134
143
  // Flag memory warning at ~200MB remaining headroom
135
144
  let isLowMemory = availableBytes < 200.0 * 1024.0 * 1024.0
136
-
145
+
137
146
  return MemoryUsage(
138
147
  nativeHeapBytes: nativeHeapBytes,
139
148
  residentBytes: residentBytes,
package/lib/index.d.ts CHANGED
@@ -84,6 +84,23 @@ export type ModelId = (typeof Models)[keyof typeof Models];
84
84
  * ```
85
85
  */
86
86
  export declare function getRecommendedBackend(): Backend;
87
+ /**
88
+ * Check whether the native LiteRT-LM module loaded successfully on this
89
+ * device. Some devices (unsupported ABI, missing native libs) can't load
90
+ * the native module at all — call this before showing any on-device AI UI
91
+ * so those devices degrade gracefully instead of throwing when a model is
92
+ * loaded. Result is cached after the first call.
93
+ *
94
+ * @returns true if native inference is available on this device
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * if (!isNativeAvailable()) {
99
+ * // hide/disable on-device AI features
100
+ * }
101
+ * ```
102
+ */
103
+ export declare function isNativeAvailable(): boolean;
87
104
  /**
88
105
  * Check if a backend configuration is supported on the current platform.
89
106
  * Returns a warning message if the configuration may have issues.
package/lib/index.js CHANGED
@@ -16,8 +16,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.GEMMA_4_E4B_IT = exports.GEMMA_4_E2B_IT = exports.GEMMA_3N_E2B_IT_INT4 = exports.Models = exports.createLLM = exports.ModelRegistry = exports.createNativeBuffer = exports.createMemoryTracker = void 0;
18
18
  exports.getRecommendedBackend = getRecommendedBackend;
19
+ exports.isNativeAvailable = isNativeAvailable;
19
20
  exports.checkBackendSupport = checkBackendSupport;
20
21
  exports.checkMultimodalSupport = checkMultimodalSupport;
22
+ const react_native_nitro_modules_1 = require("react-native-nitro-modules");
21
23
  const react_native_1 = require("react-native");
22
24
  var memoryTracker_1 = require("./memoryTracker");
23
25
  Object.defineProperty(exports, "createMemoryTracker", { enumerable: true, get: function () { return memoryTracker_1.createMemoryTracker; } });
@@ -103,6 +105,35 @@ function getRecommendedBackend() {
103
105
  // GPU is faster but may fail on some models/devices.
104
106
  return "cpu";
105
107
  }
108
+ let nativeAvailable = null;
109
+ /**
110
+ * Check whether the native LiteRT-LM module loaded successfully on this
111
+ * device. Some devices (unsupported ABI, missing native libs) can't load
112
+ * the native module at all — call this before showing any on-device AI UI
113
+ * so those devices degrade gracefully instead of throwing when a model is
114
+ * loaded. Result is cached after the first call.
115
+ *
116
+ * @returns true if native inference is available on this device
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * if (!isNativeAvailable()) {
121
+ * // hide/disable on-device AI features
122
+ * }
123
+ * ```
124
+ */
125
+ function isNativeAvailable() {
126
+ if (nativeAvailable === null) {
127
+ try {
128
+ react_native_nitro_modules_1.NitroModules.createHybridObject("ModelStore");
129
+ nativeAvailable = true;
130
+ }
131
+ catch {
132
+ nativeAvailable = false;
133
+ }
134
+ }
135
+ return nativeAvailable;
136
+ }
106
137
  /**
107
138
  * Check if a backend configuration is supported on the current platform.
108
139
  * Returns a warning message if the configuration may have issues.
@@ -3,7 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ModelRegistry = void 0;
4
4
  const react_native_nitro_modules_1 = require("react-native-nitro-modules");
5
5
  const modelPath_1 = require("./modelPath");
6
- const nativeStore = react_native_nitro_modules_1.NitroModules.createHybridObject("ModelStore");
6
+ let nativeStore = null;
7
+ // Lazily created — a module-scope createHybridObject() would throw at import
8
+ // time on devices where the native lib failed to load, crashing app startup
9
+ // before any capability check can run.
10
+ function getStore() {
11
+ if (!nativeStore) {
12
+ nativeStore = react_native_nitro_modules_1.NitroModules.createHybridObject("ModelStore");
13
+ }
14
+ return nativeStore;
15
+ }
7
16
  /**
8
17
  * High-performance Model Registry for react-native-litert-lm.
9
18
  *
@@ -19,7 +28,7 @@ exports.ModelRegistry = {
19
28
  * @returns true if cached and has size > 0
20
29
  */
21
30
  isCached(pathOrUrl) {
22
- return nativeStore.isCached((0, modelPath_1.resolveModelFileName)(pathOrUrl));
31
+ return getStore().isCached((0, modelPath_1.resolveModelFileName)(pathOrUrl));
23
32
  },
24
33
  /**
25
34
  * Get the absolute local path of a cached model.
@@ -29,7 +38,7 @@ exports.ModelRegistry = {
29
38
  * @returns The absolute local path
30
39
  */
31
40
  getFilePath(pathOrUrl) {
32
- return nativeStore.getFilePath((0, modelPath_1.resolveModelFileName)(pathOrUrl));
41
+ return getStore().getFilePath((0, modelPath_1.resolveModelFileName)(pathOrUrl));
33
42
  },
34
43
  /**
35
44
  * List all locally cached model files.
@@ -37,7 +46,7 @@ exports.ModelRegistry = {
37
46
  * @returns Array of ModelFile descriptors containing path, size, and mod time
38
47
  */
39
48
  listCachedFiles() {
40
- return nativeStore.listCachedFiles();
49
+ return getStore().listCachedFiles();
41
50
  },
42
51
  /**
43
52
  * Delete a cached model file.
@@ -46,7 +55,7 @@ exports.ModelRegistry = {
46
55
  * @param pathOrUrl Filename, local path, or download URL to delete
47
56
  */
48
57
  deleteFile(pathOrUrl) {
49
- nativeStore.deleteFile((0, modelPath_1.resolveModelFileName)(pathOrUrl));
58
+ getStore().deleteFile((0, modelPath_1.resolveModelFileName)(pathOrUrl));
50
59
  },
51
60
  /**
52
61
  * Resolve a model path or URL.
@@ -73,7 +82,7 @@ exports.ModelRegistry = {
73
82
  throw new Error(`Invalid model URL: ${cleanPath}`);
74
83
  }
75
84
  const headersJson = options?.headers ? JSON.stringify(options.headers) : "{}";
76
- return nativeStore.downloadFile(cleanPath, fileName, headersJson, (progress) => {
85
+ return getStore().downloadFile(cleanPath, fileName, headersJson, (progress) => {
77
86
  options?.onProgress?.(progress);
78
87
  });
79
88
  }
@@ -10,7 +10,6 @@ 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 dalvik.annotation.optimization.FastNative
14
13
 
15
14
 
16
15
  /**
@@ -59,7 +58,6 @@ class Func_void_double_cxx: Func_void_double {
59
58
  override fun invoke(progress: Double): Unit
60
59
  = invoke_cxx(progress)
61
60
 
62
- @FastNative
63
61
  private external fun invoke_cxx(progress: Double): Unit
64
62
  }
65
63
 
@@ -10,7 +10,6 @@ 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 dalvik.annotation.optimization.FastNative
14
13
 
15
14
 
16
15
  /**
@@ -59,7 +58,6 @@ class Func_void_std__string_bool_cxx: Func_void_std__string_bool {
59
58
  override fun invoke(token: String, done: Boolean): Unit
60
59
  = invoke_cxx(token,done)
61
60
 
62
- @FastNative
63
61
  private external fun invoke_cxx(token: String, done: Boolean): Unit
64
62
  }
65
63
 
@@ -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 dalvik.annotation.optimization.FastNative
13
14
  import com.margelo.nitro.core.Promise
14
15
  import com.margelo.nitro.core.HybridObject
15
16
 
@@ -157,6 +158,7 @@ abstract class HybridLiteRTLMSpec: HybridObject() {
157
158
  @Keep
158
159
  protected open class CxxPart(javaPart: HybridLiteRTLMSpec): HybridObject.CxxPart(javaPart) {
159
160
  // C++ JHybridLiteRTLMSpec::CxxPart::initHybrid(...)
161
+ @FastNative
160
162
  external override fun initHybrid(): HybridData
161
163
  }
162
164
  override fun createCxxPart(): CxxPart {
@@ -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 dalvik.annotation.optimization.FastNative
13
14
  import com.margelo.nitro.core.Promise
14
15
  import com.margelo.nitro.core.HybridObject
15
16
 
@@ -64,6 +65,7 @@ abstract class HybridModelStoreSpec: HybridObject() {
64
65
  @Keep
65
66
  protected open class CxxPart(javaPart: HybridModelStoreSpec): HybridObject.CxxPart(javaPart) {
66
67
  // C++ JHybridModelStoreSpec::CxxPart::initHybrid(...)
68
+ @FastNative
67
69
  external override fun initHybrid(): HybridData
68
70
  }
69
71
  override fun createCxxPart(): CxxPart {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dr33m/react-native-litert-lm",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "litertLm": {
5
5
  "version": "0.12.0",
6
6
  "androidMavenVersion": "0.12.0",
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  Role,
9
9
  GenerationStats,
10
10
  MemoryUsage,
11
+ ModelStore,
11
12
  } from "./specs/LiteRTLM.nitro";
12
13
 
13
14
  export type {
@@ -125,6 +126,36 @@ export function getRecommendedBackend(): Backend {
125
126
  return "cpu";
126
127
  }
127
128
 
129
+ let nativeAvailable: boolean | null = null;
130
+
131
+ /**
132
+ * Check whether the native LiteRT-LM module loaded successfully on this
133
+ * device. Some devices (unsupported ABI, missing native libs) can't load
134
+ * the native module at all — call this before showing any on-device AI UI
135
+ * so those devices degrade gracefully instead of throwing when a model is
136
+ * loaded. Result is cached after the first call.
137
+ *
138
+ * @returns true if native inference is available on this device
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * if (!isNativeAvailable()) {
143
+ * // hide/disable on-device AI features
144
+ * }
145
+ * ```
146
+ */
147
+ export function isNativeAvailable(): boolean {
148
+ if (nativeAvailable === null) {
149
+ try {
150
+ NitroModules.createHybridObject<ModelStore>("ModelStore");
151
+ nativeAvailable = true;
152
+ } catch {
153
+ nativeAvailable = false;
154
+ }
155
+ }
156
+ return nativeAvailable;
157
+ }
158
+
128
159
  /**
129
160
  * Check if a backend configuration is supported on the current platform.
130
161
  * Returns a warning message if the configuration may have issues.
@@ -9,7 +9,17 @@ export interface ModelDownloadOptions {
9
9
  onProgress?: (progress: number) => void;
10
10
  }
11
11
 
12
- const nativeStore = NitroModules.createHybridObject<ModelStore>("ModelStore");
12
+ let nativeStore: ModelStore | null = null;
13
+
14
+ // Lazily created — a module-scope createHybridObject() would throw at import
15
+ // time on devices where the native lib failed to load, crashing app startup
16
+ // before any capability check can run.
17
+ function getStore(): ModelStore {
18
+ if (!nativeStore) {
19
+ nativeStore = NitroModules.createHybridObject<ModelStore>("ModelStore");
20
+ }
21
+ return nativeStore;
22
+ }
13
23
 
14
24
  /**
15
25
  * High-performance Model Registry for react-native-litert-lm.
@@ -26,7 +36,7 @@ export const ModelRegistry = {
26
36
  * @returns true if cached and has size > 0
27
37
  */
28
38
  isCached(pathOrUrl: string): boolean {
29
- return nativeStore.isCached(resolveModelFileName(pathOrUrl));
39
+ return getStore().isCached(resolveModelFileName(pathOrUrl));
30
40
  },
31
41
 
32
42
  /**
@@ -37,7 +47,7 @@ export const ModelRegistry = {
37
47
  * @returns The absolute local path
38
48
  */
39
49
  getFilePath(pathOrUrl: string): string {
40
- return nativeStore.getFilePath(resolveModelFileName(pathOrUrl));
50
+ return getStore().getFilePath(resolveModelFileName(pathOrUrl));
41
51
  },
42
52
 
43
53
  /**
@@ -46,7 +56,7 @@ export const ModelRegistry = {
46
56
  * @returns Array of ModelFile descriptors containing path, size, and mod time
47
57
  */
48
58
  listCachedFiles(): ModelFile[] {
49
- return nativeStore.listCachedFiles();
59
+ return getStore().listCachedFiles();
50
60
  },
51
61
 
52
62
  /**
@@ -56,7 +66,7 @@ export const ModelRegistry = {
56
66
  * @param pathOrUrl Filename, local path, or download URL to delete
57
67
  */
58
68
  deleteFile(pathOrUrl: string): void {
59
- nativeStore.deleteFile(resolveModelFileName(pathOrUrl));
69
+ getStore().deleteFile(resolveModelFileName(pathOrUrl));
60
70
  },
61
71
 
62
72
  /**
@@ -89,7 +99,7 @@ export const ModelRegistry = {
89
99
 
90
100
  const headersJson = options?.headers ? JSON.stringify(options.headers) : "{}";
91
101
 
92
- return nativeStore.downloadFile(
102
+ return getStore().downloadFile(
93
103
  cleanPath,
94
104
  fileName,
95
105
  headersJson,