@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,287 @@
1
+ //
2
+ // HybridLiteRTLM+Execute.swift
3
+ // react-native-litert-lm
4
+ //
5
+ // Unified inference pipeline (execute). JS routes via src/inferenceRouting.ts;
6
+ // legacy send* methods below delegate here for direct native hybrid access.
7
+ //
8
+
9
+ import Foundation
10
+ import UIKit
11
+ import NitroModules
12
+ import CLiteRTLM
13
+
14
+
15
+
16
+ // MARK: - Preprocessed parts for thread-safety
17
+
18
+ struct PreprocessedPart {
19
+ enum Kind {
20
+ case text(String)
21
+ case imagePath(String)
22
+ case imageData(Data)
23
+ case audioPath(String)
24
+ case audioData(Data)
25
+ }
26
+ let kind: Kind
27
+ let label: String
28
+ }
29
+
30
+ // MARK: - Execute pipeline
31
+
32
+ extension HybridLiteRTLM {
33
+
34
+ /// Only used when legacy native *Async methods are invoked directly (not via createLLM JS proxy).
35
+ func executeVoid(
36
+ parts: [MultimodalPart],
37
+ onToken: @escaping (_ token: String, _ done: Bool) -> Void
38
+ ) throws -> Promise<Void> {
39
+ let voidPromise = Promise<Void>()
40
+ let resultPromise = try execute(parts: parts, onToken: onToken)
41
+ resultPromise
42
+ .then { _ in voidPromise.resolve() }
43
+ .catch { voidPromise.reject(withError: $0) }
44
+ return voidPromise
45
+ }
46
+
47
+ public func execute(parts: [MultimodalPart], onToken: ((_ token: String, _ done: Bool) -> Void)?) throws -> Promise<ExecuteResult> {
48
+ let promise = Promise<ExecuteResult>()
49
+
50
+ // Preprocess all JSI-bound data on the caller thread synchronously
51
+ var preprocessed: [PreprocessedPart] = []
52
+ for part in parts {
53
+ switch part.type {
54
+ case .text:
55
+ let txt = part.text ?? ""
56
+ preprocessed.append(PreprocessedPart(kind: .text(txt), label: txt))
57
+ case .image:
58
+ if let path = part.path {
59
+ preprocessed.append(PreprocessedPart(kind: .imagePath(path), label: "[Image]"))
60
+ } else if let buf = part.imageBuffer {
61
+ let data = buf.toData(copyIfNeeded: true)
62
+ preprocessed.append(PreprocessedPart(kind: .imageData(data), label: "[Image]"))
63
+ }
64
+ case .audio:
65
+ if let path = part.path {
66
+ preprocessed.append(PreprocessedPart(kind: .audioPath(path), label: "[Audio]"))
67
+ } else if let buf = part.audioBuffer {
68
+ let data = buf.toData(copyIfNeeded: true)
69
+ preprocessed.append(PreprocessedPart(kind: .audioData(data), label: "[Audio]"))
70
+ }
71
+ }
72
+ }
73
+
74
+ let userLabel = preprocessed.map { $0.label }.joined(separator: " ").trimmingCharacters(in: .whitespaces)
75
+
76
+ queue.async {
77
+ guard let conversation = self.conversation else {
78
+ promise.reject(withError: NSError(domain: "LiteRTLM", code: 400,
79
+ userInfo: [NSLocalizedDescriptionKey: "LiteRTLM: No model loaded. Call loadModel() first."]))
80
+ return
81
+ }
82
+
83
+ let payload: (json: String, tempFiles: [String])
84
+ do { payload = try self.buildExecutePayload(preprocessed) }
85
+ catch { promise.reject(withError: error); return }
86
+
87
+ let msgJson = payload.json
88
+ let tempFiles = payload.tempFiles
89
+ let cleanup = { tempFiles.forEach { try? FileManager.default.removeItem(atPath: $0) } }
90
+
91
+ if let onToken = onToken {
92
+ self.runExecuteStreaming(
93
+ conversation: conversation,
94
+ msgJson: msgJson,
95
+ userLabel: userLabel,
96
+ onToken: onToken,
97
+ promise: promise,
98
+ cleanup: cleanup
99
+ )
100
+ } else {
101
+ self.runExecuteBlocking(
102
+ conversation: conversation,
103
+ msgJson: msgJson,
104
+ userLabel: userLabel,
105
+ promise: promise,
106
+ cleanup: cleanup
107
+ )
108
+ }
109
+ }
110
+
111
+ return promise
112
+ }
113
+
114
+ // MARK: - Payload / media
115
+
116
+ private func validateMediaPath(_ path: String, label: String) throws {
117
+ if !FileManager.default.fileExists(atPath: path) {
118
+ throw NSError(
119
+ domain: "LiteRTLM", code: 404,
120
+ userInfo: [NSLocalizedDescriptionKey: "\(label) file not found: \(path)"]
121
+ )
122
+ }
123
+ }
124
+
125
+ private func buildExecutePayload(_ preprocessed: [PreprocessedPart]) throws -> (json: String, tempFiles: [String]) {
126
+ struct Desc { let kind: String; let text: String?; let file: String? }
127
+ var descs: [Desc] = []
128
+ var temps: [String] = []
129
+
130
+ do {
131
+ for part in preprocessed {
132
+ switch part.kind {
133
+ case .text(let text):
134
+ descs.append(Desc(kind: "text", text: text, file: nil))
135
+ case .imagePath(let path):
136
+ try validateMediaPath(path, label: "Image")
137
+ let scaled = scaleImageIfNeeded(path)
138
+ if scaled != path { temps.append(scaled) }
139
+ descs.append(Desc(kind: "image", text: nil, file: scaled))
140
+ case .imageData(let data):
141
+ let raw = try saveDataToTempFile(data, ext: "jpg")
142
+ temps.append(raw)
143
+ let scaled = scaleImageIfNeeded(raw)
144
+ if scaled != raw { temps.append(scaled) }
145
+ descs.append(Desc(kind: "image", text: nil, file: scaled))
146
+ case .audioPath(let path):
147
+ try validateMediaPath(path, label: "Audio")
148
+ descs.append(Desc(kind: "audio", text: nil, file: path))
149
+ case .audioData(let data):
150
+ let raw = try saveDataToTempFile(data, ext: "wav")
151
+ temps.append(raw)
152
+ descs.append(Desc(kind: "audio", text: nil, file: raw))
153
+ }
154
+ }
155
+ } catch {
156
+ temps.forEach { try? FileManager.default.removeItem(atPath: $0) }
157
+ throw error
158
+ }
159
+
160
+ if descs.count == 1 && descs[0].kind == "text" {
161
+ let payload: [String: Any] = ["role": "user", "content": descs[0].text ?? ""]
162
+ let data = try JSONSerialization.data(withJSONObject: payload, options: [])
163
+ if let jsonString = String(data: data, encoding: .utf8) {
164
+ return (jsonString, temps)
165
+ }
166
+ }
167
+
168
+ var contents: [[String: String]] = []
169
+ for d in descs {
170
+ if d.kind == "text" {
171
+ contents.append(["type": "text", "text": d.text ?? ""])
172
+ } else if d.kind == "image" {
173
+ contents.append(["type": "image", "path": d.file ?? ""])
174
+ } else {
175
+ contents.append(["type": "audio", "path": d.file ?? ""])
176
+ }
177
+ }
178
+
179
+ let payload: [String: Any] = ["role": "user", "content": contents]
180
+ let data = try JSONSerialization.data(withJSONObject: payload, options: [])
181
+ guard let jsonString = String(data: data, encoding: .utf8) else {
182
+ throw NSError(domain: "LiteRTLM", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to encode JSON payload"])
183
+ }
184
+ return (jsonString, temps)
185
+ }
186
+
187
+ private func scaleImageIfNeeded(_ imagePath: String, maxDimension: Int = 1024) -> String {
188
+ guard let image = UIImage(contentsOfFile: imagePath) else { return imagePath }
189
+ let w = Int(image.size.width), h = Int(image.size.height)
190
+ guard max(w, h) > maxDimension else { return imagePath }
191
+ let scale = CGFloat(maxDimension) / CGFloat(max(w, h))
192
+ let newSize = CGSize(width: CGFloat(w) * scale, height: CGFloat(h) * scale)
193
+ let renderer = UIGraphicsImageRenderer(size: newSize)
194
+ let scaled = renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: newSize)) }
195
+ let tmp = (NSTemporaryDirectory() as NSString)
196
+ .appendingPathComponent("litert_scaled_\(UUID().uuidString).jpg")
197
+ if let data = scaled.jpegData(compressionQuality: 0.9) {
198
+ do {
199
+ try data.write(to: URL(fileURLWithPath: tmp))
200
+ return tmp
201
+ } catch {
202
+ NSLog("[LiteRTLM] Failed to write scaled image to temp file: \(error.localizedDescription)")
203
+ return imagePath
204
+ }
205
+ }
206
+ return imagePath
207
+ }
208
+
209
+ private func saveDataToTempFile(_ data: Data, ext: String) throws -> String {
210
+ let tmp = (NSTemporaryDirectory() as NSString)
211
+ .appendingPathComponent("litert_buf_\(UUID().uuidString).\(ext)")
212
+ try data.write(to: URL(fileURLWithPath: tmp))
213
+ return tmp
214
+ }
215
+
216
+ // MARK: - Streaming / blocking runners
217
+
218
+ private func runExecuteBlocking(
219
+ conversation: OpaquePointer,
220
+ msgJson: String,
221
+ userLabel: String,
222
+ promise: Promise<ExecuteResult>,
223
+ cleanup: @escaping () -> Void
224
+ ) {
225
+ let startTime = Date()
226
+ guard let response = litert_lm_conversation_send_message(conversation, msgJson, nil, nil) else {
227
+ cleanup()
228
+ promise.reject(withError: NSError(domain: "LiteRTLM", code: 500,
229
+ userInfo: [NSLocalizedDescriptionKey: "LiteRTLM: execute failed."]))
230
+ return
231
+ }
232
+ defer { litert_lm_json_response_delete(response) }
233
+
234
+ var result = ""
235
+ if let rs = litert_lm_json_response_get_string(response) {
236
+ result = extractTextFromResponse(String(cString: rs))
237
+ .trimmingCharacters(in: .whitespacesAndNewlines)
238
+ }
239
+
240
+ commitExecuteTurn(
241
+ userLabel: userLabel,
242
+ modelResponse: result,
243
+ startTime: startTime,
244
+ conversation: conversation,
245
+ tokenCount: 0
246
+ )
247
+ cleanup()
248
+ // iOS: tool calls not yet captured from C API — return empty array
249
+ promise.resolve(withResult: ExecuteResult(text: result, toolCalls: [], thinkingText: ""))
250
+ }
251
+
252
+
253
+
254
+ func commitExecuteTurn(
255
+ userLabel: String,
256
+ modelResponse: String,
257
+ startTime: Date,
258
+ conversation: OpaquePointer?,
259
+ tokenCount: Int
260
+ ) {
261
+ let totalTime = Date().timeIntervalSince(startTime)
262
+ var compTok = Double(tokenCount)
263
+ var tps = 0.0, ttft = 0.0
264
+ if let conversation = conversation,
265
+ let bi = litert_lm_conversation_get_benchmark_info(conversation) {
266
+ let turns = litert_lm_benchmark_info_get_num_decode_turns(bi)
267
+ if turns > 0 {
268
+ let li = turns - 1
269
+ tps = litert_lm_benchmark_info_get_decode_tokens_per_sec_at(bi, li)
270
+ compTok = Double(litert_lm_benchmark_info_get_decode_token_count_at(bi, li))
271
+ }
272
+ ttft = litert_lm_benchmark_info_get_time_to_first_token(bi)
273
+ litert_lm_benchmark_info_delete(bi)
274
+ }
275
+ let pt = Double(userLabel.count) / 4.0
276
+ if compTok == 0.0 { compTok = Double(modelResponse.count) / 4.0 }
277
+ lastStats = GenerationStats(
278
+ promptTokens: pt, completionTokens: compTok,
279
+ totalTokens: pt + compTok, timeToFirstToken: ttft,
280
+ totalTime: totalTime,
281
+ tokensPerSecond: tps > 0 ? tps : (compTok / totalTime)
282
+ )
283
+ history.append(Message(role: .user, content: userLabel))
284
+ history.append(Message(role: .model, content: modelResponse))
285
+ }
286
+ }
287
+
@@ -0,0 +1,153 @@
1
+ //
2
+ // HybridLiteRTLM+Streaming.swift
3
+ // react-native-litert-lm
4
+ //
5
+ // Streaming context and callback runners for the unified execute pipeline.
6
+ //
7
+
8
+ import Foundation
9
+ import NitroModules
10
+ import CLiteRTLM
11
+
12
+ class ExecuteStreamContext {
13
+ let userLabel: String
14
+ let startTime: Date
15
+ let onToken: (_ token: String, _ done: Bool) -> Void
16
+ let promise: Promise<ExecuteResult>
17
+ let parent: HybridLiteRTLM
18
+ let cleanup: () -> Void
19
+ var rawResponse: String = ""
20
+ var fullResponse: String = ""
21
+ var lastEmittedLength: Int = 0
22
+ var tokenCount: Int = 0
23
+
24
+ init(
25
+ userLabel: String,
26
+ startTime: Date,
27
+ onToken: @escaping (_ token: String, _ done: Bool) -> Void,
28
+ promise: Promise<ExecuteResult>,
29
+ parent: HybridLiteRTLM,
30
+ cleanup: @escaping () -> Void
31
+ ) {
32
+ self.userLabel = userLabel
33
+ self.startTime = startTime
34
+ self.onToken = onToken
35
+ self.promise = promise
36
+ self.parent = parent
37
+ self.cleanup = cleanup
38
+ }
39
+ }
40
+
41
+ extension HybridLiteRTLM {
42
+
43
+ func runExecuteStreaming(
44
+ conversation: OpaquePointer,
45
+ msgJson: String,
46
+ userLabel: String,
47
+ onToken: @escaping (_ token: String, _ done: Bool) -> Void,
48
+ promise: Promise<ExecuteResult>,
49
+ cleanup: @escaping () -> Void
50
+ ) {
51
+ let ctx = ExecuteStreamContext(
52
+ userLabel: userLabel, startTime: Date(),
53
+ onToken: onToken, promise: promise, parent: self,
54
+ cleanup: cleanup
55
+ )
56
+ let ptr = Unmanaged.passRetained(ctx).toOpaque()
57
+
58
+ let cb: LiteRtLmStreamCallback = { ptr, chunk, isFinal, errorMsg in
59
+ guard let ptr = ptr else { return }
60
+ let ctx = Unmanaged<ExecuteStreamContext>.fromOpaque(ptr).takeUnretainedValue()
61
+
62
+ if let errorMsg = errorMsg {
63
+ let msg = String(cString: errorMsg)
64
+ ctx.onToken("Error: \(msg)", true)
65
+ ctx.cleanup()
66
+ ctx.promise.reject(withError: NSError(domain: "LiteRTLM", code: 500,
67
+ userInfo: [NSLocalizedDescriptionKey: msg]))
68
+ Unmanaged<ExecuteStreamContext>.fromOpaque(ptr).release()
69
+ return
70
+ }
71
+
72
+ if isFinal {
73
+ ctx.parent.finalizeExecuteStream(ctx: ctx, streamPtr: ptr)
74
+ return
75
+ }
76
+
77
+ if let chunk = chunk {
78
+ ctx.parent.emitExecuteStreamChunk(ctx: ctx, chunk: chunk)
79
+ }
80
+ }
81
+
82
+ let status = litert_lm_conversation_send_message_stream(
83
+ conversation, msgJson, nil, nil, cb, ptr)
84
+ if status != 0 {
85
+ Unmanaged<ExecuteStreamContext>.fromOpaque(ptr).release()
86
+ cleanup()
87
+ promise.reject(withError: NSError(domain: "LiteRTLM", code: Int(status),
88
+ userInfo: [NSLocalizedDescriptionKey: "LiteRTLM: execute streaming failed."]))
89
+ }
90
+ }
91
+
92
+ func finalizeExecuteStream(ctx: ExecuteStreamContext, streamPtr: UnsafeMutableRawPointer) {
93
+ let cleaned = stripControlTokens(ctx.rawResponse)
94
+ .trimmingCharacters(in: .whitespacesAndNewlines)
95
+ var finalText = cleaned
96
+ if !ctx.userLabel.isEmpty && finalText.hasPrefix(ctx.userLabel) {
97
+ finalText = String(finalText.dropFirst(ctx.userLabel.count))
98
+ .trimmingCharacters(in: .whitespacesAndNewlines)
99
+ }
100
+ if finalText.count > ctx.lastEmittedLength {
101
+ let si = finalText.index(finalText.startIndex, offsetBy: ctx.lastEmittedLength)
102
+ ctx.onToken(String(finalText[si...]), false)
103
+ }
104
+ ctx.fullResponse = finalText
105
+
106
+ queue.async {
107
+ self.commitExecuteTurn(
108
+ userLabel: ctx.userLabel,
109
+ modelResponse: ctx.fullResponse,
110
+ startTime: ctx.startTime,
111
+ conversation: self.conversation,
112
+ tokenCount: ctx.tokenCount
113
+ )
114
+ ctx.onToken("", true)
115
+ ctx.cleanup()
116
+ // iOS: tool calls not yet captured from C API — return empty array
117
+ ctx.promise.resolve(withResult: ExecuteResult(text: ctx.fullResponse, toolCalls: [], thinkingText: ""))
118
+ Unmanaged<ExecuteStreamContext>.fromOpaque(streamPtr).release()
119
+ }
120
+ }
121
+
122
+ func emitExecuteStreamChunk(ctx: ExecuteStreamContext, chunk: UnsafePointer<CChar>) {
123
+ let token = String(cString: chunk)
124
+ let raw = token.hasPrefix("{") && token.contains("\"role\"")
125
+ ? extractTextFromResponse(token) : token
126
+ ctx.rawResponse += raw
127
+ let cleaned = stripControlTokens(ctx.rawResponse)
128
+ .trimmingLeadingCharacters(in: .whitespacesAndNewlines)
129
+ var processed = cleaned
130
+ if !ctx.userLabel.isEmpty && processed.hasPrefix(ctx.userLabel) {
131
+ processed = String(processed.dropFirst(ctx.userLabel.count))
132
+ .trimmingLeadingCharacters(in: .whitespacesAndNewlines)
133
+ }
134
+ let safe = safeEmitLength(processed)
135
+ if safe > ctx.lastEmittedLength {
136
+ let chars = Array(processed)
137
+ ctx.onToken(String(chars[ctx.lastEmittedLength..<safe]), false)
138
+ ctx.lastEmittedLength = safe
139
+ ctx.tokenCount += 1
140
+ }
141
+ }
142
+ }
143
+
144
+ private extension String {
145
+ func trimmingLeadingCharacters(in characterSet: CharacterSet) -> String {
146
+ guard let index = firstIndex(where: { char in
147
+ !char.unicodeScalars.allSatisfy { characterSet.contains($0) }
148
+ }) else {
149
+ return ""
150
+ }
151
+ return String(self[index...])
152
+ }
153
+ }