@dr33m/react-native-litert-lm 0.5.5 → 0.6.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/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +38 -2
- package/ios/HybridLiteRTLM+Execute.swift +12 -0
- package/ios/HybridLiteRTLM.swift +53 -6
- package/lib/__mocks__/react-native-nitro-modules.d.ts +2 -0
- package/lib/__mocks__/react-native-nitro-modules.js +1 -0
- package/lib/__tests__/modelFactory.test.js +19 -0
- package/lib/modelFactory.js +13 -0
- package/lib/specs/LiteRTLM.nitro.d.ts +17 -0
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +13 -0
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +1 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_std__string_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +6 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridModelStoreSpec.kt +2 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +6 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +11 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +1 -0
- package/package.json +1 -1
- package/src/__mocks__/react-native-nitro-modules.ts +1 -0
- package/src/__tests__/modelFactory.test.ts +21 -0
- package/src/modelFactory.ts +14 -0
- package/src/specs/LiteRTLM.nitro.ts +18 -0
|
@@ -563,6 +563,26 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
563
563
|
createNewConversation()
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
+
override fun resetConversationWith(messages: Array<Message>) {
|
|
567
|
+
val seed = messages.map { msg ->
|
|
568
|
+
LiteRTMessage(
|
|
569
|
+
role = when (msg.role) {
|
|
570
|
+
Role.MODEL -> com.google.ai.edge.litertlm.Role.MODEL
|
|
571
|
+
Role.SYSTEM -> com.google.ai.edge.litertlm.Role.SYSTEM
|
|
572
|
+
else -> com.google.ai.edge.litertlm.Role.USER
|
|
573
|
+
},
|
|
574
|
+
contents = Contents.of(Content.Text(msg.content))
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
synchronized(history) {
|
|
578
|
+
history.clear()
|
|
579
|
+
history.addAll(messages.toList())
|
|
580
|
+
}
|
|
581
|
+
// Recreating the conversation is what actually frees the old KV cache;
|
|
582
|
+
// the seed is replayed into the new one without triggering generation.
|
|
583
|
+
createNewConversation(seed)
|
|
584
|
+
}
|
|
585
|
+
|
|
566
586
|
override fun isReady(): Boolean {
|
|
567
587
|
return isLoaded_
|
|
568
588
|
}
|
|
@@ -575,7 +595,10 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
575
595
|
return lastStats
|
|
576
596
|
}
|
|
577
597
|
|
|
578
|
-
|
|
598
|
+
/** Shared by the public [getMemoryUsage] override and the pre-flight guard
|
|
599
|
+
* in [execute] — both need the same real, OS-level reading, not an
|
|
600
|
+
* estimate. */
|
|
601
|
+
private fun currentMemoryUsage(): MemoryUsage {
|
|
579
602
|
// Native heap: allocated bytes from Debug APIs (most accurate for native allocations)
|
|
580
603
|
val nativeHeapBytes = Debug.getNativeHeapAllocatedSize().toDouble()
|
|
581
604
|
|
|
@@ -619,6 +642,8 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
619
642
|
)
|
|
620
643
|
}
|
|
621
644
|
|
|
645
|
+
override fun getMemoryUsage(): MemoryUsage = currentMemoryUsage()
|
|
646
|
+
|
|
622
647
|
override fun checkModelCapabilities(modelPath: String): ModelCapabilities {
|
|
623
648
|
var supportsSpeculativeDecoding = false
|
|
624
649
|
try {
|
|
@@ -668,7 +693,7 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
668
693
|
}
|
|
669
694
|
}
|
|
670
695
|
|
|
671
|
-
private fun createNewConversation() {
|
|
696
|
+
private fun createNewConversation(initialMessages: List<LiteRTMessage> = emptyList()) {
|
|
672
697
|
ensureLoaded()
|
|
673
698
|
// v0.10.2 enforces single-session: close existing conversation first
|
|
674
699
|
conversation?.let { oldConv ->
|
|
@@ -711,6 +736,7 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
711
736
|
temperature = temperature.toDouble(),
|
|
712
737
|
),
|
|
713
738
|
systemInstruction = systemPrompt?.let { Contents.of(Content.Text(it)) },
|
|
739
|
+
initialMessages = initialMessages,
|
|
714
740
|
tools = lmTools ?: emptyList()
|
|
715
741
|
)
|
|
716
742
|
// TODO: maxOutputTokens is not configurable on Android — the Kotlin SDK's
|
|
@@ -784,6 +810,16 @@ class HybridLiteRTLM : HybridLiteRTLMSpec() {
|
|
|
784
810
|
|
|
785
811
|
return Promise.parallel {
|
|
786
812
|
ensureLoaded()
|
|
813
|
+
|
|
814
|
+
// Refuse rather than risk it: growing the KV-cache under real memory
|
|
815
|
+
// pressure can fail at the native engine level in a way that never
|
|
816
|
+
// surfaces as a catchable Kotlin exception — it takes the whole
|
|
817
|
+
// process down. A plain RuntimeException here is a normal rejected
|
|
818
|
+
// Promise for callers, same contract as any other execute() failure.
|
|
819
|
+
if (currentMemoryUsage().isLowMemory) {
|
|
820
|
+
throw RuntimeException("LiteRTLM: Device memory is critically low; refusing to continue generation.")
|
|
821
|
+
}
|
|
822
|
+
|
|
787
823
|
// Clear any previous tool calls before new inference
|
|
788
824
|
pendingToolCalls.clear()
|
|
789
825
|
|
|
@@ -80,6 +80,18 @@ extension HybridLiteRTLM {
|
|
|
80
80
|
return
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Refuse rather than risk it: growing the KV-cache under real memory
|
|
84
|
+
// pressure can fail inside the underlying C engine in a way that
|
|
85
|
+
// never surfaces as a catchable Swift error — it takes the whole
|
|
86
|
+
// process down (Jetsam or a hard native failure). An NSError here
|
|
87
|
+
// is a normal rejected Promise for callers, same contract as any
|
|
88
|
+
// other execute() failure.
|
|
89
|
+
if Self.currentMemoryUsage().isLowMemory {
|
|
90
|
+
promise.reject(withError: NSError(domain: "LiteRTLM", code: 507,
|
|
91
|
+
userInfo: [NSLocalizedDescriptionKey: "LiteRTLM: Device memory is critically low; refusing to continue generation."]))
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
let payload: (json: String, tempFiles: [String])
|
|
84
96
|
do { payload = try self.buildExecutePayload(preprocessed) }
|
|
85
97
|
catch { promise.reject(withError: error); return }
|
package/ios/HybridLiteRTLM.swift
CHANGED
|
@@ -92,6 +92,26 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
public func resetConversationWith(messages: [Message]) throws {
|
|
96
|
+
queue.sync {
|
|
97
|
+
history = messages
|
|
98
|
+
lastStats = GenerationStats(
|
|
99
|
+
promptTokens: 0.0,
|
|
100
|
+
completionTokens: 0.0,
|
|
101
|
+
totalTokens: 0.0,
|
|
102
|
+
timeToFirstToken: 0.0,
|
|
103
|
+
totalTime: 0.0,
|
|
104
|
+
tokensPerSecond: 0.0
|
|
105
|
+
)
|
|
106
|
+
if isLoaded && engine != nil {
|
|
107
|
+
// Recreating the conversation is what actually frees the old KV
|
|
108
|
+
// cache; the seed is replayed into the new one without
|
|
109
|
+
// triggering generation.
|
|
110
|
+
createNewConversation(initialMessages: messages)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
95
115
|
public func getStats() throws -> GenerationStats {
|
|
96
116
|
return queue.sync { lastStats }
|
|
97
117
|
}
|
|
@@ -111,9 +131,18 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
|
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
public func getMemoryUsage() throws -> MemoryUsage {
|
|
134
|
+
return Self.currentMemoryUsage()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Shared by the public `getMemoryUsage()` override and the pre-flight
|
|
138
|
+
/// guard in `HybridLiteRTLM+Execute.swift` — both need the same real,
|
|
139
|
+
/// OS-level reading, not an estimate. `static` since it needs no
|
|
140
|
+
/// instance state, which also makes it trivially callable from the
|
|
141
|
+
/// extension file.
|
|
142
|
+
static func currentMemoryUsage() -> MemoryUsage {
|
|
114
143
|
var residentBytes: Double = 0.0
|
|
115
144
|
var nativeHeapBytes: Double = 0.0
|
|
116
|
-
|
|
145
|
+
|
|
117
146
|
// Retrieve process resident set size (RSS) via Mach basic task info
|
|
118
147
|
var info = mach_task_basic_info()
|
|
119
148
|
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size / MemoryLayout<integer_t>.size)
|
|
@@ -122,18 +151,18 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
|
|
|
122
151
|
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
|
|
123
152
|
}
|
|
124
153
|
}
|
|
125
|
-
|
|
154
|
+
|
|
126
155
|
if kerr == KERN_SUCCESS {
|
|
127
156
|
residentBytes = Double(info.resident_size)
|
|
128
157
|
nativeHeapBytes = Double(info.resident_size)
|
|
129
158
|
}
|
|
130
|
-
|
|
159
|
+
|
|
131
160
|
// os_proc_available_memory reports actual headroom available before Jetsam termination (iOS 13+)
|
|
132
161
|
let availableBytes = Double(os_proc_available_memory())
|
|
133
|
-
|
|
162
|
+
|
|
134
163
|
// Flag memory warning at ~200MB remaining headroom
|
|
135
164
|
let isLowMemory = availableBytes < 200.0 * 1024.0 * 1024.0
|
|
136
|
-
|
|
165
|
+
|
|
137
166
|
return MemoryUsage(
|
|
138
167
|
nativeHeapBytes: nativeHeapBytes,
|
|
139
168
|
residentBytes: residentBytes,
|
|
@@ -384,7 +413,7 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
|
|
|
384
413
|
|
|
385
414
|
// MARK: - Internal Engine Helpers
|
|
386
415
|
|
|
387
|
-
private func createNewConversation() {
|
|
416
|
+
private func createNewConversation(initialMessages: [Message] = []) {
|
|
388
417
|
guard let engine = self.engine else { return }
|
|
389
418
|
|
|
390
419
|
if let oldConv = self.conversation {
|
|
@@ -437,6 +466,24 @@ public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protoco
|
|
|
437
466
|
}
|
|
438
467
|
}
|
|
439
468
|
|
|
469
|
+
if !initialMessages.isEmpty {
|
|
470
|
+
let payload = initialMessages.map { msg -> [String: String] in
|
|
471
|
+
let role: String
|
|
472
|
+
switch msg.role {
|
|
473
|
+
case .model: role = "model"
|
|
474
|
+
case .system: role = "system"
|
|
475
|
+
default: role = "user"
|
|
476
|
+
}
|
|
477
|
+
return ["role": role, "content": msg.content]
|
|
478
|
+
}
|
|
479
|
+
if let data = try? JSONSerialization.data(withJSONObject: payload, options: []),
|
|
480
|
+
let jsonString = String(data: data, encoding: .utf8) {
|
|
481
|
+
jsonString.withCString { messagesC in
|
|
482
|
+
litert_lm_conversation_config_set_messages(convConfig, messagesC)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
440
487
|
self.conversation = litert_lm_conversation_create(engine, convConfig)
|
|
441
488
|
}
|
|
442
489
|
|
|
@@ -14,6 +14,7 @@ export declare const mockLiteRTLM: {
|
|
|
14
14
|
sendMessageWithAudioAsync: jest.Mock<Promise<void>, [msg: string, audioPath: string, onToken: (token: string, done: boolean) => void], any>;
|
|
15
15
|
getHistory: jest.Mock<never[], [], any>;
|
|
16
16
|
resetConversation: jest.Mock<any, any, any>;
|
|
17
|
+
resetConversationWith: jest.Mock<any, any, any>;
|
|
17
18
|
getStats: jest.Mock<{
|
|
18
19
|
promptTokens: number;
|
|
19
20
|
completionTokens: number;
|
|
@@ -54,6 +55,7 @@ export declare const NitroModules: {
|
|
|
54
55
|
sendMessageWithAudioAsync: jest.Mock<Promise<void>, [msg: string, audioPath: string, onToken: (token: string, done: boolean) => void], any>;
|
|
55
56
|
getHistory: jest.Mock<never[], [], any>;
|
|
56
57
|
resetConversation: jest.Mock<any, any, any>;
|
|
58
|
+
resetConversationWith: jest.Mock<any, any, any>;
|
|
57
59
|
getStats: jest.Mock<{
|
|
58
60
|
promptTokens: number;
|
|
59
61
|
completionTokens: number;
|
|
@@ -56,6 +56,7 @@ exports.mockLiteRTLM = {
|
|
|
56
56
|
sendMessageWithAudioAsync: jest.fn((msg, audioPath, onToken) => mockExecute([{ type: "text", text: msg }, { type: "audio", path: audioPath }], onToken).then(() => { })),
|
|
57
57
|
getHistory: jest.fn(() => []),
|
|
58
58
|
resetConversation: jest.fn(),
|
|
59
|
+
resetConversationWith: jest.fn(),
|
|
59
60
|
getStats: jest.fn(() => ({
|
|
60
61
|
promptTokens: 10,
|
|
61
62
|
completionTokens: 20,
|
|
@@ -33,6 +33,25 @@ describe('modelFactory Security & Proxy Unit Tests', () => {
|
|
|
33
33
|
expect(react_native_nitro_modules_1.mockLiteRTLM.resetConversation).toHaveBeenCalled();
|
|
34
34
|
expect(react_native_nitro_modules_1.mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
|
|
35
35
|
});
|
|
36
|
+
it('should successfully proxy resetConversationWith and record memory metrics', async () => {
|
|
37
|
+
const seed = [{ role: 'user', content: 'earlier turn' }];
|
|
38
|
+
await llm.resetConversationWith(seed);
|
|
39
|
+
expect(react_native_nitro_modules_1.mockLiteRTLM.resetConversationWith).toHaveBeenCalledWith(seed);
|
|
40
|
+
expect(react_native_nitro_modules_1.mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
it('should report resetConversationWith as absent on older native builds', () => {
|
|
43
|
+
// The app feature-detects this method, so the proxy must not hand back a
|
|
44
|
+
// wrapper the native side cannot service.
|
|
45
|
+
const original = react_native_nitro_modules_1.mockLiteRTLM.resetConversationWith;
|
|
46
|
+
// @ts-expect-error deliberately removing the method to simulate old native
|
|
47
|
+
delete react_native_nitro_modules_1.mockLiteRTLM.resetConversationWith;
|
|
48
|
+
try {
|
|
49
|
+
expect((0, modelFactory_1.createLLM)().resetConversationWith).toBeUndefined();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
react_native_nitro_modules_1.mockLiteRTLM.resetConversationWith = original;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
36
55
|
it('should successfully proxy sendMessageAsync and record memory metrics when done', async () => {
|
|
37
56
|
const onToken = jest.fn();
|
|
38
57
|
await llm.sendMessageAsync("Async prompt", onToken);
|
package/lib/modelFactory.js
CHANGED
|
@@ -95,6 +95,19 @@ function createLLM(options) {
|
|
|
95
95
|
return result;
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
|
+
if (prop === "resetConversationWith") {
|
|
99
|
+
// Absent on native builds older than this JS wrapper, and callers
|
|
100
|
+
// feature-detect it — so fall through rather than handing back a
|
|
101
|
+
// wrapper that would fail at the bridge.
|
|
102
|
+
if (typeof target.resetConversationWith !== "function") {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return (messages) => {
|
|
106
|
+
const result = target.resetConversationWith(messages);
|
|
107
|
+
recordMemorySnapshot();
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
98
111
|
if (prop === "sendToolResponse") {
|
|
99
112
|
return (responses, onToken) => {
|
|
100
113
|
if (onToken) {
|
|
@@ -313,6 +313,23 @@ export interface LiteRTLM extends HybridObject<{
|
|
|
313
313
|
* Clear the conversation context and start fresh.
|
|
314
314
|
*/
|
|
315
315
|
resetConversation(): void;
|
|
316
|
+
/**
|
|
317
|
+
* Clear the conversation and re-seed it with prior turns.
|
|
318
|
+
*
|
|
319
|
+
* The engine allocates one KV cache per conversation, sized by
|
|
320
|
+
* `maxContextTokens`. Long tool-calling sessions fill it, and overflowing it
|
|
321
|
+
* aborts the process rather than raising a catchable error. Recreating the
|
|
322
|
+
* conversation frees the cache, but `resetConversation()` alone also throws
|
|
323
|
+
* away the thread.
|
|
324
|
+
*
|
|
325
|
+
* This seeds the fresh conversation with `messages` as prior context, so a
|
|
326
|
+
* caller can drop the oldest turns and keep the recent ones. The system
|
|
327
|
+
* prompt and tools configured at `loadModel` are reapplied automatically.
|
|
328
|
+
* No generation is triggered and no reply is produced.
|
|
329
|
+
*
|
|
330
|
+
* @param messages Prior turns, oldest first.
|
|
331
|
+
*/
|
|
332
|
+
resetConversationWith(messages: Message[]): void;
|
|
316
333
|
/**
|
|
317
334
|
* Check if a model is loaded and ready for inference.
|
|
318
335
|
*/
|
|
@@ -288,6 +288,19 @@ namespace margelo::nitro::litertlm {
|
|
|
288
288
|
static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("resetConversation");
|
|
289
289
|
method(_javaPart);
|
|
290
290
|
}
|
|
291
|
+
void JHybridLiteRTLMSpec::resetConversationWith(const std::vector<Message>& messages) {
|
|
292
|
+
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JArrayClass<JMessage>> /* messages */)>("resetConversationWith");
|
|
293
|
+
method(_javaPart, [&](auto&& __input) {
|
|
294
|
+
size_t __size = __input.size();
|
|
295
|
+
jni::local_ref<jni::JArrayClass<JMessage>> __array = jni::JArrayClass<JMessage>::newArray(__size);
|
|
296
|
+
for (size_t __i = 0; __i < __size; __i++) {
|
|
297
|
+
const auto& __element = __input[__i];
|
|
298
|
+
auto __elementJni = JMessage::fromCpp(__element);
|
|
299
|
+
__array->setElement(__i, *__elementJni);
|
|
300
|
+
}
|
|
301
|
+
return __array;
|
|
302
|
+
}(messages));
|
|
303
|
+
}
|
|
291
304
|
bool JHybridLiteRTLMSpec::isReady() {
|
|
292
305
|
static const auto method = _javaPart->javaClassStatic()->getMethod<jboolean()>("isReady");
|
|
293
306
|
auto __result = method(_javaPart);
|
|
@@ -66,6 +66,7 @@ namespace margelo::nitro::litertlm {
|
|
|
66
66
|
std::shared_ptr<Promise<void>> sendMessageAsync(const std::string& message, const std::function<void(const std::string& /* token */, bool /* done */)>& onToken) override;
|
|
67
67
|
std::vector<Message> getHistory() override;
|
|
68
68
|
void resetConversation() override;
|
|
69
|
+
void resetConversationWith(const std::vector<Message>& messages) override;
|
|
69
70
|
bool isReady() override;
|
|
70
71
|
GenerationStats getStats() override;
|
|
71
72
|
double countTokens(const std::string& text) override;
|
package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_double.kt
CHANGED
|
@@ -10,7 +10,6 @@ package com.margelo.nitro.dev.litert.litertlm
|
|
|
10
10
|
import androidx.annotation.Keep
|
|
11
11
|
import com.facebook.jni.HybridData
|
|
12
12
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
13
|
-
import dalvik.annotation.optimization.FastNative
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -59,7 +58,6 @@ class Func_void_double_cxx: Func_void_double {
|
|
|
59
58
|
override fun invoke(progress: Double): Unit
|
|
60
59
|
= invoke_cxx(progress)
|
|
61
60
|
|
|
62
|
-
@FastNative
|
|
63
61
|
private external fun invoke_cxx(progress: Double): Unit
|
|
64
62
|
}
|
|
65
63
|
|
|
@@ -10,7 +10,6 @@ package com.margelo.nitro.dev.litert.litertlm
|
|
|
10
10
|
import androidx.annotation.Keep
|
|
11
11
|
import com.facebook.jni.HybridData
|
|
12
12
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
13
|
-
import dalvik.annotation.optimization.FastNative
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -59,7 +58,6 @@ class Func_void_std__string_bool_cxx: Func_void_std__string_bool {
|
|
|
59
58
|
override fun invoke(token: String, done: Boolean): Unit
|
|
60
59
|
= invoke_cxx(token,done)
|
|
61
60
|
|
|
62
|
-
@FastNative
|
|
63
61
|
private external fun invoke_cxx(token: String, done: Boolean): Unit
|
|
64
62
|
}
|
|
65
63
|
|
|
@@ -10,6 +10,7 @@ package com.margelo.nitro.dev.litert.litertlm
|
|
|
10
10
|
import androidx.annotation.Keep
|
|
11
11
|
import com.facebook.jni.HybridData
|
|
12
12
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
13
|
+
import dalvik.annotation.optimization.FastNative
|
|
13
14
|
import com.margelo.nitro.core.Promise
|
|
14
15
|
import com.margelo.nitro.core.HybridObject
|
|
15
16
|
|
|
@@ -97,6 +98,10 @@ abstract class HybridLiteRTLMSpec: HybridObject() {
|
|
|
97
98
|
@Keep
|
|
98
99
|
abstract fun resetConversation(): Unit
|
|
99
100
|
|
|
101
|
+
@DoNotStrip
|
|
102
|
+
@Keep
|
|
103
|
+
abstract fun resetConversationWith(messages: Array<Message>): Unit
|
|
104
|
+
|
|
100
105
|
@DoNotStrip
|
|
101
106
|
@Keep
|
|
102
107
|
abstract fun isReady(): Boolean
|
|
@@ -157,6 +162,7 @@ abstract class HybridLiteRTLMSpec: HybridObject() {
|
|
|
157
162
|
@Keep
|
|
158
163
|
protected open class CxxPart(javaPart: HybridLiteRTLMSpec): HybridObject.CxxPart(javaPart) {
|
|
159
164
|
// C++ JHybridLiteRTLMSpec::CxxPart::initHybrid(...)
|
|
165
|
+
@FastNative
|
|
160
166
|
external override fun initHybrid(): HybridData
|
|
161
167
|
}
|
|
162
168
|
override fun createCxxPart(): CxxPart {
|
|
@@ -10,6 +10,7 @@ package com.margelo.nitro.dev.litert.litertlm
|
|
|
10
10
|
import androidx.annotation.Keep
|
|
11
11
|
import com.facebook.jni.HybridData
|
|
12
12
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
13
|
+
import dalvik.annotation.optimization.FastNative
|
|
13
14
|
import com.margelo.nitro.core.Promise
|
|
14
15
|
import com.margelo.nitro.core.HybridObject
|
|
15
16
|
|
|
@@ -64,6 +65,7 @@ abstract class HybridModelStoreSpec: HybridObject() {
|
|
|
64
65
|
@Keep
|
|
65
66
|
protected open class CxxPart(javaPart: HybridModelStoreSpec): HybridObject.CxxPart(javaPart) {
|
|
66
67
|
// C++ JHybridModelStoreSpec::CxxPart::initHybrid(...)
|
|
68
|
+
@FastNative
|
|
67
69
|
external override fun initHybrid(): HybridData
|
|
68
70
|
}
|
|
69
71
|
override fun createCxxPart(): CxxPart {
|
|
@@ -206,6 +206,12 @@ namespace margelo::nitro::litertlm {
|
|
|
206
206
|
std::rethrow_exception(__result.error());
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
+
inline void resetConversationWith(const std::vector<Message>& messages) override {
|
|
210
|
+
auto __result = _swiftPart.resetConversationWith(messages);
|
|
211
|
+
if (__result.hasError()) [[unlikely]] {
|
|
212
|
+
std::rethrow_exception(__result.error());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
209
215
|
inline bool isReady() override {
|
|
210
216
|
auto __result = _swiftPart.isReady();
|
|
211
217
|
if (__result.hasError()) [[unlikely]] {
|
|
@@ -25,6 +25,7 @@ public protocol HybridLiteRTLMSpec_protocol: HybridObject {
|
|
|
25
25
|
func sendMessageAsync(message: String, onToken: @escaping (_ token: String, _ done: Bool) -> Void) throws -> Promise<Void>
|
|
26
26
|
func getHistory() throws -> [Message]
|
|
27
27
|
func resetConversation() throws -> Void
|
|
28
|
+
func resetConversationWith(messages: [Message]) throws -> Void
|
|
28
29
|
func isReady() throws -> Bool
|
|
29
30
|
func getStats() throws -> GenerationStats
|
|
30
31
|
func countTokens(text: String) throws -> Double
|
|
@@ -370,6 +370,17 @@ open class HybridLiteRTLMSpec_cxx {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
@inline(__always)
|
|
374
|
+
public final func resetConversationWith(messages: bridge.std__vector_Message_) -> bridge.Result_void_ {
|
|
375
|
+
do {
|
|
376
|
+
try self.__implementation.resetConversationWith(messages: messages.map({ __item in __item }))
|
|
377
|
+
return bridge.create_Result_void_()
|
|
378
|
+
} catch (let __error) {
|
|
379
|
+
let __exceptionPtr = __error.toCpp()
|
|
380
|
+
return bridge.create_Result_void_(__exceptionPtr)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
373
384
|
@inline(__always)
|
|
374
385
|
public final func isReady() -> bridge.Result_bool_ {
|
|
375
386
|
do {
|
|
@@ -26,6 +26,7 @@ namespace margelo::nitro::litertlm {
|
|
|
26
26
|
prototype.registerHybridMethod("sendMessageAsync", &HybridLiteRTLMSpec::sendMessageAsync);
|
|
27
27
|
prototype.registerHybridMethod("getHistory", &HybridLiteRTLMSpec::getHistory);
|
|
28
28
|
prototype.registerHybridMethod("resetConversation", &HybridLiteRTLMSpec::resetConversation);
|
|
29
|
+
prototype.registerHybridMethod("resetConversationWith", &HybridLiteRTLMSpec::resetConversationWith);
|
|
29
30
|
prototype.registerHybridMethod("isReady", &HybridLiteRTLMSpec::isReady);
|
|
30
31
|
prototype.registerHybridMethod("getStats", &HybridLiteRTLMSpec::getStats);
|
|
31
32
|
prototype.registerHybridMethod("countTokens", &HybridLiteRTLMSpec::countTokens);
|
|
@@ -90,6 +90,7 @@ namespace margelo::nitro::litertlm {
|
|
|
90
90
|
virtual std::shared_ptr<Promise<void>> sendMessageAsync(const std::string& message, const std::function<void(const std::string& /* token */, bool /* done */)>& onToken) = 0;
|
|
91
91
|
virtual std::vector<Message> getHistory() = 0;
|
|
92
92
|
virtual void resetConversation() = 0;
|
|
93
|
+
virtual void resetConversationWith(const std::vector<Message>& messages) = 0;
|
|
93
94
|
virtual bool isReady() = 0;
|
|
94
95
|
virtual GenerationStats getStats() = 0;
|
|
95
96
|
virtual double countTokens(const std::string& text) = 0;
|
package/package.json
CHANGED
|
@@ -49,6 +49,27 @@ describe('modelFactory Security & Proxy Unit Tests', () => {
|
|
|
49
49
|
expect(mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
it('should successfully proxy resetConversationWith and record memory metrics', async () => {
|
|
53
|
+
const seed = [{ role: 'user', content: 'earlier turn' }];
|
|
54
|
+
await llm.resetConversationWith(seed as never);
|
|
55
|
+
|
|
56
|
+
expect(mockLiteRTLM.resetConversationWith).toHaveBeenCalledWith(seed);
|
|
57
|
+
expect(mockLiteRTLM.getMemoryUsage).toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should report resetConversationWith as absent on older native builds', () => {
|
|
61
|
+
// The app feature-detects this method, so the proxy must not hand back a
|
|
62
|
+
// wrapper the native side cannot service.
|
|
63
|
+
const original = mockLiteRTLM.resetConversationWith;
|
|
64
|
+
// @ts-expect-error deliberately removing the method to simulate old native
|
|
65
|
+
delete mockLiteRTLM.resetConversationWith;
|
|
66
|
+
try {
|
|
67
|
+
expect(createLLM().resetConversationWith).toBeUndefined();
|
|
68
|
+
} finally {
|
|
69
|
+
mockLiteRTLM.resetConversationWith = original;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
52
73
|
it('should successfully proxy sendMessageAsync and record memory metrics when done', async () => {
|
|
53
74
|
const onToken = jest.fn();
|
|
54
75
|
await llm.sendMessageAsync("Async prompt", onToken);
|
package/src/modelFactory.ts
CHANGED
|
@@ -130,6 +130,20 @@ export function createLLM(options?: {
|
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
if (prop === "resetConversationWith") {
|
|
134
|
+
// Absent on native builds older than this JS wrapper, and callers
|
|
135
|
+
// feature-detect it — so fall through rather than handing back a
|
|
136
|
+
// wrapper that would fail at the bridge.
|
|
137
|
+
if (typeof (target as any).resetConversationWith !== "function") {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
return (messages: unknown[]) => {
|
|
141
|
+
const result = (target as any).resetConversationWith(messages);
|
|
142
|
+
recordMemorySnapshot();
|
|
143
|
+
return result;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
if (prop === "sendToolResponse") {
|
|
134
148
|
return (responses: any[], onToken?: TokenCallback) => {
|
|
135
149
|
if (onToken) {
|
|
@@ -364,6 +364,24 @@ export interface LiteRTLM extends HybridObject<{
|
|
|
364
364
|
*/
|
|
365
365
|
resetConversation(): void;
|
|
366
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Clear the conversation and re-seed it with prior turns.
|
|
369
|
+
*
|
|
370
|
+
* The engine allocates one KV cache per conversation, sized by
|
|
371
|
+
* `maxContextTokens`. Long tool-calling sessions fill it, and overflowing it
|
|
372
|
+
* aborts the process rather than raising a catchable error. Recreating the
|
|
373
|
+
* conversation frees the cache, but `resetConversation()` alone also throws
|
|
374
|
+
* away the thread.
|
|
375
|
+
*
|
|
376
|
+
* This seeds the fresh conversation with `messages` as prior context, so a
|
|
377
|
+
* caller can drop the oldest turns and keep the recent ones. The system
|
|
378
|
+
* prompt and tools configured at `loadModel` are reapplied automatically.
|
|
379
|
+
* No generation is triggered and no reply is produced.
|
|
380
|
+
*
|
|
381
|
+
* @param messages Prior turns, oldest first.
|
|
382
|
+
*/
|
|
383
|
+
resetConversationWith(messages: Message[]): void;
|
|
384
|
+
|
|
367
385
|
/**
|
|
368
386
|
* Check if a model is loaded and ready for inference.
|
|
369
387
|
*/
|