@dr33m/react-native-litert-lm 0.5.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/LICENSE +21 -0
- package/README.md +627 -0
- package/android/CMakeLists.txt +32 -0
- package/android/build.gradle +104 -0
- package/android/src/main/AndroidManifest.xml +19 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +888 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridModelStore.kt +189 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/LiteRTLMRegistry.kt +32 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/MultimodalPartFactories.kt +15 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/StreamingCallbackListener.kt +89 -0
- package/android/src/main/java/dev/litert/litertlm/LiteRTLMInitProvider.kt +57 -0
- package/android/src/main/java/dev/litert/litertlm/LiteRTLMPackage.kt +43 -0
- package/android/src/test/java/com/margelo/nitro/core/Promise.kt +46 -0
- package/android/src/test/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMTest.kt +122 -0
- package/app.plugin.js +58 -0
- package/cpp/cpp-adapter.cpp +21 -0
- package/cpp/include/README.md +32 -0
- package/cpp/include/litert_lm_engine.h +516 -0
- package/cpp/include/stb_image.h +7988 -0
- package/ios/HybridLiteRTLM+Execute.swift +287 -0
- package/ios/HybridLiteRTLM+Streaming.swift +153 -0
- package/ios/HybridLiteRTLM.swift +541 -0
- package/ios/HybridModelStore.swift +206 -0
- package/ios/MultimodalPart+Factories.swift +22 -0
- package/ios/Tests/HybridLiteRTLMTests.swift +187 -0
- package/lib/__mocks__/react-native-nitro-modules.d.ts +81 -0
- package/lib/__mocks__/react-native-nitro-modules.js +98 -0
- package/lib/__tests__/hooks.test.d.ts +1 -0
- package/lib/__tests__/hooks.test.js +175 -0
- package/lib/__tests__/inferenceRouting.test.d.ts +1 -0
- package/lib/__tests__/inferenceRouting.test.js +28 -0
- package/lib/__tests__/memoryTracker.test.d.ts +1 -0
- package/lib/__tests__/memoryTracker.test.js +74 -0
- package/lib/__tests__/modelFactory.test.d.ts +1 -0
- package/lib/__tests__/modelFactory.test.js +107 -0
- package/lib/__tests__/modelPath.test.d.ts +1 -0
- package/lib/__tests__/modelPath.test.js +12 -0
- package/lib/__tests__/modelRegistry.test.d.ts +1 -0
- package/lib/__tests__/modelRegistry.test.js +61 -0
- package/lib/hooks.d.ts +46 -0
- package/lib/hooks.js +159 -0
- package/lib/index.d.ts +142 -0
- package/lib/index.js +184 -0
- package/lib/inferenceRouting.d.ts +18 -0
- package/lib/inferenceRouting.js +63 -0
- package/lib/memoryTracker.d.ts +128 -0
- package/lib/memoryTracker.js +155 -0
- package/lib/modelFactory.d.ts +24 -0
- package/lib/modelFactory.js +128 -0
- package/lib/modelPath.d.ts +8 -0
- package/lib/modelPath.js +18 -0
- package/lib/modelRegistry.d.ts +55 -0
- package/lib/modelRegistry.js +82 -0
- package/lib/specs/LiteRTLM.nitro.d.ts +380 -0
- package/lib/specs/LiteRTLM.nitro.js +2 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/LiteRTLM+autolinking.cmake +83 -0
- package/nitrogen/generated/android/LiteRTLM+autolinking.gradle +27 -0
- package/nitrogen/generated/android/LiteRTLMOnLoad.cpp +74 -0
- package/nitrogen/generated/android/LiteRTLMOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JBackend.hpp +61 -0
- package/nitrogen/generated/android/c++/JExecuteResult.hpp +86 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_bool.hpp +76 -0
- package/nitrogen/generated/android/c++/JGenerationStats.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +371 -0
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +83 -0
- package/nitrogen/generated/android/c++/JHybridModelStoreSpec.cpp +101 -0
- package/nitrogen/generated/android/c++/JHybridModelStoreSpec.hpp +67 -0
- package/nitrogen/generated/android/c++/JLLMConfig.hpp +125 -0
- package/nitrogen/generated/android/c++/JMemoryUsage.hpp +69 -0
- package/nitrogen/generated/android/c++/JMessage.hpp +63 -0
- package/nitrogen/generated/android/c++/JModelFile.hpp +69 -0
- package/nitrogen/generated/android/c++/JMultimodalPart.hpp +78 -0
- package/nitrogen/generated/android/c++/JPartType.hpp +61 -0
- package/nitrogen/generated/android/c++/JRole.hpp +61 -0
- package/nitrogen/generated/android/c++/JToolCall.hpp +61 -0
- package/nitrogen/generated/android/c++/JToolDefinition.hpp +65 -0
- package/nitrogen/generated/android/c++/JToolResponse.hpp +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Backend.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ExecuteResult.kt +61 -0
- 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/Func_void_std__string_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/GenerationStats.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +165 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridModelStoreSpec.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +106 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LiteRTLMOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MemoryUsage.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Message.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ModelFile.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MultimodalPart.kt +71 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/PartType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Role.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolCall.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolDefinition.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolResponse.kt +56 -0
- package/nitrogen/generated/ios/LiteRTLM+autolinking.rb +62 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.cpp +98 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.hpp +585 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Umbrella.hpp +93 -0
- package/nitrogen/generated/ios/LiteRTLMAutolinking.mm +41 -0
- package/nitrogen/generated/ios/LiteRTLMAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +279 -0
- package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.hpp +117 -0
- package/nitrogen/generated/ios/swift/Backend.swift +44 -0
- package/nitrogen/generated/ios/swift/ExecuteResult.swift +45 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_ExecuteResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/GenerationStats.swift +54 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +75 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +516 -0
- package/nitrogen/generated/ios/swift/HybridModelStoreSpec.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridModelStoreSpec_cxx.swift +203 -0
- package/nitrogen/generated/ios/swift/LLMConfig.swift +239 -0
- package/nitrogen/generated/ios/swift/MemoryUsage.swift +44 -0
- package/nitrogen/generated/ios/swift/Message.swift +34 -0
- package/nitrogen/generated/ios/swift/ModelFile.swift +44 -0
- package/nitrogen/generated/ios/swift/MultimodalPart.swift +101 -0
- package/nitrogen/generated/ios/swift/PartType.swift +44 -0
- package/nitrogen/generated/ios/swift/Role.swift +44 -0
- package/nitrogen/generated/ios/swift/ToolCall.swift +34 -0
- package/nitrogen/generated/ios/swift/ToolDefinition.swift +39 -0
- package/nitrogen/generated/ios/swift/ToolResponse.swift +34 -0
- package/nitrogen/generated/shared/c++/Backend.hpp +80 -0
- package/nitrogen/generated/shared/c++/ExecuteResult.hpp +94 -0
- package/nitrogen/generated/shared/c++/GenerationStats.hpp +103 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +41 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +109 -0
- package/nitrogen/generated/shared/c++/HybridModelStoreSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridModelStoreSpec.hpp +71 -0
- package/nitrogen/generated/shared/c++/LLMConfig.hpp +134 -0
- package/nitrogen/generated/shared/c++/MemoryUsage.hpp +95 -0
- package/nitrogen/generated/shared/c++/Message.hpp +89 -0
- package/nitrogen/generated/shared/c++/ModelFile.hpp +95 -0
- package/nitrogen/generated/shared/c++/MultimodalPart.hpp +103 -0
- package/nitrogen/generated/shared/c++/PartType.hpp +80 -0
- package/nitrogen/generated/shared/c++/Role.hpp +80 -0
- package/nitrogen/generated/shared/c++/ToolCall.hpp +87 -0
- package/nitrogen/generated/shared/c++/ToolDefinition.hpp +91 -0
- package/nitrogen/generated/shared/c++/ToolResponse.hpp +87 -0
- package/package.json +115 -0
- package/react-native-litert-lm.podspec +62 -0
- package/react-native.config.js +16 -0
- package/scripts/download-ios-frameworks.sh +39 -0
- package/scripts/framework-source.js +46 -0
- package/scripts/postinstall.js +145 -0
- package/src/__mocks__/react-native-nitro-modules.ts +132 -0
- package/src/__tests__/hooks.test.ts +216 -0
- package/src/__tests__/inferenceRouting.test.ts +36 -0
- package/src/__tests__/memoryTracker.test.ts +87 -0
- package/src/__tests__/modelFactory.test.ts +153 -0
- package/src/__tests__/modelPath.test.ts +14 -0
- package/src/__tests__/modelRegistry.test.ts +73 -0
- package/src/hooks.ts +234 -0
- package/src/index.ts +214 -0
- package/src/inferenceRouting.ts +80 -0
- package/src/memoryTracker.ts +268 -0
- package/src/modelFactory.ts +163 -0
- package/src/modelPath.ts +16 -0
- package/src/modelRegistry.ts +104 -0
- package/src/specs/LiteRTLM.nitro.ts +451 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HybridLiteRTLM.swift
|
|
3
|
+
// react-native-litert-lm
|
|
4
|
+
//
|
|
5
|
+
// Created by Antigravity on 2026-05-19.
|
|
6
|
+
// Copyright © 2026 Margelo. All rights reserved.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
import NitroModules
|
|
11
|
+
import CLiteRTLM
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
public class HybridLiteRTLM: HybridLiteRTLMSpec_base, HybridLiteRTLMSpec_protocol {
|
|
15
|
+
|
|
16
|
+
// MARK: - Internal (for extension files)
|
|
17
|
+
|
|
18
|
+
/// Dedicated background serial queue to protect the JSI/JS thread from blocking and deadlocks (User Rule #1).
|
|
19
|
+
let queue = DispatchQueue(label: "dev.litert.engine", qos: .userInteractive)
|
|
20
|
+
|
|
21
|
+
/// Opaque pointer to the active conversation state.
|
|
22
|
+
var conversation: OpaquePointer?
|
|
23
|
+
|
|
24
|
+
/// Conversation history.
|
|
25
|
+
var history: [Message] = []
|
|
26
|
+
|
|
27
|
+
/// Latest inference generation statistics.
|
|
28
|
+
var lastStats = GenerationStats(
|
|
29
|
+
promptTokens: 0.0,
|
|
30
|
+
completionTokens: 0.0,
|
|
31
|
+
totalTokens: 0.0,
|
|
32
|
+
timeToFirstToken: 0.0,
|
|
33
|
+
totalTime: 0.0,
|
|
34
|
+
tokensPerSecond: 0.0
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
var loadedModelPath: String?
|
|
38
|
+
let modelStore = HybridModelStore()
|
|
39
|
+
|
|
40
|
+
// MARK: - Private state
|
|
41
|
+
|
|
42
|
+
/// Opaque pointer to the LiteRT LM C Engine.
|
|
43
|
+
private var engine: OpaquePointer?
|
|
44
|
+
|
|
45
|
+
/// Thread-safe status flag.
|
|
46
|
+
private var isLoaded = false
|
|
47
|
+
|
|
48
|
+
// Default configuration variables
|
|
49
|
+
private var backend: Backend = .cpu
|
|
50
|
+
private var temperature: Double = 0.7
|
|
51
|
+
private var topK: Int = 40
|
|
52
|
+
private var topP: Double = 0.95
|
|
53
|
+
private var maxContextTokens: Int = 4096
|
|
54
|
+
private var maxOutputTokens: Int = 1024
|
|
55
|
+
private var systemPrompt: String?
|
|
56
|
+
private var tools: [ToolDefinition]?
|
|
57
|
+
private var enableSpeculativeDecoding: Bool = false
|
|
58
|
+
|
|
59
|
+
/// Approximate model weight size to inform the JS engine's garbage collection.
|
|
60
|
+
public var memorySize: Int {
|
|
61
|
+
return 1024 * 1024 * 1024 // ~1GB proxy
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
deinit {
|
|
65
|
+
closeInternal()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// MARK: - Core Hybrid Object API
|
|
69
|
+
|
|
70
|
+
public func isReady() throws -> Bool {
|
|
71
|
+
return queue.sync { isLoaded }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public func getHistory() throws -> [Message] {
|
|
75
|
+
return queue.sync { history }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public func resetConversation() throws {
|
|
79
|
+
queue.sync {
|
|
80
|
+
history.removeAll()
|
|
81
|
+
lastStats = GenerationStats(
|
|
82
|
+
promptTokens: 0.0,
|
|
83
|
+
completionTokens: 0.0,
|
|
84
|
+
totalTokens: 0.0,
|
|
85
|
+
timeToFirstToken: 0.0,
|
|
86
|
+
totalTime: 0.0,
|
|
87
|
+
tokensPerSecond: 0.0
|
|
88
|
+
)
|
|
89
|
+
if isLoaded && engine != nil {
|
|
90
|
+
createNewConversation()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public func getStats() throws -> GenerationStats {
|
|
96
|
+
return queue.sync { lastStats }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public func countTokens(text: String) throws -> Double {
|
|
100
|
+
return queue.sync {
|
|
101
|
+
guard let engine = self.engine else {
|
|
102
|
+
return -1.0
|
|
103
|
+
}
|
|
104
|
+
guard let result = litert_lm_engine_tokenize(engine, text) else {
|
|
105
|
+
return -1.0
|
|
106
|
+
}
|
|
107
|
+
let numTokens = litert_lm_tokenize_result_get_num_tokens(result)
|
|
108
|
+
litert_lm_tokenize_result_delete(result)
|
|
109
|
+
return Double(numTokens)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public func getMemoryUsage() throws -> MemoryUsage {
|
|
114
|
+
var residentBytes: Double = 0.0
|
|
115
|
+
var nativeHeapBytes: Double = 0.0
|
|
116
|
+
|
|
117
|
+
// Retrieve process resident set size (RSS) via Mach basic task info
|
|
118
|
+
var info = mach_task_basic_info()
|
|
119
|
+
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size / MemoryLayout<integer_t>.size)
|
|
120
|
+
let kerr = withUnsafeMutablePointer(to: &info) {
|
|
121
|
+
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
|
122
|
+
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if kerr == KERN_SUCCESS {
|
|
127
|
+
residentBytes = Double(info.resident_size)
|
|
128
|
+
nativeHeapBytes = Double(info.resident_size)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// os_proc_available_memory reports actual headroom available before Jetsam termination (iOS 13+)
|
|
132
|
+
let availableBytes = Double(os_proc_available_memory())
|
|
133
|
+
|
|
134
|
+
// Flag memory warning at ~200MB remaining headroom
|
|
135
|
+
let isLowMemory = availableBytes < 200.0 * 1024.0 * 1024.0
|
|
136
|
+
|
|
137
|
+
return MemoryUsage(
|
|
138
|
+
nativeHeapBytes: nativeHeapBytes,
|
|
139
|
+
residentBytes: residentBytes,
|
|
140
|
+
availableMemoryBytes: availableBytes,
|
|
141
|
+
isLowMemory: isLowMemory
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public func getActiveBackend() throws -> Backend {
|
|
146
|
+
return backend
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public func stopGeneration() throws {
|
|
150
|
+
queue.async {
|
|
151
|
+
guard let conversation = self.conversation else { return }
|
|
152
|
+
litert_lm_conversation_cancel_process(conversation)
|
|
153
|
+
NSLog("[LiteRTLM] stopGeneration: cancelled active inference")
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public func close() throws {
|
|
158
|
+
queue.sync {
|
|
159
|
+
closeInternal()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// MARK: - Async Operations
|
|
164
|
+
|
|
165
|
+
public func loadModel(modelPath: String, config: LLMConfig?) throws -> Promise<Void> {
|
|
166
|
+
let promise = Promise<Void>()
|
|
167
|
+
|
|
168
|
+
queue.async {
|
|
169
|
+
// Teardown any previous contexts
|
|
170
|
+
self.closeInternal()
|
|
171
|
+
|
|
172
|
+
// Extract configurations
|
|
173
|
+
if let config = config {
|
|
174
|
+
if let b = config.backend { self.backend = b }
|
|
175
|
+
if let t = config.temperature { self.temperature = t }
|
|
176
|
+
if let k = config.topK { self.topK = Int(k) }
|
|
177
|
+
if let p = config.topP { self.topP = p }
|
|
178
|
+
// New split fields take priority over legacy maxTokens
|
|
179
|
+
if let ctx = config.maxContextTokens { self.maxContextTokens = Int(ctx) }
|
|
180
|
+
if let out = config.maxOutputTokens { self.maxOutputTokens = Int(out) }
|
|
181
|
+
// Legacy: if only maxTokens is set, map to both for backward compat
|
|
182
|
+
if config.maxContextTokens == nil && config.maxOutputTokens == nil,
|
|
183
|
+
let m = config.maxTokens {
|
|
184
|
+
self.maxContextTokens = Int(m)
|
|
185
|
+
self.maxOutputTokens = Int(m)
|
|
186
|
+
}
|
|
187
|
+
if let s = config.systemPrompt { self.systemPrompt = s }
|
|
188
|
+
self.tools = config.tools
|
|
189
|
+
self.enableSpeculativeDecoding = config.enableSpeculativeDecoding ?? false
|
|
190
|
+
} else {
|
|
191
|
+
self.tools = nil
|
|
192
|
+
self.enableSpeculativeDecoding = false
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Map main backend string
|
|
196
|
+
let mainBackendStr = self.backend == .gpu ? "gpu" : (self.backend == .npu ? "gpu" : "cpu")
|
|
197
|
+
|
|
198
|
+
//Sniff multimodal support
|
|
199
|
+
let isMultimodal = config?.multimodal ?? (modelPath.lowercased().contains("3n") || modelPath.lowercased().contains("gemma3"))
|
|
200
|
+
let visionBackend = isMultimodal ? "gpu" : nil
|
|
201
|
+
let audioBackend = isMultimodal ? "cpu" : nil
|
|
202
|
+
|
|
203
|
+
var rawEngine: OpaquePointer? = nil
|
|
204
|
+
|
|
205
|
+
// Set LiteRT C Log Level to WARNING (2) for clean production output
|
|
206
|
+
litert_lm_set_min_log_level(2)
|
|
207
|
+
|
|
208
|
+
// Creation helper with scoped FFI pointer lifetime
|
|
209
|
+
let createEngine = { (main: String, vision: String?, audio: String?) -> OpaquePointer? in
|
|
210
|
+
let settings = modelPath.withCString { modelC in
|
|
211
|
+
self.withOptionalCString(main) { mainC in
|
|
212
|
+
self.withOptionalCString(vision) { visionC in
|
|
213
|
+
self.withOptionalCString(audio) { audioC in
|
|
214
|
+
return litert_lm_engine_settings_create(modelC, mainC, visionC, audioC)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
guard let s = settings else { return nil }
|
|
221
|
+
defer { litert_lm_engine_settings_delete(s) }
|
|
222
|
+
|
|
223
|
+
litert_lm_engine_settings_set_max_num_tokens(s, Int32(self.maxContextTokens))
|
|
224
|
+
litert_lm_engine_settings_enable_benchmark(s)
|
|
225
|
+
|
|
226
|
+
if self.enableSpeculativeDecoding {
|
|
227
|
+
if let loadedFile = litert_lm_loaded_file_create((modelPath as NSString).utf8String) {
|
|
228
|
+
let hasMtp = litert_lm_loaded_file_has_speculative_decoding_support(loadedFile)
|
|
229
|
+
litert_lm_loaded_file_delete(loadedFile)
|
|
230
|
+
if hasMtp {
|
|
231
|
+
litert_lm_engine_settings_set_enable_speculative_decoding(s, true)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Cache dir set to parent directory of model path
|
|
237
|
+
let cacheDir = (modelPath as NSString).deletingLastPathComponent
|
|
238
|
+
cacheDir.withCString { cacheC in
|
|
239
|
+
litert_lm_engine_settings_set_cache_dir(s, cacheC)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return litert_lm_engine_create(s)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Attempt primary backend configuration
|
|
246
|
+
rawEngine = createEngine(mainBackendStr, visionBackend, audioBackend)
|
|
247
|
+
|
|
248
|
+
// Fallback sequence if GPU/NPU fails to initialize
|
|
249
|
+
if rawEngine == nil {
|
|
250
|
+
if mainBackendStr != "cpu" {
|
|
251
|
+
NSLog("[LiteRTLM] %@ backend failed — trying fallback chain...", mainBackendStr.uppercased())
|
|
252
|
+
}
|
|
253
|
+
// Fallback 1: CPU execution with GPU acceleration for heavy Vision parameters
|
|
254
|
+
rawEngine = createEngine("cpu", "gpu", "cpu")
|
|
255
|
+
|
|
256
|
+
if rawEngine == nil {
|
|
257
|
+
// Fallback 2: Full CPU execution for all modalities
|
|
258
|
+
rawEngine = createEngine("cpu", "cpu", "cpu")
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if rawEngine == nil {
|
|
262
|
+
// Fallback 3: Text-only CPU execution (skip vision executor mapping)
|
|
263
|
+
rawEngine = createEngine("cpu", nil, nil)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if rawEngine != nil {
|
|
267
|
+
NSLog("[LiteRTLM] %@ backend unavailable — fell back to CPU successfully", mainBackendStr.uppercased())
|
|
268
|
+
self.backend = .cpu
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
guard let engine = rawEngine else {
|
|
273
|
+
promise.reject(withError: NSError(domain: "LiteRTLM", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to construct LiteRT-LM engine. Checked backends and fallback chains."]))
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
self.engine = engine
|
|
278
|
+
self.createNewConversation()
|
|
279
|
+
self.loadedModelPath = modelPath
|
|
280
|
+
|
|
281
|
+
guard self.conversation != nil else {
|
|
282
|
+
self.closeInternal()
|
|
283
|
+
promise.reject(withError: NSError(domain: "LiteRTLM", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to create conversation context."]))
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
self.isLoaded = true
|
|
288
|
+
promise.resolve()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return promise
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Legacy inference — shapes mirror src/inferenceRouting.ts; JS createLLM routes via execute.
|
|
295
|
+
public func sendMessage(message: String) throws -> Promise<ExecuteResult> {
|
|
296
|
+
try execute(parts: [.textPart(message)], onToken: nil)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public func sendMessageAsync(
|
|
300
|
+
message: String,
|
|
301
|
+
onToken: @escaping (_ token: String, _ done: Bool) -> Void
|
|
302
|
+
) throws -> Promise<Void> {
|
|
303
|
+
try executeVoid(parts: [.textPart(message)], onToken: onToken)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
public func sendMessageWithImage(message: String, imagePath: String) throws -> Promise<ExecuteResult> {
|
|
307
|
+
try execute(parts: [.textPart(message), .imagePart(imagePath)], onToken: nil)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
public func sendMessageWithImageAsync(
|
|
311
|
+
message: String, imagePath: String,
|
|
312
|
+
onToken: @escaping (_ token: String, _ done: Bool) -> Void
|
|
313
|
+
) throws -> Promise<Void> {
|
|
314
|
+
try executeVoid(parts: [.textPart(message), .imagePart(imagePath)], onToken: onToken)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
public func sendMessageWithAudioAsync(
|
|
318
|
+
message: String, audioPath: String,
|
|
319
|
+
onToken: @escaping (_ token: String, _ done: Bool) -> Void
|
|
320
|
+
) throws -> Promise<Void> {
|
|
321
|
+
try executeVoid(parts: [.textPart(message), .audioPart(audioPath)], onToken: onToken)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
public func sendMessageWithAudio(message: String, audioPath: String) throws -> Promise<ExecuteResult> {
|
|
325
|
+
try execute(parts: [.textPart(message), .audioPart(audioPath)], onToken: nil)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
public func sendMultimodalMessage(parts: [MultimodalPart]) throws -> Promise<ExecuteResult> {
|
|
329
|
+
try execute(parts: parts, onToken: nil)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
public func sendToolResponse(
|
|
333
|
+
responses: [ToolResponse],
|
|
334
|
+
onToken: ((_ token: String, _ done: Bool) -> Void)?
|
|
335
|
+
) throws -> Promise<ExecuteResult> {
|
|
336
|
+
// Format tool results as a message and send to the conversation
|
|
337
|
+
let toolResultText = responses.map { "Tool '\($0.name)' result: \($0.responseJson)" }
|
|
338
|
+
.joined(separator: "\n")
|
|
339
|
+
return try execute(parts: [.textPart(toolResultText)], onToken: onToken)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
public func downloadModel(
|
|
343
|
+
url: String,
|
|
344
|
+
fileName: String,
|
|
345
|
+
onProgress: ((Double) -> Void)?
|
|
346
|
+
) throws -> Promise<String> {
|
|
347
|
+
return try modelStore.downloadFile(
|
|
348
|
+
url: url,
|
|
349
|
+
fileName: fileName,
|
|
350
|
+
headersJson: "{}",
|
|
351
|
+
onProgress: { progress in
|
|
352
|
+
onProgress?(progress)
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
public func deleteModel(fileName: String) throws -> Promise<Void> {
|
|
358
|
+
let promise = Promise<Void>()
|
|
359
|
+
|
|
360
|
+
queue.async {
|
|
361
|
+
do {
|
|
362
|
+
try self.modelStore.deleteFile(fileName: fileName)
|
|
363
|
+
let currentlyLoadedName = self.loadedModelPath.map { ($0 as NSString).lastPathComponent.lowercased() }
|
|
364
|
+
if let loadedName = currentlyLoadedName, loadedName == fileName.lowercased() {
|
|
365
|
+
if self.isLoaded {
|
|
366
|
+
self.closeInternal()
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
promise.resolve()
|
|
370
|
+
} catch {
|
|
371
|
+
promise.reject(withError: error)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return promise
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// MARK: - Internal Engine Helpers
|
|
379
|
+
|
|
380
|
+
private func createNewConversation() {
|
|
381
|
+
guard let engine = self.engine else { return }
|
|
382
|
+
|
|
383
|
+
if let oldConv = self.conversation {
|
|
384
|
+
litert_lm_conversation_delete(oldConv)
|
|
385
|
+
self.conversation = nil
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
guard let convConfig = litert_lm_conversation_config_create() else { return }
|
|
389
|
+
defer { litert_lm_conversation_config_delete(convConfig) }
|
|
390
|
+
|
|
391
|
+
guard let sessionConfig = litert_lm_session_config_create() else { return }
|
|
392
|
+
defer { litert_lm_session_config_delete(sessionConfig) }
|
|
393
|
+
|
|
394
|
+
litert_lm_session_config_set_max_output_tokens(sessionConfig, Int32(self.maxOutputTokens))
|
|
395
|
+
|
|
396
|
+
var sampler = LiteRtLmSamplerParams()
|
|
397
|
+
sampler.type = kLiteRtLmSamplerTypeTopP
|
|
398
|
+
sampler.top_k = Int32(self.topK)
|
|
399
|
+
sampler.top_p = Float(self.topP)
|
|
400
|
+
sampler.temperature = Float(self.temperature)
|
|
401
|
+
sampler.seed = 0
|
|
402
|
+
withUnsafePointer(to: &sampler) { samplerPtr in
|
|
403
|
+
litert_lm_session_config_set_sampler_params(sessionConfig, samplerPtr)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
litert_lm_conversation_config_set_session_config(convConfig, sessionConfig)
|
|
407
|
+
|
|
408
|
+
if let systemPrompt = self.systemPrompt {
|
|
409
|
+
let systemMsgJson = "{\"role\":\"system\",\"content\":\"" + escapeJson(systemPrompt) + "\"}"
|
|
410
|
+
systemMsgJson.withCString { systemMsgC in
|
|
411
|
+
litert_lm_conversation_config_set_system_message(convConfig, systemMsgC)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if let tools = self.tools, !tools.isEmpty {
|
|
416
|
+
var toolsArray: [[String: Any]] = []
|
|
417
|
+
for tool in tools {
|
|
418
|
+
var functionMap: [String: Any] = ["name": tool.name, "description": tool.description]
|
|
419
|
+
if let data = tool.parametersJson.data(using: .utf8),
|
|
420
|
+
let parsedParams = try? JSONSerialization.jsonObject(with: data, options: []) {
|
|
421
|
+
functionMap["parameters"] = parsedParams
|
|
422
|
+
}
|
|
423
|
+
toolsArray.append(["type": "function", "function": functionMap])
|
|
424
|
+
}
|
|
425
|
+
if let data = try? JSONSerialization.data(withJSONObject: toolsArray, options: []),
|
|
426
|
+
let jsonString = String(data: data, encoding: .utf8) {
|
|
427
|
+
jsonString.withCString { toolsC in
|
|
428
|
+
litert_lm_conversation_config_set_tools(convConfig, toolsC)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
self.conversation = litert_lm_conversation_create(engine, convConfig)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private func closeInternal() {
|
|
437
|
+
isLoaded = false
|
|
438
|
+
history.removeAll()
|
|
439
|
+
loadedModelPath = nil
|
|
440
|
+
|
|
441
|
+
if let conversation = self.conversation {
|
|
442
|
+
litert_lm_conversation_delete(conversation)
|
|
443
|
+
self.conversation = nil
|
|
444
|
+
}
|
|
445
|
+
if let engine = self.engine {
|
|
446
|
+
litert_lm_engine_delete(engine)
|
|
447
|
+
self.engine = nil
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
lastStats = GenerationStats(
|
|
451
|
+
promptTokens: 0.0,
|
|
452
|
+
completionTokens: 0.0,
|
|
453
|
+
totalTokens: 0.0,
|
|
454
|
+
timeToFirstToken: 0.0,
|
|
455
|
+
totalTime: 0.0,
|
|
456
|
+
tokensPerSecond: 0.0
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// MARK: - Internal Preprocessing Helpers (for extension files)
|
|
461
|
+
|
|
462
|
+
private let kControlTokens = [
|
|
463
|
+
"<end_of_turn>",
|
|
464
|
+
"<start_of_turn>model",
|
|
465
|
+
"<start_of_turn>user",
|
|
466
|
+
"<start_of_turn>",
|
|
467
|
+
"<eos>"
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
func escapeJson(_ input: String) -> String {
|
|
471
|
+
var output = ""
|
|
472
|
+
for char in input {
|
|
473
|
+
switch char {
|
|
474
|
+
case "\"": output += "\\\""
|
|
475
|
+
case "\\": output += "\\\\"
|
|
476
|
+
case "\n": output += "\\n"
|
|
477
|
+
case "\r": output += "\\r"
|
|
478
|
+
case "\t": output += "\\t"
|
|
479
|
+
case "\u{0008}": output += "\\b"
|
|
480
|
+
case "\u{000c}": output += "\\f"
|
|
481
|
+
default: output.append(char)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return output
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
func stripControlTokens(_ text: String) -> String {
|
|
488
|
+
var result = text
|
|
489
|
+
for tok in kControlTokens {
|
|
490
|
+
result = result.replacingOccurrences(of: tok, with: "")
|
|
491
|
+
}
|
|
492
|
+
return result
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
func safeEmitLength(_ text: String) -> Int {
|
|
496
|
+
let chars = Array(text)
|
|
497
|
+
guard let lastAngleIdx = chars.lastIndex(of: "<") else {
|
|
498
|
+
return chars.count
|
|
499
|
+
}
|
|
500
|
+
let suffix = String(chars[lastAngleIdx...])
|
|
501
|
+
for tok in kControlTokens {
|
|
502
|
+
if tok.hasPrefix(suffix) && suffix.count < tok.count {
|
|
503
|
+
return lastAngleIdx
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return chars.count
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
func extractTextFromResponse(_ jsonResponse: String) -> String {
|
|
510
|
+
guard let data = jsonResponse.data(using: .utf8) else {
|
|
511
|
+
return stripControlTokens(jsonResponse)
|
|
512
|
+
}
|
|
513
|
+
do {
|
|
514
|
+
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
|
|
515
|
+
if let content = json["content"] {
|
|
516
|
+
if let contentString = content as? String {
|
|
517
|
+
return stripControlTokens(contentString)
|
|
518
|
+
} else if let contentArray = content as? [[String: Any]] {
|
|
519
|
+
var textResult = ""
|
|
520
|
+
for part in contentArray {
|
|
521
|
+
if let type = part["type"] as? String, type == "text", let text = part["text"] as? String {
|
|
522
|
+
textResult += text
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return stripControlTokens(textResult)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} catch {}
|
|
530
|
+
return stripControlTokens(jsonResponse)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private func withOptionalCString<R>(_ string: String?, _ block: (UnsafePointer<CChar>?) -> R) -> R {
|
|
534
|
+
if let string = string {
|
|
535
|
+
return string.withCString { block($0) }
|
|
536
|
+
} else {
|
|
537
|
+
return block(nil)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
}
|