local-llm-rn 1.0.2 → 1.0.3
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/android/src/main/cpp/LocalLLM.cpp +124 -54
- package/android/src/main/java/com/hilum/localllm/LocalLLMModule.kt +39 -10
- package/cpp/hilum/hilum_llm.cpp +55 -20
- package/cpp/hilum/hilum_llm.h +6 -4
- package/ios/LocalLLM.mm +135 -30
- package/local-llm-rn.podspec +6 -2
- package/package.json +23 -3
- package/src/NativeLocalLLM.ts +1 -0
- package/src/cache.ts +22 -2
- package/src/model-manager.ts +14 -2
- package/src/native-bridge.ts +60 -19
- package/src/rn-downloader.ts +10 -5
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
#include <android/log.h>
|
|
11
11
|
|
|
12
12
|
#include <string>
|
|
13
|
+
#include <memory>
|
|
13
14
|
#include <unordered_map>
|
|
14
15
|
#include <mutex>
|
|
15
16
|
#include <atomic>
|
|
@@ -51,6 +52,12 @@ static std::unordered_map<std::string, hilum_context *> g_contexts;
|
|
|
51
52
|
static std::unordered_map<std::string, hilum_mtmd *> g_mtmd_contexts;
|
|
52
53
|
static std::unordered_map<std::string, hilum_emb_ctx *> g_emb_contexts;
|
|
53
54
|
|
|
55
|
+
using ModelOwner = std::shared_ptr<hilum_model>;
|
|
56
|
+
static std::unordered_map<std::string, ModelOwner> g_model_owners;
|
|
57
|
+
static std::unordered_map<std::string, std::vector<ModelOwner>> g_context_model_owners;
|
|
58
|
+
static std::unordered_map<std::string, ModelOwner> g_mtmd_model_owners;
|
|
59
|
+
static std::unordered_map<std::string, ModelOwner> g_emb_model_owners;
|
|
60
|
+
|
|
54
61
|
// ── Log state ────────────────────────────────────────────────────────────────
|
|
55
62
|
|
|
56
63
|
static std::atomic<bool> g_log_events_enabled{false};
|
|
@@ -60,6 +67,29 @@ static std::mutex g_log_mutex;
|
|
|
60
67
|
|
|
61
68
|
// ── JNI helpers ──────────────────────────────────────────────────────────────
|
|
62
69
|
|
|
70
|
+
static JNIEnv *get_jni_env(JavaVM *jvm, bool &detach) {
|
|
71
|
+
detach = false;
|
|
72
|
+
if (!jvm) {
|
|
73
|
+
LOGE("Cannot access JNI environment: JavaVM is null");
|
|
74
|
+
return nullptr;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
JNIEnv *env = nullptr;
|
|
78
|
+
const jint status = jvm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6);
|
|
79
|
+
if (status == JNI_OK) return env;
|
|
80
|
+
if (status != JNI_EDETACHED) {
|
|
81
|
+
LOGE("Cannot access JNI environment: GetEnv returned %d", status);
|
|
82
|
+
return nullptr;
|
|
83
|
+
}
|
|
84
|
+
if (jvm->AttachCurrentThread(&env, nullptr) != JNI_OK) {
|
|
85
|
+
LOGE("Cannot access JNI environment: AttachCurrentThread failed");
|
|
86
|
+
return nullptr;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
detach = true;
|
|
90
|
+
return env;
|
|
91
|
+
}
|
|
92
|
+
|
|
63
93
|
static std::string jstring_to_std(JNIEnv *env, jstring jstr) {
|
|
64
94
|
if (!jstr) return "";
|
|
65
95
|
const char *chars = env->GetStringUTFChars(jstr, nullptr);
|
|
@@ -339,6 +369,7 @@ JNI_FN(nativeLoadModel)(JNIEnv *env, jobject thiz, jstring path, jobject options
|
|
|
339
369
|
{
|
|
340
370
|
std::lock_guard<std::mutex> lock(g_mutex);
|
|
341
371
|
g_models[modelId] = model;
|
|
372
|
+
g_model_owners[modelId] = ModelOwner(model, hilum_model_free);
|
|
342
373
|
}
|
|
343
374
|
LOGI("Model loaded: %s", modelId.c_str());
|
|
344
375
|
return std_to_jstring(env, modelId);
|
|
@@ -356,11 +387,15 @@ JNI_FN(nativeGetModelSize)(JNIEnv *env, jobject thiz, jstring modelId) {
|
|
|
356
387
|
JNIEXPORT void JNICALL
|
|
357
388
|
JNI_FN(nativeFreeModel)(JNIEnv *env, jobject thiz, jstring modelId) {
|
|
358
389
|
std::string id = jstring_to_std(env, modelId);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
390
|
+
ModelOwner owner;
|
|
391
|
+
{
|
|
392
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
393
|
+
g_models.erase(id);
|
|
394
|
+
auto it = g_model_owners.find(id);
|
|
395
|
+
if (it != g_model_owners.end()) {
|
|
396
|
+
owner = std::move(it->second);
|
|
397
|
+
g_model_owners.erase(it);
|
|
398
|
+
}
|
|
364
399
|
}
|
|
365
400
|
}
|
|
366
401
|
|
|
@@ -373,6 +408,7 @@ JNI_FN(nativeCreateContext)(JNIEnv *env, jobject thiz, jstring modelId, jobject
|
|
|
373
408
|
auto it = g_models.find(mid);
|
|
374
409
|
if (it == g_models.end()) return std_to_jstring(env, "");
|
|
375
410
|
|
|
411
|
+
std::vector<ModelOwner> owners{g_model_owners[mid]};
|
|
376
412
|
hilum_context_params params = hilum_context_default_params();
|
|
377
413
|
if (options) {
|
|
378
414
|
params.n_ctx = (uint32_t)get_int_from_map(env, options, "n_ctx", params.n_ctx);
|
|
@@ -386,7 +422,10 @@ JNI_FN(nativeCreateContext)(JNIEnv *env, jobject thiz, jstring modelId, jobject
|
|
|
386
422
|
std::string draftId = get_string_from_map(env, options, "draft_model_id");
|
|
387
423
|
if (!draftId.empty()) {
|
|
388
424
|
auto dit = g_models.find(draftId);
|
|
389
|
-
if (dit != g_models.end())
|
|
425
|
+
if (dit != g_models.end()) {
|
|
426
|
+
params.draft_model = dit->second;
|
|
427
|
+
owners.push_back(g_model_owners[draftId]);
|
|
428
|
+
}
|
|
390
429
|
}
|
|
391
430
|
}
|
|
392
431
|
|
|
@@ -396,6 +435,7 @@ JNI_FN(nativeCreateContext)(JNIEnv *env, jobject thiz, jstring modelId, jobject
|
|
|
396
435
|
|
|
397
436
|
std::string ctxId = generate_uuid();
|
|
398
437
|
g_contexts[ctxId] = ctx;
|
|
438
|
+
g_context_model_owners[ctxId] = std::move(owners);
|
|
399
439
|
return std_to_jstring(env, ctxId);
|
|
400
440
|
}
|
|
401
441
|
|
|
@@ -411,12 +451,22 @@ JNI_FN(nativeGetContextSize)(JNIEnv *env, jobject thiz, jstring contextId) {
|
|
|
411
451
|
JNIEXPORT void JNICALL
|
|
412
452
|
JNI_FN(nativeFreeContext)(JNIEnv *env, jobject thiz, jstring contextId) {
|
|
413
453
|
std::string id = jstring_to_std(env, contextId);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
g_contexts.
|
|
454
|
+
hilum_context *context = nullptr;
|
|
455
|
+
std::vector<ModelOwner> owners;
|
|
456
|
+
{
|
|
457
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
458
|
+
auto it = g_contexts.find(id);
|
|
459
|
+
if (it != g_contexts.end()) {
|
|
460
|
+
context = it->second;
|
|
461
|
+
g_contexts.erase(it);
|
|
462
|
+
}
|
|
463
|
+
auto owner = g_context_model_owners.find(id);
|
|
464
|
+
if (owner != g_context_model_owners.end()) {
|
|
465
|
+
owners = std::move(owner->second);
|
|
466
|
+
g_context_model_owners.erase(owner);
|
|
467
|
+
}
|
|
419
468
|
}
|
|
469
|
+
if (context) hilum_context_free(context);
|
|
420
470
|
}
|
|
421
471
|
|
|
422
472
|
// ── Warmup ───────────────────────────────────────────────────────────────────
|
|
@@ -638,12 +688,9 @@ JNI_FN(nativeStartStream)(JNIEnv *env, jobject thiz, jstring modelId, jstring co
|
|
|
638
688
|
[](const char *token, int32_t token_len, void *ud) -> bool {
|
|
639
689
|
auto *s = static_cast<StreamState *>(ud);
|
|
640
690
|
|
|
641
|
-
JNIEnv *env;
|
|
642
691
|
bool detach = false;
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
detach = true;
|
|
646
|
-
}
|
|
692
|
+
JNIEnv *env = get_jni_env(s->jvm, detach);
|
|
693
|
+
if (!env) return false;
|
|
647
694
|
|
|
648
695
|
jclass cls = env->GetObjectClass(s->moduleRef);
|
|
649
696
|
jmethodID emitMethod = env->GetMethodID(cls, "emitToken",
|
|
@@ -823,6 +870,7 @@ JNI_FN(nativeLoadProjector)(JNIEnv *env, jobject thiz, jstring modelId,
|
|
|
823
870
|
|
|
824
871
|
std::string mtmdId = generate_uuid();
|
|
825
872
|
g_mtmd_contexts[mtmdId] = mtmd;
|
|
873
|
+
g_mtmd_model_owners[mtmdId] = g_model_owners[mid];
|
|
826
874
|
return std_to_jstring(env, mtmdId);
|
|
827
875
|
}
|
|
828
876
|
|
|
@@ -838,12 +886,22 @@ JNI_FN(nativeSupportVision)(JNIEnv *env, jobject thiz, jstring mtmdId) {
|
|
|
838
886
|
JNIEXPORT void JNICALL
|
|
839
887
|
JNI_FN(nativeFreeMtmdContext)(JNIEnv *env, jobject thiz, jstring mtmdId) {
|
|
840
888
|
std::string id = jstring_to_std(env, mtmdId);
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
g_mtmd_contexts.
|
|
889
|
+
hilum_mtmd *mtmd = nullptr;
|
|
890
|
+
ModelOwner owner;
|
|
891
|
+
{
|
|
892
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
893
|
+
auto it = g_mtmd_contexts.find(id);
|
|
894
|
+
if (it != g_mtmd_contexts.end()) {
|
|
895
|
+
mtmd = it->second;
|
|
896
|
+
g_mtmd_contexts.erase(it);
|
|
897
|
+
}
|
|
898
|
+
auto modelOwner = g_mtmd_model_owners.find(id);
|
|
899
|
+
if (modelOwner != g_mtmd_model_owners.end()) {
|
|
900
|
+
owner = std::move(modelOwner->second);
|
|
901
|
+
g_mtmd_model_owners.erase(modelOwner);
|
|
902
|
+
}
|
|
846
903
|
}
|
|
904
|
+
if (mtmd) hilum_mtmd_free(mtmd);
|
|
847
905
|
}
|
|
848
906
|
|
|
849
907
|
JNIEXPORT jstring JNICALL
|
|
@@ -960,12 +1018,9 @@ JNI_FN(nativeStartStreamVision)(JNIEnv *env, jobject thiz, jstring modelId,
|
|
|
960
1018
|
[](const char *token, int32_t token_len, void *ud) -> bool {
|
|
961
1019
|
auto *s = static_cast<StreamState *>(ud);
|
|
962
1020
|
|
|
963
|
-
JNIEnv *env;
|
|
964
1021
|
bool detach = false;
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
detach = true;
|
|
968
|
-
}
|
|
1022
|
+
JNIEnv *env = get_jni_env(s->jvm, detach);
|
|
1023
|
+
if (!env) return false;
|
|
969
1024
|
|
|
970
1025
|
jclass cls = env->GetObjectClass(s->moduleRef);
|
|
971
1026
|
jmethodID emitMethod = env->GetMethodID(cls, "emitToken",
|
|
@@ -1043,9 +1098,31 @@ JNI_FN(nativeCreateEmbeddingContext)(JNIEnv *env, jobject thiz, jstring modelId,
|
|
|
1043
1098
|
|
|
1044
1099
|
std::string ctxId = generate_uuid();
|
|
1045
1100
|
g_emb_contexts[ctxId] = ectx;
|
|
1101
|
+
g_emb_model_owners[ctxId] = g_model_owners[mid];
|
|
1046
1102
|
return std_to_jstring(env, ctxId);
|
|
1047
1103
|
}
|
|
1048
1104
|
|
|
1105
|
+
JNIEXPORT void JNICALL
|
|
1106
|
+
JNI_FN(nativeFreeEmbeddingContext)(JNIEnv *env, jobject thiz, jstring contextId) {
|
|
1107
|
+
std::string id = jstring_to_std(env, contextId);
|
|
1108
|
+
hilum_emb_ctx *context = nullptr;
|
|
1109
|
+
ModelOwner owner;
|
|
1110
|
+
{
|
|
1111
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
1112
|
+
auto it = g_emb_contexts.find(id);
|
|
1113
|
+
if (it != g_emb_contexts.end()) {
|
|
1114
|
+
context = it->second;
|
|
1115
|
+
g_emb_contexts.erase(it);
|
|
1116
|
+
}
|
|
1117
|
+
auto modelOwner = g_emb_model_owners.find(id);
|
|
1118
|
+
if (modelOwner != g_emb_model_owners.end()) {
|
|
1119
|
+
owner = std::move(modelOwner->second);
|
|
1120
|
+
g_emb_model_owners.erase(modelOwner);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (context) hilum_emb_context_free(context);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1049
1126
|
JNIEXPORT jobject JNICALL
|
|
1050
1127
|
JNI_FN(nativeEmbed)(JNIEnv *env, jobject thiz, jstring contextId, jstring modelId,
|
|
1051
1128
|
jobject tokens) {
|
|
@@ -1174,10 +1251,10 @@ JNI_FN(nativeStartBatch)(JNIEnv *env, jobject thiz, jstring modelId, jstring con
|
|
|
1174
1251
|
if (mi == g_models.end() || ci == g_contexts.end()) {
|
|
1175
1252
|
jclass cls = env->GetObjectClass(thiz);
|
|
1176
1253
|
jmethodID emitMethod = env->GetMethodID(cls, "emitBatchToken",
|
|
1177
|
-
"(Ljava/lang/String;ILjava/lang/String;ZLjava/lang/String;)V");
|
|
1254
|
+
"(Ljava/lang/String;ILjava/lang/String;ZLjava/lang/String;Ljava/lang/String;)V");
|
|
1178
1255
|
jstring jcid = std_to_jstring(env, cid);
|
|
1179
1256
|
jstring jerr = env->NewStringUTF("Not found");
|
|
1180
|
-
env->CallVoidMethod(thiz, emitMethod, jcid, -1, nullptr, JNI_TRUE, jerr);
|
|
1257
|
+
env->CallVoidMethod(thiz, emitMethod, jcid, -1, nullptr, JNI_TRUE, nullptr, jerr);
|
|
1181
1258
|
env->DeleteLocalRef(jcid);
|
|
1182
1259
|
env->DeleteLocalRef(jerr);
|
|
1183
1260
|
env->DeleteLocalRef(cls);
|
|
@@ -1208,32 +1285,29 @@ JNI_FN(nativeStartBatch)(JNIEnv *env, jobject thiz, jstring modelId, jstring con
|
|
|
1208
1285
|
};
|
|
1209
1286
|
BatchState *state = new BatchState{moduleRef, cid, g_jvm};
|
|
1210
1287
|
|
|
1211
|
-
hilum_generate_batch(model, ctx, prompt_ptrs.data(), n_seqs, gc.params,
|
|
1288
|
+
hilum_error batchErr = hilum_generate_batch(model, ctx, prompt_ptrs.data(), n_seqs, gc.params,
|
|
1212
1289
|
[](hilum_batch_event event, void *ud) -> bool {
|
|
1213
1290
|
auto *s = static_cast<BatchState *>(ud);
|
|
1214
1291
|
|
|
1215
|
-
JNIEnv *env;
|
|
1216
1292
|
bool detach = false;
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
detach = true;
|
|
1220
|
-
}
|
|
1293
|
+
JNIEnv *env = get_jni_env(s->jvm, detach);
|
|
1294
|
+
if (!env) return false;
|
|
1221
1295
|
|
|
1222
1296
|
jclass cls = env->GetObjectClass(s->moduleRef);
|
|
1223
1297
|
jmethodID emitMethod = env->GetMethodID(cls, "emitBatchToken",
|
|
1224
|
-
"(Ljava/lang/String;ILjava/lang/String;ZLjava/lang/String;)V");
|
|
1298
|
+
"(Ljava/lang/String;ILjava/lang/String;ZLjava/lang/String;Ljava/lang/String;)V");
|
|
1225
1299
|
jstring jcid = env->NewStringUTF(s->ctxId.c_str());
|
|
1226
1300
|
|
|
1227
1301
|
if (event.done) {
|
|
1228
1302
|
jstring reason = event.finish_reason
|
|
1229
1303
|
? env->NewStringUTF(event.finish_reason) : env->NewStringUTF("stop");
|
|
1230
1304
|
env->CallVoidMethod(s->moduleRef, emitMethod, jcid, event.seq_index,
|
|
1231
|
-
nullptr, JNI_TRUE, reason);
|
|
1305
|
+
nullptr, JNI_TRUE, reason, nullptr);
|
|
1232
1306
|
env->DeleteLocalRef(reason);
|
|
1233
1307
|
} else {
|
|
1234
1308
|
jstring jtok = env->NewStringUTF(std::string(event.token, event.token_len).c_str());
|
|
1235
1309
|
env->CallVoidMethod(s->moduleRef, emitMethod, jcid, event.seq_index,
|
|
1236
|
-
jtok, JNI_FALSE, nullptr);
|
|
1310
|
+
jtok, JNI_FALSE, nullptr, nullptr);
|
|
1237
1311
|
env->DeleteLocalRef(jtok);
|
|
1238
1312
|
}
|
|
1239
1313
|
env->DeleteLocalRef(jcid);
|
|
@@ -1242,6 +1316,17 @@ JNI_FN(nativeStartBatch)(JNIEnv *env, jobject thiz, jstring modelId, jstring con
|
|
|
1242
1316
|
if (detach) s->jvm->DetachCurrentThread();
|
|
1243
1317
|
return true;
|
|
1244
1318
|
}, state);
|
|
1319
|
+
if (batchErr != HILUM_OK) {
|
|
1320
|
+
jclass cls = env->GetObjectClass(thiz);
|
|
1321
|
+
jmethodID emitMethod = env->GetMethodID(cls, "emitBatchToken",
|
|
1322
|
+
"(Ljava/lang/String;ILjava/lang/String;ZLjava/lang/String;Ljava/lang/String;)V");
|
|
1323
|
+
jstring jcid = std_to_jstring(env, cid);
|
|
1324
|
+
jstring jerr = env->NewStringUTF(hilum_error_str(batchErr));
|
|
1325
|
+
env->CallVoidMethod(thiz, emitMethod, jcid, -1, nullptr, JNI_TRUE, nullptr, jerr);
|
|
1326
|
+
env->DeleteLocalRef(jcid);
|
|
1327
|
+
env->DeleteLocalRef(jerr);
|
|
1328
|
+
env->DeleteLocalRef(cls);
|
|
1329
|
+
}
|
|
1245
1330
|
env->DeleteGlobalRef(moduleRef);
|
|
1246
1331
|
delete state;
|
|
1247
1332
|
}
|
|
@@ -1297,12 +1382,9 @@ JNI_FN(nativeEnableLogEvents)(JNIEnv *env, jobject thiz, jboolean enabled) {
|
|
|
1297
1382
|
hilum_log_set([](hilum_log_level level, const char *text, void *) {
|
|
1298
1383
|
if (!g_log_events_enabled.load(std::memory_order_relaxed)) return;
|
|
1299
1384
|
|
|
1300
|
-
JNIEnv *env;
|
|
1301
1385
|
bool detach = false;
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
detach = true;
|
|
1305
|
-
}
|
|
1386
|
+
JNIEnv *env = get_jni_env(g_jvm, detach);
|
|
1387
|
+
if (!env) return;
|
|
1306
1388
|
|
|
1307
1389
|
std::lock_guard<std::mutex> lock(g_log_mutex);
|
|
1308
1390
|
if (g_module_ref) {
|
|
@@ -1327,16 +1409,4 @@ JNI_FN(nativeEnableLogEvents)(JNIEnv *env, jobject thiz, jboolean enabled) {
|
|
|
1327
1409
|
}
|
|
1328
1410
|
}
|
|
1329
1411
|
|
|
1330
|
-
// ── Downloads (handled in Kotlin, JNI just provides storage path) ────────────
|
|
1331
|
-
|
|
1332
|
-
JNIEXPORT jstring JNICALL
|
|
1333
|
-
JNI_FN(nativeGetModelStoragePath)(JNIEnv *env, jobject thiz, jstring filesDir) {
|
|
1334
|
-
std::string dir = jstring_to_std(env, filesDir);
|
|
1335
|
-
std::string path = dir + "/local-llm/models";
|
|
1336
|
-
// Create directory if needed
|
|
1337
|
-
std::string cmd = "mkdir -p " + path;
|
|
1338
|
-
system(cmd.c_str());
|
|
1339
|
-
return std_to_jstring(env, path);
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
1412
|
} // extern "C"
|
|
@@ -21,12 +21,16 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
21
21
|
|
|
22
22
|
/** Allowed root for file I/O — prevents reading/writing arbitrary paths. */
|
|
23
23
|
private val allowedStorageRoot: String by lazy {
|
|
24
|
-
File(reactApplicationContext.filesDir, "local-llm").
|
|
24
|
+
File(reactApplicationContext.filesDir, "local-llm").canonicalPath
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
private fun isPathAllowed(path: String): Boolean {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
return try {
|
|
29
|
+
val resolved = File(path).canonicalPath
|
|
30
|
+
resolved == allowedStorageRoot || resolved.startsWith(allowedStorageRoot + File.separator)
|
|
31
|
+
} catch (_: Exception) {
|
|
32
|
+
false
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
init {
|
|
@@ -50,13 +54,14 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
50
54
|
|
|
51
55
|
@Suppress("unused")
|
|
52
56
|
fun emitBatchToken(contextId: String, seqIndex: Int, token: String?,
|
|
53
|
-
done: Boolean, finishReason: String?) {
|
|
57
|
+
done: Boolean, finishReason: String?, error: String?) {
|
|
54
58
|
val params = Arguments.createMap().apply {
|
|
55
59
|
putString("contextId", contextId)
|
|
56
60
|
putInt("seqIndex", seqIndex)
|
|
57
61
|
putBoolean("done", done)
|
|
58
62
|
if (token != null) putString("token", token)
|
|
59
63
|
if (finishReason != null) putString("finishReason", finishReason)
|
|
64
|
+
if (error != null) putString("error", error)
|
|
60
65
|
}
|
|
61
66
|
sendEvent("onBatchToken", params)
|
|
62
67
|
}
|
|
@@ -155,7 +160,9 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
155
160
|
override fun getModelSize(modelId: String): Double = nativeGetModelSize(modelId)
|
|
156
161
|
|
|
157
162
|
@ReactMethod
|
|
158
|
-
override fun freeModel(modelId: String) {
|
|
163
|
+
override fun freeModel(modelId: String) {
|
|
164
|
+
executor.execute { nativeFreeModel(modelId) }
|
|
165
|
+
}
|
|
159
166
|
|
|
160
167
|
// ── Context lifecycle ───────────────────────────────────────────────────
|
|
161
168
|
|
|
@@ -167,7 +174,9 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
167
174
|
override fun getContextSize(contextId: String): Double = nativeGetContextSize(contextId).toDouble()
|
|
168
175
|
|
|
169
176
|
@ReactMethod
|
|
170
|
-
override fun freeContext(contextId: String) {
|
|
177
|
+
override fun freeContext(contextId: String) {
|
|
178
|
+
executor.execute { nativeFreeContext(contextId) }
|
|
179
|
+
}
|
|
171
180
|
|
|
172
181
|
// ── Warmup ──────────────────────────────────────────────────────────────
|
|
173
182
|
|
|
@@ -298,7 +307,9 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
298
307
|
override fun supportVision(mtmdId: String): Boolean = nativeSupportVision(mtmdId)
|
|
299
308
|
|
|
300
309
|
@ReactMethod
|
|
301
|
-
override fun freeMtmdContext(mtmdId: String) {
|
|
310
|
+
override fun freeMtmdContext(mtmdId: String) {
|
|
311
|
+
executor.execute { nativeFreeMtmdContext(mtmdId) }
|
|
312
|
+
}
|
|
302
313
|
|
|
303
314
|
@ReactMethod
|
|
304
315
|
override fun generateVision(modelId: String, contextId: String, mtmdId: String,
|
|
@@ -340,6 +351,11 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
340
351
|
override fun createEmbeddingContext(modelId: String, options: ReadableMap): String =
|
|
341
352
|
nativeCreateEmbeddingContext(modelId, options.toHashMap())
|
|
342
353
|
|
|
354
|
+
@ReactMethod
|
|
355
|
+
override fun freeEmbeddingContext(contextId: String) {
|
|
356
|
+
executor.execute { nativeFreeEmbeddingContext(contextId) }
|
|
357
|
+
}
|
|
358
|
+
|
|
343
359
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
344
360
|
override fun embed(contextId: String, modelId: String, tokens: ReadableArray): WritableArray {
|
|
345
361
|
val tokenList = ArrayList<Int>()
|
|
@@ -402,6 +418,10 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
402
418
|
private val activeDownloads = java.util.concurrent.ConcurrentHashMap<String, Thread>()
|
|
403
419
|
|
|
404
420
|
private fun executeDownload(url: String, destPath: String, resumeFrom: Long) {
|
|
421
|
+
if (!isPathAllowed(destPath)) {
|
|
422
|
+
emitDownloadError(url, "Destination is outside local-llm storage", false)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
405
425
|
val thread = Thread {
|
|
406
426
|
try {
|
|
407
427
|
val destFile = File(destPath)
|
|
@@ -472,6 +492,10 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
472
492
|
|
|
473
493
|
@ReactMethod
|
|
474
494
|
override fun resumeDownload(url: String, destPath: String) {
|
|
495
|
+
if (!isPathAllowed(destPath)) {
|
|
496
|
+
emitDownloadError(url, "Destination is outside local-llm storage", false)
|
|
497
|
+
return
|
|
498
|
+
}
|
|
475
499
|
val destFile = File(destPath)
|
|
476
500
|
val existingBytes = if (destFile.exists()) destFile.length() else 0L
|
|
477
501
|
executeDownload(url, destPath, existingBytes)
|
|
@@ -512,10 +536,11 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
512
536
|
}
|
|
513
537
|
|
|
514
538
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
515
|
-
override fun fileExists(path: String): Boolean = File(path).exists()
|
|
539
|
+
override fun fileExists(path: String): Boolean = isPathAllowed(path) && File(path).exists()
|
|
516
540
|
|
|
517
541
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
518
|
-
override fun getFileSize(path: String): Double =
|
|
542
|
+
override fun getFileSize(path: String): Double =
|
|
543
|
+
if (isPathAllowed(path)) File(path).length().toDouble() else 0.0
|
|
519
544
|
|
|
520
545
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
521
546
|
override fun readTextFile(path: String): String? {
|
|
@@ -546,6 +571,10 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
546
571
|
override fun sha256File(path: String, promise: Promise) {
|
|
547
572
|
executor.execute {
|
|
548
573
|
try {
|
|
574
|
+
if (!isPathAllowed(path)) {
|
|
575
|
+
promise.reject("E_INVALID_PATH", "Path is outside local-llm storage: $path")
|
|
576
|
+
return@execute
|
|
577
|
+
}
|
|
549
578
|
val file = File(path)
|
|
550
579
|
if (!file.exists()) {
|
|
551
580
|
promise.reject("E_FILE_NOT_FOUND", "File not found: $path")
|
|
@@ -612,6 +641,7 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
612
641
|
private external fun nativeGetEmbeddingDimension(modelId: String): Int
|
|
613
642
|
private external fun nativeCreateEmbeddingContext(modelId: String,
|
|
614
643
|
options: HashMap<String, Any?>): String
|
|
644
|
+
private external fun nativeFreeEmbeddingContext(contextId: String)
|
|
615
645
|
private external fun nativeEmbed(contextId: String, modelId: String,
|
|
616
646
|
tokens: ArrayList<Int>): List<Any>
|
|
617
647
|
private external fun nativeEmbedBatch(contextId: String, modelId: String,
|
|
@@ -623,5 +653,4 @@ class LocalLLMModule(reactContext: ReactApplicationContext) :
|
|
|
623
653
|
options: HashMap<String, Any?>)
|
|
624
654
|
private external fun nativeSetLogLevel(level: Int)
|
|
625
655
|
private external fun nativeEnableLogEvents(enabled: Boolean)
|
|
626
|
-
private external fun nativeGetModelStoragePath(filesDir: String): String
|
|
627
656
|
}
|
package/cpp/hilum/hilum_llm.cpp
CHANGED
|
@@ -313,14 +313,25 @@ static void evict_sampler_cache(llama_context * ctx) {
|
|
|
313
313
|
|
|
314
314
|
/* ── Logging ───────────────────────────────────────────────────────────────── */
|
|
315
315
|
|
|
316
|
+
static std::mutex g_log_mutex;
|
|
317
|
+
static std::mutex g_log_set_mutex;
|
|
316
318
|
static hilum_log_callback g_log_cb = nullptr;
|
|
317
319
|
static void * g_log_ud = nullptr;
|
|
318
320
|
static std::atomic<int> g_log_min_level{0};
|
|
319
321
|
|
|
320
322
|
static void native_log_callback(enum ggml_log_level level, const char * text, void * /*ud*/) {
|
|
321
|
-
if (!g_log_cb) return;
|
|
322
323
|
if (static_cast<int>(level) < g_log_min_level.load(std::memory_order_relaxed)) return;
|
|
323
|
-
|
|
324
|
+
|
|
325
|
+
hilum_log_callback callback;
|
|
326
|
+
void * user_data;
|
|
327
|
+
{
|
|
328
|
+
std::lock_guard<std::mutex> lock(g_log_mutex);
|
|
329
|
+
callback = g_log_cb;
|
|
330
|
+
user_data = g_log_ud;
|
|
331
|
+
}
|
|
332
|
+
if (callback) {
|
|
333
|
+
callback(static_cast<hilum_log_level>(level), text, user_data);
|
|
334
|
+
}
|
|
324
335
|
}
|
|
325
336
|
|
|
326
337
|
/* ── Helpers ───────────────────────────────────────────────────────────────── */
|
|
@@ -391,8 +402,15 @@ HILUM_API const char * hilum_backend_info(void) {
|
|
|
391
402
|
return llama_print_system_info();
|
|
392
403
|
}
|
|
393
404
|
|
|
405
|
+
#define HILUM_STRINGIFY_INNER(value) #value
|
|
406
|
+
#define HILUM_STRINGIFY(value) HILUM_STRINGIFY_INNER(value)
|
|
407
|
+
|
|
394
408
|
HILUM_API const char * hilum_backend_version(void) {
|
|
395
|
-
return "hilum-llm
|
|
409
|
+
return "hilum-llm v"
|
|
410
|
+
HILUM_STRINGIFY(HILUM_API_VERSION_MAJOR) "."
|
|
411
|
+
HILUM_STRINGIFY(HILUM_API_VERSION_MINOR) "."
|
|
412
|
+
HILUM_STRINGIFY(HILUM_API_VERSION_PATCH)
|
|
413
|
+
" (hilum-local-llm-engine)";
|
|
396
414
|
}
|
|
397
415
|
|
|
398
416
|
HILUM_API uint32_t hilum_api_version(void) {
|
|
@@ -1725,6 +1743,7 @@ HILUM_API hilum_error hilum_generate_batch(
|
|
|
1725
1743
|
|
|
1726
1744
|
// 2. Create one sampler per sequence
|
|
1727
1745
|
std::vector<llama_sampler *> samplers(n_prompts);
|
|
1746
|
+
std::vector<llama_token> first_tokens(n_prompts, LLAMA_TOKEN_NULL);
|
|
1728
1747
|
const SamplerParams batch_sampler_params = to_sampler_params(params);
|
|
1729
1748
|
for (int32_t i = 0; i < n_prompts; i++) {
|
|
1730
1749
|
samplers[i] = create_sampler(
|
|
@@ -1754,6 +1773,7 @@ HILUM_API hilum_error hilum_generate_batch(
|
|
|
1754
1773
|
for (size_t offset = 0; offset < entries.size(); offset += n_batch_size) {
|
|
1755
1774
|
size_t chunk_end = std::min(entries.size(), offset + static_cast<size_t>(n_batch_size));
|
|
1756
1775
|
int32_t chunk_size = static_cast<int32_t>(chunk_end - offset);
|
|
1776
|
+
std::vector<std::pair<int32_t, int32_t>> logit_entries;
|
|
1757
1777
|
|
|
1758
1778
|
llama_batch batch = llama_batch_init(chunk_size, 0, n_prompts);
|
|
1759
1779
|
for (int32_t i = 0; i < chunk_size; i++) {
|
|
@@ -1763,24 +1783,28 @@ HILUM_API hilum_error hilum_generate_batch(
|
|
|
1763
1783
|
batch.n_seq_id[i] = 1;
|
|
1764
1784
|
batch.seq_id[i][0] = e.seq_id;
|
|
1765
1785
|
|
|
1786
|
+
batch.logits[i] = e.logits ? 1 : 0;
|
|
1766
1787
|
if (e.logits) {
|
|
1767
|
-
|
|
1768
|
-
for (size_t j = offset + i + 1; j < entries.size(); j++) {
|
|
1769
|
-
if (entries[j].seq_id == e.seq_id) { is_last = false; break; }
|
|
1770
|
-
}
|
|
1771
|
-
batch.logits[i] = is_last ? 1 : 0;
|
|
1772
|
-
} else {
|
|
1773
|
-
batch.logits[i] = 0;
|
|
1788
|
+
logit_entries.emplace_back(static_cast<int32_t>(e.seq_id), i);
|
|
1774
1789
|
}
|
|
1775
1790
|
}
|
|
1776
1791
|
batch.n_tokens = chunk_size;
|
|
1777
1792
|
|
|
1778
1793
|
int32_t ret = llama_decode(ctx->llm, batch);
|
|
1779
|
-
llama_batch_free(batch);
|
|
1780
1794
|
if (ret != 0) {
|
|
1795
|
+
llama_batch_free(batch);
|
|
1781
1796
|
for (auto * s : samplers) llama_sampler_free(s);
|
|
1782
1797
|
return HILUM_ERR_DECODE_FAILED;
|
|
1783
1798
|
}
|
|
1799
|
+
|
|
1800
|
+
// llama_decode only retains logits from the most recent call. Sample
|
|
1801
|
+
// every sequence whose terminal prompt token was decoded in this
|
|
1802
|
+
// chunk before a later chunk can replace those output rows.
|
|
1803
|
+
for (const auto & [seq, batch_index] : logit_entries) {
|
|
1804
|
+
first_tokens[seq] = llama_sampler_sample(
|
|
1805
|
+
samplers[seq], ctx->llm, batch_index);
|
|
1806
|
+
}
|
|
1807
|
+
llama_batch_free(batch);
|
|
1784
1808
|
}
|
|
1785
1809
|
}
|
|
1786
1810
|
|
|
@@ -1791,7 +1815,11 @@ HILUM_API hilum_error hilum_generate_batch(
|
|
|
1791
1815
|
|
|
1792
1816
|
// First token for each sequence
|
|
1793
1817
|
for (int32_t s = 0; s < n_prompts; s++) {
|
|
1794
|
-
llama_token token =
|
|
1818
|
+
llama_token token = first_tokens[s];
|
|
1819
|
+
if (token == LLAMA_TOKEN_NULL) {
|
|
1820
|
+
for (auto * sampler : samplers) llama_sampler_free(sampler);
|
|
1821
|
+
return HILUM_ERR_DECODE_FAILED;
|
|
1822
|
+
}
|
|
1795
1823
|
if (token == eos) {
|
|
1796
1824
|
seq_done[s] = true;
|
|
1797
1825
|
n_active--;
|
|
@@ -1813,6 +1841,11 @@ HILUM_API hilum_error hilum_generate_batch(
|
|
|
1813
1841
|
n_active--;
|
|
1814
1842
|
hilum_batch_event done_ev{s, nullptr, 0, true, "cancelled"};
|
|
1815
1843
|
callback(done_ev, user_data);
|
|
1844
|
+
} else if (seq_generated[s] >= params.max_tokens) {
|
|
1845
|
+
seq_done[s] = true;
|
|
1846
|
+
n_active--;
|
|
1847
|
+
hilum_batch_event done_ev{s, nullptr, 0, true, "length"};
|
|
1848
|
+
callback(done_ev, user_data);
|
|
1816
1849
|
}
|
|
1817
1850
|
}
|
|
1818
1851
|
|
|
@@ -1839,7 +1872,10 @@ HILUM_API hilum_error hilum_generate_batch(
|
|
|
1839
1872
|
|
|
1840
1873
|
int32_t ret = llama_decode(ctx->llm, batch);
|
|
1841
1874
|
llama_batch_free(batch);
|
|
1842
|
-
if (ret != 0)
|
|
1875
|
+
if (ret != 0) {
|
|
1876
|
+
for (auto * sampler : samplers) llama_sampler_free(sampler);
|
|
1877
|
+
return HILUM_ERR_DECODE_FAILED;
|
|
1878
|
+
}
|
|
1843
1879
|
|
|
1844
1880
|
for (int32_t i = 0; i < batch_count; i++) {
|
|
1845
1881
|
int32_t s = active_seqs[i];
|
|
@@ -1939,14 +1975,13 @@ HILUM_API hilum_error hilum_quantize(
|
|
|
1939
1975
|
/* ── Logging ───────────────────────────────────────────────────────────────── */
|
|
1940
1976
|
|
|
1941
1977
|
HILUM_API void hilum_log_set(hilum_log_callback callback, void * user_data) {
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
if (callback) {
|
|
1948
|
-
llama_log_set(native_log_callback, nullptr);
|
|
1978
|
+
std::lock_guard<std::mutex> set_lock(g_log_set_mutex);
|
|
1979
|
+
{
|
|
1980
|
+
std::lock_guard<std::mutex> lock(g_log_mutex);
|
|
1981
|
+
g_log_cb = callback;
|
|
1982
|
+
g_log_ud = user_data;
|
|
1949
1983
|
}
|
|
1984
|
+
llama_log_set(callback ? native_log_callback : nullptr, nullptr);
|
|
1950
1985
|
}
|
|
1951
1986
|
|
|
1952
1987
|
HILUM_API void hilum_log_set_level(hilum_log_level min_level) {
|
package/cpp/hilum/hilum_llm.h
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
#define HILUM_API_VERSION_MAJOR 1
|
|
32
32
|
#define HILUM_API_VERSION_MINOR 0
|
|
33
|
-
#define HILUM_API_VERSION_PATCH
|
|
33
|
+
#define HILUM_API_VERSION_PATCH 1
|
|
34
34
|
#define HILUM_API_VERSION \
|
|
35
35
|
((uint32_t)(((HILUM_API_VERSION_MAJOR) << 16) | ((HILUM_API_VERSION_MINOR) << 8) | (HILUM_API_VERSION_PATCH)))
|
|
36
36
|
|
|
@@ -38,14 +38,16 @@
|
|
|
38
38
|
#include <stdbool.h>
|
|
39
39
|
#include <stddef.h>
|
|
40
40
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
41
|
+
#if defined(_WIN32) && defined(HILUM_SHARED)
|
|
42
|
+
# if defined(HILUM_BUILD)
|
|
43
43
|
# define HILUM_API __declspec(dllexport)
|
|
44
44
|
# else
|
|
45
45
|
# define HILUM_API __declspec(dllimport)
|
|
46
46
|
# endif
|
|
47
|
-
#
|
|
47
|
+
#elif !defined(_WIN32)
|
|
48
48
|
# define HILUM_API __attribute__((visibility("default")))
|
|
49
|
+
#else
|
|
50
|
+
# define HILUM_API
|
|
49
51
|
#endif
|
|
50
52
|
|
|
51
53
|
#ifdef __cplusplus
|
package/ios/LocalLLM.mm
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
#include <string>
|
|
18
18
|
#include <unordered_map>
|
|
19
|
+
#include <memory>
|
|
19
20
|
#include <mutex>
|
|
20
21
|
#include <atomic>
|
|
21
22
|
|
|
@@ -25,10 +26,6 @@
|
|
|
25
26
|
#import <os/proc.h>
|
|
26
27
|
#import <CommonCrypto/CommonDigest.h>
|
|
27
28
|
|
|
28
|
-
#ifdef RCT_NEW_ARCH_ENABLED
|
|
29
|
-
#import <memory>
|
|
30
|
-
#endif
|
|
31
|
-
|
|
32
29
|
// ── UUID generation ──────────────────────────────────────────────────────────
|
|
33
30
|
|
|
34
31
|
static NSString *generateUUID() {
|
|
@@ -50,9 +47,11 @@ static NSString *allowedStorageRoot() {
|
|
|
50
47
|
/// Returns YES if the path is under the allowed storage root (Application Support/local-llm/).
|
|
51
48
|
/// This prevents callers from reading/writing arbitrary filesystem locations.
|
|
52
49
|
static BOOL isPathAllowed(NSString *path) {
|
|
53
|
-
NSString *resolved = [path stringByStandardizingPath];
|
|
54
|
-
NSString *root = [allowedStorageRoot() stringByStandardizingPath]
|
|
55
|
-
|
|
50
|
+
NSString *resolved = [[path stringByStandardizingPath] stringByResolvingSymlinksInPath];
|
|
51
|
+
NSString *root = [[allowedStorageRoot() stringByStandardizingPath]
|
|
52
|
+
stringByResolvingSymlinksInPath];
|
|
53
|
+
NSString *rootPrefix = [root stringByAppendingString:@"/"];
|
|
54
|
+
return [resolved isEqualToString:root] || [resolved hasPrefix:rootPrefix];
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
// ── Handle maps ──────────────────────────────────────────────────────────────
|
|
@@ -64,6 +63,12 @@ static std::unordered_map<std::string, hilum_context *> g_contexts;
|
|
|
64
63
|
static std::unordered_map<std::string, hilum_mtmd *> g_mtmd_contexts;
|
|
65
64
|
static std::unordered_map<std::string, hilum_emb_ctx *> g_emb_contexts;
|
|
66
65
|
|
|
66
|
+
using ModelOwner = std::shared_ptr<hilum_model>;
|
|
67
|
+
static std::unordered_map<std::string, ModelOwner> g_model_owners;
|
|
68
|
+
static std::unordered_map<std::string, std::vector<ModelOwner>> g_context_model_owners;
|
|
69
|
+
static std::unordered_map<std::string, ModelOwner> g_mtmd_model_owners;
|
|
70
|
+
static std::unordered_map<std::string, ModelOwner> g_emb_model_owners;
|
|
71
|
+
|
|
67
72
|
// ── Log state ────────────────────────────────────────────────────────────────
|
|
68
73
|
|
|
69
74
|
static std::atomic<bool> g_log_events_enabled{false};
|
|
@@ -303,7 +308,9 @@ RCT_EXPORT_METHOD(loadModel:(NSString *)path
|
|
|
303
308
|
NSString *modelId = generateUUID();
|
|
304
309
|
{
|
|
305
310
|
std::lock_guard<std::mutex> lock(g_mutex);
|
|
306
|
-
|
|
311
|
+
const std::string id = [modelId UTF8String];
|
|
312
|
+
g_models[id] = model;
|
|
313
|
+
g_model_owners[id] = ModelOwner(model, hilum_model_free);
|
|
307
314
|
}
|
|
308
315
|
resolve(modelId);
|
|
309
316
|
});
|
|
@@ -317,12 +324,19 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getModelSize:(NSString *)modelId) {
|
|
|
317
324
|
}
|
|
318
325
|
|
|
319
326
|
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(freeModel:(NSString *)modelId) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
327
|
+
dispatch_async(inference_queue(), ^{
|
|
328
|
+
ModelOwner owner;
|
|
329
|
+
{
|
|
330
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
331
|
+
const std::string id = [modelId UTF8String];
|
|
332
|
+
g_models.erase(id);
|
|
333
|
+
auto it = g_model_owners.find(id);
|
|
334
|
+
if (it != g_model_owners.end()) {
|
|
335
|
+
owner = std::move(it->second);
|
|
336
|
+
g_model_owners.erase(it);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
326
340
|
return nil;
|
|
327
341
|
}
|
|
328
342
|
|
|
@@ -354,7 +368,15 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(createContext:(NSString *)modelId
|
|
|
354
368
|
if (err != HILUM_OK) return @"";
|
|
355
369
|
|
|
356
370
|
NSString *ctxId = generateUUID();
|
|
357
|
-
|
|
371
|
+
const std::string contextKey = [ctxId UTF8String];
|
|
372
|
+
g_contexts[contextKey] = ctx;
|
|
373
|
+
auto & owners = g_context_model_owners[contextKey];
|
|
374
|
+
owners.push_back(g_model_owners[[modelId UTF8String]]);
|
|
375
|
+
if (options[@"draft_model_id"]) {
|
|
376
|
+
const std::string draftKey = [options[@"draft_model_id"] UTF8String];
|
|
377
|
+
auto owner = g_model_owners.find(draftKey);
|
|
378
|
+
if (owner != g_model_owners.end()) owners.push_back(owner->second);
|
|
379
|
+
}
|
|
358
380
|
return ctxId;
|
|
359
381
|
}
|
|
360
382
|
|
|
@@ -366,12 +388,27 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getContextSize:(NSString *)contextId) {
|
|
|
366
388
|
}
|
|
367
389
|
|
|
368
390
|
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(freeContext:(NSString *)contextId) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
391
|
+
dispatch_async(inference_queue(), ^{
|
|
392
|
+
hilum_context *context = nullptr;
|
|
393
|
+
std::vector<ModelOwner> owners;
|
|
394
|
+
{
|
|
395
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
396
|
+
const std::string id = [contextId UTF8String];
|
|
397
|
+
auto it = g_contexts.find(id);
|
|
398
|
+
if (it != g_contexts.end()) {
|
|
399
|
+
context = it->second;
|
|
400
|
+
g_contexts.erase(it);
|
|
401
|
+
}
|
|
402
|
+
auto owner = g_context_model_owners.find(id);
|
|
403
|
+
if (owner != g_context_model_owners.end()) {
|
|
404
|
+
owners = std::move(owner->second);
|
|
405
|
+
g_context_model_owners.erase(owner);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (context) {
|
|
409
|
+
hilum_context_free(context);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
375
412
|
return nil;
|
|
376
413
|
}
|
|
377
414
|
|
|
@@ -663,7 +700,9 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(loadProjector:(NSString *)modelId
|
|
|
663
700
|
if (err != HILUM_OK) return @"";
|
|
664
701
|
|
|
665
702
|
NSString *mtmdId = generateUUID();
|
|
666
|
-
|
|
703
|
+
const std::string mtmdKey = [mtmdId UTF8String];
|
|
704
|
+
g_mtmd_contexts[mtmdKey] = mtmd;
|
|
705
|
+
g_mtmd_model_owners[mtmdKey] = g_model_owners[[modelId UTF8String]];
|
|
667
706
|
return mtmdId;
|
|
668
707
|
}
|
|
669
708
|
|
|
@@ -675,12 +714,27 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(supportVision:(NSString *)mtmdId) {
|
|
|
675
714
|
}
|
|
676
715
|
|
|
677
716
|
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(freeMtmdContext:(NSString *)mtmdId) {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
717
|
+
dispatch_async(inference_queue(), ^{
|
|
718
|
+
hilum_mtmd *mtmd = nullptr;
|
|
719
|
+
ModelOwner owner;
|
|
720
|
+
{
|
|
721
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
722
|
+
const std::string id = [mtmdId UTF8String];
|
|
723
|
+
auto it = g_mtmd_contexts.find(id);
|
|
724
|
+
if (it != g_mtmd_contexts.end()) {
|
|
725
|
+
mtmd = it->second;
|
|
726
|
+
g_mtmd_contexts.erase(it);
|
|
727
|
+
}
|
|
728
|
+
auto modelOwner = g_mtmd_model_owners.find(id);
|
|
729
|
+
if (modelOwner != g_mtmd_model_owners.end()) {
|
|
730
|
+
owner = std::move(modelOwner->second);
|
|
731
|
+
g_mtmd_model_owners.erase(modelOwner);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (mtmd) {
|
|
735
|
+
hilum_mtmd_free(mtmd);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
684
738
|
return nil;
|
|
685
739
|
}
|
|
686
740
|
|
|
@@ -848,10 +902,37 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(createEmbeddingContext:(NSString *)modelI
|
|
|
848
902
|
if (err != HILUM_OK) return @"";
|
|
849
903
|
|
|
850
904
|
NSString *ctxId = generateUUID();
|
|
851
|
-
|
|
905
|
+
const std::string contextKey = [ctxId UTF8String];
|
|
906
|
+
g_emb_contexts[contextKey] = ectx;
|
|
907
|
+
g_emb_model_owners[contextKey] = g_model_owners[[modelId UTF8String]];
|
|
852
908
|
return ctxId;
|
|
853
909
|
}
|
|
854
910
|
|
|
911
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(freeEmbeddingContext:(NSString *)contextId) {
|
|
912
|
+
dispatch_async(inference_queue(), ^{
|
|
913
|
+
hilum_emb_ctx *context = nullptr;
|
|
914
|
+
ModelOwner owner;
|
|
915
|
+
{
|
|
916
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
917
|
+
const std::string id = [contextId UTF8String];
|
|
918
|
+
auto it = g_emb_contexts.find(id);
|
|
919
|
+
if (it != g_emb_contexts.end()) {
|
|
920
|
+
context = it->second;
|
|
921
|
+
g_emb_contexts.erase(it);
|
|
922
|
+
}
|
|
923
|
+
auto modelOwner = g_emb_model_owners.find(id);
|
|
924
|
+
if (modelOwner != g_emb_model_owners.end()) {
|
|
925
|
+
owner = std::move(modelOwner->second);
|
|
926
|
+
g_emb_model_owners.erase(modelOwner);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (context) {
|
|
930
|
+
hilum_emb_context_free(context);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
return nil;
|
|
934
|
+
}
|
|
935
|
+
|
|
855
936
|
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(embed:(NSString *)contextId
|
|
856
937
|
modelId:(NSString *)modelId
|
|
857
938
|
tokens:(NSArray<NSNumber *> *)tokens) {
|
|
@@ -957,7 +1038,7 @@ RCT_EXPORT_METHOD(startBatch:(NSString *)modelId
|
|
|
957
1038
|
};
|
|
958
1039
|
BatchState state = { self, contextId };
|
|
959
1040
|
|
|
960
|
-
hilum_generate_batch(model, ctx, prompt_ptrs.data(), n_seqs, gc.params,
|
|
1041
|
+
hilum_error batchError = hilum_generate_batch(model, ctx, prompt_ptrs.data(), n_seqs, gc.params,
|
|
961
1042
|
[](hilum_batch_event event, void *ud) -> bool {
|
|
962
1043
|
auto *s = static_cast<BatchState *>(ud);
|
|
963
1044
|
if (event.done) {
|
|
@@ -977,6 +1058,12 @@ RCT_EXPORT_METHOD(startBatch:(NSString *)modelId
|
|
|
977
1058
|
}
|
|
978
1059
|
return true;
|
|
979
1060
|
}, &state);
|
|
1061
|
+
if (batchError != HILUM_OK) {
|
|
1062
|
+
[self sendEventWithName:@"onBatchToken" body:@{
|
|
1063
|
+
@"contextId": contextId, @"done": @YES, @"error": @(hilum_error_str(batchError)),
|
|
1064
|
+
@"seqIndex": @(-1)
|
|
1065
|
+
}];
|
|
1066
|
+
}
|
|
980
1067
|
});
|
|
981
1068
|
}
|
|
982
1069
|
|
|
@@ -1033,6 +1120,12 @@ RCT_EXPORT_METHOD(enableLogEvents:(BOOL)enabled) {
|
|
|
1033
1120
|
// ── Downloads ────────────────────────────────────────────────────────────────
|
|
1034
1121
|
|
|
1035
1122
|
RCT_EXPORT_METHOD(downloadModel:(NSString *)url destPath:(NSString *)destPath) {
|
|
1123
|
+
if (!isPathAllowed(destPath)) {
|
|
1124
|
+
[self sendEventWithName:@"onDownloadError" body:@{
|
|
1125
|
+
@"url": url ?: @"", @"error": @"Destination is outside local-llm storage", @"resumable": @NO
|
|
1126
|
+
}];
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1036
1129
|
NSURL *nsUrl = [NSURL URLWithString:url];
|
|
1037
1130
|
if (!nsUrl) return;
|
|
1038
1131
|
_downloadDelegate.destPaths[url] = destPath;
|
|
@@ -1041,6 +1134,12 @@ RCT_EXPORT_METHOD(downloadModel:(NSString *)url destPath:(NSString *)destPath) {
|
|
|
1041
1134
|
}
|
|
1042
1135
|
|
|
1043
1136
|
RCT_EXPORT_METHOD(resumeDownload:(NSString *)url destPath:(NSString *)destPath) {
|
|
1137
|
+
if (!isPathAllowed(destPath)) {
|
|
1138
|
+
[self sendEventWithName:@"onDownloadError" body:@{
|
|
1139
|
+
@"url": url ?: @"", @"error": @"Destination is outside local-llm storage", @"resumable": @NO
|
|
1140
|
+
}];
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1044
1143
|
NSData *data = _downloadDelegate.resumeData[url];
|
|
1045
1144
|
_downloadDelegate.destPaths[url] = destPath;
|
|
1046
1145
|
if (data) {
|
|
@@ -1118,10 +1217,12 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getModelStoragePath) {
|
|
|
1118
1217
|
}
|
|
1119
1218
|
|
|
1120
1219
|
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(fileExists:(NSString *)path) {
|
|
1220
|
+
if (!isPathAllowed(path)) return @(NO);
|
|
1121
1221
|
return @([[NSFileManager defaultManager] fileExistsAtPath:path]);
|
|
1122
1222
|
}
|
|
1123
1223
|
|
|
1124
1224
|
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getFileSize:(NSString *)path) {
|
|
1225
|
+
if (!isPathAllowed(path)) return @0;
|
|
1125
1226
|
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
|
|
1126
1227
|
NSNumber *size = attrs[NSFileSize];
|
|
1127
1228
|
return size ?: @0;
|
|
@@ -1159,6 +1260,10 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(removePath:(NSString *)path) {
|
|
|
1159
1260
|
RCT_EXPORT_METHOD(sha256File:(NSString *)path
|
|
1160
1261
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
1161
1262
|
reject:(RCTPromiseRejectBlock)reject) {
|
|
1263
|
+
if (!isPathAllowed(path)) {
|
|
1264
|
+
reject(@"E_INVALID_PATH", @"Path is outside local-llm storage", nil);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1162
1267
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
1163
1268
|
NSInputStream *stream = [NSInputStream inputStreamWithFileAtPath:path];
|
|
1164
1269
|
if (!stream) {
|
package/local-llm-rn.podspec
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
1
5
|
Pod::Spec.new do |s|
|
|
2
6
|
s.name = "local-llm-rn"
|
|
3
|
-
s.version = "
|
|
7
|
+
s.version = package["version"]
|
|
4
8
|
s.summary = "Local LLM inference for React Native"
|
|
5
9
|
s.homepage = "https://github.com/hilum-labs/local-llm-rn"
|
|
6
10
|
s.license = { :type => "MIT", :file => "LICENSE" }
|
|
7
11
|
s.author = "Hilum Labs"
|
|
8
|
-
s.source = { :git => "https://github.com/hilum-labs/local-llm-rn.git", :tag => s.version
|
|
12
|
+
s.source = { :git => "https://github.com/hilum-labs/local-llm-rn.git", :tag => "v#{s.version}" }
|
|
9
13
|
s.platforms = { :ios => "16.0" }
|
|
10
14
|
|
|
11
15
|
# Only the RN bridge (ios/) needs ARC; ggml Metal .m files use manual retain/release
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-llm-rn",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"packageManager": "pnpm@10.29.2",
|
|
5
5
|
"description": "On-device LLM inference for React Native with Metal (iOS) and Vulkan (Android) GPU acceleration",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"smoke:native-bridge": "vitest run tests/native-contract.test.ts tests/native-bridge.test.ts tests/local-llm.test.ts tests/vision.test.ts && bash scripts/verify-vendoring.sh",
|
|
23
|
+
"verify:core-version": "node scripts/verify-core-version.mjs",
|
|
24
|
+
"verify:release": "node scripts/validate-release.mjs",
|
|
23
25
|
"verify:vendoring": "bash scripts/verify-vendoring.sh",
|
|
24
26
|
"prepare": "bash scripts/prepare.sh",
|
|
25
27
|
"prepack": "bash scripts/prepack.sh",
|
|
@@ -32,14 +34,32 @@
|
|
|
32
34
|
"react-native": ">=0.76"
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|
|
35
|
-
"local-llm-js-core": "0.
|
|
37
|
+
"local-llm-js-core": "0.3.0"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"@types/react": "^18.0.0",
|
|
39
41
|
"react": "^18.0.0",
|
|
40
42
|
"react-native": "^0.76.0",
|
|
41
43
|
"typescript": "^5.5.0",
|
|
42
|
-
"
|
|
44
|
+
"vite": "7.3.5",
|
|
45
|
+
"vitest": "^4.1.10"
|
|
46
|
+
},
|
|
47
|
+
"pnpm": {
|
|
48
|
+
"overrides": {
|
|
49
|
+
"@babel/plugin-transform-modules-systemjs@>=7.12.0 <7.29.4": "7.29.7",
|
|
50
|
+
"@babel/core@<7.29.6": "7.29.7",
|
|
51
|
+
"brace-expansion@<2": "1.1.16",
|
|
52
|
+
"esbuild@<0.28.1": "0.28.1",
|
|
53
|
+
"js-yaml@<4": "3.15.0",
|
|
54
|
+
"node-forge@<=1.3.3": "1.4.0",
|
|
55
|
+
"picomatch@<3": "2.3.2",
|
|
56
|
+
"picomatch@>=4 <4.0.5": "4.0.5",
|
|
57
|
+
"shell-quote@<=1.8.3": "1.10.0",
|
|
58
|
+
"postcss@<8.5.10": "8.5.19",
|
|
59
|
+
"vite@>=7 <7.3.5": "7.3.5",
|
|
60
|
+
"ws@>=6 <7": "6.2.5",
|
|
61
|
+
"ws@>=7 <8": "7.5.12"
|
|
62
|
+
}
|
|
43
63
|
},
|
|
44
64
|
"codegenConfig": {
|
|
45
65
|
"name": "LocalLLMSpec",
|
package/src/NativeLocalLLM.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface Spec extends TurboModule {
|
|
|
57
57
|
|
|
58
58
|
getEmbeddingDimension(modelId: string): number;
|
|
59
59
|
createEmbeddingContext(modelId: string, options: Object): string;
|
|
60
|
+
freeEmbeddingContext(contextId: string): void;
|
|
60
61
|
embed(contextId: string, modelId: string, tokens: number[]): number[];
|
|
61
62
|
embedBatch(contextId: string, modelId: string, tokenArrays: number[][]): number[][];
|
|
62
63
|
|
package/src/cache.ts
CHANGED
|
@@ -16,6 +16,25 @@ function joinPath(...parts: string[]): string {
|
|
|
16
16
|
.join('/');
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function normalizePath(path: string): string {
|
|
20
|
+
const absolute = path.startsWith('/');
|
|
21
|
+
const segments: string[] = [];
|
|
22
|
+
|
|
23
|
+
for (const segment of path.replace(/\\/g, '/').split('/')) {
|
|
24
|
+
if (!segment || segment === '.') continue;
|
|
25
|
+
if (segment === '..') {
|
|
26
|
+
if (segments.length === 0) {
|
|
27
|
+
throw new LocalLLMError(LocalLLMErrorCode.INVALID_PATH, `Path "${path}" escapes its root`);
|
|
28
|
+
}
|
|
29
|
+
segments.pop();
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
segments.push(segment);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return `${absolute ? '/' : ''}${segments.join('/')}` || (absolute ? '/' : '.');
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
export class ModelCache implements ModelRegistry {
|
|
20
39
|
readonly cacheDir: string;
|
|
21
40
|
private indexPath: string;
|
|
@@ -27,8 +46,9 @@ export class ModelCache implements ModelRegistry {
|
|
|
27
46
|
|
|
28
47
|
/** Ensure a path is within the cache directory to prevent path traversal. */
|
|
29
48
|
private assertSafePath(path: string): void {
|
|
30
|
-
const normalized = path
|
|
31
|
-
|
|
49
|
+
const normalized = normalizePath(path);
|
|
50
|
+
const cacheRoot = normalizePath(this.cacheDir).replace(/\/$/, '');
|
|
51
|
+
if (normalized !== cacheRoot && !normalized.startsWith(`${cacheRoot}/`)) {
|
|
32
52
|
throw new LocalLLMError(
|
|
33
53
|
LocalLLMErrorCode.INVALID_PATH,
|
|
34
54
|
`Path "${path}" is outside the cache directory "${this.cacheDir}"`,
|
package/src/model-manager.ts
CHANGED
|
@@ -35,7 +35,19 @@ export class ModelManager {
|
|
|
35
35
|
async downloadModel(url: string, options?: DownloadOptions): Promise<string> {
|
|
36
36
|
const resolvedUrl = resolveModelUrl(url);
|
|
37
37
|
const cached = await this.cache.getCachedModel(resolvedUrl);
|
|
38
|
-
if (cached)
|
|
38
|
+
if (cached) {
|
|
39
|
+
const actualSize = NativeLocalLLM.getFileSize(cached.path);
|
|
40
|
+
const expectedSize = options?.expectedSize ?? cached.size;
|
|
41
|
+
const sizeMatches = actualSize > 0 && actualSize === expectedSize;
|
|
42
|
+
const hashMatches = !options?.expectedSha256
|
|
43
|
+
|| await NativeLocalLLM.sha256File(cached.path) === options.expectedSha256.toLowerCase();
|
|
44
|
+
|
|
45
|
+
if (sizeMatches && hashMatches) return cached.path;
|
|
46
|
+
|
|
47
|
+
// Never return a cache entry whose bytes no longer match its recorded or
|
|
48
|
+
// caller-provided integrity metadata. Remove it and perform a clean download.
|
|
49
|
+
await this.cache.removeModel(resolvedUrl);
|
|
50
|
+
}
|
|
39
51
|
|
|
40
52
|
const targetPath = this.cache.targetPath(resolvedUrl);
|
|
41
53
|
await this.downloader.download(resolvedUrl, targetPath, options?.onProgress);
|
|
@@ -48,7 +60,7 @@ export class ModelManager {
|
|
|
48
60
|
`Downloaded file is empty: ${resolvedUrl}`,
|
|
49
61
|
);
|
|
50
62
|
}
|
|
51
|
-
if (options?.expectedSize && size !== options.expectedSize) {
|
|
63
|
+
if (options?.expectedSize !== undefined && size !== options.expectedSize) {
|
|
52
64
|
NativeLocalLLM.removePath(targetPath);
|
|
53
65
|
throw new LocalLLMError(
|
|
54
66
|
LocalLLMErrorCode.DOWNLOAD_INTEGRITY_MISMATCH,
|
package/src/native-bridge.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import NativeLocalLLM from './NativeLocalLLM';
|
|
2
2
|
import { NativeEventEmitter, NativeModules } from 'react-native';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
NativeAddon,
|
|
5
|
+
NativeModel,
|
|
6
|
+
NativeContext,
|
|
7
|
+
NativeEmbeddingContext,
|
|
8
|
+
NativeMtmdContext,
|
|
9
|
+
} from 'local-llm-js-core/native';
|
|
4
10
|
import { getDeviceCapabilities } from './device';
|
|
5
11
|
import { LocalLLMError, LocalLLMErrorCode } from './errors';
|
|
6
12
|
|
|
@@ -134,13 +140,20 @@ export function createReactNativeAddon(): NativeAddon {
|
|
|
134
140
|
|
|
135
141
|
inferStream: (model, ctx, prompt, options, callback) => {
|
|
136
142
|
const ctxId = unwrapId(ctx);
|
|
143
|
+
let settled = false;
|
|
137
144
|
const sub = emitter.addListener('onToken', (event) => {
|
|
138
|
-
if (event.contextId !== ctxId) return;
|
|
139
|
-
if (event.error) { callback(new LocalLLMError(LocalLLMErrorCode.STREAM_FAILED, event.error), null);
|
|
140
|
-
if (event.done) {
|
|
145
|
+
if (event.contextId !== ctxId || settled) return;
|
|
146
|
+
if (event.error) { settled = true; sub.remove(); callback(new LocalLLMError(LocalLLMErrorCode.STREAM_FAILED, event.error), null); return; }
|
|
147
|
+
if (event.done) { settled = true; sub.remove(); callback(null, null); return; }
|
|
141
148
|
callback(null, event.token);
|
|
142
149
|
});
|
|
143
|
-
|
|
150
|
+
try {
|
|
151
|
+
NativeLocalLLM.startStream(unwrapId(model), ctxId, prompt, options ?? {});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
settled = true;
|
|
154
|
+
sub.remove();
|
|
155
|
+
callback(error instanceof Error ? error : new Error(String(error)), null);
|
|
156
|
+
}
|
|
144
157
|
},
|
|
145
158
|
|
|
146
159
|
createMtmdContext: (model, projectorPath, options) => {
|
|
@@ -160,15 +173,22 @@ export function createReactNativeAddon(): NativeAddon {
|
|
|
160
173
|
inferStreamVision: (model, ctx, mtmdCtx, prompt, imageBuffers, options, callback) => {
|
|
161
174
|
const ctxId = unwrapId(ctx);
|
|
162
175
|
const base64s = imageBuffers.map((buf) => toBase64(buf));
|
|
176
|
+
let settled = false;
|
|
163
177
|
const sub = emitter.addListener('onToken', (event) => {
|
|
164
|
-
if (event.contextId !== ctxId) return;
|
|
165
|
-
if (event.error) { callback(new LocalLLMError(LocalLLMErrorCode.VISION_FAILED, event.error), null);
|
|
166
|
-
if (event.done) {
|
|
178
|
+
if (event.contextId !== ctxId || settled) return;
|
|
179
|
+
if (event.error) { settled = true; sub.remove(); callback(new LocalLLMError(LocalLLMErrorCode.VISION_FAILED, event.error), null); return; }
|
|
180
|
+
if (event.done) { settled = true; sub.remove(); callback(null, null); return; }
|
|
167
181
|
callback(null, event.token);
|
|
168
182
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
try {
|
|
184
|
+
NativeLocalLLM.startStreamVision(
|
|
185
|
+
unwrapId(model), ctxId, unwrapId(mtmdCtx), prompt, base64s, options ?? {},
|
|
186
|
+
);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
settled = true;
|
|
189
|
+
sub.remove();
|
|
190
|
+
callback(error instanceof Error ? error : new Error(String(error)), null);
|
|
191
|
+
}
|
|
172
192
|
},
|
|
173
193
|
|
|
174
194
|
jsonSchemaToGrammar: (schemaJson) => NativeLocalLLM.jsonSchemaToGrammar(schemaJson),
|
|
@@ -176,8 +196,9 @@ export function createReactNativeAddon(): NativeAddon {
|
|
|
176
196
|
getEmbeddingDimension: (model) => NativeLocalLLM.getEmbeddingDimension(unwrapId(model)),
|
|
177
197
|
createEmbeddingContext: (model, options) => {
|
|
178
198
|
const id = NativeLocalLLM.createEmbeddingContext(unwrapId(model), options ?? {});
|
|
179
|
-
return wrapId(id, '
|
|
199
|
+
return wrapId(id, 'NativeEmbeddingContext') as NativeEmbeddingContext;
|
|
180
200
|
},
|
|
201
|
+
freeEmbeddingContext: (ctx) => NativeLocalLLM.freeEmbeddingContext(unwrapId(ctx)),
|
|
181
202
|
embed: (ctx, model, tokens) => {
|
|
182
203
|
const plain = tokens instanceof Int32Array ? Array.from(tokens) : tokens;
|
|
183
204
|
return new Float32Array(NativeLocalLLM.embed(unwrapId(ctx), unwrapId(model), plain));
|
|
@@ -190,19 +211,34 @@ export function createReactNativeAddon(): NativeAddon {
|
|
|
190
211
|
|
|
191
212
|
inferBatch: (model, ctx, prompts, options, callback) => {
|
|
192
213
|
const ctxId = unwrapId(ctx);
|
|
193
|
-
|
|
214
|
+
const completedSequences = new Set<number>();
|
|
215
|
+
let settled = false;
|
|
194
216
|
const sub = emitter.addListener('onBatchToken', (event) => {
|
|
195
|
-
if (event.contextId !== ctxId) return;
|
|
196
|
-
if (event.error) {
|
|
217
|
+
if (event.contextId !== ctxId || settled) return;
|
|
218
|
+
if (event.error) {
|
|
219
|
+
settled = true;
|
|
220
|
+
sub.remove();
|
|
221
|
+
callback(new LocalLLMError(LocalLLMErrorCode.INFERENCE_FAILED, event.error), null, event.seqIndex, null);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
197
224
|
if (event.done) {
|
|
225
|
+
completedSequences.add(event.seqIndex);
|
|
226
|
+
if (completedSequences.size >= prompts.length) {
|
|
227
|
+
settled = true;
|
|
228
|
+
sub.remove();
|
|
229
|
+
}
|
|
198
230
|
callback(null, null, event.seqIndex, event.finishReason ?? null);
|
|
199
|
-
completedCount++;
|
|
200
|
-
if (completedCount >= prompts.length) sub.remove();
|
|
201
231
|
return;
|
|
202
232
|
}
|
|
203
233
|
callback(null, event.token, event.seqIndex, null);
|
|
204
234
|
});
|
|
205
|
-
|
|
235
|
+
try {
|
|
236
|
+
NativeLocalLLM.startBatch(unwrapId(model), ctxId, prompts, options as any);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
settled = true;
|
|
239
|
+
sub.remove();
|
|
240
|
+
callback(error instanceof Error ? error : new Error(String(error)), null, -1, null);
|
|
241
|
+
}
|
|
206
242
|
},
|
|
207
243
|
|
|
208
244
|
quantize: (inputPath, outputPath, options, callback) => {
|
|
@@ -210,7 +246,12 @@ export function createReactNativeAddon(): NativeAddon {
|
|
|
210
246
|
sub.remove();
|
|
211
247
|
callback(event.error ? new LocalLLMError(LocalLLMErrorCode.QUANTIZE_FAILED, event.error) : null);
|
|
212
248
|
});
|
|
213
|
-
|
|
249
|
+
try {
|
|
250
|
+
NativeLocalLLM.quantize(inputPath, outputPath, options);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
sub.remove();
|
|
253
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
254
|
+
}
|
|
214
255
|
},
|
|
215
256
|
|
|
216
257
|
setLogCallback: (() => {
|
package/src/rn-downloader.ts
CHANGED
|
@@ -33,11 +33,16 @@ export function createNativeDownloader(): DownloadAdapter {
|
|
|
33
33
|
}),
|
|
34
34
|
];
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
resumableUrls.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
try {
|
|
37
|
+
if (resumableUrls.has(url)) {
|
|
38
|
+
resumableUrls.delete(url);
|
|
39
|
+
NativeLocalLLM.resumeDownload(url, destPath);
|
|
40
|
+
} else {
|
|
41
|
+
NativeLocalLLM.downloadModel(url, destPath);
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
subs.forEach((s) => s.remove());
|
|
45
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
41
46
|
}
|
|
42
47
|
});
|
|
43
48
|
},
|