react-native-litert-lm 0.1.1 → 0.2.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 +149 -31
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +307 -61
- package/cpp/HybridLiteRTLM.cpp +85 -31
- package/cpp/HybridLiteRTLM.hpp +4 -0
- package/cpp/include/stb_image.h +7988 -0
- package/lib/hooks.d.ts +16 -0
- package/lib/hooks.js +114 -0
- package/lib/index.d.ts +27 -2
- package/lib/index.js +50 -6
- package/lib/modelFactory.d.ts +5 -0
- package/lib/modelFactory.js +42 -0
- package/lib/specs/LiteRTLM.nitro.d.ts +19 -0
- package/lib/templates.d.ts +51 -0
- package/lib/templates.js +81 -0
- package/nitrogen/generated/android/LiteRTLMOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +33 -1
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +2 -0
- package/nitrogen/generated/android/c++/JLLMConfig.hpp +6 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +5 -2
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +2 -0
- package/nitrogen/generated/shared/c++/LLMConfig.hpp +7 -2
- package/package.json +1 -1
- package/src/hooks.ts +152 -0
- package/src/index.ts +41 -3
- package/src/modelFactory.ts +49 -0
- package/src/specs/LiteRTLM.nitro.ts +26 -0
- package/src/templates.ts +105 -0
package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_double.kt
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// Func_void_double.kt
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
package com.margelo.nitro.dev.litert.litertlm
|
|
9
|
+
|
|
10
|
+
import androidx.annotation.Keep
|
|
11
|
+
import com.facebook.jni.HybridData
|
|
12
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
13
|
+
import dalvik.annotation.optimization.FastNative
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Represents the JavaScript callback `(progress: number) => void`.
|
|
18
|
+
* This can be either implemented in C++ (in which case it might be a callback coming from JS),
|
|
19
|
+
* or in Kotlin/Java (in which case it is a native callback).
|
|
20
|
+
*/
|
|
21
|
+
@DoNotStrip
|
|
22
|
+
@Keep
|
|
23
|
+
@Suppress("ClassName", "RedundantUnitReturnType")
|
|
24
|
+
fun interface Func_void_double: (Double) -> Unit {
|
|
25
|
+
/**
|
|
26
|
+
* Call the given JS callback.
|
|
27
|
+
* @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted.
|
|
28
|
+
*/
|
|
29
|
+
@DoNotStrip
|
|
30
|
+
@Keep
|
|
31
|
+
override fun invoke(progress: Double): Unit
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Represents the JavaScript callback `(progress: number) => void`.
|
|
36
|
+
* This is implemented in C++, via a `std::function<...>`.
|
|
37
|
+
* The callback might be coming from JS.
|
|
38
|
+
*/
|
|
39
|
+
@DoNotStrip
|
|
40
|
+
@Keep
|
|
41
|
+
@Suppress(
|
|
42
|
+
"KotlinJniMissingFunction", "unused",
|
|
43
|
+
"RedundantSuppression", "RedundantUnitReturnType", "FunctionName",
|
|
44
|
+
"ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName",
|
|
45
|
+
)
|
|
46
|
+
class Func_void_double_cxx: Func_void_double {
|
|
47
|
+
@DoNotStrip
|
|
48
|
+
@Keep
|
|
49
|
+
private val mHybridData: HybridData
|
|
50
|
+
|
|
51
|
+
@DoNotStrip
|
|
52
|
+
@Keep
|
|
53
|
+
private constructor(hybridData: HybridData) {
|
|
54
|
+
mHybridData = hybridData
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@DoNotStrip
|
|
58
|
+
@Keep
|
|
59
|
+
override fun invoke(progress: Double): Unit
|
|
60
|
+
= invoke_cxx(progress)
|
|
61
|
+
|
|
62
|
+
@FastNative
|
|
63
|
+
private external fun invoke_cxx(progress: Double): Unit
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Represents the JavaScript callback `(progress: number) => void`.
|
|
68
|
+
* This is implemented in Java/Kotlin, via a `(Double) -> Unit`.
|
|
69
|
+
* The callback is always coming from native.
|
|
70
|
+
*/
|
|
71
|
+
@DoNotStrip
|
|
72
|
+
@Keep
|
|
73
|
+
@Suppress("ClassName", "RedundantUnitReturnType", "unused")
|
|
74
|
+
class Func_void_double_java(private val function: (Double) -> Unit): Func_void_double {
|
|
75
|
+
@DoNotStrip
|
|
76
|
+
@Keep
|
|
77
|
+
override fun invoke(progress: Double): Unit {
|
|
78
|
+
return this.function(progress)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -58,6 +58,19 @@ abstract class HybridLiteRTLMSpec: HybridObject() {
|
|
|
58
58
|
@Keep
|
|
59
59
|
abstract fun sendMessageWithImage(message: String, imagePath: String): Promise<String>
|
|
60
60
|
|
|
61
|
+
abstract fun downloadModel(url: String, fileName: String, onProgress: ((progress: Double) -> Unit)?): Promise<String>
|
|
62
|
+
|
|
63
|
+
@DoNotStrip
|
|
64
|
+
@Keep
|
|
65
|
+
private fun downloadModel_cxx(url: String, fileName: String, onProgress: Func_void_double?): Promise<String> {
|
|
66
|
+
val __result = downloadModel(url, fileName, onProgress?.let { it })
|
|
67
|
+
return __result
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@DoNotStrip
|
|
71
|
+
@Keep
|
|
72
|
+
abstract fun deleteModel(fileName: String): Promise<Unit>
|
|
73
|
+
|
|
61
74
|
@DoNotStrip
|
|
62
75
|
@Keep
|
|
63
76
|
abstract fun sendMessageWithAudio(message: String, audioPath: String): Promise<String>
|
package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt
CHANGED
|
@@ -17,6 +17,9 @@ import com.facebook.proguard.annotations.DoNotStrip
|
|
|
17
17
|
@DoNotStrip
|
|
18
18
|
@Keep
|
|
19
19
|
data class LLMConfig(
|
|
20
|
+
@DoNotStrip
|
|
21
|
+
@Keep
|
|
22
|
+
val systemPrompt: String?,
|
|
20
23
|
@DoNotStrip
|
|
21
24
|
@Keep
|
|
22
25
|
val backend: Backend?,
|
|
@@ -43,8 +46,8 @@ data class LLMConfig(
|
|
|
43
46
|
@Keep
|
|
44
47
|
@Suppress("unused")
|
|
45
48
|
@JvmStatic
|
|
46
|
-
private fun fromCpp(backend: Backend?, maxTokens: Double?, temperature: Double?, topK: Double?, topP: Double?): LLMConfig {
|
|
47
|
-
return LLMConfig(backend, maxTokens, temperature, topK, topP)
|
|
49
|
+
private fun fromCpp(systemPrompt: String?, backend: Backend?, maxTokens: Double?, temperature: Double?, topK: Double?, topP: Double?): LLMConfig {
|
|
50
|
+
return LLMConfig(systemPrompt, backend, maxTokens, temperature, topK, topP)
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
}
|
|
@@ -17,6 +17,8 @@ namespace margelo::nitro::litertlm {
|
|
|
17
17
|
prototype.registerHybridMethod("loadModel", &HybridLiteRTLMSpec::loadModel);
|
|
18
18
|
prototype.registerHybridMethod("sendMessage", &HybridLiteRTLMSpec::sendMessage);
|
|
19
19
|
prototype.registerHybridMethod("sendMessageWithImage", &HybridLiteRTLMSpec::sendMessageWithImage);
|
|
20
|
+
prototype.registerHybridMethod("downloadModel", &HybridLiteRTLMSpec::downloadModel);
|
|
21
|
+
prototype.registerHybridMethod("deleteModel", &HybridLiteRTLMSpec::deleteModel);
|
|
20
22
|
prototype.registerHybridMethod("sendMessageWithAudio", &HybridLiteRTLMSpec::sendMessageWithAudio);
|
|
21
23
|
prototype.registerHybridMethod("sendMessageAsync", &HybridLiteRTLMSpec::sendMessageAsync);
|
|
22
24
|
prototype.registerHybridMethod("getHistory", &HybridLiteRTLMSpec::getHistory);
|
|
@@ -63,6 +63,8 @@ namespace margelo::nitro::litertlm {
|
|
|
63
63
|
virtual std::shared_ptr<Promise<void>> loadModel(const std::string& modelPath, const std::optional<LLMConfig>& config) = 0;
|
|
64
64
|
virtual std::shared_ptr<Promise<std::string>> sendMessage(const std::string& message) = 0;
|
|
65
65
|
virtual std::shared_ptr<Promise<std::string>> sendMessageWithImage(const std::string& message, const std::string& imagePath) = 0;
|
|
66
|
+
virtual std::shared_ptr<Promise<std::string>> downloadModel(const std::string& url, const std::string& fileName, const std::optional<std::function<void(double /* progress */)>>& onProgress) = 0;
|
|
67
|
+
virtual std::shared_ptr<Promise<void>> deleteModel(const std::string& fileName) = 0;
|
|
66
68
|
virtual std::shared_ptr<Promise<std::string>> sendMessageWithAudio(const std::string& message, const std::string& audioPath) = 0;
|
|
67
69
|
virtual void sendMessageAsync(const std::string& message, const std::function<void(const std::string& /* token */, bool /* done */)>& onToken) = 0;
|
|
68
70
|
virtual std::vector<Message> getHistory() = 0;
|
|
@@ -31,8 +31,9 @@
|
|
|
31
31
|
// Forward declaration of `Backend` to properly resolve imports.
|
|
32
32
|
namespace margelo::nitro::litertlm { enum class Backend; }
|
|
33
33
|
|
|
34
|
-
#include
|
|
34
|
+
#include <string>
|
|
35
35
|
#include <optional>
|
|
36
|
+
#include "Backend.hpp"
|
|
36
37
|
|
|
37
38
|
namespace margelo::nitro::litertlm {
|
|
38
39
|
|
|
@@ -41,6 +42,7 @@ namespace margelo::nitro::litertlm {
|
|
|
41
42
|
*/
|
|
42
43
|
struct LLMConfig final {
|
|
43
44
|
public:
|
|
45
|
+
std::optional<std::string> systemPrompt SWIFT_PRIVATE;
|
|
44
46
|
std::optional<Backend> backend SWIFT_PRIVATE;
|
|
45
47
|
std::optional<double> maxTokens SWIFT_PRIVATE;
|
|
46
48
|
std::optional<double> temperature SWIFT_PRIVATE;
|
|
@@ -49,7 +51,7 @@ namespace margelo::nitro::litertlm {
|
|
|
49
51
|
|
|
50
52
|
public:
|
|
51
53
|
LLMConfig() = default;
|
|
52
|
-
explicit LLMConfig(std::optional<Backend> backend, std::optional<double> maxTokens, std::optional<double> temperature, std::optional<double> topK, std::optional<double> topP): backend(backend), maxTokens(maxTokens), temperature(temperature), topK(topK), topP(topP) {}
|
|
54
|
+
explicit LLMConfig(std::optional<std::string> systemPrompt, std::optional<Backend> backend, std::optional<double> maxTokens, std::optional<double> temperature, std::optional<double> topK, std::optional<double> topP): systemPrompt(systemPrompt), backend(backend), maxTokens(maxTokens), temperature(temperature), topK(topK), topP(topP) {}
|
|
53
55
|
|
|
54
56
|
public:
|
|
55
57
|
friend bool operator==(const LLMConfig& lhs, const LLMConfig& rhs) = default;
|
|
@@ -65,6 +67,7 @@ namespace margelo::nitro {
|
|
|
65
67
|
static inline margelo::nitro::litertlm::LLMConfig fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
|
|
66
68
|
jsi::Object obj = arg.asObject(runtime);
|
|
67
69
|
return margelo::nitro::litertlm::LLMConfig(
|
|
70
|
+
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "systemPrompt"))),
|
|
68
71
|
JSIConverter<std::optional<margelo::nitro::litertlm::Backend>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "backend"))),
|
|
69
72
|
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "maxTokens"))),
|
|
70
73
|
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "temperature"))),
|
|
@@ -74,6 +77,7 @@ namespace margelo::nitro {
|
|
|
74
77
|
}
|
|
75
78
|
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::litertlm::LLMConfig& arg) {
|
|
76
79
|
jsi::Object obj(runtime);
|
|
80
|
+
obj.setProperty(runtime, PropNameIDCache::get(runtime, "systemPrompt"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.systemPrompt));
|
|
77
81
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "backend"), JSIConverter<std::optional<margelo::nitro::litertlm::Backend>>::toJSI(runtime, arg.backend));
|
|
78
82
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "maxTokens"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.maxTokens));
|
|
79
83
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "temperature"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.temperature));
|
|
@@ -89,6 +93,7 @@ namespace margelo::nitro {
|
|
|
89
93
|
if (!nitro::isPlainObject(runtime, obj)) {
|
|
90
94
|
return false;
|
|
91
95
|
}
|
|
96
|
+
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "systemPrompt")))) return false;
|
|
92
97
|
if (!JSIConverter<std::optional<margelo::nitro::litertlm::Backend>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "backend")))) return false;
|
|
93
98
|
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "maxTokens")))) return false;
|
|
94
99
|
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "temperature")))) return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-litert-lm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "High-performance LLM inference for React Native using LiteRT-LM. Optimized for Gemma 3n and other on-device language models.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hugh Chen (https://github.com/hung-yueh)",
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
2
|
+
import { LiteRTLM, LLMConfig } from "./index";
|
|
3
|
+
import { createLLM } from "./modelFactory";
|
|
4
|
+
|
|
5
|
+
export interface UseModelConfig extends LLMConfig {
|
|
6
|
+
autoLoad?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UseModelResult {
|
|
10
|
+
model: LiteRTLM | null;
|
|
11
|
+
isReady: boolean;
|
|
12
|
+
isGenerating: boolean;
|
|
13
|
+
downloadProgress: number;
|
|
14
|
+
error: string | null;
|
|
15
|
+
generate: (prompt: string) => Promise<string>;
|
|
16
|
+
reset: () => void;
|
|
17
|
+
deleteModel: (fileName: string) => Promise<void>;
|
|
18
|
+
load: () => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useModel(
|
|
22
|
+
pathOrUrl: string,
|
|
23
|
+
config?: UseModelConfig,
|
|
24
|
+
): UseModelResult {
|
|
25
|
+
const modelRef = useRef<LiteRTLM | null>(null);
|
|
26
|
+
const [isReady, setIsReady] = useState(false);
|
|
27
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
28
|
+
const [downloadProgress, setDownloadProgress] = useState(0);
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
// Extract autoLoad (default true)
|
|
32
|
+
const autoLoad = config?.autoLoad ?? true;
|
|
33
|
+
|
|
34
|
+
// Initialize the model instance
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
modelRef.current = createLLM();
|
|
37
|
+
let isMounted = true;
|
|
38
|
+
|
|
39
|
+
// Cleanup on unmount
|
|
40
|
+
return () => {
|
|
41
|
+
isMounted = false;
|
|
42
|
+
try {
|
|
43
|
+
modelRef.current?.close();
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.warn("Failed to close model", e);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const load = useCallback(async () => {
|
|
51
|
+
setIsReady(false);
|
|
52
|
+
setError(null);
|
|
53
|
+
setDownloadProgress(0);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
let modelPath = pathOrUrl;
|
|
57
|
+
|
|
58
|
+
// Handle URL download manually to capture progress
|
|
59
|
+
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
60
|
+
const fileName = pathOrUrl.split("/").pop() || "model.bin";
|
|
61
|
+
|
|
62
|
+
if (modelRef.current) {
|
|
63
|
+
modelPath = await modelRef.current.downloadModel(
|
|
64
|
+
pathOrUrl,
|
|
65
|
+
fileName,
|
|
66
|
+
(progress) => {
|
|
67
|
+
setDownloadProgress(progress);
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (modelRef.current) {
|
|
74
|
+
// Create a clean config object for native loadModel (excluding autoLoad)
|
|
75
|
+
const nativeConfig: LLMConfig = { ...config };
|
|
76
|
+
delete (nativeConfig as any).autoLoad;
|
|
77
|
+
|
|
78
|
+
await modelRef.current.loadModel(modelPath, nativeConfig);
|
|
79
|
+
setIsReady(true);
|
|
80
|
+
}
|
|
81
|
+
} catch (e: any) {
|
|
82
|
+
setError(e.message || "Failed to load model");
|
|
83
|
+
console.error(e);
|
|
84
|
+
}
|
|
85
|
+
}, [pathOrUrl, config]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (autoLoad) {
|
|
89
|
+
load();
|
|
90
|
+
}
|
|
91
|
+
}, [autoLoad, load]);
|
|
92
|
+
|
|
93
|
+
const generate = useCallback(
|
|
94
|
+
async (prompt: string): Promise<string> => {
|
|
95
|
+
if (!modelRef.current || !isReady) {
|
|
96
|
+
throw new Error("Model not ready");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setIsGenerating(true);
|
|
100
|
+
try {
|
|
101
|
+
return new Promise<string>((resolve, reject) => {
|
|
102
|
+
let fullResponse = "";
|
|
103
|
+
try {
|
|
104
|
+
modelRef.current?.sendMessageAsync(
|
|
105
|
+
prompt,
|
|
106
|
+
(token: string, done: boolean) => {
|
|
107
|
+
fullResponse += token;
|
|
108
|
+
if (done) {
|
|
109
|
+
resolve(fullResponse);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
} catch (e: any) {
|
|
114
|
+
reject(e);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
} catch (e: any) {
|
|
118
|
+
setError(e.message || "Generation failed");
|
|
119
|
+
throw e;
|
|
120
|
+
} finally {
|
|
121
|
+
setIsGenerating(false);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
[isReady],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const reset = useCallback(() => {
|
|
128
|
+
if (modelRef.current) {
|
|
129
|
+
modelRef.current.resetConversation();
|
|
130
|
+
}
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
const deleteModel = useCallback(async (fileName: string): Promise<void> => {
|
|
134
|
+
if (modelRef.current) {
|
|
135
|
+
await modelRef.current.deleteModel(fileName);
|
|
136
|
+
setIsReady(false);
|
|
137
|
+
setDownloadProgress(0);
|
|
138
|
+
}
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
model: modelRef.current,
|
|
143
|
+
isReady,
|
|
144
|
+
isGenerating,
|
|
145
|
+
downloadProgress,
|
|
146
|
+
error,
|
|
147
|
+
generate,
|
|
148
|
+
reset,
|
|
149
|
+
deleteModel,
|
|
150
|
+
load,
|
|
151
|
+
};
|
|
152
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,16 @@ export type {
|
|
|
18
18
|
GenerationStats,
|
|
19
19
|
} from "./specs/LiteRTLM.nitro";
|
|
20
20
|
|
|
21
|
+
// Re-export template utilities
|
|
22
|
+
export type { ChatMessage } from "./templates";
|
|
23
|
+
export {
|
|
24
|
+
applyGemmaTemplate,
|
|
25
|
+
applyPhiTemplate,
|
|
26
|
+
applyLlamaTemplate,
|
|
27
|
+
} from "./templates";
|
|
28
|
+
|
|
29
|
+
export * from "./hooks";
|
|
30
|
+
|
|
21
31
|
/**
|
|
22
32
|
* Creates a new LiteRT-LM inference engine instance.
|
|
23
33
|
*
|
|
@@ -51,9 +61,7 @@ export type {
|
|
|
51
61
|
* llm.close();
|
|
52
62
|
* ```
|
|
53
63
|
*/
|
|
54
|
-
export
|
|
55
|
-
return NitroModules.createHybridObject<LiteRTLM>("LiteRTLM");
|
|
56
|
-
}
|
|
64
|
+
export { createLLM } from "./modelFactory";
|
|
57
65
|
|
|
58
66
|
/**
|
|
59
67
|
* Pre-defined model identifiers for common models.
|
|
@@ -123,3 +131,33 @@ export function checkBackendSupport(backend: Backend): string | undefined {
|
|
|
123
131
|
|
|
124
132
|
return undefined;
|
|
125
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if multimodal features (image/audio) are supported on the current platform.
|
|
137
|
+
* Returns an error message if not supported, undefined if OK.
|
|
138
|
+
*
|
|
139
|
+
* @returns Error message if multimodal is not supported, undefined if OK
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const error = checkMultimodalSupport();
|
|
144
|
+
* if (error) {
|
|
145
|
+
* console.warn(error);
|
|
146
|
+
* // Fall back to text-only
|
|
147
|
+
* } else {
|
|
148
|
+
* llm.sendMessageWithImage('Describe this', imagePath);
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export function checkMultimodalSupport(): string | undefined {
|
|
153
|
+
if (Platform.OS === "ios") {
|
|
154
|
+
return "Multimodal (image/audio) is not yet supported on iOS. LiteRT-LM iOS SDK is pending.";
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Download URL for the Gemma 3n E2B IT INT4 model.
|
|
161
|
+
*/
|
|
162
|
+
export const GEMMA_3N_E2B_IT_INT4 =
|
|
163
|
+
"https://litert.dev/gemma-3n-E2B-it-int4.litertlm";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NitroModules } from "react-native-nitro-modules";
|
|
2
|
+
import { LiteRTLM, LLMConfig } from "./specs/LiteRTLM.nitro";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a new LiteRT-LM inference engine instance.
|
|
6
|
+
*/
|
|
7
|
+
export function createLLM(): LiteRTLM {
|
|
8
|
+
const native = NitroModules.createHybridObject<LiteRTLM>("LiteRTLM");
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
...native,
|
|
12
|
+
loadModel: async (pathOrUrl: string, config?: LLMConfig) => {
|
|
13
|
+
let modelPath = pathOrUrl;
|
|
14
|
+
|
|
15
|
+
// Check if it's a URL
|
|
16
|
+
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
17
|
+
// Extract filename from URL
|
|
18
|
+
const fileName = pathOrUrl.split("/").pop();
|
|
19
|
+
if (!fileName) {
|
|
20
|
+
throw new Error(`Invalid model URL: ${pathOrUrl}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log(`Checking model at ${pathOrUrl}...`);
|
|
24
|
+
modelPath = await native.downloadModel(
|
|
25
|
+
pathOrUrl,
|
|
26
|
+
fileName,
|
|
27
|
+
(progress) => {
|
|
28
|
+
console.log(`Download progress: ${progress}`);
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
console.log(`Model downloaded to: ${modelPath}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return native.loadModel(modelPath, config);
|
|
35
|
+
},
|
|
36
|
+
// Bind valid methods to native instance
|
|
37
|
+
sendMessage: native.sendMessage.bind(native),
|
|
38
|
+
sendMessageAsync: native.sendMessageAsync.bind(native),
|
|
39
|
+
sendMessageWithImage: native.sendMessageWithImage.bind(native),
|
|
40
|
+
sendMessageWithAudio: native.sendMessageWithAudio.bind(native),
|
|
41
|
+
getHistory: native.getHistory.bind(native),
|
|
42
|
+
resetConversation: native.resetConversation.bind(native),
|
|
43
|
+
isReady: native.isReady.bind(native),
|
|
44
|
+
getStats: native.getStats.bind(native),
|
|
45
|
+
close: native.close.bind(native),
|
|
46
|
+
downloadModel: native.downloadModel.bind(native),
|
|
47
|
+
deleteModel: native.deleteModel.bind(native),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -21,6 +21,13 @@ export type Role = "user" | "model" | "system";
|
|
|
21
21
|
* Configuration options for loading an LLM.
|
|
22
22
|
*/
|
|
23
23
|
export interface LLMConfig {
|
|
24
|
+
/**
|
|
25
|
+
* System prompt to set the model's behavior.
|
|
26
|
+
* This is prepended to the conversation to guide model responses.
|
|
27
|
+
* @example "You are a helpful coding assistant."
|
|
28
|
+
*/
|
|
29
|
+
systemPrompt?: string;
|
|
30
|
+
|
|
24
31
|
/**
|
|
25
32
|
* Primary compute backend for text generation.
|
|
26
33
|
* - 'cpu': CPU inference (slower but always available)
|
|
@@ -138,6 +145,25 @@ export interface LiteRTLM extends HybridObject<{
|
|
|
138
145
|
*/
|
|
139
146
|
sendMessageWithImage(message: string, imagePath: string): Promise<string>;
|
|
140
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Download a model file from a URL.
|
|
150
|
+
* @param url URL to download from.
|
|
151
|
+
* @param fileName Filename to save as (in app's files directory).
|
|
152
|
+
* @param onProgress Callback for download progress (0.0 - 1.0).
|
|
153
|
+
* @returns Absolute path to the downloaded file.
|
|
154
|
+
*/
|
|
155
|
+
downloadModel(
|
|
156
|
+
url: string,
|
|
157
|
+
fileName: string,
|
|
158
|
+
onProgress?: (progress: number) => void,
|
|
159
|
+
): Promise<string>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Delete a downloaded model file.
|
|
163
|
+
* @param fileName Filename to delete (in app's files directory).
|
|
164
|
+
*/
|
|
165
|
+
deleteModel(fileName: string): Promise<void>;
|
|
166
|
+
|
|
141
167
|
/**
|
|
142
168
|
* Send a text message with audio (multimodal).
|
|
143
169
|
* @param message User message text.
|
package/src/templates.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt template utilities for different LLM families.
|
|
3
|
+
*
|
|
4
|
+
* LiteRT-LM's Conversation API may handle templates internally for some models,
|
|
5
|
+
* but these utilities give developers explicit control for custom workflows
|
|
6
|
+
* or when using models with different template formats.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { applyGemmaTemplate, ChatMessage } from 'react-native-litert-lm';
|
|
11
|
+
*
|
|
12
|
+
* const history: ChatMessage[] = [
|
|
13
|
+
* { role: 'user', content: 'What is React Native?' },
|
|
14
|
+
* { role: 'model', content: 'React Native is a framework for building...' },
|
|
15
|
+
* { role: 'user', content: 'How do I use hooks?' }
|
|
16
|
+
* ];
|
|
17
|
+
*
|
|
18
|
+
* const prompt = applyGemmaTemplate(history, 'You are a helpful coding assistant.');
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A message in a conversation.
|
|
24
|
+
*/
|
|
25
|
+
export type ChatMessage = {
|
|
26
|
+
role: "user" | "model" | "system";
|
|
27
|
+
content: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Apply Gemma chat template (Gemma 2, Gemma 3, Gemma 3n).
|
|
32
|
+
*
|
|
33
|
+
* @param history Array of previous messages
|
|
34
|
+
* @param systemPrompt Optional system prompt
|
|
35
|
+
* @returns Formatted prompt string
|
|
36
|
+
*/
|
|
37
|
+
export function applyGemmaTemplate(
|
|
38
|
+
history: ChatMessage[],
|
|
39
|
+
systemPrompt?: string,
|
|
40
|
+
): string {
|
|
41
|
+
let result = "";
|
|
42
|
+
|
|
43
|
+
if (systemPrompt) {
|
|
44
|
+
result += `<start_of_turn>system\n${systemPrompt}<end_of_turn>\n`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const m of history) {
|
|
48
|
+
result += `<start_of_turn>${m.role}\n${m.content}<end_of_turn>\n`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
result += "<start_of_turn>model\n";
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Apply Phi chat template (Phi-3, Phi-4).
|
|
57
|
+
*
|
|
58
|
+
* @param history Array of previous messages
|
|
59
|
+
* @param systemPrompt Optional system prompt
|
|
60
|
+
* @returns Formatted prompt string
|
|
61
|
+
*/
|
|
62
|
+
export function applyPhiTemplate(
|
|
63
|
+
history: ChatMessage[],
|
|
64
|
+
systemPrompt?: string,
|
|
65
|
+
): string {
|
|
66
|
+
let result = "";
|
|
67
|
+
|
|
68
|
+
if (systemPrompt) {
|
|
69
|
+
result += `<|system|>\n${systemPrompt}<|end|>\n`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const m of history) {
|
|
73
|
+
const role = m.role === "model" ? "assistant" : m.role;
|
|
74
|
+
result += `<|${role}|>\n${m.content}<|end|>\n`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
result += "<|assistant|>\n";
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Apply Llama 3 chat template.
|
|
83
|
+
*
|
|
84
|
+
* @param history Array of previous messages
|
|
85
|
+
* @param systemPrompt Optional system prompt
|
|
86
|
+
* @returns Formatted prompt string
|
|
87
|
+
*/
|
|
88
|
+
export function applyLlamaTemplate(
|
|
89
|
+
history: ChatMessage[],
|
|
90
|
+
systemPrompt?: string,
|
|
91
|
+
): string {
|
|
92
|
+
let result = "<|begin_of_text|>";
|
|
93
|
+
|
|
94
|
+
if (systemPrompt) {
|
|
95
|
+
result += `<|start_header_id|>system<|end_header_id|>\n\n${systemPrompt}<|eot_id|>`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const m of history) {
|
|
99
|
+
const role = m.role === "model" ? "assistant" : m.role;
|
|
100
|
+
result += `<|start_header_id|>${role}<|end_header_id|>\n\n${m.content}<|eot_id|>`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
result += "<|start_header_id|>assistant<|end_header_id|>\n\n";
|
|
104
|
+
return result;
|
|
105
|
+
}
|