@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,206 @@
1
+ //
2
+ // HybridModelStore.swift
3
+ // react-native-litert-lm
4
+ //
5
+ // Created by Antigravity on 2026-06-04.
6
+ // Copyright © 2026 Margelo. All rights reserved.
7
+ //
8
+
9
+ import Foundation
10
+ import NitroModules
11
+
12
+ public class HybridModelStore: HybridModelStoreSpec_base, HybridModelStoreSpec_protocol {
13
+
14
+ private let queue = DispatchQueue(label: "dev.litert.modelstore", qos: .utility)
15
+
16
+ private var modelsDirectory: String {
17
+ let cachesDir = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first ?? NSTemporaryDirectory()
18
+ return (cachesDir as NSString).appendingPathComponent("litert_models")
19
+ }
20
+
21
+ private func sanitizeFileName(_ fileName: String) throws {
22
+ if fileName.contains("..") || fileName.contains("/") || fileName.contains("\\") {
23
+ throw NSError(
24
+ domain: "LiteRTLM.ModelStore",
25
+ code: 400,
26
+ userInfo: [NSLocalizedDescriptionKey: "Invalid filename: path traversal or directory separators are not allowed."]
27
+ )
28
+ }
29
+ }
30
+
31
+ public func isCached(fileName: String) throws -> Bool {
32
+ try sanitizeFileName(fileName)
33
+ let filePath = (modelsDirectory as NSString).appendingPathComponent(fileName)
34
+ let fileManager = FileManager.default
35
+ if fileManager.fileExists(atPath: filePath) {
36
+ let attrs = try? fileManager.attributesOfItem(atPath: filePath)
37
+ if let fileSize = attrs?[.size] as? UInt64, fileSize > 0 {
38
+ return true
39
+ }
40
+ }
41
+ return false
42
+ }
43
+
44
+ public func getFilePath(fileName: String) throws -> String {
45
+ try sanitizeFileName(fileName)
46
+ return (modelsDirectory as NSString).appendingPathComponent(fileName)
47
+ }
48
+
49
+ public func listCachedFiles() throws -> [ModelFile] {
50
+ let fileManager = FileManager.default
51
+ let dir = modelsDirectory
52
+
53
+ guard fileManager.fileExists(atPath: dir) else {
54
+ return []
55
+ }
56
+
57
+ let contents = try fileManager.contentsOfDirectory(atPath: dir)
58
+ var files: [ModelFile] = []
59
+
60
+ for name in contents {
61
+ if name.hasSuffix(".tmp") { continue }
62
+ let path = (dir as NSString).appendingPathComponent(name)
63
+ var isDir: ObjCBool = false
64
+ if fileManager.fileExists(atPath: path, isDirectory: &isDir), !isDir.boolValue {
65
+ let attrs = try? fileManager.attributesOfItem(atPath: path)
66
+ let size = (attrs?[.size] as? UInt64) ?? 0
67
+ let modDate = (attrs?[.modificationDate] as? Date) ?? Date()
68
+
69
+ files.append(ModelFile(
70
+ fileName: name,
71
+ absolutePath: path,
72
+ sizeBytes: Double(size),
73
+ lastModifiedMs: modDate.timeIntervalSince1970 * 1000.0
74
+ ))
75
+ }
76
+ }
77
+ return files
78
+ }
79
+
80
+ public func deleteFile(fileName: String) throws {
81
+ try sanitizeFileName(fileName)
82
+ let filePath = (modelsDirectory as NSString).appendingPathComponent(fileName)
83
+ let fileManager = FileManager.default
84
+ if fileManager.fileExists(atPath: filePath) {
85
+ try fileManager.removeItem(atPath: filePath)
86
+ }
87
+ }
88
+
89
+ public func downloadFile(
90
+ url: String,
91
+ fileName: String,
92
+ headersJson: String,
93
+ onProgress: @escaping (_ progress: Double) -> Void
94
+ ) throws -> Promise<String> {
95
+ let promise = Promise<String>()
96
+
97
+ queue.async {
98
+ do {
99
+ try self.sanitizeFileName(fileName)
100
+ let destPath = (self.modelsDirectory as NSString).appendingPathComponent(fileName)
101
+ let fileManager = FileManager.default
102
+
103
+ if !fileManager.fileExists(atPath: self.modelsDirectory) {
104
+ try fileManager.createDirectory(atPath: self.modelsDirectory, withIntermediateDirectories: true, attributes: nil)
105
+ }
106
+
107
+ // Fast cache check
108
+ if fileManager.fileExists(atPath: destPath) {
109
+ let attrs = try fileManager.attributesOfItem(atPath: destPath)
110
+ if let fileSize = attrs[.size] as? UInt64, fileSize > 0 {
111
+ onProgress(1.0)
112
+ promise.resolve(withResult: destPath)
113
+ return
114
+ }
115
+ }
116
+
117
+ guard let downloadUrl = URL(string: url), downloadUrl.scheme?.lowercased() == "https" else {
118
+ promise.reject(withError: NSError(domain: "LiteRTLM.ModelStore", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid download URL: HTTPS is required for security."]))
119
+ return
120
+ }
121
+
122
+ onProgress(0.0)
123
+
124
+ // Parse headers JSON
125
+ var customHeaders: [String: String] = [:]
126
+ if !headersJson.isEmpty {
127
+ if let data = headersJson.data(using: .utf8),
128
+ let parsed = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
129
+ customHeaders = parsed
130
+ }
131
+ }
132
+
133
+ let sessionConfig = URLSessionConfiguration.default
134
+ sessionConfig.timeoutIntervalForRequest = 30
135
+ sessionConfig.timeoutIntervalForResource = 3600
136
+
137
+ let session = URLSession(configuration: sessionConfig)
138
+ var progressHandler: NSKeyValueObservation?
139
+
140
+ var request = URLRequest(url: downloadUrl)
141
+ for (key, value) in customHeaders {
142
+ request.addValue(value, forHTTPHeaderField: key)
143
+ }
144
+
145
+ let tempDestFile = (self.modelsDirectory as NSString).appendingPathComponent("\(fileName).tmp")
146
+
147
+ let task = session.downloadTask(with: request) { location, response, error in
148
+ progressHandler?.invalidate()
149
+
150
+ if let error = error {
151
+ promise.reject(withError: error)
152
+ return
153
+ }
154
+
155
+ if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode >= 400 {
156
+ promise.reject(withError: NSError(domain: "LiteRTLM.ModelStore", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "HTTP \(httpResponse.statusCode)"]))
157
+ return
158
+ }
159
+
160
+ guard let location = location else {
161
+ promise.reject(withError: NSError(domain: "LiteRTLM.ModelStore", code: 500, userInfo: [NSLocalizedDescriptionKey: "No download location found."]))
162
+ return
163
+ }
164
+
165
+ do {
166
+ // Rename/move atomically via temp file helper
167
+ if fileManager.fileExists(atPath: tempDestFile) {
168
+ try fileManager.removeItem(atPath: tempDestFile)
169
+ }
170
+ try fileManager.moveItem(at: location, to: URL(fileURLWithPath: tempDestFile))
171
+
172
+ if fileManager.fileExists(atPath: destPath) {
173
+ try fileManager.removeItem(atPath: destPath)
174
+ }
175
+ try fileManager.moveItem(at: URL(fileURLWithPath: tempDestFile), to: URL(fileURLWithPath: destPath))
176
+
177
+ onProgress(1.0)
178
+ promise.resolve(withResult: destPath)
179
+ } catch {
180
+ promise.reject(withError: error)
181
+ }
182
+ }
183
+
184
+ var lastUpdate = Date()
185
+ progressHandler = task.observe(\.countOfBytesReceived, options: [.new]) { task, _ in
186
+ let expected = task.countOfBytesExpectedToReceive
187
+ if expected > 0 {
188
+ let now = Date()
189
+ if now.timeIntervalSince(lastUpdate) > 0.1 {
190
+ let progress = Double(task.countOfBytesReceived) / Double(expected)
191
+ onProgress(progress)
192
+ lastUpdate = now
193
+ }
194
+ }
195
+ }
196
+
197
+ task.resume()
198
+ session.finishTasksAndInvalidate()
199
+ } catch {
200
+ promise.reject(withError: error)
201
+ }
202
+ }
203
+
204
+ return promise
205
+ }
206
+ }
@@ -0,0 +1,22 @@
1
+ //
2
+ // MultimodalPart+Factories.swift
3
+ // react-native-litert-lm
4
+ //
5
+ // Mirrors src/inferenceRouting.ts — keep shapes in sync for native direct hybrid access.
6
+ //
7
+
8
+ import NitroModules
9
+
10
+ extension MultimodalPart {
11
+ static func textPart(_ text: String) -> MultimodalPart {
12
+ MultimodalPart(type: .text, text: text, path: nil, imageBuffer: nil, audioBuffer: nil)
13
+ }
14
+
15
+ static func imagePart(_ path: String) -> MultimodalPart {
16
+ MultimodalPart(type: .image, text: nil, path: path, imageBuffer: nil, audioBuffer: nil)
17
+ }
18
+
19
+ static func audioPart(_ path: String) -> MultimodalPart {
20
+ MultimodalPart(type: .audio, text: nil, path: path, imageBuffer: nil, audioBuffer: nil)
21
+ }
22
+ }
@@ -0,0 +1,187 @@
1
+ import XCTest
2
+ @testable import LiteRTLM
3
+
4
+ class HybridLiteRTLMTests: XCTestCase {
5
+ var bridge: HybridLiteRTLM!
6
+
7
+ override func setUp() {
8
+ super.setUp()
9
+ bridge = HybridLiteRTLM()
10
+ }
11
+
12
+ override func tearDown() {
13
+ try? bridge.close()
14
+ bridge = nil
15
+ super.tearDown()
16
+ }
17
+
18
+ func testPathTraversalRejection() async throws {
19
+ let traversals = ["../../etc/passwd", "/absolute/path/file", "subdir\\..\\file", "..", "../", "..\\"]
20
+ for traversal in traversals {
21
+ do {
22
+ let promise = try bridge.deleteModel(fileName: traversal)
23
+ _ = try await promise.await()
24
+ XCTFail("Should have failed for traversal: \(traversal)")
25
+ } catch {
26
+ let nsError = error as NSError
27
+ XCTAssertEqual(nsError.domain, "LiteRTLM")
28
+ XCTAssertEqual(nsError.code, 400)
29
+ XCTAssertTrue(nsError.localizedDescription.contains("path traversal") || nsError.localizedDescription.contains("directory separators"))
30
+ }
31
+ }
32
+ }
33
+
34
+ func testNonHTTPSDownloadRejection() async throws {
35
+ do {
36
+ let promise = try bridge.downloadModel(url: "http://insecure-domain.com/model.bin", fileName: "model.bin", onProgress: nil)
37
+ _ = try await promise.await()
38
+ XCTFail("Should have blocked insecure HTTP downloads")
39
+ } catch {
40
+ let nsError = error as NSError
41
+ XCTAssertEqual(nsError.domain, "LiteRTLM")
42
+ XCTAssertEqual(nsError.code, 400)
43
+ XCTAssertTrue(nsError.localizedDescription.contains("HTTPS is required"))
44
+ }
45
+ }
46
+
47
+ func testMemoryTelemetry() {
48
+ XCTAssertNoThrow(try bridge.getMemoryUsage())
49
+ if let mem = try? bridge.getMemoryUsage() {
50
+ XCTAssertGreaterThanOrEqual(mem.nativeHeapBytes, 0.0)
51
+ XCTAssertGreaterThanOrEqual(mem.residentBytes, 0.0)
52
+ XCTAssertGreaterThanOrEqual(mem.availableMemoryBytes, 0.0)
53
+ }
54
+ }
55
+
56
+ func testSendMessageAsyncRejectsWithoutModel() async throws {
57
+ do {
58
+ let promise = try bridge.sendMessageAsync(message: "hello") { _, _ in }
59
+ _ = try await promise.await()
60
+ XCTFail("Should have failed without model")
61
+ } catch {
62
+ let nsError = error as NSError
63
+ XCTAssertEqual(nsError.domain, "LiteRTLM")
64
+ XCTAssertEqual(nsError.code, 400)
65
+ }
66
+ }
67
+
68
+ func testSendMessageWithImageAsyncRejectsWithoutModel() async throws {
69
+ do {
70
+ let promise = try bridge.sendMessageWithImageAsync(message: "hello", imagePath: "/tmp/image.jpg") { _, _ in }
71
+ _ = try await promise.await()
72
+ XCTFail("Should have failed without model")
73
+ } catch {
74
+ let nsError = error as NSError
75
+ XCTAssertEqual(nsError.domain, "LiteRTLM")
76
+ XCTAssertEqual(nsError.code, 400)
77
+ }
78
+ }
79
+
80
+ func testSendMessageWithAudioAsyncRejectsWithoutModel() async throws {
81
+ do {
82
+ let promise = try bridge.sendMessageWithAudioAsync(message: "hello", audioPath: "/tmp/audio.wav") { _, _ in }
83
+ _ = try await promise.await()
84
+ XCTFail("Should have failed without model")
85
+ } catch {
86
+ let nsError = error as NSError
87
+ XCTAssertEqual(nsError.domain, "LiteRTLM")
88
+ XCTAssertEqual(nsError.code, 400)
89
+ }
90
+ }
91
+
92
+ func testSendMessageWithImageAsyncRejectsFileNotFound() async throws {
93
+ do {
94
+ let promise = try bridge.sendMessageWithImageAsync(message: "hello", imagePath: "/nonexistent/image.jpg") { _, _ in }
95
+ _ = try await promise.await()
96
+ XCTFail("Should have failed without model")
97
+ } catch {
98
+ let nsError = error as NSError
99
+ XCTAssertEqual(nsError.domain, "LiteRTLM")
100
+ }
101
+ }
102
+
103
+ func testSendMessageWithAudioAsyncRejectsFileNotFound() async throws {
104
+ do {
105
+ let promise = try bridge.sendMessageWithAudioAsync(message: "hello", audioPath: "/nonexistent/audio.wav") { _, _ in }
106
+ _ = try await promise.await()
107
+ XCTFail("Should have failed without model")
108
+ } catch {
109
+ let nsError = error as NSError
110
+ XCTAssertEqual(nsError.domain, "LiteRTLM")
111
+ }
112
+ }
113
+
114
+ func testInitialStats() {
115
+ XCTAssertNoThrow(try bridge.getStats())
116
+ if let stats = try? bridge.getStats() {
117
+ XCTAssertEqual(stats.promptTokens, 0.0)
118
+ XCTAssertEqual(stats.completionTokens, 0.0)
119
+ XCTAssertEqual(stats.totalTokens, 0.0)
120
+ XCTAssertEqual(stats.timeToFirstToken, 0.0)
121
+ XCTAssertEqual(stats.totalTime, 0.0)
122
+ XCTAssertEqual(stats.tokensPerSecond, 0.0)
123
+ }
124
+ }
125
+
126
+ func testDeleteModelCleanupLogic() async throws {
127
+ bridge.loadedModelPath = "/path/to/my_loaded_model.litertlm"
128
+
129
+ let promise1 = try bridge.deleteModel(fileName: "other_model.litertlm")
130
+ _ = try await promise1.await()
131
+ XCTAssertEqual(bridge.loadedModelPath, "/path/to/my_loaded_model.litertlm")
132
+
133
+ let promise2 = try bridge.deleteModel(fileName: "my_loaded_model.litertlm")
134
+ _ = try await promise2.await()
135
+ XCTAssertNil(bridge.loadedModelPath)
136
+ }
137
+
138
+ func testExecutePathPrecedenceOverBuffer() async throws {
139
+ let pathPart = MultimodalPart(
140
+ type: .image,
141
+ text: nil,
142
+ path: "/nonexistent/precedence_test_image.jpg",
143
+ imageBuffer: nil,
144
+ audioBuffer: nil
145
+ )
146
+
147
+ do {
148
+ let promise = try bridge.execute(parts: [pathPart], onToken: nil)
149
+ _ = try await promise.await()
150
+ XCTFail("Should have failed")
151
+ } catch {
152
+ let nsError = error as NSError
153
+ XCTAssertTrue(nsError.localizedDescription.contains("file not found: /nonexistent/precedence_test_image.jpg"))
154
+ }
155
+ }
156
+
157
+ func testExecuteTempFileCleanupOnError() async throws {
158
+ let dummyData = Data([0, 1, 2, 3])
159
+ let buffer = ArrayBuffer(dummyData)
160
+
161
+ let bufferPart = MultimodalPart(
162
+ type: .image,
163
+ text: nil,
164
+ path: nil,
165
+ imageBuffer: buffer,
166
+ audioBuffer: nil
167
+ )
168
+
169
+ let invalidPathPart = MultimodalPart(
170
+ type: .image,
171
+ text: nil,
172
+ path: "/nonexistent/invalid_file_cleanup_test.jpg",
173
+ imageBuffer: nil,
174
+ audioBuffer: nil
175
+ )
176
+
177
+ do {
178
+ let promise = try bridge.execute(parts: [bufferPart, invalidPathPart], onToken: nil)
179
+ _ = try await promise.await()
180
+ XCTFail("Should have failed")
181
+ } catch {
182
+ let tempDirAfter = try FileManager.default.contentsOfDirectory(atPath: NSTemporaryDirectory())
183
+ let leakedFiles = tempDirAfter.filter { $0.contains("litert_buf_") }
184
+ XCTAssertEqual(leakedFiles.count, 0)
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,81 @@
1
+ import type { MultimodalPart } from "../specs/LiteRTLM.nitro";
2
+ export declare const mockLiteRTLM: {
3
+ isReady: jest.Mock<boolean, [], any>;
4
+ loadModel: jest.Mock<any, any, any>;
5
+ execute: jest.Mock<Promise<string>, [parts: MultimodalPart[], onToken?: ((token: string, done: boolean) => void) | undefined], any>;
6
+ sendMessage: jest.Mock<Promise<string>, [message: string], any>;
7
+ sendMessageWithImage: jest.Mock<Promise<string>, [message: string, imagePath: string], any>;
8
+ downloadModel: jest.Mock<Promise<string>, [url: string, fileName: string, onProgress?: ((progress: number) => void) | undefined], any>;
9
+ deleteModel: jest.Mock<any, any, any>;
10
+ sendMessageWithAudio: jest.Mock<Promise<string>, [message: string, audioPath: string], any>;
11
+ sendMultimodalMessage: jest.Mock<Promise<string>, [parts: MultimodalPart[]], any>;
12
+ sendMessageAsync: jest.Mock<Promise<void>, [msg: string, onToken: (token: string, done: boolean) => void], any>;
13
+ sendMessageWithImageAsync: jest.Mock<Promise<void>, [msg: string, imagePath: string, onToken: (token: string, done: boolean) => void], any>;
14
+ sendMessageWithAudioAsync: jest.Mock<Promise<void>, [msg: string, audioPath: string, onToken: (token: string, done: boolean) => void], any>;
15
+ getHistory: jest.Mock<never[], [], any>;
16
+ resetConversation: jest.Mock<any, any, any>;
17
+ getStats: jest.Mock<{
18
+ promptTokens: number;
19
+ completionTokens: number;
20
+ totalTokens: number;
21
+ timeToFirstToken: number;
22
+ totalTime: number;
23
+ tokensPerSecond: number;
24
+ }, [], any>;
25
+ countTokens: jest.Mock<number, [], any>;
26
+ getMemoryUsage: jest.Mock<{
27
+ nativeHeapBytes: number;
28
+ residentBytes: number;
29
+ availableMemoryBytes: number;
30
+ isLowMemory: boolean;
31
+ }, [], any>;
32
+ close: jest.Mock<any, any, any>;
33
+ };
34
+ export declare const mockModelStore: {
35
+ isCached: jest.Mock<boolean, [fileName: string], any>;
36
+ getFilePath: jest.Mock<string, [fileName: string], any>;
37
+ listCachedFiles: jest.Mock<never[], [], any>;
38
+ deleteFile: jest.Mock<any, [fileName: string], any>;
39
+ downloadFile: jest.Mock<Promise<string>, [url: string, fileName: string, headersJson: string, onProgress: (progress: number) => void], any>;
40
+ };
41
+ export declare const NitroModules: {
42
+ createHybridObject: jest.Mock<{
43
+ isReady: jest.Mock<boolean, [], any>;
44
+ loadModel: jest.Mock<any, any, any>;
45
+ execute: jest.Mock<Promise<string>, [parts: MultimodalPart[], onToken?: ((token: string, done: boolean) => void) | undefined], any>;
46
+ sendMessage: jest.Mock<Promise<string>, [message: string], any>;
47
+ sendMessageWithImage: jest.Mock<Promise<string>, [message: string, imagePath: string], any>;
48
+ downloadModel: jest.Mock<Promise<string>, [url: string, fileName: string, onProgress?: ((progress: number) => void) | undefined], any>;
49
+ deleteModel: jest.Mock<any, any, any>;
50
+ sendMessageWithAudio: jest.Mock<Promise<string>, [message: string, audioPath: string], any>;
51
+ sendMultimodalMessage: jest.Mock<Promise<string>, [parts: MultimodalPart[]], any>;
52
+ sendMessageAsync: jest.Mock<Promise<void>, [msg: string, onToken: (token: string, done: boolean) => void], any>;
53
+ sendMessageWithImageAsync: jest.Mock<Promise<void>, [msg: string, imagePath: string, onToken: (token: string, done: boolean) => void], any>;
54
+ sendMessageWithAudioAsync: jest.Mock<Promise<void>, [msg: string, audioPath: string, onToken: (token: string, done: boolean) => void], any>;
55
+ getHistory: jest.Mock<never[], [], any>;
56
+ resetConversation: jest.Mock<any, any, any>;
57
+ getStats: jest.Mock<{
58
+ promptTokens: number;
59
+ completionTokens: number;
60
+ totalTokens: number;
61
+ timeToFirstToken: number;
62
+ totalTime: number;
63
+ tokensPerSecond: number;
64
+ }, [], any>;
65
+ countTokens: jest.Mock<number, [], any>;
66
+ getMemoryUsage: jest.Mock<{
67
+ nativeHeapBytes: number;
68
+ residentBytes: number;
69
+ availableMemoryBytes: number;
70
+ isLowMemory: boolean;
71
+ }, [], any>;
72
+ close: jest.Mock<any, any, any>;
73
+ } | {
74
+ isCached: jest.Mock<boolean, [fileName: string], any>;
75
+ getFilePath: jest.Mock<string, [fileName: string], any>;
76
+ listCachedFiles: jest.Mock<never[], [], any>;
77
+ deleteFile: jest.Mock<any, [fileName: string], any>;
78
+ downloadFile: jest.Mock<Promise<string>, [url: string, fileName: string, headersJson: string, onProgress: (progress: number) => void], any>;
79
+ }, [name: string], any>;
80
+ createNativeArrayBuffer: jest.Mock<ArrayBuffer, [size: number], any>;
81
+ };
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NitroModules = exports.mockModelStore = exports.mockLiteRTLM = void 0;
4
+ function streamMockTokens(parts, onToken) {
5
+ const hasImage = parts.some((p) => p.type === "image");
6
+ const hasAudio = parts.some((p) => p.type === "audio");
7
+ if (hasImage) {
8
+ onToken("Mock vision ", false);
9
+ onToken("token", true);
10
+ }
11
+ else if (hasAudio) {
12
+ onToken("Mock audio ", false);
13
+ onToken("token", true);
14
+ }
15
+ else {
16
+ onToken("Mock ", false);
17
+ onToken("token", true);
18
+ }
19
+ }
20
+ function mockExecuteResponse(parts) {
21
+ const hasImage = parts.some((p) => p.type === "image");
22
+ const hasAudio = parts.some((p) => p.type === "audio");
23
+ return hasImage
24
+ ? "Mock vision token"
25
+ : hasAudio
26
+ ? "Mock audio token"
27
+ : "Mock token";
28
+ }
29
+ const mockExecute = jest.fn((parts, onToken) => {
30
+ if (onToken) {
31
+ streamMockTokens(parts, onToken);
32
+ }
33
+ return Promise.resolve(mockExecuteResponse(parts));
34
+ });
35
+ exports.mockLiteRTLM = {
36
+ isReady: jest.fn(() => false),
37
+ loadModel: jest.fn().mockResolvedValue(undefined),
38
+ execute: mockExecute,
39
+ sendMessage: jest.fn((message) => mockExecute([{ type: "text", text: message }])),
40
+ sendMessageWithImage: jest.fn((message, imagePath) => mockExecute([
41
+ { type: "text", text: message },
42
+ { type: "image", path: imagePath },
43
+ ])),
44
+ downloadModel: jest.fn(async (url, fileName, onProgress) => {
45
+ onProgress?.(1.0);
46
+ return "/mock/path/model.litertlm";
47
+ }),
48
+ deleteModel: jest.fn().mockResolvedValue(undefined),
49
+ sendMessageWithAudio: jest.fn((message, audioPath) => mockExecute([
50
+ { type: "text", text: message },
51
+ { type: "audio", path: audioPath },
52
+ ])),
53
+ sendMultimodalMessage: jest.fn((parts) => mockExecute(parts)),
54
+ sendMessageAsync: jest.fn((msg, onToken) => mockExecute([{ type: "text", text: msg }], onToken).then(() => { })),
55
+ sendMessageWithImageAsync: jest.fn((msg, imagePath, onToken) => mockExecute([{ type: "text", text: msg }, { type: "image", path: imagePath }], onToken).then(() => { })),
56
+ sendMessageWithAudioAsync: jest.fn((msg, audioPath, onToken) => mockExecute([{ type: "text", text: msg }, { type: "audio", path: audioPath }], onToken).then(() => { })),
57
+ getHistory: jest.fn(() => []),
58
+ resetConversation: jest.fn(),
59
+ getStats: jest.fn(() => ({
60
+ promptTokens: 10,
61
+ completionTokens: 20,
62
+ totalTokens: 30,
63
+ timeToFirstToken: 5,
64
+ totalTime: 50,
65
+ tokensPerSecond: 400,
66
+ })),
67
+ countTokens: jest.fn(() => -1),
68
+ getMemoryUsage: jest.fn(() => ({
69
+ nativeHeapBytes: 1000000,
70
+ residentBytes: 2000000,
71
+ availableMemoryBytes: 4000000,
72
+ isLowMemory: false,
73
+ })),
74
+ close: jest.fn(),
75
+ };
76
+ exports.mockModelStore = {
77
+ isCached: jest.fn((fileName) => false),
78
+ getFilePath: jest.fn((fileName) => `/mock/path/${fileName}`),
79
+ listCachedFiles: jest.fn(() => []),
80
+ deleteFile: jest.fn((fileName) => {
81
+ return exports.mockLiteRTLM.deleteModel(fileName);
82
+ }),
83
+ downloadFile: jest.fn(async (url, fileName, headersJson, onProgress) => exports.mockLiteRTLM.downloadModel(url, fileName, onProgress)),
84
+ };
85
+ exports.NitroModules = {
86
+ createHybridObject: jest.fn((name) => {
87
+ if (name === "LiteRTLM") {
88
+ return exports.mockLiteRTLM;
89
+ }
90
+ if (name === "ModelStore") {
91
+ return exports.mockModelStore;
92
+ }
93
+ throw new Error(`Mock not implemented for hybrid object: ${name}`);
94
+ }),
95
+ createNativeArrayBuffer: jest.fn((size) => {
96
+ return new ArrayBuffer(size);
97
+ }),
98
+ };
@@ -0,0 +1 @@
1
+ export {};