@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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +627 -0
  3. package/android/CMakeLists.txt +32 -0
  4. package/android/build.gradle +104 -0
  5. package/android/src/main/AndroidManifest.xml +19 -0
  6. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +888 -0
  7. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridModelStore.kt +189 -0
  8. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/LiteRTLMRegistry.kt +32 -0
  9. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/MultimodalPartFactories.kt +15 -0
  10. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/StreamingCallbackListener.kt +89 -0
  11. package/android/src/main/java/dev/litert/litertlm/LiteRTLMInitProvider.kt +57 -0
  12. package/android/src/main/java/dev/litert/litertlm/LiteRTLMPackage.kt +43 -0
  13. package/android/src/test/java/com/margelo/nitro/core/Promise.kt +46 -0
  14. package/android/src/test/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMTest.kt +122 -0
  15. package/app.plugin.js +58 -0
  16. package/cpp/cpp-adapter.cpp +21 -0
  17. package/cpp/include/README.md +32 -0
  18. package/cpp/include/litert_lm_engine.h +516 -0
  19. package/cpp/include/stb_image.h +7988 -0
  20. package/ios/HybridLiteRTLM+Execute.swift +287 -0
  21. package/ios/HybridLiteRTLM+Streaming.swift +153 -0
  22. package/ios/HybridLiteRTLM.swift +541 -0
  23. package/ios/HybridModelStore.swift +206 -0
  24. package/ios/MultimodalPart+Factories.swift +22 -0
  25. package/ios/Tests/HybridLiteRTLMTests.swift +187 -0
  26. package/lib/__mocks__/react-native-nitro-modules.d.ts +81 -0
  27. package/lib/__mocks__/react-native-nitro-modules.js +98 -0
  28. package/lib/__tests__/hooks.test.d.ts +1 -0
  29. package/lib/__tests__/hooks.test.js +175 -0
  30. package/lib/__tests__/inferenceRouting.test.d.ts +1 -0
  31. package/lib/__tests__/inferenceRouting.test.js +28 -0
  32. package/lib/__tests__/memoryTracker.test.d.ts +1 -0
  33. package/lib/__tests__/memoryTracker.test.js +74 -0
  34. package/lib/__tests__/modelFactory.test.d.ts +1 -0
  35. package/lib/__tests__/modelFactory.test.js +107 -0
  36. package/lib/__tests__/modelPath.test.d.ts +1 -0
  37. package/lib/__tests__/modelPath.test.js +12 -0
  38. package/lib/__tests__/modelRegistry.test.d.ts +1 -0
  39. package/lib/__tests__/modelRegistry.test.js +61 -0
  40. package/lib/hooks.d.ts +46 -0
  41. package/lib/hooks.js +159 -0
  42. package/lib/index.d.ts +142 -0
  43. package/lib/index.js +184 -0
  44. package/lib/inferenceRouting.d.ts +18 -0
  45. package/lib/inferenceRouting.js +63 -0
  46. package/lib/memoryTracker.d.ts +128 -0
  47. package/lib/memoryTracker.js +155 -0
  48. package/lib/modelFactory.d.ts +24 -0
  49. package/lib/modelFactory.js +128 -0
  50. package/lib/modelPath.d.ts +8 -0
  51. package/lib/modelPath.js +18 -0
  52. package/lib/modelRegistry.d.ts +55 -0
  53. package/lib/modelRegistry.js +82 -0
  54. package/lib/specs/LiteRTLM.nitro.d.ts +380 -0
  55. package/lib/specs/LiteRTLM.nitro.js +2 -0
  56. package/nitrogen/generated/.gitattributes +1 -0
  57. package/nitrogen/generated/android/LiteRTLM+autolinking.cmake +83 -0
  58. package/nitrogen/generated/android/LiteRTLM+autolinking.gradle +27 -0
  59. package/nitrogen/generated/android/LiteRTLMOnLoad.cpp +74 -0
  60. package/nitrogen/generated/android/LiteRTLMOnLoad.hpp +34 -0
  61. package/nitrogen/generated/android/c++/JBackend.hpp +61 -0
  62. package/nitrogen/generated/android/c++/JExecuteResult.hpp +86 -0
  63. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  64. package/nitrogen/generated/android/c++/JFunc_void_std__string_bool.hpp +76 -0
  65. package/nitrogen/generated/android/c++/JGenerationStats.hpp +77 -0
  66. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +371 -0
  67. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +83 -0
  68. package/nitrogen/generated/android/c++/JHybridModelStoreSpec.cpp +101 -0
  69. package/nitrogen/generated/android/c++/JHybridModelStoreSpec.hpp +67 -0
  70. package/nitrogen/generated/android/c++/JLLMConfig.hpp +125 -0
  71. package/nitrogen/generated/android/c++/JMemoryUsage.hpp +69 -0
  72. package/nitrogen/generated/android/c++/JMessage.hpp +63 -0
  73. package/nitrogen/generated/android/c++/JModelFile.hpp +69 -0
  74. package/nitrogen/generated/android/c++/JMultimodalPart.hpp +78 -0
  75. package/nitrogen/generated/android/c++/JPartType.hpp +61 -0
  76. package/nitrogen/generated/android/c++/JRole.hpp +61 -0
  77. package/nitrogen/generated/android/c++/JToolCall.hpp +61 -0
  78. package/nitrogen/generated/android/c++/JToolDefinition.hpp +65 -0
  79. package/nitrogen/generated/android/c++/JToolResponse.hpp +61 -0
  80. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Backend.kt +24 -0
  81. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ExecuteResult.kt +61 -0
  82. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_double.kt +80 -0
  83. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_std__string_bool.kt +80 -0
  84. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/GenerationStats.kt +76 -0
  85. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +165 -0
  86. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridModelStoreSpec.kt +76 -0
  87. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +106 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LiteRTLMOnLoad.kt +35 -0
  89. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MemoryUsage.kt +66 -0
  90. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Message.kt +56 -0
  91. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ModelFile.kt +66 -0
  92. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MultimodalPart.kt +71 -0
  93. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/PartType.kt +24 -0
  94. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Role.kt +24 -0
  95. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolCall.kt +56 -0
  96. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolDefinition.kt +61 -0
  97. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolResponse.kt +56 -0
  98. package/nitrogen/generated/ios/LiteRTLM+autolinking.rb +62 -0
  99. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.cpp +98 -0
  100. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.hpp +585 -0
  101. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Umbrella.hpp +93 -0
  102. package/nitrogen/generated/ios/LiteRTLMAutolinking.mm +41 -0
  103. package/nitrogen/generated/ios/LiteRTLMAutolinking.swift +38 -0
  104. package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.cpp +11 -0
  105. package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +279 -0
  106. package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.cpp +11 -0
  107. package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.hpp +117 -0
  108. package/nitrogen/generated/ios/swift/Backend.swift +44 -0
  109. package/nitrogen/generated/ios/swift/ExecuteResult.swift +45 -0
  110. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  111. package/nitrogen/generated/ios/swift/Func_void_ExecuteResult.swift +46 -0
  112. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  113. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  114. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  115. package/nitrogen/generated/ios/swift/Func_void_std__string_bool.swift +46 -0
  116. package/nitrogen/generated/ios/swift/GenerationStats.swift +54 -0
  117. package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +75 -0
  118. package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +516 -0
  119. package/nitrogen/generated/ios/swift/HybridModelStoreSpec.swift +59 -0
  120. package/nitrogen/generated/ios/swift/HybridModelStoreSpec_cxx.swift +203 -0
  121. package/nitrogen/generated/ios/swift/LLMConfig.swift +239 -0
  122. package/nitrogen/generated/ios/swift/MemoryUsage.swift +44 -0
  123. package/nitrogen/generated/ios/swift/Message.swift +34 -0
  124. package/nitrogen/generated/ios/swift/ModelFile.swift +44 -0
  125. package/nitrogen/generated/ios/swift/MultimodalPart.swift +101 -0
  126. package/nitrogen/generated/ios/swift/PartType.swift +44 -0
  127. package/nitrogen/generated/ios/swift/Role.swift +44 -0
  128. package/nitrogen/generated/ios/swift/ToolCall.swift +34 -0
  129. package/nitrogen/generated/ios/swift/ToolDefinition.swift +39 -0
  130. package/nitrogen/generated/ios/swift/ToolResponse.swift +34 -0
  131. package/nitrogen/generated/shared/c++/Backend.hpp +80 -0
  132. package/nitrogen/generated/shared/c++/ExecuteResult.hpp +94 -0
  133. package/nitrogen/generated/shared/c++/GenerationStats.hpp +103 -0
  134. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +41 -0
  135. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +109 -0
  136. package/nitrogen/generated/shared/c++/HybridModelStoreSpec.cpp +25 -0
  137. package/nitrogen/generated/shared/c++/HybridModelStoreSpec.hpp +71 -0
  138. package/nitrogen/generated/shared/c++/LLMConfig.hpp +134 -0
  139. package/nitrogen/generated/shared/c++/MemoryUsage.hpp +95 -0
  140. package/nitrogen/generated/shared/c++/Message.hpp +89 -0
  141. package/nitrogen/generated/shared/c++/ModelFile.hpp +95 -0
  142. package/nitrogen/generated/shared/c++/MultimodalPart.hpp +103 -0
  143. package/nitrogen/generated/shared/c++/PartType.hpp +80 -0
  144. package/nitrogen/generated/shared/c++/Role.hpp +80 -0
  145. package/nitrogen/generated/shared/c++/ToolCall.hpp +87 -0
  146. package/nitrogen/generated/shared/c++/ToolDefinition.hpp +91 -0
  147. package/nitrogen/generated/shared/c++/ToolResponse.hpp +87 -0
  148. package/package.json +115 -0
  149. package/react-native-litert-lm.podspec +62 -0
  150. package/react-native.config.js +16 -0
  151. package/scripts/download-ios-frameworks.sh +39 -0
  152. package/scripts/framework-source.js +46 -0
  153. package/scripts/postinstall.js +145 -0
  154. package/src/__mocks__/react-native-nitro-modules.ts +132 -0
  155. package/src/__tests__/hooks.test.ts +216 -0
  156. package/src/__tests__/inferenceRouting.test.ts +36 -0
  157. package/src/__tests__/memoryTracker.test.ts +87 -0
  158. package/src/__tests__/modelFactory.test.ts +153 -0
  159. package/src/__tests__/modelPath.test.ts +14 -0
  160. package/src/__tests__/modelRegistry.test.ts +73 -0
  161. package/src/hooks.ts +234 -0
  162. package/src/index.ts +214 -0
  163. package/src/inferenceRouting.ts +80 -0
  164. package/src/memoryTracker.ts +268 -0
  165. package/src/modelFactory.ts +163 -0
  166. package/src/modelPath.ts +16 -0
  167. package/src/modelRegistry.ts +104 -0
  168. 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
+ }