@capgo/capacitor-llm 8.0.10 → 8.0.12

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.
@@ -1,11 +1,13 @@
1
1
  import Foundation
2
2
  import Capacitor
3
+ import ObjectiveC
4
+ import Darwin
3
5
  #if canImport(FoundationModels)
4
6
  import FoundationModels
5
7
  #endif
6
8
  // For CocoaPods, MediaPipeTasksGenAI should always be available
7
9
  // For SPM, we need conditional import when SPM support is added
8
- #if canImport(MediaPipeTasksGenAI)
10
+ #if canImport(MediaPipeTasksGenAI) && !targetEnvironment(macCatalyst)
9
11
  import MediaPipeTasksGenAI
10
12
  #endif
11
13
 
@@ -16,7 +18,10 @@ import MediaPipeTasksGenAI
16
18
 
17
19
  @objc(LLMPlugin)
18
20
  public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
19
- private let pluginVersion: String = "8.0.10"
21
+ private let pluginVersion: String = "8.0.12"
22
+ #if targetEnvironment(macCatalyst)
23
+ private let catalystNotSupportedMessage = "CapgoLLM is not available on Mac Catalyst. Use an iOS/iPadOS target instead."
24
+ #endif
20
25
  public let identifier = "LLMPlugin"
21
26
  public let jsName = "CapgoLLM"
22
27
  public var pluginMethods: [CAPPluginMethod] = [
@@ -31,6 +36,23 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
31
36
  private enum ModelType {
32
37
  case appleIntelligence
33
38
  case mediaPipe
39
+ case execuTorch
40
+ }
41
+
42
+ private final class ExecuTorchChatSession {
43
+ private var turns: [(role: String, content: String)] = []
44
+
45
+ func buildPrompt(userMessage: String) -> String {
46
+ var parts = turns.map { "\($0.role): \($0.content)" }
47
+ parts.append("User: \(userMessage)")
48
+ parts.append("Assistant:")
49
+ return parts.joined(separator: "\n")
50
+ }
51
+
52
+ func addTurn(userMessage: String, assistantMessage: String) {
53
+ turns.append((role: "User", content: userMessage))
54
+ turns.append((role: "Assistant", content: assistantMessage))
55
+ }
34
56
  }
35
57
 
36
58
  private var currentModelType: ModelType = .appleIntelligence
@@ -49,16 +71,26 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
49
71
  private var appleChats: [String: Any] = [:]
50
72
  #endif
51
73
 
52
- #if COCOAPODS || canImport(MediaPipeTasksGenAI)
74
+ #if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
53
75
  private var llmInference: LlmInference?
54
76
  #endif
55
77
 
78
+ private var execuTorchRunner: DynamicExecuTorchRunner?
79
+ private var execuTorchChats: [String: ExecuTorchChatSession] = [:]
80
+ private let execuTorchGenerationLock = NSLock()
81
+ private var execuTorchIsGenerating = false
82
+
56
83
  @objc func setModel(_ call: CAPPluginCall) {
57
84
  guard let path = call.getString("path") else {
58
85
  call.reject("Path is required")
59
86
  return
60
87
  }
61
88
 
89
+ #if targetEnvironment(macCatalyst)
90
+ call.reject(catalystNotSupportedMessage)
91
+ return
92
+ #endif
93
+
62
94
  print("[CapgoLLM] Setting model path: \(path)")
63
95
 
64
96
  // Extract model parameters
@@ -66,13 +98,33 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
66
98
  let topk = call.getInt("topk") ?? 40
67
99
  let temperature = call.getFloat("temperature") ?? 0.8
68
100
  let modelType = call.getString("modelType") // Optional model type parameter
101
+ let engine = call.getString("engine")?.lowercased() ?? "auto"
102
+ let tokenizerPath = call.getString("tokenizerPath")
103
+ let specialTokens = call.getArray("specialTokens", String.self) ?? []
104
+ let sequenceLength = call.getInt("sequenceLength") ?? maxTokens
105
+
106
+ guard sequenceLength > 0 else {
107
+ call.reject("sequenceLength must be > 0")
108
+ return
109
+ }
110
+
111
+ guard !isExecuTorchGenerating() else {
112
+ call.reject("Cannot switch model while ExecuTorch is generating. Wait for aiFinished.")
113
+ return
114
+ }
69
115
 
70
116
  modelPath = path
117
+ execuTorchChats.removeAll()
118
+ #if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
119
+ llmInference = nil
120
+ #endif
121
+ execuTorchRunner = nil
71
122
  isReady = false
72
123
 
73
124
  // Check if user wants to use Apple Intelligence
74
- if path.lowercased() == "apple intelligence" {
125
+ if engine == "apple" || path.lowercased() == "apple intelligence" {
75
126
  print("[CapgoLLM] Switching to Apple Intelligence")
127
+ execuTorchRunner = nil
76
128
  currentModelType = .appleIntelligence
77
129
  isReady = true
78
130
  notifyListeners("readinessChange", data: ["readiness": "ready"])
@@ -80,7 +132,22 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
80
132
  return
81
133
  }
82
134
 
83
- #if canImport(MediaPipeTasksGenAI)
135
+ if shouldUseExecuTorch(engine: engine, path: path, tokenizerPath: tokenizerPath) {
136
+ currentModelType = .execuTorch
137
+ setExecuTorchModel(
138
+ call,
139
+ path: path,
140
+ tokenizerPath: tokenizerPath,
141
+ modelType: modelType,
142
+ specialTokens: specialTokens,
143
+ sequenceLength: sequenceLength,
144
+ temperature: temperature
145
+ )
146
+ return
147
+ }
148
+
149
+ #if !targetEnvironment(macCatalyst) && canImport(MediaPipeTasksGenAI)
150
+ currentModelType = .mediaPipe
84
151
  Task {
85
152
  do {
86
153
  let modelURL: URL
@@ -145,17 +212,124 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
145
212
  notifyListeners("readinessChange", data: ["readiness": "ready"])
146
213
  call.resolve()
147
214
  } catch {
215
+ llmInference = nil
216
+ isReady = false
148
217
  print("[CapgoLLM] Error loading model: \(error.localizedDescription)")
149
218
  notifyListeners("readinessChange", data: ["readiness": "Failed to load model: \(error.localizedDescription)"])
150
219
  call.reject("Failed to load model: \(error.localizedDescription)")
151
220
  }
152
221
  }
153
222
  #else
154
- call.reject("MediaPipe is not available. Please install via CocoaPods.")
223
+ call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
155
224
  #endif
156
225
  }
157
226
 
227
+ private func shouldUseExecuTorch(engine: String, path: String, tokenizerPath: String?) -> Bool {
228
+ if path.lowercased() == "apple intelligence" {
229
+ return false
230
+ }
231
+ if engine == "executorch" {
232
+ return true
233
+ }
234
+ if engine == "apple" || engine == "mediapipe" {
235
+ return false
236
+ }
237
+ if tokenizerPath?.isEmpty == false {
238
+ return true
239
+ }
240
+ return path.lowercased().hasSuffix(".pte")
241
+ }
242
+
243
+ private func setExecuTorchModel(
244
+ _ call: CAPPluginCall,
245
+ path: String,
246
+ tokenizerPath: String?,
247
+ modelType: String?,
248
+ specialTokens: [String],
249
+ sequenceLength: Int,
250
+ temperature: Float
251
+ ) {
252
+ execuTorchRunner = nil
253
+
254
+ guard sequenceLength > 0 else {
255
+ call.reject("sequenceLength must be > 0")
256
+ return
257
+ }
258
+
259
+ guard DynamicExecuTorchRunner.isAvailable else {
260
+ call.reject("ExecuTorch is not linked in this iOS app. Add the ExecuTorch Swift Package products to the app target.")
261
+ return
262
+ }
263
+
264
+ guard let tokenizerPath = tokenizerPath, !tokenizerPath.isEmpty else {
265
+ call.reject("tokenizerPath is required for ExecuTorch models")
266
+ return
267
+ }
268
+
269
+ Task {
270
+ do {
271
+ let modelURL = try resolveModelURL(path: path, modelType: modelType, defaultExtension: "pte")
272
+ let tokenizerURL = try resolveModelURL(path: tokenizerPath, modelType: nil, defaultExtension: "model")
273
+
274
+ print("[CapgoLLM] Initializing ExecuTorch with model at: \(modelURL.path)")
275
+ print("[CapgoLLM] Initializing ExecuTorch with tokenizer at: \(tokenizerURL.path)")
276
+
277
+ execuTorchRunner = try DynamicExecuTorchRunner(
278
+ modelPath: modelURL.path,
279
+ tokenizerPath: tokenizerURL.path,
280
+ specialTokens: specialTokens,
281
+ sequenceLength: sequenceLength,
282
+ temperature: Double(temperature)
283
+ )
284
+ currentModelType = .execuTorch
285
+ isReady = true
286
+
287
+ notifyListeners("readinessChange", data: ["readiness": "ready"])
288
+ call.resolve()
289
+ } catch {
290
+ execuTorchRunner = nil
291
+ isReady = false
292
+ print("[CapgoLLM] Error loading ExecuTorch model: \(error.localizedDescription)")
293
+ notifyListeners("readinessChange", data: ["readiness": "Failed to load ExecuTorch model: \(error.localizedDescription)"])
294
+ call.reject("Failed to load ExecuTorch model: \(error.localizedDescription)")
295
+ }
296
+ }
297
+ }
298
+
299
+ private func resolveModelURL(path: String, modelType: String?, defaultExtension: String) throws -> URL {
300
+ if path.hasPrefix("/") {
301
+ return URL(fileURLWithPath: path)
302
+ }
303
+
304
+ let fileName: String
305
+ let fileExtension: String
306
+
307
+ if let providedType = modelType, !providedType.isEmpty {
308
+ fileName = (path as NSString).deletingPathExtension
309
+ fileExtension = providedType
310
+ } else {
311
+ fileName = (path as NSString).deletingPathExtension
312
+ let extractedExtension = (path as NSString).pathExtension
313
+ fileExtension = extractedExtension.isEmpty ? defaultExtension : extractedExtension
314
+ }
315
+
316
+ guard let bundlePath = Bundle.main.path(forResource: fileName, ofType: fileExtension) else {
317
+ throw NSError(
318
+ domain: "CapgoLLM",
319
+ code: 404,
320
+ userInfo: [NSLocalizedDescriptionKey: "Model file not found in bundle: \(path)"]
321
+ )
322
+ }
323
+
324
+ return URL(fileURLWithPath: bundlePath)
325
+ }
326
+
158
327
  @objc func createChat(_ call: CAPPluginCall) {
328
+ #if targetEnvironment(macCatalyst)
329
+ call.reject(catalystNotSupportedMessage)
330
+ return
331
+ #endif
332
+
159
333
  print("[CapgoLLM] Creating chat with model type: \(currentModelType)")
160
334
  switch currentModelType {
161
335
  case .appleIntelligence:
@@ -176,7 +350,7 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
176
350
  #endif
177
351
 
178
352
  case .mediaPipe:
179
- #if COCOAPODS || canImport(MediaPipeTasksGenAI)
353
+ #if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
180
354
  guard llmInference != nil else {
181
355
  print("[CapgoLLM] Error: Model not loaded when creating chat")
182
356
  call.reject("Model not loaded")
@@ -190,12 +364,30 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
190
364
  "id": id
191
365
  ])
192
366
  #else
193
- call.reject("MediaPipe is not available. Please install via CocoaPods.")
367
+ call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
194
368
  #endif
369
+
370
+ case .execuTorch:
371
+ guard execuTorchRunner != nil else {
372
+ call.reject("ExecuTorch model not loaded")
373
+ return
374
+ }
375
+ let id = UUID().uuidString
376
+ execuTorchChats[id] = ExecuTorchChatSession()
377
+ print("[CapgoLLM] Created ExecuTorch chat with ID: \(id)")
378
+ call.resolve([
379
+ "id": id
380
+ ])
195
381
  }
196
382
  }
197
383
 
198
384
  @objc func getReadiness(_ call: CAPPluginCall) {
385
+ #if targetEnvironment(macCatalyst)
386
+ call.resolve(["readiness": "unsupported_on_maccatalyst"])
387
+ notifyListeners("readinessChange", data: ["readiness": "unsupported_on_maccatalyst"])
388
+ return
389
+ #endif
390
+
199
391
  let readiness = getReadinessStatus()
200
392
  call.resolve(["readiness": readiness])
201
393
  // Also notify listeners
@@ -228,10 +420,18 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
228
420
 
229
421
  case .mediaPipe:
230
422
  return isReady ? "ready" : "not_ready"
423
+
424
+ case .execuTorch:
425
+ return isReady ? "ready" : "not_ready"
231
426
  }
232
427
  }
233
428
 
234
429
  @objc func sendMessage(_ call: CAPPluginCall) {
430
+ #if targetEnvironment(macCatalyst)
431
+ call.reject(catalystNotSupportedMessage)
432
+ return
433
+ #endif
434
+
235
435
  let chatId = call.getString("chatId", "")
236
436
  guard let message = call.getString("message") else {
237
437
  call.reject("message not found")
@@ -318,7 +518,7 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
318
518
  #endif
319
519
 
320
520
  case .mediaPipe:
321
- #if COCOAPODS || canImport(MediaPipeTasksGenAI)
521
+ #if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
322
522
  guard let inference = llmInference else {
323
523
  call.reject("Model not loaded")
324
524
  return
@@ -358,9 +558,73 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
358
558
  }
359
559
  }
360
560
  #else
361
- call.reject("MediaPipe is not available. Please install via CocoaPods.")
561
+ call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
362
562
  #endif
563
+
564
+ case .execuTorch:
565
+ guard isReady, let runner = execuTorchRunner else {
566
+ call.reject("ExecuTorch model not loaded")
567
+ return
568
+ }
569
+ guard let chat = execuTorchChats[chatId] else {
570
+ call.reject("chat not found")
571
+ return
572
+ }
573
+ guard beginExecuTorchGeneration() else {
574
+ call.reject("ExecuTorch is already generating. Wait for aiFinished before sending another message.")
575
+ return
576
+ }
577
+
578
+ Task {
579
+ defer {
580
+ self.finishExecuTorchGeneration()
581
+ }
582
+
583
+ do {
584
+ let prompt = chat.buildPrompt(userMessage: message)
585
+ var fullResponse = ""
586
+ print("[CapgoLLM] Attempting to generate ExecuTorch response for message: \(message)")
587
+ try runner.generate(prompt) { token in
588
+ self.notifyListeners("textFromAi", data: [
589
+ "chatId": chatId,
590
+ "text": token,
591
+ "isChunk": true
592
+ ])
593
+ fullResponse += token
594
+ }
595
+ chat.addTurn(userMessage: message, assistantMessage: fullResponse)
596
+ self.notifyListeners("aiFinished", data: ["chatId": chatId])
597
+ call.resolve()
598
+ } catch {
599
+ print("[CapgoLLM] ExecuTorch error details: \(error)")
600
+ call.reject("Failed to generate ExecuTorch response: \(error.localizedDescription)")
601
+ }
602
+ }
603
+ }
604
+ }
605
+
606
+ private func isExecuTorchGenerating() -> Bool {
607
+ execuTorchGenerationLock.lock()
608
+ defer { execuTorchGenerationLock.unlock() }
609
+ return execuTorchIsGenerating
610
+ }
611
+
612
+ private func beginExecuTorchGeneration() -> Bool {
613
+ execuTorchGenerationLock.lock()
614
+ defer { execuTorchGenerationLock.unlock() }
615
+
616
+ if execuTorchIsGenerating {
617
+ return false
363
618
  }
619
+
620
+ execuTorchIsGenerating = true
621
+ return true
622
+ }
623
+
624
+ private func finishExecuTorchGeneration() {
625
+ execuTorchGenerationLock.lock()
626
+ execuTorchIsGenerating = false
627
+ execuTorchGenerationLock.unlock()
364
628
  }
365
629
 
366
630
  @objc func downloadModel(_ call: CAPPluginCall) {
@@ -445,6 +709,165 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
445
709
  }
446
710
  }
447
711
 
712
+ private typealias ExecuTorchAllocFunction = @convention(c) (AnyClass, Selector) -> Unmanaged<AnyObject>
713
+ private typealias ExecuTorchInitRunnerFunction = @convention(c) (
714
+ AnyObject,
715
+ Selector,
716
+ NSString,
717
+ NSString,
718
+ NSArray
719
+ ) -> Unmanaged<AnyObject>
720
+ private typealias ExecuTorchLoadFunction = @convention(c) (
721
+ AnyObject,
722
+ Selector,
723
+ UnsafeMutablePointer<NSError?>
724
+ ) -> Bool
725
+ private typealias ExecuTorchConfigBlock = @convention(block) (NSObject) -> Void
726
+ private typealias ExecuTorchInitConfigFunction = @convention(c) (
727
+ AnyObject,
728
+ Selector,
729
+ ExecuTorchConfigBlock
730
+ ) -> Unmanaged<AnyObject>
731
+ private typealias ExecuTorchTokenCallback = @convention(block) (NSString) -> Void
732
+ private typealias ExecuTorchGenerateFunction = @convention(c) (
733
+ AnyObject,
734
+ Selector,
735
+ NSString,
736
+ AnyObject,
737
+ ExecuTorchTokenCallback,
738
+ UnsafeMutablePointer<NSError?>
739
+ ) -> Bool
740
+ private typealias ExecuTorchVoidFunction = @convention(c) (AnyObject, Selector) -> Void
741
+
742
+ private final class DynamicExecuTorchRunner {
743
+ private static let runnerInitSelector = NSSelectorFromString("initWithModelPath:tokenizerPath:specialTokens:")
744
+ private static let runnerLoadSelector = NSSelectorFromString("loadWithError:")
745
+ private static let runnerResetSelector = NSSelectorFromString("reset")
746
+ private static let runnerGenerateSelector = NSSelectorFromString("generateWithPrompt:config:tokenCallback:error:")
747
+ private static let configInitSelector = NSSelectorFromString("initWithBlock:")
748
+
749
+ static var isAvailable: Bool {
750
+ guard let runnerClass = findClass("ExecuTorchLLMTextRunner"),
751
+ let configClass = findClass("ExecuTorchLLMConfig") else {
752
+ return false
753
+ }
754
+
755
+ return hasInstanceMethod(runnerClass, runnerInitSelector) &&
756
+ hasInstanceMethod(runnerClass, runnerLoadSelector) &&
757
+ hasInstanceMethod(runnerClass, runnerResetSelector) &&
758
+ hasInstanceMethod(runnerClass, runnerGenerateSelector) &&
759
+ hasInstanceMethod(configClass, configInitSelector)
760
+ }
761
+
762
+ private let runner: AnyObject
763
+ private let sequenceLength: Int
764
+ private let temperature: Double
765
+
766
+ init(
767
+ modelPath: String,
768
+ tokenizerPath: String,
769
+ specialTokens: [String],
770
+ sequenceLength: Int,
771
+ temperature: Double
772
+ ) throws {
773
+ guard let runnerClass = Self.findClass("ExecuTorchLLMTextRunner") else {
774
+ throw Self.error("ExecuTorchLLMTextRunner was not found. Link ExecuTorch in the app target.")
775
+ }
776
+ try Self.requireInstanceMethod(Self.runnerInitSelector, on: runnerClass)
777
+ try Self.requireInstanceMethod(Self.runnerLoadSelector, on: runnerClass)
778
+
779
+ let alloc: ExecuTorchAllocFunction = try Self.objcMessage()
780
+ let initRunner: ExecuTorchInitRunnerFunction = try Self.objcMessage()
781
+ let load: ExecuTorchLoadFunction = try Self.objcMessage()
782
+
783
+ let allocatedRunner = alloc(runnerClass, NSSelectorFromString("alloc")).takeRetainedValue()
784
+ runner = initRunner(
785
+ allocatedRunner,
786
+ Self.runnerInitSelector,
787
+ modelPath as NSString,
788
+ tokenizerPath as NSString,
789
+ specialTokens as NSArray
790
+ ).takeRetainedValue()
791
+ self.sequenceLength = sequenceLength
792
+ self.temperature = temperature
793
+
794
+ var loadError: NSError?
795
+ guard load(runner, Self.runnerLoadSelector, &loadError) else {
796
+ throw loadError ?? Self.error("ExecuTorch model failed to load")
797
+ }
798
+ }
799
+
800
+ func generate(_ prompt: String, tokenCallback: @escaping (String) -> Void) throws {
801
+ guard let configClass = Self.findClass("ExecuTorchLLMConfig") else {
802
+ throw Self.error("ExecuTorchLLMConfig was not found. Link ExecuTorch in the app target.")
803
+ }
804
+ guard let runnerClass = object_getClass(runner) else {
805
+ throw Self.error("ExecuTorch runner class was not found")
806
+ }
807
+ try Self.requireInstanceMethod(Self.runnerResetSelector, on: runnerClass)
808
+ try Self.requireInstanceMethod(Self.runnerGenerateSelector, on: runnerClass)
809
+ try Self.requireInstanceMethod(Self.configInitSelector, on: configClass)
810
+
811
+ let sendVoid: ExecuTorchVoidFunction = try Self.objcMessage()
812
+ let alloc: ExecuTorchAllocFunction = try Self.objcMessage()
813
+ let initConfig: ExecuTorchInitConfigFunction = try Self.objcMessage()
814
+ let generate: ExecuTorchGenerateFunction = try Self.objcMessage()
815
+
816
+ sendVoid(runner, Self.runnerResetSelector)
817
+
818
+ let allocatedConfig = alloc(configClass, NSSelectorFromString("alloc")).takeRetainedValue()
819
+ let configBlock: ExecuTorchConfigBlock = { [temperature, sequenceLength] config in
820
+ config.setValue(temperature, forKey: "temperature")
821
+ config.setValue(sequenceLength, forKey: "sequenceLength")
822
+ }
823
+ let config = initConfig(
824
+ allocatedConfig,
825
+ Self.configInitSelector,
826
+ configBlock
827
+ ).takeRetainedValue()
828
+
829
+ let callback: ExecuTorchTokenCallback = { token in
830
+ tokenCallback(token as String)
831
+ }
832
+ var generateError: NSError?
833
+ guard generate(
834
+ runner,
835
+ Self.runnerGenerateSelector,
836
+ prompt as NSString,
837
+ config,
838
+ callback,
839
+ &generateError
840
+ ) else {
841
+ throw generateError ?? Self.error("ExecuTorch generation failed")
842
+ }
843
+ }
844
+
845
+ private static func objcMessage<Function>() throws -> Function {
846
+ guard let handle = dlopen(nil, RTLD_NOW), let symbol = dlsym(handle, "objc_msgSend") else {
847
+ throw error("Unable to resolve objc_msgSend")
848
+ }
849
+ return unsafeBitCast(symbol, to: Function.self)
850
+ }
851
+
852
+ private static func requireInstanceMethod(_ selector: Selector, on cls: AnyClass) throws {
853
+ guard hasInstanceMethod(cls, selector) else {
854
+ throw error("ExecuTorch API selector missing: \(NSStringFromSelector(selector))")
855
+ }
856
+ }
857
+
858
+ private static func hasInstanceMethod(_ cls: AnyClass, _ selector: Selector) -> Bool {
859
+ class_getInstanceMethod(cls, selector) != nil
860
+ }
861
+
862
+ private static func findClass(_ name: String) -> AnyClass? {
863
+ NSClassFromString(name) ?? NSClassFromString("ExecuTorchLLM.\(name)")
864
+ }
865
+
866
+ private static func error(_ description: String) -> NSError {
867
+ NSError(domain: "CapgoLLM.ExecuTorch", code: 1, userInfo: [NSLocalizedDescriptionKey: description])
868
+ }
869
+ }
870
+
448
871
  // Download delegate class to handle progress updates
449
872
  class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
450
873
  weak var plugin: CAPPlugin?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-llm",
3
- "version": "8.0.10",
3
+ "version": "8.0.12",
4
4
  "description": "Adds support for LLM locally runned for Capacitor",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",