react-native-litert-lm 0.3.6 → 0.4.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 (87) hide show
  1. package/README.md +207 -158
  2. package/android/build.gradle +12 -0
  3. package/android/src/main/AndroidManifest.xml +5 -0
  4. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +316 -63
  5. package/android/src/main/java/dev/litert/litertlm/LiteRTLMPackage.kt +19 -2
  6. package/android/src/test/java/com/margelo/nitro/core/Promise.kt +46 -0
  7. package/android/src/test/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMTest.kt +83 -0
  8. package/cpp/include/README.md +9 -11
  9. package/ios/HybridLiteRTLM.swift +1058 -0
  10. package/ios/Tests/HybridLiteRTLMTests.swift +67 -0
  11. package/lib/__mocks__/react-native-nitro-modules.d.ts +61 -0
  12. package/lib/__mocks__/react-native-nitro-modules.js +50 -0
  13. package/lib/__tests__/hooks.test.d.ts +1 -0
  14. package/lib/__tests__/hooks.test.js +124 -0
  15. package/lib/__tests__/memoryTracker.test.d.ts +1 -0
  16. package/lib/__tests__/memoryTracker.test.js +74 -0
  17. package/lib/__tests__/modelFactory.test.d.ts +1 -0
  18. package/lib/__tests__/modelFactory.test.js +52 -0
  19. package/lib/hooks.js +1 -1
  20. package/lib/index.d.ts +2 -4
  21. package/lib/index.js +12 -7
  22. package/lib/modelFactory.js +62 -63
  23. package/lib/specs/LiteRTLM.nitro.d.ts +71 -2
  24. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +62 -7
  25. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +3 -1
  26. package/nitrogen/generated/android/c++/JLLMConfig.hpp +40 -3
  27. package/nitrogen/generated/android/c++/JMultimodalPart.hpp +74 -0
  28. package/nitrogen/generated/android/c++/JPartType.hpp +61 -0
  29. package/nitrogen/generated/android/c++/JToolDefinition.hpp +65 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/GenerationStats.kt +23 -0
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +10 -2
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +46 -3
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MemoryUsage.kt +19 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Message.kt +15 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MultimodalPart.kt +66 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/PartType.kt +24 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolDefinition.kt +61 -0
  38. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.cpp +57 -1
  39. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.hpp +414 -3
  40. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Umbrella.hpp +41 -3
  41. package/nitrogen/generated/ios/LiteRTLMAutolinking.mm +4 -6
  42. package/nitrogen/generated/ios/LiteRTLMAutolinking.swift +10 -0
  43. package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.cpp +11 -0
  44. package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +224 -0
  45. package/nitrogen/generated/ios/swift/Backend.swift +44 -0
  46. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  47. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  48. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  49. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  50. package/nitrogen/generated/ios/swift/Func_void_std__string_bool.swift +46 -0
  51. package/nitrogen/generated/ios/swift/GenerationStats.swift +54 -0
  52. package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +69 -0
  53. package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +383 -0
  54. package/nitrogen/generated/ios/swift/LLMConfig.swift +203 -0
  55. package/nitrogen/generated/ios/swift/MemoryUsage.swift +44 -0
  56. package/nitrogen/generated/ios/swift/Message.swift +34 -0
  57. package/nitrogen/generated/ios/swift/MultimodalPart.swift +83 -0
  58. package/nitrogen/generated/ios/swift/PartType.swift +44 -0
  59. package/nitrogen/generated/ios/swift/Role.swift +44 -0
  60. package/nitrogen/generated/ios/swift/ToolDefinition.swift +39 -0
  61. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +2 -0
  62. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +7 -2
  63. package/nitrogen/generated/shared/c++/LLMConfig.hpp +22 -2
  64. package/nitrogen/generated/shared/c++/MultimodalPart.hpp +99 -0
  65. package/nitrogen/generated/shared/c++/PartType.hpp +80 -0
  66. package/nitrogen/generated/shared/c++/ToolDefinition.hpp +91 -0
  67. package/package.json +16 -8
  68. package/react-native-litert-lm.podspec +15 -19
  69. package/scripts/download-ios-frameworks.sh +14 -48
  70. package/scripts/postinstall.js +1 -2
  71. package/src/__mocks__/react-native-nitro-modules.ts +48 -0
  72. package/src/__tests__/hooks.test.ts +153 -0
  73. package/src/__tests__/memoryTracker.test.ts +87 -0
  74. package/src/__tests__/modelFactory.test.ts +68 -0
  75. package/src/hooks.ts +1 -1
  76. package/src/index.ts +12 -9
  77. package/src/modelFactory.ts +82 -80
  78. package/src/specs/LiteRTLM.nitro.ts +80 -2
  79. package/cpp/HybridLiteRTLM.cpp +0 -838
  80. package/cpp/HybridLiteRTLM.hpp +0 -167
  81. package/cpp/IOSDownloadHelper.h +0 -24
  82. package/ios/IOSDownloadHelper.mm +0 -129
  83. package/scripts/build-ios-engine.sh +0 -302
  84. package/scripts/stubs/cxx_bridge_stubs.cc +0 -224
  85. package/scripts/stubs/gemma_model_constraint_provider.cc +0 -46
  86. package/scripts/stubs/llguidance_stubs.c +0 -101
  87. package/src/templates.ts +0 -105
@@ -0,0 +1,67 @@
1
+ import XCTest
2
+ @testable import LiteRTLM
3
+
4
+ class HybridLiteRTLMTests: XCTestCase {
5
+ var bridge: HybridLiteRTLM!
6
+
7
+ override func setUp() {
8
+ super.setUp()
9
+ bridge = HybridLiteRTLM()
10
+ }
11
+
12
+ override func tearDown() {
13
+ try? bridge.close()
14
+ bridge = nil
15
+ super.tearDown()
16
+ }
17
+
18
+ func testPathTraversalRejection() async throws {
19
+ let traversals = ["../../etc/passwd", "/absolute/path/file", "subdir\\..\\file", "..", "../", "..\\"]
20
+ for traversal in traversals {
21
+ do {
22
+ let promise = try bridge.deleteModel(fileName: traversal)
23
+ _ = try await promise.await()
24
+ XCTFail("Should have failed for traversal: \(traversal)")
25
+ } catch {
26
+ let nsError = error as NSError
27
+ XCTAssertEqual(nsError.domain, "LiteRTLM")
28
+ XCTAssertEqual(nsError.code, 400)
29
+ XCTAssertTrue(nsError.localizedDescription.contains("path traversal") || nsError.localizedDescription.contains("directory separators"))
30
+ }
31
+ }
32
+ }
33
+
34
+ func testNonHTTPSDownloadRejection() async throws {
35
+ do {
36
+ let promise = try bridge.downloadModel(url: "http://insecure-domain.com/model.bin", fileName: "model.bin", onProgress: nil)
37
+ _ = try await promise.await()
38
+ XCTFail("Should have blocked insecure HTTP downloads")
39
+ } catch {
40
+ let nsError = error as NSError
41
+ XCTAssertEqual(nsError.domain, "LiteRTLM")
42
+ XCTAssertEqual(nsError.code, 400)
43
+ XCTAssertTrue(nsError.localizedDescription.contains("HTTPS is required"))
44
+ }
45
+ }
46
+
47
+ func testMemoryTelemetry() {
48
+ XCTAssertNoThrow(try bridge.getMemoryUsage())
49
+ if let mem = try? bridge.getMemoryUsage() {
50
+ XCTAssertGreaterThanOrEqual(mem.nativeHeapBytes, 0.0)
51
+ XCTAssertGreaterThanOrEqual(mem.residentBytes, 0.0)
52
+ XCTAssertGreaterThanOrEqual(mem.availableMemoryBytes, 0.0)
53
+ }
54
+ }
55
+
56
+ func testInitialStats() {
57
+ XCTAssertNoThrow(try bridge.getStats())
58
+ if let stats = try? bridge.getStats() {
59
+ XCTAssertEqual(stats.promptTokens, 0.0)
60
+ XCTAssertEqual(stats.completionTokens, 0.0)
61
+ XCTAssertEqual(stats.totalTokens, 0.0)
62
+ XCTAssertEqual(stats.timeToFirstToken, 0.0)
63
+ XCTAssertEqual(stats.totalTime, 0.0)
64
+ XCTAssertEqual(stats.tokensPerSecond, 0.0)
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,61 @@
1
+ export declare const mockLiteRTLM: {
2
+ isReady: jest.Mock<boolean, [], any>;
3
+ loadModel: jest.Mock<any, any, any>;
4
+ sendMessage: jest.Mock<any, any, any>;
5
+ sendMessageWithImage: jest.Mock<any, any, any>;
6
+ downloadModel: jest.Mock<Promise<string>, [url: any, fileName: any, onProgress: any], any>;
7
+ deleteModel: jest.Mock<any, any, any>;
8
+ sendMessageWithAudio: jest.Mock<any, any, any>;
9
+ sendMultimodalMessage: jest.Mock<any, any, any>;
10
+ sendMessageAsync: jest.Mock<Promise<void>, [msg: any, onToken: any], any>;
11
+ getHistory: jest.Mock<never[], [], any>;
12
+ resetConversation: jest.Mock<any, any, any>;
13
+ getStats: jest.Mock<{
14
+ promptTokens: number;
15
+ completionTokens: number;
16
+ totalTokens: number;
17
+ timeToFirstToken: number;
18
+ totalTime: number;
19
+ tokensPerSecond: number;
20
+ }, [], any>;
21
+ countTokens: jest.Mock<number, [], any>;
22
+ getMemoryUsage: jest.Mock<{
23
+ nativeHeapBytes: number;
24
+ residentBytes: number;
25
+ availableMemoryBytes: number;
26
+ isLowMemory: boolean;
27
+ }, [], any>;
28
+ close: jest.Mock<any, any, any>;
29
+ };
30
+ export declare const NitroModules: {
31
+ createHybridObject: jest.Mock<{
32
+ isReady: jest.Mock<boolean, [], any>;
33
+ loadModel: jest.Mock<any, any, any>;
34
+ sendMessage: jest.Mock<any, any, any>;
35
+ sendMessageWithImage: jest.Mock<any, any, any>;
36
+ downloadModel: jest.Mock<Promise<string>, [url: any, fileName: any, onProgress: any], any>;
37
+ deleteModel: jest.Mock<any, any, any>;
38
+ sendMessageWithAudio: jest.Mock<any, any, any>;
39
+ sendMultimodalMessage: jest.Mock<any, any, any>;
40
+ sendMessageAsync: jest.Mock<Promise<void>, [msg: any, onToken: any], any>;
41
+ getHistory: jest.Mock<never[], [], any>;
42
+ resetConversation: jest.Mock<any, any, any>;
43
+ getStats: jest.Mock<{
44
+ promptTokens: number;
45
+ completionTokens: number;
46
+ totalTokens: number;
47
+ timeToFirstToken: number;
48
+ totalTime: number;
49
+ tokensPerSecond: number;
50
+ }, [], any>;
51
+ countTokens: jest.Mock<number, [], any>;
52
+ getMemoryUsage: jest.Mock<{
53
+ nativeHeapBytes: number;
54
+ residentBytes: number;
55
+ availableMemoryBytes: number;
56
+ isLowMemory: boolean;
57
+ }, [], any>;
58
+ close: jest.Mock<any, any, any>;
59
+ }, [name: string], any>;
60
+ createNativeArrayBuffer: jest.Mock<ArrayBuffer, [size: number], any>;
61
+ };
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NitroModules = exports.mockLiteRTLM = void 0;
4
+ exports.mockLiteRTLM = {
5
+ isReady: jest.fn(() => false),
6
+ loadModel: jest.fn().mockResolvedValue(undefined),
7
+ sendMessage: jest.fn().mockResolvedValue("Mock response"),
8
+ sendMessageWithImage: jest.fn().mockResolvedValue("Mock vision response"),
9
+ downloadModel: jest.fn(async (url, fileName, onProgress) => {
10
+ onProgress?.(1.0);
11
+ return "/mock/path/model.litertlm";
12
+ }),
13
+ deleteModel: jest.fn().mockResolvedValue(undefined),
14
+ sendMessageWithAudio: jest.fn().mockResolvedValue("Mock audio response"),
15
+ sendMultimodalMessage: jest.fn().mockResolvedValue("Mock multimodal response"),
16
+ sendMessageAsync: jest.fn((msg, onToken) => {
17
+ onToken("Mock ", false);
18
+ onToken("token", true);
19
+ return Promise.resolve();
20
+ }),
21
+ getHistory: jest.fn(() => []),
22
+ resetConversation: jest.fn(),
23
+ getStats: jest.fn(() => ({
24
+ promptTokens: 10,
25
+ completionTokens: 20,
26
+ totalTokens: 30,
27
+ timeToFirstToken: 5,
28
+ totalTime: 50,
29
+ tokensPerSecond: 400,
30
+ })),
31
+ countTokens: jest.fn(() => -1),
32
+ getMemoryUsage: jest.fn(() => ({
33
+ nativeHeapBytes: 1000000,
34
+ residentBytes: 2000000,
35
+ availableMemoryBytes: 4000000,
36
+ isLowMemory: false,
37
+ })),
38
+ close: jest.fn(),
39
+ };
40
+ exports.NitroModules = {
41
+ createHybridObject: jest.fn((name) => {
42
+ if (name === "LiteRTLM") {
43
+ return exports.mockLiteRTLM;
44
+ }
45
+ throw new Error(`Mock not implemented for hybrid object: ${name}`);
46
+ }),
47
+ createNativeArrayBuffer: jest.fn((size) => {
48
+ return new ArrayBuffer(size);
49
+ }),
50
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // Configure React act environment
7
+ global.IS_REACT_ACT_ENVIRONMENT = true;
8
+ const hooks_1 = require("../hooks");
9
+ const react_native_nitro_modules_1 = require("../__mocks__/react-native-nitro-modules");
10
+ const react_test_renderer_1 = __importDefault(require("react-test-renderer"));
11
+ const react_1 = __importDefault(require("react"));
12
+ // Mock react-native
13
+ jest.mock('react-native', () => ({
14
+ Platform: {
15
+ OS: 'ios',
16
+ select: jest.fn((dict) => dict.ios),
17
+ },
18
+ }));
19
+ // Helper to render and test hooks using react-test-renderer
20
+ function renderHook(callback, initialProps) {
21
+ let result = { current: null };
22
+ const TestComponent = ({ props }) => {
23
+ result.current = callback(props);
24
+ return null;
25
+ };
26
+ let renderer;
27
+ react_test_renderer_1.default.act(() => {
28
+ renderer = react_test_renderer_1.default.create(react_1.default.createElement(TestComponent, { props: initialProps }));
29
+ });
30
+ const rerender = (newProps) => {
31
+ react_test_renderer_1.default.act(() => {
32
+ renderer.update(react_1.default.createElement(TestComponent, { props: newProps }));
33
+ });
34
+ };
35
+ const unmount = () => {
36
+ react_test_renderer_1.default.act(() => {
37
+ renderer.unmount();
38
+ });
39
+ };
40
+ return { result, rerender, unmount };
41
+ }
42
+ describe('useModel React Hook Unit Tests', () => {
43
+ beforeEach(() => {
44
+ jest.clearAllMocks();
45
+ });
46
+ it('should initialize with correct default state and call loadModel automatically when autoLoad is true', async () => {
47
+ let hookResult;
48
+ await react_test_renderer_1.default.act(async () => {
49
+ hookResult = renderHook(() => (0, hooks_1.useModel)('https://example.com/model.litertlm', { autoLoad: true }));
50
+ });
51
+ expect(hookResult.result.current.isReady).toBe(true);
52
+ expect(hookResult.result.current.isGenerating).toBe(false);
53
+ expect(hookResult.result.current.downloadProgress).toBe(1); // loadModel completed
54
+ expect(hookResult.result.current.error).toBeNull();
55
+ expect(react_native_nitro_modules_1.mockLiteRTLM.loadModel).toHaveBeenCalled();
56
+ });
57
+ it('should not call loadModel automatically when autoLoad is false', async () => {
58
+ let hookResult;
59
+ await react_test_renderer_1.default.act(async () => {
60
+ hookResult = renderHook(() => (0, hooks_1.useModel)('https://example.com/model.litertlm', { autoLoad: false }));
61
+ });
62
+ expect(hookResult.result.current.isReady).toBe(false);
63
+ expect(react_native_nitro_modules_1.mockLiteRTLM.loadModel).not.toHaveBeenCalled();
64
+ // Call load manually
65
+ await react_test_renderer_1.default.act(async () => {
66
+ await hookResult.result.current.load();
67
+ });
68
+ expect(hookResult.result.current.isReady).toBe(true);
69
+ expect(react_native_nitro_modules_1.mockLiteRTLM.loadModel).toHaveBeenCalled();
70
+ });
71
+ it('should handle model load failure gracefully', async () => {
72
+ react_native_nitro_modules_1.mockLiteRTLM.loadModel.mockRejectedValueOnce(new Error("Model load failed"));
73
+ let hookResult;
74
+ await react_test_renderer_1.default.act(async () => {
75
+ hookResult = renderHook(() => (0, hooks_1.useModel)('https://example.com/model.litertlm', { autoLoad: true }));
76
+ });
77
+ expect(hookResult.result.current.isReady).toBe(false);
78
+ expect(hookResult.result.current.error).toBe("Model load failed");
79
+ });
80
+ it('should generate text successfully and trigger memory summary update', async () => {
81
+ let hookResult;
82
+ await react_test_renderer_1.default.act(async () => {
83
+ hookResult = renderHook(() => (0, hooks_1.useModel)('https://example.com/model.litertlm', {
84
+ autoLoad: true,
85
+ enableMemoryTracking: true
86
+ }));
87
+ });
88
+ let response = "";
89
+ await react_test_renderer_1.default.act(async () => {
90
+ response = await hookResult.result.current.generate("Test prompt");
91
+ });
92
+ expect(response).toBe("Mock token");
93
+ expect(react_native_nitro_modules_1.mockLiteRTLM.sendMessageAsync).toHaveBeenCalled();
94
+ expect(hookResult.result.current.memorySummary).toBeDefined();
95
+ });
96
+ it('should reset conversation correctly', async () => {
97
+ let hookResult;
98
+ await react_test_renderer_1.default.act(async () => {
99
+ hookResult = renderHook(() => (0, hooks_1.useModel)('https://example.com/model.litertlm', { autoLoad: true }));
100
+ });
101
+ hookResult.result.current.reset();
102
+ expect(react_native_nitro_modules_1.mockLiteRTLM.resetConversation).toHaveBeenCalled();
103
+ });
104
+ it('should delete model file correctly', async () => {
105
+ let hookResult;
106
+ await react_test_renderer_1.default.act(async () => {
107
+ hookResult = renderHook(() => (0, hooks_1.useModel)('https://example.com/model.litertlm', { autoLoad: true }));
108
+ });
109
+ await react_test_renderer_1.default.act(async () => {
110
+ await hookResult.result.current.deleteModel();
111
+ });
112
+ expect(react_native_nitro_modules_1.mockLiteRTLM.deleteModel).toHaveBeenCalledWith('model.litertlm');
113
+ expect(hookResult.result.current.isReady).toBe(false);
114
+ expect(hookResult.result.current.downloadProgress).toBe(0);
115
+ });
116
+ it('should call close on unmount', async () => {
117
+ let hookResult;
118
+ await react_test_renderer_1.default.act(async () => {
119
+ hookResult = renderHook(() => (0, hooks_1.useModel)('https://example.com/model.litertlm', { autoLoad: false }));
120
+ });
121
+ hookResult.unmount();
122
+ expect(react_native_nitro_modules_1.mockLiteRTLM.close).toHaveBeenCalled();
123
+ });
124
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const memoryTracker_1 = require("../memoryTracker");
4
+ const react_native_nitro_modules_1 = require("react-native-nitro-modules");
5
+ describe('MemoryTracker Unit Tests', () => {
6
+ beforeEach(() => {
7
+ jest.clearAllMocks();
8
+ });
9
+ it('should allocate correct native-backed ArrayBuffer size on initialization', () => {
10
+ const tracker = (0, memoryTracker_1.createMemoryTracker)(10);
11
+ expect(react_native_nitro_modules_1.NitroModules.createNativeArrayBuffer).toHaveBeenCalledWith(10 * 4 * 8); // 10 snapshots * 4 fields * 8 bytes/Float64
12
+ expect(tracker.getCapacity()).toBe(10);
13
+ expect(tracker.getSnapshotCount()).toBe(0);
14
+ });
15
+ it('should record snapshots correctly and retrieve them', () => {
16
+ const tracker = (0, memoryTracker_1.createMemoryTracker)(5);
17
+ const snapshot1 = {
18
+ timestamp: 1000,
19
+ nativeHeapBytes: 100,
20
+ residentBytes: 200,
21
+ availableMemoryBytes: 500,
22
+ };
23
+ expect(tracker.record(snapshot1)).toBe(true);
24
+ expect(tracker.getSnapshotCount()).toBe(1);
25
+ expect(tracker.getLatestSnapshot()).toEqual(snapshot1);
26
+ const snapshots = tracker.getSnapshots();
27
+ expect(snapshots).toHaveLength(1);
28
+ expect(snapshots[0]).toEqual(snapshot1);
29
+ });
30
+ it('should reject new snapshots and return false when capacity is reached', () => {
31
+ const tracker = (0, memoryTracker_1.createMemoryTracker)(2);
32
+ expect(tracker.record({ timestamp: 1, nativeHeapBytes: 10, residentBytes: 20, availableMemoryBytes: 50 })).toBe(true);
33
+ expect(tracker.record({ timestamp: 2, nativeHeapBytes: 20, residentBytes: 30, availableMemoryBytes: 40 })).toBe(true);
34
+ expect(tracker.record({ timestamp: 3, nativeHeapBytes: 30, residentBytes: 40, availableMemoryBytes: 30 })).toBe(false);
35
+ expect(tracker.getSnapshotCount()).toBe(2);
36
+ });
37
+ it('should calculate correct peak resident memory size', () => {
38
+ const tracker = (0, memoryTracker_1.createMemoryTracker)(5);
39
+ tracker.record({ timestamp: 1, nativeHeapBytes: 100, residentBytes: 150, availableMemoryBytes: 1000 });
40
+ tracker.record({ timestamp: 2, nativeHeapBytes: 120, residentBytes: 300, availableMemoryBytes: 1000 });
41
+ tracker.record({ timestamp: 3, nativeHeapBytes: 110, residentBytes: 200, availableMemoryBytes: 1000 });
42
+ expect(tracker.getPeakMemory()).toBe(300);
43
+ });
44
+ it('should calculate accurate memory summary statistics', () => {
45
+ const tracker = (0, memoryTracker_1.createMemoryTracker)(5);
46
+ tracker.record({ timestamp: 1, nativeHeapBytes: 50, residentBytes: 100, availableMemoryBytes: 1000 });
47
+ tracker.record({ timestamp: 2, nativeHeapBytes: 150, residentBytes: 300, availableMemoryBytes: 800 });
48
+ tracker.record({ timestamp: 3, nativeHeapBytes: 100, residentBytes: 200, availableMemoryBytes: 900 });
49
+ const summary = tracker.getSummary();
50
+ expect(summary.snapshotCount).toBe(3);
51
+ expect(summary.peakResidentBytes).toBe(300);
52
+ expect(summary.averageResidentBytes).toBe(200); // (100 + 300 + 200) / 3
53
+ expect(summary.currentResidentBytes).toBe(200);
54
+ expect(summary.peakNativeHeapBytes).toBe(150);
55
+ expect(summary.currentNativeHeapBytes).toBe(100);
56
+ expect(summary.residentDeltaBytes).toBe(100); // currentRss(200) - firstRss(100)
57
+ expect(summary.trackerBufferSizeBytes).toBe(5 * 4 * 8);
58
+ });
59
+ it('should preserve buffer but reset internal state when reset() is called', () => {
60
+ const tracker = (0, memoryTracker_1.createMemoryTracker)(5);
61
+ tracker.record({ timestamp: 1, nativeHeapBytes: 50, residentBytes: 100, availableMemoryBytes: 1000 });
62
+ expect(tracker.getSnapshotCount()).toBe(1);
63
+ tracker.reset();
64
+ expect(tracker.getSnapshotCount()).toBe(0);
65
+ expect(tracker.getLatestSnapshot()).toBeUndefined();
66
+ expect(tracker.getSnapshots()).toEqual([]);
67
+ });
68
+ it('should allow standalone native ArrayBuffer allocation via createNativeBuffer', () => {
69
+ const size = 128;
70
+ const buffer = (0, memoryTracker_1.createNativeBuffer)(size);
71
+ expect(react_native_nitro_modules_1.NitroModules.createNativeArrayBuffer).toHaveBeenCalledWith(size);
72
+ expect(buffer.byteLength).toBe(size);
73
+ });
74
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const modelFactory_1 = require("../modelFactory");
4
+ const react_native_nitro_modules_1 = require("../__mocks__/react-native-nitro-modules");
5
+ describe('modelFactory Security & Proxy Unit Tests', () => {
6
+ let llm;
7
+ beforeEach(() => {
8
+ jest.clearAllMocks();
9
+ llm = (0, modelFactory_1.createLLM)({ enableMemoryTracking: true });
10
+ });
11
+ it('should block insecure HTTP downloads', async () => {
12
+ await expect(llm.loadModel('http://example.com/model.litertlm'))
13
+ .rejects.toThrow('Insecure HTTP URLs are not allowed for model downloads');
14
+ });
15
+ it('should allow secure HTTPS downloads and strip query parameters', async () => {
16
+ await llm.loadModel('https://example.com/model.litertlm?token=123');
17
+ expect(react_native_nitro_modules_1.mockLiteRTLM.downloadModel).toHaveBeenCalledWith('https://example.com/model.litertlm?token=123', 'model.litertlm', expect.any(Function));
18
+ expect(react_native_nitro_modules_1.mockLiteRTLM.loadModel).toHaveBeenCalledWith('/mock/path/model.litertlm', undefined);
19
+ });
20
+ it('should throw an error for invalid model URL', async () => {
21
+ await expect(llm.loadModel('https://example.com/'))
22
+ .rejects.toThrow('Invalid model URL: https://example.com/');
23
+ });
24
+ it('should successfully proxy sendMessage and record memory metrics', async () => {
25
+ const response = await llm.sendMessage("Test prompt");
26
+ expect(response).toBe("Mock response");
27
+ expect(react_native_nitro_modules_1.mockLiteRTLM.sendMessage).toHaveBeenCalledWith("Test prompt");
28
+ expect(react_native_nitro_modules_1.mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
29
+ expect(llm.memoryTracker?.getSnapshotCount()).toBe(1); // sendMessage records one
30
+ });
31
+ it('should successfully proxy resetConversation and record memory metrics', async () => {
32
+ await llm.resetConversation();
33
+ expect(react_native_nitro_modules_1.mockLiteRTLM.resetConversation).toHaveBeenCalled();
34
+ expect(react_native_nitro_modules_1.mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
35
+ });
36
+ it('should successfully proxy sendMessageAsync and record memory metrics when done', async () => {
37
+ const onToken = jest.fn();
38
+ await llm.sendMessageAsync("Async prompt", onToken);
39
+ expect(onToken).toHaveBeenCalledWith("Mock ", false);
40
+ expect(onToken).toHaveBeenCalledWith("token", true);
41
+ expect(react_native_nitro_modules_1.mockLiteRTLM.sendMessageAsync).toHaveBeenCalled();
42
+ expect(react_native_nitro_modules_1.mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
43
+ });
44
+ it('should successfully access memoryTracker and getSnapshots when memory tracking is enabled', () => {
45
+ expect(llm.memoryTracker).toBeDefined();
46
+ expect(llm.memoryTracker?.getCapacity()).toBe(256);
47
+ });
48
+ it('should not initialize memoryTracker when enableMemoryTracking option is false', () => {
49
+ const untrackedLLM = (0, modelFactory_1.createLLM)({ enableMemoryTracking: false });
50
+ expect(untrackedLLM.memoryTracker).toBeUndefined();
51
+ });
52
+ });
package/lib/hooks.js CHANGED
@@ -105,7 +105,7 @@ function useModel(pathOrUrl, config) {
105
105
  refreshMemorySummary();
106
106
  resolve(fullResponse);
107
107
  }
108
- });
108
+ }).catch(reject);
109
109
  }
110
110
  catch (e) {
111
111
  reject(e);
package/lib/index.d.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import type { Backend } from "./specs/LiteRTLM.nitro";
2
2
  export type { LiteRTLM, LLMConfig, Message, Backend, Role, GenerationStats, MemoryUsage, } from "./specs/LiteRTLM.nitro";
3
- export type { ChatMessage } from "./templates";
4
- export { applyGemmaTemplate, applyPhiTemplate, applyLlamaTemplate, } from "./templates";
5
3
  export type { MemorySnapshot, MemoryTracker, MemoryTrackerSummary, } from "./memoryTracker";
6
4
  export { createMemoryTracker, createNativeBuffer } from "./memoryTracker";
7
5
  export type { LiteRTLMInstance } from "./modelFactory";
@@ -110,8 +108,8 @@ export declare function checkBackendSupport(backend: Backend): string | undefine
110
108
  */
111
109
  export declare function checkMultimodalSupport(): string | undefined;
112
110
  /**
113
- * Download URL for the Gemma 3n E2B IT INT4 model.
114
- * Note: Requires a HuggingFace account (gated model).
111
+ * Download URL for the Gemma 3n E2B IT INT4 model (~1.3 GB).
112
+ * Public hosted on litert.dev, no authentication required.
115
113
  */
116
114
  export declare const GEMMA_3N_E2B_IT_INT4 = "https://litert.dev/gemma-3n-E2B-it-int4.litertlm";
117
115
  /**
package/lib/index.js CHANGED
@@ -14,15 +14,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.GEMMA_4_E4B_IT = exports.GEMMA_4_E2B_IT = exports.GEMMA_3N_E2B_IT_INT4 = exports.Models = exports.createLLM = exports.createNativeBuffer = exports.createMemoryTracker = exports.applyLlamaTemplate = exports.applyPhiTemplate = exports.applyGemmaTemplate = void 0;
17
+ exports.GEMMA_4_E4B_IT = exports.GEMMA_4_E2B_IT = exports.GEMMA_3N_E2B_IT_INT4 = exports.Models = exports.createLLM = exports.createNativeBuffer = exports.createMemoryTracker = void 0;
18
18
  exports.getRecommendedBackend = getRecommendedBackend;
19
19
  exports.checkBackendSupport = checkBackendSupport;
20
20
  exports.checkMultimodalSupport = checkMultimodalSupport;
21
21
  const react_native_1 = require("react-native");
22
- var templates_1 = require("./templates");
23
- Object.defineProperty(exports, "applyGemmaTemplate", { enumerable: true, get: function () { return templates_1.applyGemmaTemplate; } });
24
- Object.defineProperty(exports, "applyPhiTemplate", { enumerable: true, get: function () { return templates_1.applyPhiTemplate; } });
25
- Object.defineProperty(exports, "applyLlamaTemplate", { enumerable: true, get: function () { return templates_1.applyLlamaTemplate; } });
26
22
  var memoryTracker_1 = require("./memoryTracker");
27
23
  Object.defineProperty(exports, "createMemoryTracker", { enumerable: true, get: function () { return memoryTracker_1.createMemoryTracker; } });
28
24
  Object.defineProperty(exports, "createNativeBuffer", { enumerable: true, get: function () { return memoryTracker_1.createNativeBuffer; } });
@@ -116,6 +112,15 @@ function getRecommendedBackend() {
116
112
  * ```
117
113
  */
118
114
  function checkBackendSupport(backend) {
115
+ if (backend === "gpu") {
116
+ if (react_native_1.Platform.OS === "android") {
117
+ // LiteRT-LM GPU delegate requires OpenCL, which is unavailable
118
+ // on most Samsung/Qualcomm devices. Only Pixel devices reliably expose it.
119
+ return "GPU backend requires OpenCL support, which is unavailable on most Samsung and Qualcomm devices.";
120
+ }
121
+ // iOS always supports GPU via Metal
122
+ return undefined;
123
+ }
119
124
  if (backend === "npu") {
120
125
  if (react_native_1.Platform.OS === "android") {
121
126
  return "NPU backend requires compatible hardware (Qualcomm Hexagon, MediaTek APU, etc.). Will fall back to GPU if unavailable.";
@@ -150,8 +155,8 @@ function checkMultimodalSupport() {
150
155
  return undefined;
151
156
  }
152
157
  /**
153
- * Download URL for the Gemma 3n E2B IT INT4 model.
154
- * Note: Requires a HuggingFace account (gated model).
158
+ * Download URL for the Gemma 3n E2B IT INT4 model (~1.3 GB).
159
+ * Public hosted on litert.dev, no authentication required.
155
160
  */
156
161
  exports.GEMMA_3N_E2B_IT_INT4 = "https://litert.dev/gemma-3n-E2B-it-int4.litertlm";
157
162
  /**
@@ -38,72 +38,71 @@ function createLLM(options) {
38
38
  // Ignore errors during memory tracking - it's non-critical
39
39
  }
40
40
  };
41
- return {
42
- ...native,
43
- memoryTracker: tracker,
44
- loadModel: async (pathOrUrl, config, onDownloadProgress) => {
45
- let modelPath = pathOrUrl;
46
- // Check if it's a URL enforce HTTPS for model downloads
47
- if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
48
- if (pathOrUrl.startsWith("http://")) {
49
- throw new Error("Insecure HTTP URLs are not allowed for model downloads. " +
50
- "Use HTTPS instead: " +
51
- pathOrUrl.replace("http://", "https://"));
52
- }
53
- // Extract filename from URL
54
- const fileName = pathOrUrl.split("/").pop();
55
- if (!fileName) {
56
- throw new Error(`Invalid model URL: ${pathOrUrl}`);
57
- }
58
- console.log(`Checking model at ${pathOrUrl}...`);
59
- modelPath = await native.downloadModel(pathOrUrl, fileName, (progress) => {
60
- onDownloadProgress?.(progress);
61
- });
62
- console.log(`Model downloaded to: ${modelPath}`);
41
+ const augmentedLoadModel = async (pathOrUrl, config, onDownloadProgress) => {
42
+ let modelPath = pathOrUrl;
43
+ // Check if it's a URL — enforce HTTPS for model downloads
44
+ if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
45
+ if (pathOrUrl.startsWith("http://")) {
46
+ throw new Error("Insecure HTTP URLs are not allowed for model downloads. " +
47
+ "Use HTTPS instead: " +
48
+ pathOrUrl.replace("http://", "https://"));
63
49
  }
64
- const result = await native.loadModel(modelPath, config);
65
- // Record initial memory snapshot after model load
66
- if (tracker) {
67
- tracker.reset();
68
- recordMemorySnapshot();
50
+ // Extract filename from URL, stripping query parameters
51
+ const urlWithoutQuery = pathOrUrl.split("?")[0];
52
+ const fileName = urlWithoutQuery.split("/").pop();
53
+ if (!fileName) {
54
+ throw new Error(`Invalid model URL: ${pathOrUrl}`);
69
55
  }
70
- return result;
71
- },
72
- sendMessage: async (...args) => {
73
- const result = await native.sendMessage(...args);
74
- recordMemorySnapshot();
75
- return result;
76
- },
77
- sendMessageAsync: (...args) => {
78
- const [message, onToken] = args;
79
- native.sendMessageAsync(message, (token, done) => {
80
- onToken(token, done);
81
- if (done) {
82
- recordMemorySnapshot();
83
- }
56
+ console.log(`Checking model at ${pathOrUrl}...`);
57
+ modelPath = await native.downloadModel(pathOrUrl, fileName, (progress) => {
58
+ onDownloadProgress?.(progress);
84
59
  });
85
- },
86
- sendMessageWithImage: async (...args) => {
87
- const result = await native.sendMessageWithImage(...args);
88
- recordMemorySnapshot();
89
- return result;
90
- },
91
- sendMessageWithAudio: async (...args) => {
92
- const result = await native.sendMessageWithAudio(...args);
93
- recordMemorySnapshot();
94
- return result;
95
- },
96
- getHistory: native.getHistory.bind(native),
97
- resetConversation: () => {
98
- native.resetConversation();
99
- // KV cache is cleared on reset, record the drop
60
+ console.log(`Model downloaded to: ${modelPath}`);
61
+ }
62
+ const result = await native.loadModel(modelPath, config);
63
+ // Record initial memory snapshot after model load
64
+ if (tracker) {
65
+ tracker.reset();
100
66
  recordMemorySnapshot();
101
- },
102
- isReady: native.isReady.bind(native),
103
- getStats: native.getStats.bind(native),
104
- getMemoryUsage: native.getMemoryUsage.bind(native),
105
- close: native.close.bind(native),
106
- downloadModel: native.downloadModel.bind(native),
107
- deleteModel: native.deleteModel.bind(native),
67
+ }
68
+ return result;
108
69
  };
70
+ const SNAPSHOT_TRIGGERS = new Set([
71
+ "sendMessage",
72
+ "sendMessageWithImage",
73
+ "sendMessageWithAudio",
74
+ "resetConversation",
75
+ ]);
76
+ return new Proxy(native, {
77
+ get(target, prop, receiver) {
78
+ if (prop === "memoryTracker") {
79
+ return tracker;
80
+ }
81
+ if (prop === "loadModel") {
82
+ return augmentedLoadModel;
83
+ }
84
+ const original = Reflect.get(target, prop, receiver);
85
+ if (typeof original !== "function") {
86
+ return original;
87
+ }
88
+ if (prop === "sendMessageAsync") {
89
+ return (message, onToken) => {
90
+ return original.call(target, message, (token, done) => {
91
+ onToken(token, done);
92
+ if (done) {
93
+ recordMemorySnapshot();
94
+ }
95
+ });
96
+ };
97
+ }
98
+ if (SNAPSHOT_TRIGGERS.has(prop)) {
99
+ return async (...args) => {
100
+ const result = await original.apply(target, args);
101
+ recordMemorySnapshot();
102
+ return result;
103
+ };
104
+ }
105
+ return original.bind(target);
106
+ },
107
+ });
109
108
  }