react-native-litert-lm 0.3.7 → 0.4.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 +153 -135
- package/android/build.gradle +12 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +276 -62
- package/android/src/main/java/dev/litert/litertlm/LiteRTLMPackage.kt +19 -2
- package/android/src/test/java/com/margelo/nitro/core/Promise.kt +46 -0
- package/android/src/test/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMTest.kt +105 -0
- package/ios/HybridLiteRTLM.swift +1344 -0
- package/ios/Tests/HybridLiteRTLMTests.swift +113 -0
- package/lib/__mocks__/react-native-nitro-modules.d.ts +65 -0
- package/lib/__mocks__/react-native-nitro-modules.js +60 -0
- package/lib/__tests__/hooks.test.d.ts +1 -0
- package/lib/__tests__/hooks.test.js +124 -0
- package/lib/__tests__/memoryTracker.test.d.ts +1 -0
- package/lib/__tests__/memoryTracker.test.js +74 -0
- package/lib/__tests__/modelFactory.test.d.ts +1 -0
- package/lib/__tests__/modelFactory.test.js +68 -0
- package/lib/hooks.js +27 -3
- package/lib/index.d.ts +6 -2
- package/lib/index.js +8 -8
- package/lib/modelFactory.js +82 -63
- package/lib/specs/LiteRTLM.nitro.d.ts +87 -2
- package/nitrogen/generated/android/LiteRTLMOnLoad.cpp +2 -2
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +94 -9
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +5 -1
- package/nitrogen/generated/android/c++/JLLMConfig.hpp +40 -3
- package/nitrogen/generated/android/c++/JMultimodalPart.hpp +74 -0
- package/nitrogen/generated/android/c++/JPartType.hpp +61 -0
- package/nitrogen/generated/android/c++/JToolDefinition.hpp +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/GenerationStats.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +28 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +46 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MemoryUsage.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Message.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MultimodalPart.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/PartType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolDefinition.kt +61 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.cpp +57 -1
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.hpp +414 -3
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Umbrella.hpp +41 -3
- package/nitrogen/generated/ios/LiteRTLMAutolinking.mm +4 -6
- package/nitrogen/generated/ios/LiteRTLMAutolinking.swift +10 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +240 -0
- package/nitrogen/generated/ios/swift/Backend.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/GenerationStats.swift +54 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +71 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +431 -0
- package/nitrogen/generated/ios/swift/LLMConfig.swift +203 -0
- package/nitrogen/generated/ios/swift/MemoryUsage.swift +44 -0
- package/nitrogen/generated/ios/swift/Message.swift +34 -0
- package/nitrogen/generated/ios/swift/MultimodalPart.swift +83 -0
- package/nitrogen/generated/ios/swift/PartType.swift +44 -0
- package/nitrogen/generated/ios/swift/Role.swift +44 -0
- package/nitrogen/generated/ios/swift/ToolDefinition.swift +39 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +9 -2
- package/nitrogen/generated/shared/c++/LLMConfig.hpp +22 -2
- package/nitrogen/generated/shared/c++/MultimodalPart.hpp +99 -0
- package/nitrogen/generated/shared/c++/PartType.hpp +80 -0
- package/nitrogen/generated/shared/c++/ToolDefinition.hpp +91 -0
- package/package.json +22 -11
- package/react-native-litert-lm.podspec +17 -19
- package/scripts/download-ios-frameworks.sh +17 -50
- package/scripts/framework-source.js +46 -0
- package/scripts/postinstall.js +40 -18
- package/src/__mocks__/react-native-nitro-modules.ts +58 -0
- package/src/__tests__/hooks.test.ts +153 -0
- package/src/__tests__/memoryTracker.test.ts +87 -0
- package/src/__tests__/modelFactory.test.ts +96 -0
- package/src/hooks.ts +29 -7
- package/src/index.ts +7 -10
- package/src/modelFactory.ts +104 -80
- package/src/specs/LiteRTLM.nitro.ts +106 -2
- package/cpp/HybridLiteRTLM.cpp +0 -939
- package/cpp/HybridLiteRTLM.hpp +0 -169
- package/cpp/IOSDownloadHelper.h +0 -24
- package/ios/IOSDownloadHelper.mm +0 -129
- package/scripts/build-ios-engine.sh +0 -302
- package/scripts/stubs/cxx_bridge_stubs.cc +0 -224
- package/scripts/stubs/gemma_model_constraint_provider.cc +0 -46
- package/scripts/stubs/llguidance_stubs.c +0 -101
- package/src/templates.ts +0 -105
|
@@ -0,0 +1,113 @@
|
|
|
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 testSendMessageWithImageAsyncRejectsWithoutModel() async throws {
|
|
57
|
+
do {
|
|
58
|
+
let promise = try bridge.sendMessageWithImageAsync(message: "hello", imagePath: "/tmp/image.jpg") { _, _ in }
|
|
59
|
+
_ = try await promise.await()
|
|
60
|
+
XCTFail("Should have failed without model")
|
|
61
|
+
} catch {
|
|
62
|
+
let nsError = error as NSError
|
|
63
|
+
XCTAssertEqual(nsError.domain, "LiteRTLM")
|
|
64
|
+
XCTAssertEqual(nsError.code, 400)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func testSendMessageWithAudioAsyncRejectsWithoutModel() async throws {
|
|
69
|
+
do {
|
|
70
|
+
let promise = try bridge.sendMessageWithAudioAsync(message: "hello", audioPath: "/tmp/audio.wav") { _, _ in }
|
|
71
|
+
_ = try await promise.await()
|
|
72
|
+
XCTFail("Should have failed without model")
|
|
73
|
+
} catch {
|
|
74
|
+
let nsError = error as NSError
|
|
75
|
+
XCTAssertEqual(nsError.domain, "LiteRTLM")
|
|
76
|
+
XCTAssertEqual(nsError.code, 400)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func testSendMessageWithImageAsyncRejectsFileNotFound() async throws {
|
|
81
|
+
do {
|
|
82
|
+
let promise = try bridge.sendMessageWithImageAsync(message: "hello", imagePath: "/nonexistent/image.jpg") { _, _ in }
|
|
83
|
+
_ = try await promise.await()
|
|
84
|
+
XCTFail("Should have failed without model")
|
|
85
|
+
} catch {
|
|
86
|
+
let nsError = error as NSError
|
|
87
|
+
XCTAssertEqual(nsError.domain, "LiteRTLM")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func testSendMessageWithAudioAsyncRejectsFileNotFound() async throws {
|
|
92
|
+
do {
|
|
93
|
+
let promise = try bridge.sendMessageWithAudioAsync(message: "hello", audioPath: "/nonexistent/audio.wav") { _, _ in }
|
|
94
|
+
_ = try await promise.await()
|
|
95
|
+
XCTFail("Should have failed without model")
|
|
96
|
+
} catch {
|
|
97
|
+
let nsError = error as NSError
|
|
98
|
+
XCTAssertEqual(nsError.domain, "LiteRTLM")
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func testInitialStats() {
|
|
103
|
+
XCTAssertNoThrow(try bridge.getStats())
|
|
104
|
+
if let stats = try? bridge.getStats() {
|
|
105
|
+
XCTAssertEqual(stats.promptTokens, 0.0)
|
|
106
|
+
XCTAssertEqual(stats.completionTokens, 0.0)
|
|
107
|
+
XCTAssertEqual(stats.totalTokens, 0.0)
|
|
108
|
+
XCTAssertEqual(stats.timeToFirstToken, 0.0)
|
|
109
|
+
XCTAssertEqual(stats.totalTime, 0.0)
|
|
110
|
+
XCTAssertEqual(stats.tokensPerSecond, 0.0)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
sendMessageWithImageAsync: jest.Mock<Promise<void>, [msg: any, imagePath: any, onToken: any], any>;
|
|
12
|
+
sendMessageWithAudioAsync: jest.Mock<Promise<void>, [msg: any, audioPath: any, onToken: any], any>;
|
|
13
|
+
getHistory: jest.Mock<never[], [], any>;
|
|
14
|
+
resetConversation: jest.Mock<any, any, any>;
|
|
15
|
+
getStats: jest.Mock<{
|
|
16
|
+
promptTokens: number;
|
|
17
|
+
completionTokens: number;
|
|
18
|
+
totalTokens: number;
|
|
19
|
+
timeToFirstToken: number;
|
|
20
|
+
totalTime: number;
|
|
21
|
+
tokensPerSecond: number;
|
|
22
|
+
}, [], any>;
|
|
23
|
+
countTokens: jest.Mock<number, [], any>;
|
|
24
|
+
getMemoryUsage: jest.Mock<{
|
|
25
|
+
nativeHeapBytes: number;
|
|
26
|
+
residentBytes: number;
|
|
27
|
+
availableMemoryBytes: number;
|
|
28
|
+
isLowMemory: boolean;
|
|
29
|
+
}, [], any>;
|
|
30
|
+
close: jest.Mock<any, any, any>;
|
|
31
|
+
};
|
|
32
|
+
export declare const NitroModules: {
|
|
33
|
+
createHybridObject: jest.Mock<{
|
|
34
|
+
isReady: jest.Mock<boolean, [], any>;
|
|
35
|
+
loadModel: jest.Mock<any, any, any>;
|
|
36
|
+
sendMessage: jest.Mock<any, any, any>;
|
|
37
|
+
sendMessageWithImage: jest.Mock<any, any, any>;
|
|
38
|
+
downloadModel: jest.Mock<Promise<string>, [url: any, fileName: any, onProgress: any], any>;
|
|
39
|
+
deleteModel: jest.Mock<any, any, any>;
|
|
40
|
+
sendMessageWithAudio: jest.Mock<any, any, any>;
|
|
41
|
+
sendMultimodalMessage: jest.Mock<any, any, any>;
|
|
42
|
+
sendMessageAsync: jest.Mock<Promise<void>, [msg: any, onToken: any], any>;
|
|
43
|
+
sendMessageWithImageAsync: jest.Mock<Promise<void>, [msg: any, imagePath: any, onToken: any], any>;
|
|
44
|
+
sendMessageWithAudioAsync: jest.Mock<Promise<void>, [msg: any, audioPath: any, onToken: any], any>;
|
|
45
|
+
getHistory: jest.Mock<never[], [], any>;
|
|
46
|
+
resetConversation: jest.Mock<any, any, any>;
|
|
47
|
+
getStats: jest.Mock<{
|
|
48
|
+
promptTokens: number;
|
|
49
|
+
completionTokens: number;
|
|
50
|
+
totalTokens: number;
|
|
51
|
+
timeToFirstToken: number;
|
|
52
|
+
totalTime: number;
|
|
53
|
+
tokensPerSecond: number;
|
|
54
|
+
}, [], any>;
|
|
55
|
+
countTokens: jest.Mock<number, [], any>;
|
|
56
|
+
getMemoryUsage: jest.Mock<{
|
|
57
|
+
nativeHeapBytes: number;
|
|
58
|
+
residentBytes: number;
|
|
59
|
+
availableMemoryBytes: number;
|
|
60
|
+
isLowMemory: boolean;
|
|
61
|
+
}, [], any>;
|
|
62
|
+
close: jest.Mock<any, any, any>;
|
|
63
|
+
}, [name: string], any>;
|
|
64
|
+
createNativeArrayBuffer: jest.Mock<ArrayBuffer, [size: number], any>;
|
|
65
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
sendMessageWithImageAsync: jest.fn((msg, imagePath, onToken) => {
|
|
22
|
+
onToken("Mock vision ", false);
|
|
23
|
+
onToken("token", true);
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
}),
|
|
26
|
+
sendMessageWithAudioAsync: jest.fn((msg, audioPath, onToken) => {
|
|
27
|
+
onToken("Mock audio ", false);
|
|
28
|
+
onToken("token", true);
|
|
29
|
+
return Promise.resolve();
|
|
30
|
+
}),
|
|
31
|
+
getHistory: jest.fn(() => []),
|
|
32
|
+
resetConversation: jest.fn(),
|
|
33
|
+
getStats: jest.fn(() => ({
|
|
34
|
+
promptTokens: 10,
|
|
35
|
+
completionTokens: 20,
|
|
36
|
+
totalTokens: 30,
|
|
37
|
+
timeToFirstToken: 5,
|
|
38
|
+
totalTime: 50,
|
|
39
|
+
tokensPerSecond: 400,
|
|
40
|
+
})),
|
|
41
|
+
countTokens: jest.fn(() => -1),
|
|
42
|
+
getMemoryUsage: jest.fn(() => ({
|
|
43
|
+
nativeHeapBytes: 1000000,
|
|
44
|
+
residentBytes: 2000000,
|
|
45
|
+
availableMemoryBytes: 4000000,
|
|
46
|
+
isLowMemory: false,
|
|
47
|
+
})),
|
|
48
|
+
close: jest.fn(),
|
|
49
|
+
};
|
|
50
|
+
exports.NitroModules = {
|
|
51
|
+
createHybridObject: jest.fn((name) => {
|
|
52
|
+
if (name === "LiteRTLM") {
|
|
53
|
+
return exports.mockLiteRTLM;
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Mock not implemented for hybrid object: ${name}`);
|
|
56
|
+
}),
|
|
57
|
+
createNativeArrayBuffer: jest.fn((size) => {
|
|
58
|
+
return new ArrayBuffer(size);
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
@@ -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,68 @@
|
|
|
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 proxy sendMessageWithImageAsync and record memory metrics when done', async () => {
|
|
45
|
+
const onToken = jest.fn();
|
|
46
|
+
await llm.sendMessageWithImageAsync("Vision prompt", "/path/to/image.jpg", onToken);
|
|
47
|
+
expect(onToken).toHaveBeenCalledWith("Mock vision ", false);
|
|
48
|
+
expect(onToken).toHaveBeenCalledWith("token", true);
|
|
49
|
+
expect(react_native_nitro_modules_1.mockLiteRTLM.sendMessageWithImageAsync).toHaveBeenCalledWith("Vision prompt", "/path/to/image.jpg", expect.any(Function));
|
|
50
|
+
expect(react_native_nitro_modules_1.mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
it('should successfully proxy sendMessageWithAudioAsync and record memory metrics when done', async () => {
|
|
53
|
+
const onToken = jest.fn();
|
|
54
|
+
await llm.sendMessageWithAudioAsync("Audio prompt", "/path/to/audio.wav", onToken);
|
|
55
|
+
expect(onToken).toHaveBeenCalledWith("Mock audio ", false);
|
|
56
|
+
expect(onToken).toHaveBeenCalledWith("token", true);
|
|
57
|
+
expect(react_native_nitro_modules_1.mockLiteRTLM.sendMessageWithAudioAsync).toHaveBeenCalledWith("Audio prompt", "/path/to/audio.wav", expect.any(Function));
|
|
58
|
+
expect(react_native_nitro_modules_1.mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
it('should successfully access memoryTracker and getSnapshots when memory tracking is enabled', () => {
|
|
61
|
+
expect(llm.memoryTracker).toBeDefined();
|
|
62
|
+
expect(llm.memoryTracker?.getCapacity()).toBe(256);
|
|
63
|
+
});
|
|
64
|
+
it('should not initialize memoryTracker when enableMemoryTracking option is false', () => {
|
|
65
|
+
const untrackedLLM = (0, modelFactory_1.createLLM)({ enableMemoryTracking: false });
|
|
66
|
+
expect(untrackedLLM.memoryTracker).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
package/lib/hooks.js
CHANGED
|
@@ -28,6 +28,11 @@ function useModel(pathOrUrl, config) {
|
|
|
28
28
|
const temperature = config?.temperature;
|
|
29
29
|
const topK = config?.topK;
|
|
30
30
|
const topP = config?.topP;
|
|
31
|
+
const validate = config?.validate;
|
|
32
|
+
const multimodal = config?.multimodal;
|
|
33
|
+
const tools = config?.tools;
|
|
34
|
+
const enableSpeculativeDecoding = config?.enableSpeculativeDecoding;
|
|
35
|
+
const toolsKey = tools ? JSON.stringify(tools) : undefined;
|
|
31
36
|
// Build a stable config object from the destructured primitives
|
|
32
37
|
const nativeConfig = (0, react_1.useMemo)(() => ({
|
|
33
38
|
...(backend !== undefined && { backend }),
|
|
@@ -36,7 +41,24 @@ function useModel(pathOrUrl, config) {
|
|
|
36
41
|
...(temperature !== undefined && { temperature }),
|
|
37
42
|
...(topK !== undefined && { topK }),
|
|
38
43
|
...(topP !== undefined && { topP }),
|
|
39
|
-
|
|
44
|
+
...(validate !== undefined && { validate }),
|
|
45
|
+
...(multimodal !== undefined && { multimodal }),
|
|
46
|
+
...(tools !== undefined && { tools }),
|
|
47
|
+
...(enableSpeculativeDecoding !== undefined && {
|
|
48
|
+
enableSpeculativeDecoding,
|
|
49
|
+
}),
|
|
50
|
+
}), [
|
|
51
|
+
backend,
|
|
52
|
+
systemPrompt,
|
|
53
|
+
maxTokens,
|
|
54
|
+
temperature,
|
|
55
|
+
topK,
|
|
56
|
+
topP,
|
|
57
|
+
validate,
|
|
58
|
+
multimodal,
|
|
59
|
+
toolsKey,
|
|
60
|
+
enableSpeculativeDecoding,
|
|
61
|
+
]);
|
|
40
62
|
/**
|
|
41
63
|
* Refresh memory summary from the tracker's native buffer.
|
|
42
64
|
*/
|
|
@@ -99,13 +121,15 @@ function useModel(pathOrUrl, config) {
|
|
|
99
121
|
return new Promise((resolve, reject) => {
|
|
100
122
|
let fullResponse = "";
|
|
101
123
|
try {
|
|
102
|
-
modelRef.current
|
|
124
|
+
modelRef.current
|
|
125
|
+
?.sendMessageAsync(prompt, (token, done) => {
|
|
103
126
|
fullResponse += token;
|
|
104
127
|
if (done) {
|
|
105
128
|
refreshMemorySummary();
|
|
106
129
|
resolve(fullResponse);
|
|
107
130
|
}
|
|
108
|
-
})
|
|
131
|
+
})
|
|
132
|
+
.catch(reject);
|
|
109
133
|
}
|
|
110
134
|
catch (e) {
|
|
111
135
|
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";
|
|
@@ -95,6 +93,12 @@ export declare function checkBackendSupport(backend: Backend): string | undefine
|
|
|
95
93
|
* Check if multimodal features (image/audio) are supported on the current platform.
|
|
96
94
|
* Returns an error message if not supported, undefined if OK.
|
|
97
95
|
*
|
|
96
|
+
* Both iOS (v0.12.0 CLiteRTLM xcframework) and Android (LiteRT-LM SDK) ship the
|
|
97
|
+
* vision/audio executor ops, so there is no platform-level block. Whether a
|
|
98
|
+
* given call succeeds depends on the **loaded model**: only multimodal models
|
|
99
|
+
* (e.g. Gemma 3n) bundle the vision/audio executors. Pass `multimodal: true` to
|
|
100
|
+
* `loadModel` for such models, or rely on filename sniffing ("3n"/"gemma3").
|
|
101
|
+
*
|
|
98
102
|
* @returns Error message if multimodal is not supported, undefined if OK
|
|
99
103
|
*
|
|
100
104
|
* @example
|
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 =
|
|
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; } });
|
|
@@ -139,6 +135,12 @@ function checkBackendSupport(backend) {
|
|
|
139
135
|
* Check if multimodal features (image/audio) are supported on the current platform.
|
|
140
136
|
* Returns an error message if not supported, undefined if OK.
|
|
141
137
|
*
|
|
138
|
+
* Both iOS (v0.12.0 CLiteRTLM xcframework) and Android (LiteRT-LM SDK) ship the
|
|
139
|
+
* vision/audio executor ops, so there is no platform-level block. Whether a
|
|
140
|
+
* given call succeeds depends on the **loaded model**: only multimodal models
|
|
141
|
+
* (e.g. Gemma 3n) bundle the vision/audio executors. Pass `multimodal: true` to
|
|
142
|
+
* `loadModel` for such models, or rely on filename sniffing ("3n"/"gemma3").
|
|
143
|
+
*
|
|
142
144
|
* @returns Error message if multimodal is not supported, undefined if OK
|
|
143
145
|
*
|
|
144
146
|
* @example
|
|
@@ -153,9 +155,7 @@ function checkBackendSupport(backend) {
|
|
|
153
155
|
* ```
|
|
154
156
|
*/
|
|
155
157
|
function checkMultimodalSupport() {
|
|
156
|
-
|
|
157
|
-
return "Multimodal (image/audio) is not available on iOS. The XCFramework lacks compiled vision and audio executor ops.";
|
|
158
|
-
}
|
|
158
|
+
// Supported on both platforms with a multimodal model loaded.
|
|
159
159
|
return undefined;
|
|
160
160
|
}
|
|
161
161
|
/**
|