react-native-litert-lm 0.3.7 → 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.
- package/README.md +153 -135
- package/android/build.gradle +12 -0
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +159 -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 +83 -0
- package/ios/HybridLiteRTLM.swift +1058 -0
- package/ios/Tests/HybridLiteRTLMTests.swift +67 -0
- package/lib/__mocks__/react-native-nitro-modules.d.ts +61 -0
- package/lib/__mocks__/react-native-nitro-modules.js +50 -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 +52 -0
- package/lib/hooks.js +1 -1
- package/lib/index.d.ts +0 -2
- package/lib/index.js +1 -5
- package/lib/modelFactory.js +62 -63
- package/lib/specs/LiteRTLM.nitro.d.ts +71 -2
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +62 -7
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +3 -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 +10 -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 +224 -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 +69 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +383 -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 +2 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +7 -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 +16 -8
- package/react-native-litert-lm.podspec +15 -19
- package/scripts/download-ios-frameworks.sh +14 -48
- package/scripts/postinstall.js +1 -2
- package/src/__mocks__/react-native-nitro-modules.ts +48 -0
- package/src/__tests__/hooks.test.ts +153 -0
- package/src/__tests__/memoryTracker.test.ts +87 -0
- package/src/__tests__/modelFactory.test.ts +68 -0
- package/src/hooks.ts +1 -1
- package/src/index.ts +0 -7
- package/src/modelFactory.ts +82 -80
- package/src/specs/LiteRTLM.nitro.ts +80 -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
|
@@ -9,36 +9,28 @@ Pod::Spec.new do |s|
|
|
|
9
9
|
s.homepage = package["homepage"]
|
|
10
10
|
s.license = package["license"]
|
|
11
11
|
s.authors = package["author"]
|
|
12
|
-
s.platforms = { :ios => "15.
|
|
12
|
+
s.platforms = { :ios => "15.1" }
|
|
13
|
+
s.module_name = "LiteRTLM"
|
|
13
14
|
s.source = { :git => package["repository"]["url"], :tag => "#{s.version}" }
|
|
14
15
|
|
|
15
|
-
s.swift_version = '5.
|
|
16
|
+
s.swift_version = '5.9'
|
|
16
17
|
|
|
17
18
|
s.source_files = [
|
|
18
|
-
#
|
|
19
|
-
"
|
|
20
|
-
#
|
|
21
|
-
"ios/**/*.{m,mm}",
|
|
22
|
-
# Nitrogen generated iOS bridge
|
|
19
|
+
# Swift, Objective-C/C++ implementation & autolinking
|
|
20
|
+
"ios/*.{swift,m,mm}",
|
|
21
|
+
# Nitrogen generated iOS bridge & shared C++ interfaces
|
|
23
22
|
"nitrogen/generated/ios/**/*.{mm,swift}",
|
|
23
|
+
"nitrogen/generated/shared/c++/**/*.{hpp,cpp}",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
-
#
|
|
27
|
-
s
|
|
28
|
-
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
# Prebuilt LiteRT-LM C engine (static library built from Bazel //c:engine target).
|
|
32
|
-
# Downloaded from GitHub releases by postinstall.js, or built locally via:
|
|
33
|
-
# scripts/build-ios-engine.sh
|
|
34
|
-
s.vendored_frameworks = 'ios/Frameworks/LiteRTLM.xcframework'
|
|
26
|
+
# Prebuilt LiteRT-LM C engine (static library).
|
|
27
|
+
# Downloaded from Google's release via: scripts/download-ios-frameworks.sh
|
|
28
|
+
s.vendored_frameworks = 'ios/Frameworks/CLiteRTLM.xcframework'
|
|
35
29
|
|
|
36
30
|
s.pod_target_xcconfig = {
|
|
37
31
|
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++20',
|
|
38
|
-
'
|
|
32
|
+
'SWIFT_VERSION' => '5.9',
|
|
39
33
|
'HEADER_SEARCH_PATHS' => [
|
|
40
|
-
'"$(PODS_TARGET_SRCROOT)/cpp"',
|
|
41
|
-
'"$(PODS_TARGET_SRCROOT)/cpp/include"',
|
|
42
34
|
'"$(PODS_TARGET_SRCROOT)/nitrogen/generated/shared/c++"',
|
|
43
35
|
'"$(PODS_TARGET_SRCROOT)/nitrogen/generated/ios"',
|
|
44
36
|
].join(' '),
|
|
@@ -59,6 +51,10 @@ Pod::Spec.new do |s|
|
|
|
59
51
|
s.frameworks = ['Metal', 'MetalPerformanceShaders', 'Accelerate', 'CoreML', 'CoreGraphics']
|
|
60
52
|
s.libraries = ['c++']
|
|
61
53
|
|
|
54
|
+
s.test_spec 'Tests' do |test_spec|
|
|
55
|
+
test_spec.source_files = 'ios/Tests/**/*.{swift}'
|
|
56
|
+
end
|
|
57
|
+
|
|
62
58
|
install_modules_dependencies(s)
|
|
63
59
|
end
|
|
64
60
|
|
|
@@ -1,72 +1,38 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# download-ios-frameworks.sh
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# releases. If the prebuilt asset is not available, falls back to building
|
|
6
|
-
# from source via Bazel (see build-ios-engine.sh).
|
|
7
|
-
#
|
|
8
|
-
# The XCFramework contains a static library compiled from the LiteRT-LM
|
|
9
|
-
# C engine (//c:engine Bazel target) for both device (arm64) and simulator
|
|
10
|
-
# (sim_arm64).
|
|
11
|
-
#
|
|
12
|
-
# Usage:
|
|
13
|
-
# ./scripts/download-ios-frameworks.sh
|
|
3
|
+
# Downloads the official prebuilt LiteRT-LM iOS framework (CLiteRTLM.xcframework)
|
|
4
|
+
# directly from the google-ai-edge/LiteRT-LM releases.
|
|
14
5
|
|
|
15
6
|
set -euo pipefail
|
|
16
7
|
|
|
17
8
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
18
9
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
19
10
|
OUTPUT_DIR="$PROJECT_ROOT/ios/Frameworks"
|
|
20
|
-
C_API_HEADER_DIR="$PROJECT_ROOT/cpp/include"
|
|
21
11
|
|
|
22
12
|
LITERT_LM_VERSION="$(node -e "console.log(require('$PROJECT_ROOT/package.json').litertLm.iosGitTag)")"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# Read version from package.json
|
|
26
|
-
PACKAGE_VERSION=$(node -e "console.log(require('$PROJECT_ROOT/package.json').version)" 2>/dev/null || echo "0.0.0")
|
|
27
|
-
GITHUB_REPO="hung-yueh/react-native-litert-lm"
|
|
28
|
-
ASSET_NAME="LiteRTLM-ios-frameworks.zip"
|
|
13
|
+
RELEASE_URL="https://github.com/google-ai-edge/LiteRT-LM/releases/download/${LITERT_LM_VERSION}/CLiteRTLM.xcframework.zip"
|
|
29
14
|
|
|
30
15
|
# Skip if already present
|
|
31
|
-
if [ -d "$OUTPUT_DIR
|
|
32
|
-
echo "[LiteRT-LM] iOS
|
|
16
|
+
if [ -d "$OUTPUT_DIR/CLiteRTLM.xcframework" ]; then
|
|
17
|
+
echo "[LiteRT-LM] iOS CLiteRTLM.xcframework already present, skipping download."
|
|
33
18
|
exit 0
|
|
34
19
|
fi
|
|
35
20
|
|
|
36
|
-
|
|
37
|
-
echo "[LiteRT-LM] Vendoring C API header..."
|
|
38
|
-
mkdir -p "$C_API_HEADER_DIR"
|
|
39
|
-
curl -fsSL -o "$C_API_HEADER_DIR/litert_lm_engine.h" \
|
|
40
|
-
"${GITHUB_RAW}/c/engine.h" 2>/dev/null || true
|
|
41
|
-
|
|
42
|
-
# ---- Try downloading prebuilt from our GitHub releases --------------------
|
|
43
|
-
RELEASE_URL="https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/${ASSET_NAME}"
|
|
44
|
-
|
|
45
|
-
echo "[LiteRT-LM] Attempting to download prebuilt iOS engine from:"
|
|
21
|
+
echo "[LiteRT-LM] Downloading prebuilt iOS engine from Google's release:"
|
|
46
22
|
echo " ${RELEASE_URL}"
|
|
47
23
|
|
|
24
|
+
mkdir -p "$OUTPUT_DIR"
|
|
48
25
|
TMP_ZIP="$PROJECT_ROOT/.ios-frameworks-tmp.zip"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
26
|
+
|
|
27
|
+
if curl -fsSL -o "$TMP_ZIP" "$RELEASE_URL"; then
|
|
28
|
+
echo "[LiteRT-LM] Download successful, extracting to $OUTPUT_DIR..."
|
|
29
|
+
rm -rf "$OUTPUT_DIR/CLiteRTLM.xcframework"
|
|
53
30
|
unzip -o -q "$TMP_ZIP" -d "$OUTPUT_DIR"
|
|
54
31
|
rm -f "$TMP_ZIP"
|
|
55
|
-
echo "[LiteRT-LM] ✅ iOS frameworks installed
|
|
32
|
+
echo "[LiteRT-LM] ✅ iOS frameworks successfully installed."
|
|
56
33
|
exit 0
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
rm -f "$TMP_ZIP"
|
|
60
|
-
echo "[LiteRT-LM] Prebuilt not available for v${PACKAGE_VERSION}."
|
|
61
|
-
|
|
62
|
-
# ---- Fall back to building from source ------------------------------------
|
|
63
|
-
echo "[LiteRT-LM] Falling back to building from source via Bazel..."
|
|
64
|
-
echo ""
|
|
65
|
-
|
|
66
|
-
if [ -x "$SCRIPT_DIR/build-ios-engine.sh" ]; then
|
|
67
|
-
exec "$SCRIPT_DIR/build-ios-engine.sh"
|
|
68
34
|
else
|
|
69
|
-
|
|
70
|
-
echo "
|
|
35
|
+
rm -f "$TMP_ZIP"
|
|
36
|
+
echo "Error: Failed to download LiteRT-LM iOS framework from ${RELEASE_URL}"
|
|
71
37
|
exit 1
|
|
72
38
|
fi
|
package/scripts/postinstall.js
CHANGED
|
@@ -108,8 +108,7 @@ async function main() {
|
|
|
108
108
|
|
|
109
109
|
log(`Error: Could not download iOS frameworks: ${err.message}`);
|
|
110
110
|
log('iOS builds will not work until frameworks are available.');
|
|
111
|
-
log('Run: ./scripts/download-ios-frameworks.sh to download manually
|
|
112
|
-
log(' or: ./scripts/build-ios-engine.sh to build from source.');
|
|
111
|
+
log('Run: ./scripts/download-ios-frameworks.sh to download manually.');
|
|
113
112
|
|
|
114
113
|
// Fail fast on macOS so users discover the problem now, not at Xcode link time.
|
|
115
114
|
// Skip SKIP_IOS_FRAMEWORK_DOWNLOAD is already checked above.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const mockLiteRTLM = {
|
|
2
|
+
isReady: jest.fn(() => false),
|
|
3
|
+
loadModel: jest.fn().mockResolvedValue(undefined),
|
|
4
|
+
sendMessage: jest.fn().mockResolvedValue("Mock response"),
|
|
5
|
+
sendMessageWithImage: jest.fn().mockResolvedValue("Mock vision response"),
|
|
6
|
+
downloadModel: jest.fn(async (url, fileName, onProgress) => {
|
|
7
|
+
onProgress?.(1.0);
|
|
8
|
+
return "/mock/path/model.litertlm";
|
|
9
|
+
}),
|
|
10
|
+
deleteModel: jest.fn().mockResolvedValue(undefined),
|
|
11
|
+
sendMessageWithAudio: jest.fn().mockResolvedValue("Mock audio response"),
|
|
12
|
+
sendMultimodalMessage: jest.fn().mockResolvedValue("Mock multimodal response"),
|
|
13
|
+
sendMessageAsync: jest.fn((msg, onToken) => {
|
|
14
|
+
onToken("Mock ", false);
|
|
15
|
+
onToken("token", true);
|
|
16
|
+
return Promise.resolve();
|
|
17
|
+
}),
|
|
18
|
+
getHistory: jest.fn(() => []),
|
|
19
|
+
resetConversation: jest.fn(),
|
|
20
|
+
getStats: jest.fn(() => ({
|
|
21
|
+
promptTokens: 10,
|
|
22
|
+
completionTokens: 20,
|
|
23
|
+
totalTokens: 30,
|
|
24
|
+
timeToFirstToken: 5,
|
|
25
|
+
totalTime: 50,
|
|
26
|
+
tokensPerSecond: 400,
|
|
27
|
+
})),
|
|
28
|
+
countTokens: jest.fn(() => -1),
|
|
29
|
+
getMemoryUsage: jest.fn(() => ({
|
|
30
|
+
nativeHeapBytes: 1000000,
|
|
31
|
+
residentBytes: 2000000,
|
|
32
|
+
availableMemoryBytes: 4000000,
|
|
33
|
+
isLowMemory: false,
|
|
34
|
+
})),
|
|
35
|
+
close: jest.fn(),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const NitroModules = {
|
|
39
|
+
createHybridObject: jest.fn((name: string) => {
|
|
40
|
+
if (name === "LiteRTLM") {
|
|
41
|
+
return mockLiteRTLM;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Mock not implemented for hybrid object: ${name}`);
|
|
44
|
+
}),
|
|
45
|
+
createNativeArrayBuffer: jest.fn((size: number) => {
|
|
46
|
+
return new ArrayBuffer(size);
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Configure React act environment
|
|
2
|
+
(global as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
3
|
+
|
|
4
|
+
import { useModel } from '../hooks';
|
|
5
|
+
import { mockLiteRTLM } from '../__mocks__/react-native-nitro-modules';
|
|
6
|
+
import TestRenderer from 'react-test-renderer';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
// Mock react-native
|
|
10
|
+
jest.mock('react-native', () => ({
|
|
11
|
+
Platform: {
|
|
12
|
+
OS: 'ios',
|
|
13
|
+
select: jest.fn((dict) => dict.ios),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Helper to render and test hooks using react-test-renderer
|
|
18
|
+
function renderHook<P, R>(callback: (props: P) => R, initialProps?: P) {
|
|
19
|
+
let result = { current: null as unknown as R };
|
|
20
|
+
|
|
21
|
+
const TestComponent = ({ props }: { props: P }) => {
|
|
22
|
+
result.current = callback(props);
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let renderer: TestRenderer.ReactTestRenderer;
|
|
27
|
+
TestRenderer.act(() => {
|
|
28
|
+
renderer = TestRenderer.create(React.createElement(TestComponent, { props: initialProps as P }));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const rerender = (newProps: P) => {
|
|
32
|
+
TestRenderer.act(() => {
|
|
33
|
+
renderer.update(React.createElement(TestComponent, { props: newProps }));
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const unmount = () => {
|
|
38
|
+
TestRenderer.act(() => {
|
|
39
|
+
renderer.unmount();
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { result, rerender, unmount };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('useModel React Hook Unit Tests', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.clearAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should initialize with correct default state and call loadModel automatically when autoLoad is true', async () => {
|
|
52
|
+
let hookResult: any;
|
|
53
|
+
|
|
54
|
+
await TestRenderer.act(async () => {
|
|
55
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(hookResult.result.current.isReady).toBe(true);
|
|
59
|
+
expect(hookResult.result.current.isGenerating).toBe(false);
|
|
60
|
+
expect(hookResult.result.current.downloadProgress).toBe(1); // loadModel completed
|
|
61
|
+
expect(hookResult.result.current.error).toBeNull();
|
|
62
|
+
expect(mockLiteRTLM.loadModel).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should not call loadModel automatically when autoLoad is false', async () => {
|
|
66
|
+
let hookResult: any;
|
|
67
|
+
|
|
68
|
+
await TestRenderer.act(async () => {
|
|
69
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: false }));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(hookResult.result.current.isReady).toBe(false);
|
|
73
|
+
expect(mockLiteRTLM.loadModel).not.toHaveBeenCalled();
|
|
74
|
+
|
|
75
|
+
// Call load manually
|
|
76
|
+
await TestRenderer.act(async () => {
|
|
77
|
+
await hookResult.result.current.load();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(hookResult.result.current.isReady).toBe(true);
|
|
81
|
+
expect(mockLiteRTLM.loadModel).toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle model load failure gracefully', async () => {
|
|
85
|
+
mockLiteRTLM.loadModel.mockRejectedValueOnce(new Error("Model load failed"));
|
|
86
|
+
let hookResult: any;
|
|
87
|
+
|
|
88
|
+
await TestRenderer.act(async () => {
|
|
89
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(hookResult.result.current.isReady).toBe(false);
|
|
93
|
+
expect(hookResult.result.current.error).toBe("Model load failed");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should generate text successfully and trigger memory summary update', async () => {
|
|
97
|
+
let hookResult: any;
|
|
98
|
+
|
|
99
|
+
await TestRenderer.act(async () => {
|
|
100
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', {
|
|
101
|
+
autoLoad: true,
|
|
102
|
+
enableMemoryTracking: true
|
|
103
|
+
}));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
let response = "";
|
|
107
|
+
await TestRenderer.act(async () => {
|
|
108
|
+
response = await hookResult.result.current.generate("Test prompt");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(response).toBe("Mock token");
|
|
112
|
+
expect(mockLiteRTLM.sendMessageAsync).toHaveBeenCalled();
|
|
113
|
+
expect(hookResult.result.current.memorySummary).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should reset conversation correctly', async () => {
|
|
117
|
+
let hookResult: any;
|
|
118
|
+
|
|
119
|
+
await TestRenderer.act(async () => {
|
|
120
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
hookResult.result.current.reset();
|
|
124
|
+
expect(mockLiteRTLM.resetConversation).toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should delete model file correctly', async () => {
|
|
128
|
+
let hookResult: any;
|
|
129
|
+
|
|
130
|
+
await TestRenderer.act(async () => {
|
|
131
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await TestRenderer.act(async () => {
|
|
135
|
+
await hookResult.result.current.deleteModel();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(mockLiteRTLM.deleteModel).toHaveBeenCalledWith('model.litertlm');
|
|
139
|
+
expect(hookResult.result.current.isReady).toBe(false);
|
|
140
|
+
expect(hookResult.result.current.downloadProgress).toBe(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should call close on unmount', async () => {
|
|
144
|
+
let hookResult: any;
|
|
145
|
+
|
|
146
|
+
await TestRenderer.act(async () => {
|
|
147
|
+
hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: false }));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
hookResult.unmount();
|
|
151
|
+
expect(mockLiteRTLM.close).toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createMemoryTracker, createNativeBuffer } from '../memoryTracker';
|
|
2
|
+
import { NitroModules } from 'react-native-nitro-modules';
|
|
3
|
+
|
|
4
|
+
describe('MemoryTracker Unit Tests', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
jest.clearAllMocks();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should allocate correct native-backed ArrayBuffer size on initialization', () => {
|
|
10
|
+
const tracker = createMemoryTracker(10);
|
|
11
|
+
expect(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
|
+
|
|
16
|
+
it('should record snapshots correctly and retrieve them', () => {
|
|
17
|
+
const tracker = createMemoryTracker(5);
|
|
18
|
+
const snapshot1 = {
|
|
19
|
+
timestamp: 1000,
|
|
20
|
+
nativeHeapBytes: 100,
|
|
21
|
+
residentBytes: 200,
|
|
22
|
+
availableMemoryBytes: 500,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
expect(tracker.record(snapshot1)).toBe(true);
|
|
26
|
+
expect(tracker.getSnapshotCount()).toBe(1);
|
|
27
|
+
expect(tracker.getLatestSnapshot()).toEqual(snapshot1);
|
|
28
|
+
|
|
29
|
+
const snapshots = tracker.getSnapshots();
|
|
30
|
+
expect(snapshots).toHaveLength(1);
|
|
31
|
+
expect(snapshots[0]).toEqual(snapshot1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should reject new snapshots and return false when capacity is reached', () => {
|
|
35
|
+
const tracker = createMemoryTracker(2);
|
|
36
|
+
|
|
37
|
+
expect(tracker.record({ timestamp: 1, nativeHeapBytes: 10, residentBytes: 20, availableMemoryBytes: 50 })).toBe(true);
|
|
38
|
+
expect(tracker.record({ timestamp: 2, nativeHeapBytes: 20, residentBytes: 30, availableMemoryBytes: 40 })).toBe(true);
|
|
39
|
+
expect(tracker.record({ timestamp: 3, nativeHeapBytes: 30, residentBytes: 40, availableMemoryBytes: 30 })).toBe(false);
|
|
40
|
+
|
|
41
|
+
expect(tracker.getSnapshotCount()).toBe(2);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should calculate correct peak resident memory size', () => {
|
|
45
|
+
const tracker = createMemoryTracker(5);
|
|
46
|
+
tracker.record({ timestamp: 1, nativeHeapBytes: 100, residentBytes: 150, availableMemoryBytes: 1000 });
|
|
47
|
+
tracker.record({ timestamp: 2, nativeHeapBytes: 120, residentBytes: 300, availableMemoryBytes: 1000 });
|
|
48
|
+
tracker.record({ timestamp: 3, nativeHeapBytes: 110, residentBytes: 200, availableMemoryBytes: 1000 });
|
|
49
|
+
|
|
50
|
+
expect(tracker.getPeakMemory()).toBe(300);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should calculate accurate memory summary statistics', () => {
|
|
54
|
+
const tracker = createMemoryTracker(5);
|
|
55
|
+
tracker.record({ timestamp: 1, nativeHeapBytes: 50, residentBytes: 100, availableMemoryBytes: 1000 });
|
|
56
|
+
tracker.record({ timestamp: 2, nativeHeapBytes: 150, residentBytes: 300, availableMemoryBytes: 800 });
|
|
57
|
+
tracker.record({ timestamp: 3, nativeHeapBytes: 100, residentBytes: 200, availableMemoryBytes: 900 });
|
|
58
|
+
|
|
59
|
+
const summary = tracker.getSummary();
|
|
60
|
+
expect(summary.snapshotCount).toBe(3);
|
|
61
|
+
expect(summary.peakResidentBytes).toBe(300);
|
|
62
|
+
expect(summary.averageResidentBytes).toBe(200); // (100 + 300 + 200) / 3
|
|
63
|
+
expect(summary.currentResidentBytes).toBe(200);
|
|
64
|
+
expect(summary.peakNativeHeapBytes).toBe(150);
|
|
65
|
+
expect(summary.currentNativeHeapBytes).toBe(100);
|
|
66
|
+
expect(summary.residentDeltaBytes).toBe(100); // currentRss(200) - firstRss(100)
|
|
67
|
+
expect(summary.trackerBufferSizeBytes).toBe(5 * 4 * 8);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should preserve buffer but reset internal state when reset() is called', () => {
|
|
71
|
+
const tracker = createMemoryTracker(5);
|
|
72
|
+
tracker.record({ timestamp: 1, nativeHeapBytes: 50, residentBytes: 100, availableMemoryBytes: 1000 });
|
|
73
|
+
|
|
74
|
+
expect(tracker.getSnapshotCount()).toBe(1);
|
|
75
|
+
tracker.reset();
|
|
76
|
+
expect(tracker.getSnapshotCount()).toBe(0);
|
|
77
|
+
expect(tracker.getLatestSnapshot()).toBeUndefined();
|
|
78
|
+
expect(tracker.getSnapshots()).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should allow standalone native ArrayBuffer allocation via createNativeBuffer', () => {
|
|
82
|
+
const size = 128;
|
|
83
|
+
const buffer = createNativeBuffer(size);
|
|
84
|
+
expect(NitroModules.createNativeArrayBuffer).toHaveBeenCalledWith(size);
|
|
85
|
+
expect(buffer.byteLength).toBe(size);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createLLM } from '../modelFactory';
|
|
2
|
+
import { mockLiteRTLM } from '../__mocks__/react-native-nitro-modules';
|
|
3
|
+
|
|
4
|
+
describe('modelFactory Security & Proxy Unit Tests', () => {
|
|
5
|
+
let llm: ReturnType<typeof createLLM>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
llm = createLLM({ enableMemoryTracking: true });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should block insecure HTTP downloads', async () => {
|
|
13
|
+
await expect(llm.loadModel('http://example.com/model.litertlm'))
|
|
14
|
+
.rejects.toThrow('Insecure HTTP URLs are not allowed for model downloads');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should allow secure HTTPS downloads and strip query parameters', async () => {
|
|
18
|
+
await llm.loadModel('https://example.com/model.litertlm?token=123');
|
|
19
|
+
|
|
20
|
+
expect(mockLiteRTLM.downloadModel).toHaveBeenCalledWith(
|
|
21
|
+
'https://example.com/model.litertlm?token=123',
|
|
22
|
+
'model.litertlm',
|
|
23
|
+
expect.any(Function)
|
|
24
|
+
);
|
|
25
|
+
expect(mockLiteRTLM.loadModel).toHaveBeenCalledWith('/mock/path/model.litertlm', undefined);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should throw an error for invalid model URL', async () => {
|
|
29
|
+
await expect(llm.loadModel('https://example.com/'))
|
|
30
|
+
.rejects.toThrow('Invalid model URL: https://example.com/');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should successfully proxy sendMessage and record memory metrics', async () => {
|
|
34
|
+
const response = await llm.sendMessage("Test prompt");
|
|
35
|
+
|
|
36
|
+
expect(response).toBe("Mock response");
|
|
37
|
+
expect(mockLiteRTLM.sendMessage).toHaveBeenCalledWith("Test prompt");
|
|
38
|
+
expect(mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
|
|
39
|
+
expect(llm.memoryTracker?.getSnapshotCount()).toBe(1); // sendMessage records one
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should successfully proxy resetConversation and record memory metrics', async () => {
|
|
43
|
+
await llm.resetConversation();
|
|
44
|
+
|
|
45
|
+
expect(mockLiteRTLM.resetConversation).toHaveBeenCalled();
|
|
46
|
+
expect(mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should successfully proxy sendMessageAsync and record memory metrics when done', async () => {
|
|
50
|
+
const onToken = jest.fn();
|
|
51
|
+
await llm.sendMessageAsync("Async prompt", onToken);
|
|
52
|
+
|
|
53
|
+
expect(onToken).toHaveBeenCalledWith("Mock ", false);
|
|
54
|
+
expect(onToken).toHaveBeenCalledWith("token", true);
|
|
55
|
+
expect(mockLiteRTLM.sendMessageAsync).toHaveBeenCalled();
|
|
56
|
+
expect(mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should successfully access memoryTracker and getSnapshots when memory tracking is enabled', () => {
|
|
60
|
+
expect(llm.memoryTracker).toBeDefined();
|
|
61
|
+
expect(llm.memoryTracker?.getCapacity()).toBe(256);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should not initialize memoryTracker when enableMemoryTracking option is false', () => {
|
|
65
|
+
const untrackedLLM = createLLM({ enableMemoryTracking: false });
|
|
66
|
+
expect(untrackedLLM.memoryTracker).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
package/src/hooks.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -20,13 +20,6 @@ export type {
|
|
|
20
20
|
MemoryUsage,
|
|
21
21
|
} from "./specs/LiteRTLM.nitro";
|
|
22
22
|
|
|
23
|
-
// Re-export template utilities
|
|
24
|
-
export type { ChatMessage } from "./templates";
|
|
25
|
-
export {
|
|
26
|
-
applyGemmaTemplate,
|
|
27
|
-
applyPhiTemplate,
|
|
28
|
-
applyLlamaTemplate,
|
|
29
|
-
} from "./templates";
|
|
30
23
|
|
|
31
24
|
// Re-export memory tracking utilities (uses NitroModules.createNativeArrayBuffer v0.35+)
|
|
32
25
|
export type {
|