@capgo/capacitor-llm 8.0.19 → 8.1.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.
@@ -1,13 +1,12 @@
1
1
  import Foundation
2
2
  import Capacitor
3
- import ObjectiveC
4
- import Darwin
5
3
  #if canImport(FoundationModels)
6
4
  import FoundationModels
7
5
  #endif
8
- // For CocoaPods, MediaPipeTasksGenAI should always be available
9
- // For SPM, we need conditional import when SPM support is added
10
- #if canImport(MediaPipeTasksGenAI) && !targetEnvironment(macCatalyst)
6
+ #if canImport(LiteRTLM)
7
+ import LiteRTLM
8
+ #endif
9
+ #if canImport(MediaPipeTasksGenAI)
11
10
  import MediaPipeTasksGenAI
12
11
  #endif
13
12
 
@@ -18,10 +17,9 @@ import MediaPipeTasksGenAI
18
17
 
19
18
  @objc(LLMPlugin)
20
19
  public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
21
- private let pluginVersion: String = "8.0.19"
22
- #if targetEnvironment(macCatalyst)
23
- private let catalystNotSupportedMessage = "CapgoLLM is not available on Mac Catalyst. Use an iOS/iPadOS target instead."
24
- #endif
20
+ private let pluginVersion: String = "8.1.0"
21
+ private let liteRtTopP: Float = 0.95
22
+
25
23
  public let identifier = "LLMPlugin"
26
24
  public let jsName = "CapgoLLM"
27
25
  public var pluginMethods: [CAPPluginMethod] = [
@@ -35,24 +33,8 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
35
33
 
36
34
  private enum ModelType {
37
35
  case appleIntelligence
36
+ case liteRtLm
38
37
  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
- }
56
38
  }
57
39
 
58
40
  private var currentModelType: ModelType = .appleIntelligence
@@ -61,16 +43,6 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
61
43
 
62
44
  #if canImport(FoundationModels)
63
45
  private var appleModel: Any? = {
64
- // `SystemLanguageModel.default` is not functional for an iOS/iPadOS
65
- // binary running "Designed for iPad" on Apple Silicon Mac, and
66
- // touching it there crashes the app during plugin instantiation —
67
- // before any of the app's own platform checks get a chance to run,
68
- // since Capacitor eagerly instantiates every registered plugin at
69
- // bridge startup. Skip it in that environment, same as the existing
70
- // Mac Catalyst guard below.
71
- if ProcessInfo.processInfo.isiOSAppOnMac {
72
- return nil
73
- }
74
46
  if #available(iOS 26.0, *) {
75
47
  return SystemLanguageModel.default
76
48
  } else {
@@ -81,17 +53,21 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
81
53
  private var appleChats: [String: Any] = [:]
82
54
  #endif
83
55
 
84
- #if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
85
- private var llmInference: LlmInference?
56
+ #if canImport(LiteRTLM)
57
+ private var liteRtEngine: LiteRTLM.Engine?
58
+ private var liteRtChats: [String: LiteRTLM.Conversation] = [:]
59
+ private var liteRtTopk: Int = 40
60
+ private var liteRtTemperature: Float = 0.8
61
+ private var liteRtRandomSeed: Int = 0
86
62
  #endif
87
63
 
88
- private var execuTorchRunner: DynamicExecuTorchRunner?
89
- private var execuTorchChats: [String: ExecuTorchChatSession] = [:]
90
- private let execuTorchGenerationLock = NSLock()
91
- private let execuTorchNotLinkedMessage = "ExecuTorch is not linked in this iOS app. " +
92
- "It is not installed by CocoaPods; add the ExecuTorch Swift Package products " +
93
- "to the app target on iOS 17+ or choose another engine."
94
- private var execuTorchIsGenerating = false
64
+ #if COCOAPODS || canImport(MediaPipeTasksGenAI)
65
+ private var llmInference: LlmInference?
66
+ private var mediaPipeChats: [String: LlmInference.Session] = [:]
67
+ private var mediaPipeTopk: Int = 40
68
+ private var mediaPipeTemperature: Float = 0.8
69
+ private var mediaPipeRandomSeed: Int = 0
70
+ #endif
95
71
 
96
72
  @objc func setModel(_ call: CAPPluginCall) {
97
73
  guard let path = call.getString("path") else {
@@ -99,45 +75,23 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
99
75
  return
100
76
  }
101
77
 
102
- #if targetEnvironment(macCatalyst)
103
- call.reject(catalystNotSupportedMessage)
104
- return
105
- #endif
106
-
107
- print("[CapgoLLM] Setting model path: \(path)")
108
-
109
- // Extract model parameters
110
78
  let maxTokens = call.getInt("maxTokens") ?? 2048
111
79
  let topk = call.getInt("topk") ?? 40
112
80
  let temperature = call.getFloat("temperature") ?? 0.8
113
- let modelType = call.getString("modelType") // Optional model type parameter
114
- let engine = call.getString("engine")?.lowercased() ?? "auto"
115
- let tokenizerPath = call.getString("tokenizerPath")
116
- let specialTokens = call.getArray("specialTokens", String.self) ?? []
117
- let sequenceLength = call.getInt("sequenceLength") ?? maxTokens
118
-
119
- guard sequenceLength > 0 else {
120
- call.reject("sequenceLength must be > 0")
121
- return
122
- }
123
-
124
- guard !isExecuTorchGenerating() else {
125
- call.reject("Cannot switch model while ExecuTorch is generating. Wait for aiFinished.")
126
- return
127
- }
81
+ let randomSeed = call.getInt("randomSeed") ?? 0
82
+ let modelType = call.getString("modelType")
128
83
 
129
84
  modelPath = path
130
- execuTorchChats.removeAll()
131
- #if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
85
+ isReady = false
86
+ clearChatSessions()
87
+ #if canImport(LiteRTLM)
88
+ liteRtEngine = nil
89
+ #endif
90
+ #if COCOAPODS || canImport(MediaPipeTasksGenAI)
132
91
  llmInference = nil
133
92
  #endif
134
- execuTorchRunner = nil
135
- isReady = false
136
93
 
137
- // Check if user wants to use Apple Intelligence
138
- if engine == "apple" || path.lowercased() == "apple intelligence" {
139
- print("[CapgoLLM] Switching to Apple Intelligence")
140
- execuTorchRunner = nil
94
+ if path.lowercased() == "apple intelligence" {
141
95
  currentModelType = .appleIntelligence
142
96
  isReady = true
143
97
  notifyListeners("readinessChange", data: ["readiness": "ready"])
@@ -145,218 +99,80 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
145
99
  return
146
100
  }
147
101
 
148
- if shouldUseExecuTorch(engine: engine, path: path, tokenizerPath: tokenizerPath) {
149
- currentModelType = .execuTorch
150
- setExecuTorchModel(
151
- call,
152
- path: path,
153
- tokenizerPath: tokenizerPath,
154
- modelType: modelType,
155
- specialTokens: specialTokens,
156
- sequenceLength: sequenceLength,
157
- temperature: temperature
158
- )
159
- return
160
- }
161
-
162
- #if !targetEnvironment(macCatalyst) && canImport(MediaPipeTasksGenAI)
163
- currentModelType = .mediaPipe
164
- Task {
165
- do {
166
- let modelURL: URL
167
-
168
- // Check if it's a path within the app bundle
169
- if path.hasPrefix("/") {
170
- modelURL = URL(fileURLWithPath: path)
171
- print("[CapgoLLM] Using absolute path: \(modelURL.path)")
172
- } else {
173
- // Try to find it in the app bundle
174
- let fileName: String
175
- let fileExtension: String
176
-
177
- // If modelType is provided, use it as the extension
178
- if let providedType = modelType, !providedType.isEmpty {
179
- // Remove any extension from path and use provided type
180
- fileName = (path as NSString).deletingPathExtension
181
- fileExtension = providedType
182
- print("[CapgoLLM] Using provided model type: \(fileExtension)")
183
- } else {
184
- // Extract from path
185
- fileName = (path as NSString).deletingPathExtension
186
- fileExtension = (path as NSString).pathExtension
187
- }
188
-
189
- print("[CapgoLLM] Looking for model in bundle: \(fileName) with type: \(fileExtension.isEmpty ? "bin" : fileExtension)")
102
+ let resolvedModelType = resolveModelType(path: path, modelType: modelType)
103
+ switch resolveModelKind(path: path, modelType: modelType) {
104
+ case .appleIntelligence:
105
+ currentModelType = .appleIntelligence
106
+ isReady = true
107
+ notifyListeners("readinessChange", data: ["readiness": "ready"])
108
+ call.resolve()
190
109
 
191
- // Use Bundle.main.path as shown in MediaPipe docs
192
- guard let bundlePath = Bundle.main.path(forResource: fileName, ofType: fileExtension.isEmpty ? "bin" : fileExtension) else {
193
- print("[CapgoLLM] Error: Model file not found in bundle: \(path)")
194
- print("[CapgoLLM] Tried resource: '\(fileName)' with type: '\(fileExtension.isEmpty ? "bin" : fileExtension)'")
195
- call.reject("Model file not found in bundle: \(path)")
196
- return
197
- }
198
- modelURL = URL(fileURLWithPath: bundlePath)
199
- print("[CapgoLLM] Found model in bundle at path: \(bundlePath)")
200
-
201
- // For MediaPipe models, verify companion .litertlm file exists
202
- if fileExtension == "task" {
203
- let companionPath = Bundle.main.path(forResource: fileName, ofType: "litertlm")
204
- if companionPath == nil {
205
- print("[CapgoLLM] Warning: Companion .litertlm file not found for \(fileName)")
206
- // Don't fail here as some models might not need it
207
- } else {
208
- print("[CapgoLLM] Found companion file at path: \(companionPath!)")
209
- }
210
- }
110
+ case .liteRtLm:
111
+ #if canImport(LiteRTLM)
112
+ Task {
113
+ do {
114
+ let modelURL = try resolveModelURL(path: path, modelType: modelType)
115
+ let engine = try await initializeLiteRtEngine(modelURL: modelURL, maxTokens: maxTokens)
116
+ liteRtEngine = engine
117
+ liteRtTopk = topk
118
+ liteRtTemperature = temperature
119
+ liteRtRandomSeed = randomSeed
120
+ currentModelType = .liteRtLm
121
+ isReady = true
122
+ notifyListeners("readinessChange", data: ["readiness": "ready"])
123
+ call.resolve()
124
+ } catch {
125
+ let message = "Failed to load model: \(error.localizedDescription)"
126
+ notifyListeners("readinessChange", data: ["readiness": message])
127
+ call.reject(message)
211
128
  }
212
-
213
- // Initialize MediaPipe LLM
214
- print("[CapgoLLM] Initializing MediaPipe with model at: \(modelURL.path)")
215
- let options = LlmInference.Options(modelPath: modelURL.path)
216
- options.maxTokens = maxTokens
217
- options.maxTopk = topk
218
-
219
- print("[CapgoLLM] Creating LlmInference instance with maxTokens: \(maxTokens), topk: \(topk), temperature: \(temperature)")
220
- llmInference = try LlmInference(options: options)
221
- currentModelType = .mediaPipe
222
- isReady = true
223
-
224
- print("[CapgoLLM] Model loaded successfully")
225
- notifyListeners("readinessChange", data: ["readiness": "ready"])
226
- call.resolve()
227
- } catch {
228
- llmInference = nil
229
- isReady = false
230
- print("[CapgoLLM] Error loading model: \(error.localizedDescription)")
231
- notifyListeners("readinessChange", data: ["readiness": "Failed to load model: \(error.localizedDescription)"])
232
- call.reject("Failed to load model: \(error.localizedDescription)")
233
129
  }
234
- }
235
- #else
236
- call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
237
- #endif
238
- }
239
-
240
- private func shouldUseExecuTorch(engine: String, path: String, tokenizerPath: String?) -> Bool {
241
- if path.lowercased() == "apple intelligence" {
242
- return false
243
- }
244
- if engine == "executorch" {
245
- return true
246
- }
247
- if engine == "apple" || engine == "mediapipe" {
248
- return false
249
- }
250
- if tokenizerPath?.isEmpty == false {
251
- return true
252
- }
253
- return path.lowercased().hasSuffix(".pte")
254
- }
255
-
256
- private func setExecuTorchModel(
257
- _ call: CAPPluginCall,
258
- path: String,
259
- tokenizerPath: String?,
260
- modelType: String?,
261
- specialTokens: [String],
262
- sequenceLength: Int,
263
- temperature: Float
264
- ) {
265
- execuTorchRunner = nil
266
-
267
- guard sequenceLength > 0 else {
268
- call.reject("sequenceLength must be > 0")
269
- return
270
- }
271
-
272
- guard DynamicExecuTorchRunner.isAvailable else {
273
- isReady = false
274
- notifyListeners("readinessChange", data: ["readiness": execuTorchNotLinkedMessage])
275
- call.reject(execuTorchNotLinkedMessage)
276
- return
277
- }
278
-
279
- guard let tokenizerPath = tokenizerPath, !tokenizerPath.isEmpty else {
280
- call.reject("tokenizerPath is required for ExecuTorch models")
281
- return
282
- }
130
+ #else
131
+ let message = resolvedModelType == "litertlm"
132
+ ? "LiteRT-LM on iOS is available only when this plugin is integrated through Swift Package Manager."
133
+ : "This custom model format is unavailable on iOS in CocoaPods builds. Use Apple Intelligence or integrate the plugin through Swift Package Manager for LiteRT-LM support."
134
+ call.reject(message)
135
+ #endif
283
136
 
284
- Task {
285
- do {
286
- let modelURL = try resolveModelURL(path: path, modelType: modelType, defaultExtension: "pte")
287
- let tokenizerURL = try resolveModelURL(path: tokenizerPath, modelType: nil, defaultExtension: "model")
288
-
289
- print("[CapgoLLM] Initializing ExecuTorch with model at: \(modelURL.path)")
290
- print("[CapgoLLM] Initializing ExecuTorch with tokenizer at: \(tokenizerURL.path)")
291
-
292
- execuTorchRunner = try DynamicExecuTorchRunner(
293
- modelPath: modelURL.path,
294
- tokenizerPath: tokenizerURL.path,
295
- specialTokens: specialTokens,
296
- sequenceLength: sequenceLength,
297
- temperature: Double(temperature)
298
- )
299
- currentModelType = .execuTorch
300
- isReady = true
301
-
302
- notifyListeners("readinessChange", data: ["readiness": "ready"])
303
- call.resolve()
304
- } catch {
305
- execuTorchRunner = nil
306
- isReady = false
307
- print("[CapgoLLM] Error loading ExecuTorch model: \(error.localizedDescription)")
308
- notifyListeners("readinessChange", data: ["readiness": "Failed to load ExecuTorch model: \(error.localizedDescription)"])
309
- call.reject("Failed to load ExecuTorch model: \(error.localizedDescription)")
137
+ case .mediaPipe:
138
+ #if canImport(MediaPipeTasksGenAI)
139
+ Task {
140
+ do {
141
+ let modelURL = try resolveModelURL(path: path, modelType: modelType)
142
+ let options = LlmInference.Options(modelPath: modelURL.path)
143
+ options.maxTokens = maxTokens
144
+ options.maxTopk = topk
145
+
146
+ llmInference = try LlmInference(options: options)
147
+ mediaPipeTopk = topk
148
+ mediaPipeTemperature = temperature
149
+ mediaPipeRandomSeed = randomSeed
150
+ currentModelType = .mediaPipe
151
+ isReady = true
152
+ notifyListeners("readinessChange", data: ["readiness": "ready"])
153
+ call.resolve()
154
+ } catch {
155
+ let message = "Failed to load model: \(error.localizedDescription)"
156
+ notifyListeners("readinessChange", data: ["readiness": message])
157
+ call.reject(message)
158
+ }
310
159
  }
160
+ #else
161
+ call.reject("MediaPipe is not available. For iOS custom .litertlm models, integrate the plugin through Swift Package Manager to use LiteRT-LM.")
162
+ #endif
311
163
  }
312
164
  }
313
165
 
314
- private func resolveModelURL(path: String, modelType: String?, defaultExtension: String) throws -> URL {
315
- if path.hasPrefix("/") {
316
- return URL(fileURLWithPath: path)
317
- }
318
-
319
- let fileName: String
320
- let fileExtension: String
321
-
322
- if let providedType = modelType, !providedType.isEmpty {
323
- fileName = (path as NSString).deletingPathExtension
324
- fileExtension = providedType
325
- } else {
326
- fileName = (path as NSString).deletingPathExtension
327
- let extractedExtension = (path as NSString).pathExtension
328
- fileExtension = extractedExtension.isEmpty ? defaultExtension : extractedExtension
329
- }
330
-
331
- guard let bundlePath = Bundle.main.path(forResource: fileName, ofType: fileExtension) else {
332
- throw NSError(
333
- domain: "CapgoLLM",
334
- code: 404,
335
- userInfo: [NSLocalizedDescriptionKey: "Model file not found in bundle: \(path)"]
336
- )
337
- }
338
-
339
- return URL(fileURLWithPath: bundlePath)
340
- }
341
-
342
166
  @objc func createChat(_ call: CAPPluginCall) {
343
- #if targetEnvironment(macCatalyst)
344
- call.reject(catalystNotSupportedMessage)
345
- return
346
- #endif
347
-
348
- print("[CapgoLLM] Creating chat with model type: \(currentModelType)")
349
167
  switch currentModelType {
350
168
  case .appleIntelligence:
351
169
  #if canImport(FoundationModels)
352
170
  if #available(iOS 26.0, *) {
353
171
  let instructions = call.getString("instructions")
354
172
  let session = LanguageModelSession(instructions: instructions)
355
- let id = UUID()
356
- appleChats[id.uuidString] = session
357
- call.resolve([
358
- "id": id.uuidString
359
- ])
173
+ let id = UUID().uuidString
174
+ appleChats[id] = session
175
+ call.resolve(["id": id])
360
176
  } else {
361
177
  call.reject("Apple Intelligence requires iOS 26.0 or later")
362
178
  }
@@ -364,48 +180,52 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
364
180
  call.reject("Apple Intelligence is not available on this device")
365
181
  #endif
366
182
 
183
+ case .liteRtLm:
184
+ #if canImport(LiteRTLM)
185
+ Task {
186
+ do {
187
+ guard let engine = liteRtEngine else {
188
+ call.reject("LiteRT-LM engine not loaded")
189
+ return
190
+ }
191
+
192
+ let conversationConfig = try makeLiteRtConversationConfig(instructions: call.getString("instructions"))
193
+ let conversation = try await engine.createConversation(with: conversationConfig)
194
+ let id = UUID().uuidString
195
+ liteRtChats[id] = conversation
196
+ call.resolve(["id": id])
197
+ } catch {
198
+ call.reject("Failed to create LiteRT-LM chat: \(error.localizedDescription)")
199
+ }
200
+ }
201
+ #else
202
+ call.reject("LiteRT-LM on iOS is available only when this plugin is integrated through Swift Package Manager.")
203
+ #endif
204
+
367
205
  case .mediaPipe:
368
- #if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
369
- guard llmInference != nil else {
370
- print("[CapgoLLM] Error: Model not loaded when creating chat")
206
+ #if COCOAPODS || canImport(MediaPipeTasksGenAI)
207
+ guard let inference = llmInference else {
371
208
  call.reject("Model not loaded")
372
209
  return
373
210
  }
374
211
 
375
- // MediaPipe doesn't use sessions, just return a dummy ID
376
- let id = UUID().uuidString
377
- print("[CapgoLLM] Created MediaPipe chat with ID: \(id)")
378
- call.resolve([
379
- "id": id
380
- ])
212
+ do {
213
+ let session = try LlmInference.Session(llmInference: inference, options: makeMediaPipeSessionOptions())
214
+ let id = UUID().uuidString
215
+ mediaPipeChats[id] = session
216
+ call.resolve(["id": id])
217
+ } catch {
218
+ call.reject("Failed to create chat: \(error.localizedDescription)")
219
+ }
381
220
  #else
382
- call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
221
+ call.reject("MediaPipe is not available. Please install via CocoaPods.")
383
222
  #endif
384
-
385
- case .execuTorch:
386
- guard execuTorchRunner != nil else {
387
- call.reject("ExecuTorch model not loaded")
388
- return
389
- }
390
- let id = UUID().uuidString
391
- execuTorchChats[id] = ExecuTorchChatSession()
392
- print("[CapgoLLM] Created ExecuTorch chat with ID: \(id)")
393
- call.resolve([
394
- "id": id
395
- ])
396
223
  }
397
224
  }
398
225
 
399
226
  @objc func getReadiness(_ call: CAPPluginCall) {
400
- #if targetEnvironment(macCatalyst)
401
- call.resolve(["readiness": "unsupported_on_maccatalyst"])
402
- notifyListeners("readinessChange", data: ["readiness": "unsupported_on_maccatalyst"])
403
- return
404
- #endif
405
-
406
227
  let readiness = getReadinessStatus()
407
228
  call.resolve(["readiness": readiness])
408
- // Also notify listeners
409
229
  notifyListeners("readinessChange", data: ["readiness": readiness])
410
230
  }
411
231
 
@@ -426,27 +246,18 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
426
246
  case .unavailable(let other):
427
247
  return "Error: \(other)"
428
248
  }
429
- } else {
430
- return "Apple Intelligence requires iOS 26.0 or later"
431
249
  }
250
+ return "Apple Intelligence requires iOS 26.0 or later"
432
251
  #else
433
252
  return "Apple Intelligence is not available on this device"
434
253
  #endif
435
254
 
436
- case .mediaPipe:
437
- return isReady ? "ready" : "not_ready"
438
-
439
- case .execuTorch:
255
+ case .liteRtLm, .mediaPipe:
440
256
  return isReady ? "ready" : "not_ready"
441
257
  }
442
258
  }
443
259
 
444
260
  @objc func sendMessage(_ call: CAPPluginCall) {
445
- #if targetEnvironment(macCatalyst)
446
- call.reject(catalystNotSupportedMessage)
447
- return
448
- #endif
449
-
450
261
  let chatId = call.getString("chatId", "")
451
262
  guard let message = call.getString("message") else {
452
263
  call.reject("message not found")
@@ -457,23 +268,7 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
457
268
  case .appleIntelligence:
458
269
  #if canImport(FoundationModels)
459
270
  if #available(iOS 26.0, *), let model = appleModel as? SystemLanguageModel {
460
- switch model.availability {
461
- case .available:
462
- break
463
- case .unavailable(.deviceNotEligible):
464
- call.reject("error deviceNotEligible error")
465
- return
466
- case .unavailable(.appleIntelligenceNotEnabled):
467
- call.reject("error appleIntelligenceNotEnabled error")
468
- return
469
- case .unavailable(.modelNotReady):
470
- call.reject("error modelNotReady error")
471
- return
472
- case .unavailable(let other):
473
- call.reject("error \(other) error")
474
- return
475
- }
476
-
271
+ guard validateAppleAvailability(model: model, call: call) else { return }
477
272
  guard let chat = appleChats[chatId] as? LanguageModelSession else {
478
273
  call.reject("chat not found")
479
274
  return
@@ -484,47 +279,7 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
484
279
  return
485
280
  }
486
281
 
487
- Task {
488
- do {
489
- let stream = chat.streamResponse(to: message)
490
- for try await chunk in stream {
491
- print("[CapgoLLM] Chunk type: \(type(of: chunk))")
492
-
493
- // Extract content from Snapshot object using Mirror reflection
494
- let textChunk: String
495
- let mirror = Mirror(reflecting: chunk)
496
-
497
- // Try to find content property
498
- if let contentProperty = mirror.children.first(where: { $0.label == "content" }),
499
- let content = contentProperty.value as? String {
500
- textChunk = content
501
- print("[CapgoLLM] Extracted content: \(content)")
502
- } else {
503
- // Fallback: try rawContent
504
- if let rawContentProperty = mirror.children.first(where: { $0.label == "rawContent" }),
505
- let rawContent = rawContentProperty.value as? String {
506
- textChunk = rawContent
507
- print("[CapgoLLM] Extracted rawContent: \(rawContent)")
508
- } else {
509
- // Final fallback
510
- textChunk = "\(chunk)"
511
- print("[CapgoLLM] Fallback to string conversion: \(textChunk)")
512
- }
513
- }
514
- let data: [String: Any] = [
515
- "chatId": chatId as String,
516
- "text": textChunk as String,
517
- "isChunk": true
518
- ]
519
- self.notifyListeners("textFromAi", data: data)
520
- }
521
- let finishData: [String: Any] = ["chatId": chatId as String]
522
- self.notifyListeners("aiFinished", data: finishData)
523
- call.resolve()
524
- } catch {
525
- call.reject("Failed to get response: \(error.localizedDescription)")
526
- }
527
- }
282
+ streamAppleResponse(chatId: chatId, message: message, chat: chat, call: call)
528
283
  } else {
529
284
  call.reject("Apple Intelligence requires iOS 26.0 or later")
530
285
  }
@@ -532,114 +287,35 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
532
287
  call.reject("Apple Intelligence is not available on this device")
533
288
  #endif
534
289
 
535
- case .mediaPipe:
536
- #if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
537
- guard let inference = llmInference else {
538
- call.reject("Model not loaded")
290
+ case .liteRtLm:
291
+ #if canImport(LiteRTLM)
292
+ guard let conversation = liteRtChats[chatId] else {
293
+ call.reject("chat not found")
539
294
  return
540
295
  }
541
296
 
542
- Task {
543
- do {
544
- print("[CapgoLLM] Attempting to generate streaming response for message: \(message)")
545
-
546
- // Use native streaming API
547
- let resultStream = inference.generateResponseAsync(inputText: message)
548
-
549
- var fullResponse = ""
550
-
551
- for try await partialResult in resultStream {
552
- print("[CapgoLLM] Partial result: \(partialResult)")
553
-
554
- // Send the chunk
555
- self.notifyListeners("textFromAi", data: [
556
- "chatId": chatId,
557
- "text": partialResult,
558
- "isChunk": true
559
- ])
560
-
561
- // Accumulate for history (if needed)
562
- fullResponse += partialResult
563
- }
564
-
565
- print("[CapgoLLM] Streaming complete. Full response: \(fullResponse)")
566
-
567
- self.notifyListeners("aiFinished", data: ["chatId": chatId])
568
- call.resolve()
569
-
570
- } catch {
571
- print("[CapgoLLM] Error details: \(error)")
572
- call.reject("Failed to generate response: \(error.localizedDescription)")
573
- }
574
- }
297
+ streamLiteRtResponse(chatId: chatId, message: message, conversation: conversation, call: call)
575
298
  #else
576
- call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
299
+ call.reject("LiteRT-LM on iOS is available only when this plugin is integrated through Swift Package Manager.")
577
300
  #endif
578
301
 
579
- case .execuTorch:
580
- guard isReady, let runner = execuTorchRunner else {
581
- call.reject("ExecuTorch model not loaded")
302
+ case .mediaPipe:
303
+ #if COCOAPODS || canImport(MediaPipeTasksGenAI)
304
+ guard llmInference != nil else {
305
+ call.reject("Model not loaded")
582
306
  return
583
307
  }
584
- guard let chat = execuTorchChats[chatId] else {
308
+
309
+ guard let session = mediaPipeChats[chatId] else {
585
310
  call.reject("chat not found")
586
311
  return
587
312
  }
588
- guard beginExecuTorchGeneration() else {
589
- call.reject("ExecuTorch is already generating. Wait for aiFinished before sending another message.")
590
- return
591
- }
592
313
 
593
- Task {
594
- defer {
595
- self.finishExecuTorchGeneration()
596
- }
597
-
598
- do {
599
- let prompt = chat.buildPrompt(userMessage: message)
600
- var fullResponse = ""
601
- print("[CapgoLLM] Attempting to generate ExecuTorch response for message: \(message)")
602
- try runner.generate(prompt) { token in
603
- self.notifyListeners("textFromAi", data: [
604
- "chatId": chatId,
605
- "text": token,
606
- "isChunk": true
607
- ])
608
- fullResponse += token
609
- }
610
- chat.addTurn(userMessage: message, assistantMessage: fullResponse)
611
- self.notifyListeners("aiFinished", data: ["chatId": chatId])
612
- call.resolve()
613
- } catch {
614
- print("[CapgoLLM] ExecuTorch error details: \(error)")
615
- call.reject("Failed to generate ExecuTorch response: \(error.localizedDescription)")
616
- }
617
- }
618
- }
619
- }
620
-
621
- private func isExecuTorchGenerating() -> Bool {
622
- execuTorchGenerationLock.lock()
623
- defer { execuTorchGenerationLock.unlock() }
624
- return execuTorchIsGenerating
625
- }
626
-
627
- private func beginExecuTorchGeneration() -> Bool {
628
- execuTorchGenerationLock.lock()
629
- defer { execuTorchGenerationLock.unlock() }
630
-
631
- if execuTorchIsGenerating {
632
- return false
314
+ streamMediaPipeResponse(chatId: chatId, message: message, session: session, call: call)
315
+ #else
316
+ call.reject("MediaPipe is not available. Please install via CocoaPods.")
317
+ #endif
633
318
  }
634
-
635
- execuTorchIsGenerating = true
636
- return true
637
- }
638
-
639
- private func finishExecuTorchGeneration() {
640
- execuTorchGenerationLock.lock()
641
- execuTorchIsGenerating = false
642
- execuTorchGenerationLock.unlock()
643
319
  }
644
320
 
645
321
  @objc func downloadModel(_ call: CAPPluginCall) {
@@ -655,19 +331,14 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
655
331
 
656
332
  let filename = call.getString("filename") ?? url.lastPathComponent
657
333
 
658
- // Get documents directory
659
334
  guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
660
335
  call.reject("Could not access documents directory")
661
336
  return
662
337
  }
663
338
 
664
339
  let destinationURL = documentsPath.appendingPathComponent(filename)
665
-
666
- // Create download task
667
340
  let session = URLSession(configuration: .default, delegate: DownloadDelegate(plugin: self), delegateQueue: nil)
668
- let downloadTask = session.downloadTask(with: url) { [weak self] (tempURL, _, error) in
669
- guard let self = self else { return }
670
-
341
+ let downloadTask = session.downloadTask(with: url) { tempURL, _, error in
671
342
  if let error = error {
672
343
  call.reject("Download failed: \(error.localizedDescription)")
673
344
  return
@@ -679,38 +350,31 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
679
350
  }
680
351
 
681
352
  do {
682
- // Remove existing file if it exists
683
353
  if FileManager.default.fileExists(atPath: destinationURL.path) {
684
354
  try FileManager.default.removeItem(at: destinationURL)
685
355
  }
686
356
 
687
- // Move downloaded file to documents directory
688
357
  try FileManager.default.moveItem(at: tempURL, to: destinationURL)
689
358
 
690
359
  var result = [
691
360
  "path": destinationURL.path
692
361
  ]
693
362
 
694
- // Handle companion URL if provided (for compatibility)
695
363
  if let companionUrlString = call.getString("companionUrl"),
696
364
  let companionUrl = URL(string: companionUrlString) {
697
-
698
365
  let companionFilename = companionUrl.lastPathComponent
699
366
  let companionDestination = documentsPath.appendingPathComponent(companionFilename)
700
367
 
701
- // Download companion file synchronously for simplicity
702
368
  do {
703
369
  let companionData = try Data(contentsOf: companionUrl)
704
370
  try companionData.write(to: companionDestination)
705
371
  result["companionPath"] = companionDestination.path
706
372
  } catch {
707
- // Log error but don't fail the whole download
708
373
  print("Failed to download companion file: \(error)")
709
374
  }
710
375
  }
711
376
 
712
377
  call.resolve(result)
713
-
714
378
  } catch {
715
379
  call.reject("Failed to save file: \(error.localizedDescription)")
716
380
  }
@@ -722,168 +386,277 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
722
386
  @objc func getPluginVersion(_ call: CAPPluginCall) {
723
387
  call.resolve(["version": self.pluginVersion])
724
388
  }
725
- }
726
389
 
727
- private typealias ExecuTorchAllocFunction = @convention(c) (AnyClass, Selector) -> Unmanaged<AnyObject>
728
- private typealias ExecuTorchInitRunnerFunction = @convention(c) (
729
- AnyObject,
730
- Selector,
731
- NSString,
732
- NSString,
733
- NSArray
734
- ) -> Unmanaged<AnyObject>
735
- private typealias ExecuTorchLoadFunction = @convention(c) (
736
- AnyObject,
737
- Selector,
738
- UnsafeMutablePointer<NSError?>
739
- ) -> Bool
740
- private typealias ExecuTorchConfigBlock = @convention(block) (NSObject) -> Void
741
- private typealias ExecuTorchInitConfigFunction = @convention(c) (
742
- AnyObject,
743
- Selector,
744
- ExecuTorchConfigBlock
745
- ) -> Unmanaged<AnyObject>
746
- private typealias ExecuTorchTokenCallback = @convention(block) (NSString) -> Void
747
- private typealias ExecuTorchGenerateFunction = @convention(c) (
748
- AnyObject,
749
- Selector,
750
- NSString,
751
- AnyObject,
752
- ExecuTorchTokenCallback,
753
- UnsafeMutablePointer<NSError?>
754
- ) -> Bool
755
- private typealias ExecuTorchVoidFunction = @convention(c) (AnyObject, Selector) -> Void
756
-
757
- private final class DynamicExecuTorchRunner {
758
- private static let runnerInitSelector = NSSelectorFromString("initWithModelPath:tokenizerPath:specialTokens:")
759
- private static let runnerLoadSelector = NSSelectorFromString("loadWithError:")
760
- private static let runnerResetSelector = NSSelectorFromString("reset")
761
- private static let runnerGenerateSelector = NSSelectorFromString("generateWithPrompt:config:tokenCallback:error:")
762
- private static let configInitSelector = NSSelectorFromString("initWithBlock:")
763
-
764
- static var isAvailable: Bool {
765
- guard let runnerClass = findClass("ExecuTorchLLMTextRunner"),
766
- let configClass = findClass("ExecuTorchLLMConfig") else {
767
- return false
390
+ private func clearChatSessions() {
391
+ #if canImport(FoundationModels)
392
+ appleChats.removeAll()
393
+ #endif
394
+ #if canImport(LiteRTLM)
395
+ liteRtChats.removeAll()
396
+ #endif
397
+ #if COCOAPODS || canImport(MediaPipeTasksGenAI)
398
+ mediaPipeChats.removeAll()
399
+ #endif
400
+ }
401
+
402
+ private func resolveModelKind(path: String, modelType: String?) -> ModelType {
403
+ let resolvedModelType = resolveModelType(path: path, modelType: modelType)
404
+ switch resolvedModelType {
405
+ case "litertlm":
406
+ return .liteRtLm
407
+ case "task":
408
+ return .mediaPipe
409
+ default:
410
+ #if canImport(LiteRTLM)
411
+ return .liteRtLm
412
+ #else
413
+ return .mediaPipe
414
+ #endif
768
415
  }
416
+ }
769
417
 
770
- return hasInstanceMethod(runnerClass, runnerInitSelector) &&
771
- hasInstanceMethod(runnerClass, runnerLoadSelector) &&
772
- hasInstanceMethod(runnerClass, runnerResetSelector) &&
773
- hasInstanceMethod(runnerClass, runnerGenerateSelector) &&
774
- hasInstanceMethod(configClass, configInitSelector)
418
+ private func resolveModelType(path: String, modelType: String?) -> String {
419
+ if let modelType, !modelType.isEmpty {
420
+ return stripURLSuffix(modelType).lowercased()
421
+ }
422
+
423
+ let normalizedPath = stripURLSuffix(path)
424
+ let pathExtension = (normalizedPath as NSString).pathExtension
425
+ return pathExtension.lowercased()
426
+ }
427
+
428
+ private func stripURLSuffix(_ value: String) -> String {
429
+ let queryIndex = value.firstIndex(of: "?")
430
+ let fragmentIndex = value.firstIndex(of: "#")
431
+ let cutoff = [queryIndex, fragmentIndex].compactMap { $0 }.min() ?? value.endIndex
432
+ return String(value[..<cutoff])
775
433
  }
776
434
 
777
- private let runner: AnyObject
778
- private let sequenceLength: Int
779
- private let temperature: Double
780
-
781
- init(
782
- modelPath: String,
783
- tokenizerPath: String,
784
- specialTokens: [String],
785
- sequenceLength: Int,
786
- temperature: Double
787
- ) throws {
788
- guard let runnerClass = Self.findClass("ExecuTorchLLMTextRunner") else {
789
- throw Self.error("ExecuTorchLLMTextRunner was not found. Link ExecuTorch in the app target.")
435
+ private func resolveModelURL(path: String, modelType: String?) throws -> URL {
436
+ if path.hasPrefix("/") {
437
+ return URL(fileURLWithPath: path)
790
438
  }
791
- try Self.requireInstanceMethod(Self.runnerInitSelector, on: runnerClass)
792
- try Self.requireInstanceMethod(Self.runnerLoadSelector, on: runnerClass)
793
-
794
- let alloc: ExecuTorchAllocFunction = try Self.objcMessage()
795
- let initRunner: ExecuTorchInitRunnerFunction = try Self.objcMessage()
796
- let load: ExecuTorchLoadFunction = try Self.objcMessage()
797
-
798
- let allocatedRunner = alloc(runnerClass, NSSelectorFromString("alloc")).takeRetainedValue()
799
- runner = initRunner(
800
- allocatedRunner,
801
- Self.runnerInitSelector,
802
- modelPath as NSString,
803
- tokenizerPath as NSString,
804
- specialTokens as NSArray
805
- ).takeRetainedValue()
806
- self.sequenceLength = sequenceLength
807
- self.temperature = temperature
808
-
809
- var loadError: NSError?
810
- guard load(runner, Self.runnerLoadSelector, &loadError) else {
811
- throw loadError ?? Self.error("ExecuTorch model failed to load")
439
+
440
+ let fileName: String
441
+ let fileExtension: String
442
+
443
+ if let modelType, !modelType.isEmpty {
444
+ fileName = (path as NSString).deletingPathExtension
445
+ fileExtension = stripURLSuffix(modelType)
446
+ } else {
447
+ let normalizedPath = stripURLSuffix(path)
448
+ fileName = (normalizedPath as NSString).deletingPathExtension
449
+ fileExtension = (normalizedPath as NSString).pathExtension
812
450
  }
451
+
452
+ let fallbackExtension = fileExtension.isEmpty ? "bin" : fileExtension
453
+ guard let bundlePath = Bundle.main.path(forResource: fileName, ofType: fallbackExtension) else {
454
+ throw NSError(
455
+ domain: "CapgoLLM",
456
+ code: 404,
457
+ userInfo: [NSLocalizedDescriptionKey: "Model file not found in bundle: \(path)"]
458
+ )
459
+ }
460
+
461
+ return URL(fileURLWithPath: bundlePath)
813
462
  }
463
+ }
814
464
 
815
- func generate(_ prompt: String, tokenCallback: @escaping (String) -> Void) throws {
816
- guard let configClass = Self.findClass("ExecuTorchLLMConfig") else {
817
- throw Self.error("ExecuTorchLLMConfig was not found. Link ExecuTorch in the app target.")
465
+ #if canImport(FoundationModels)
466
+ private extension LLMPlugin {
467
+ @available(iOS 26.0, *)
468
+ func validateAppleAvailability(model: SystemLanguageModel, call: CAPPluginCall) -> Bool {
469
+ switch model.availability {
470
+ case .available:
471
+ return true
472
+ case .unavailable(.deviceNotEligible):
473
+ call.reject("error deviceNotEligible error")
474
+ case .unavailable(.appleIntelligenceNotEnabled):
475
+ call.reject("error appleIntelligenceNotEnabled error")
476
+ case .unavailable(.modelNotReady):
477
+ call.reject("error modelNotReady error")
478
+ case .unavailable(let other):
479
+ call.reject("error \(other) error")
818
480
  }
819
- guard let runnerClass = object_getClass(runner) else {
820
- throw Self.error("ExecuTorch runner class was not found")
481
+
482
+ return false
483
+ }
484
+
485
+ @available(iOS 26.0, *)
486
+ func streamAppleResponse(chatId: String, message: String, chat: LanguageModelSession, call: CAPPluginCall) {
487
+ Task {
488
+ do {
489
+ let stream = chat.streamResponse(to: message)
490
+ for try await chunk in stream {
491
+ let textChunk = extractAppleTextChunk(from: chunk)
492
+ notifyListeners("textFromAi", data: [
493
+ "chatId": chatId,
494
+ "text": textChunk,
495
+ "isChunk": true
496
+ ])
497
+ }
498
+ notifyListeners("aiFinished", data: ["chatId": chatId])
499
+ call.resolve()
500
+ } catch {
501
+ notifyListeners("generationError", data: [
502
+ "chatId": chatId,
503
+ "error": error.localizedDescription
504
+ ])
505
+ call.reject("Failed to get response: \(error.localizedDescription)")
506
+ }
821
507
  }
822
- try Self.requireInstanceMethod(Self.runnerResetSelector, on: runnerClass)
823
- try Self.requireInstanceMethod(Self.runnerGenerateSelector, on: runnerClass)
824
- try Self.requireInstanceMethod(Self.configInitSelector, on: configClass)
508
+ }
825
509
 
826
- let sendVoid: ExecuTorchVoidFunction = try Self.objcMessage()
827
- let alloc: ExecuTorchAllocFunction = try Self.objcMessage()
828
- let initConfig: ExecuTorchInitConfigFunction = try Self.objcMessage()
829
- let generate: ExecuTorchGenerateFunction = try Self.objcMessage()
510
+ @available(iOS 26.0, *)
511
+ func extractAppleTextChunk(from chunk: Any) -> String {
512
+ let mirror = Mirror(reflecting: chunk)
830
513
 
831
- sendVoid(runner, Self.runnerResetSelector)
514
+ if let contentProperty = mirror.children.first(where: { $0.label == "content" }),
515
+ let content = contentProperty.value as? String {
516
+ return content
517
+ }
832
518
 
833
- let allocatedConfig = alloc(configClass, NSSelectorFromString("alloc")).takeRetainedValue()
834
- let configBlock: ExecuTorchConfigBlock = { [temperature, sequenceLength] config in
835
- config.setValue(temperature, forKey: "temperature")
836
- config.setValue(sequenceLength, forKey: "sequenceLength")
519
+ if let rawContentProperty = mirror.children.first(where: { $0.label == "rawContent" }),
520
+ let rawContent = rawContentProperty.value as? String {
521
+ return rawContent
837
522
  }
838
- let config = initConfig(
839
- allocatedConfig,
840
- Self.configInitSelector,
841
- configBlock
842
- ).takeRetainedValue()
843
-
844
- let callback: ExecuTorchTokenCallback = { token in
845
- tokenCallback(token as String)
523
+
524
+ return "\(chunk)"
525
+ }
526
+ }
527
+ #endif
528
+
529
+ #if canImport(LiteRTLM)
530
+ private extension LLMPlugin {
531
+ func initializeLiteRtEngine(modelURL: URL, maxTokens: Int) async throws -> LiteRTLM.Engine {
532
+ guard let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
533
+ throw NSError(
534
+ domain: "CapgoLLM",
535
+ code: 500,
536
+ userInfo: [NSLocalizedDescriptionKey: "Could not access the iOS caches directory for LiteRT-LM."]
537
+ )
846
538
  }
847
- var generateError: NSError?
848
- guard generate(
849
- runner,
850
- Self.runnerGenerateSelector,
851
- prompt as NSString,
852
- config,
853
- callback,
854
- &generateError
855
- ) else {
856
- throw generateError ?? Self.error("ExecuTorch generation failed")
539
+
540
+ let configs = [
541
+ try LiteRTLM.EngineConfig(
542
+ modelPath: modelURL.path,
543
+ backend: .gpu,
544
+ maxNumTokens: maxTokens,
545
+ cacheDir: cacheDirectory.path
546
+ ),
547
+ try LiteRTLM.EngineConfig(
548
+ modelPath: modelURL.path,
549
+ backend: .cpu(),
550
+ maxNumTokens: maxTokens,
551
+ cacheDir: cacheDirectory.path
552
+ )
553
+ ]
554
+
555
+ var lastError: Error?
556
+ for config in configs {
557
+ let engine = LiteRTLM.Engine(engineConfig: config)
558
+ do {
559
+ try await engine.initialize()
560
+ return engine
561
+ } catch {
562
+ lastError = error
563
+ }
857
564
  }
565
+
566
+ throw lastError ?? NSError(
567
+ domain: "CapgoLLM",
568
+ code: 500,
569
+ userInfo: [NSLocalizedDescriptionKey: "Failed to initialize LiteRT-LM on iOS."]
570
+ )
858
571
  }
859
572
 
860
- private static func objcMessage<Function>() throws -> Function {
861
- guard let handle = dlopen(nil, RTLD_NOW), let symbol = dlsym(handle, "objc_msgSend") else {
862
- throw error("Unable to resolve objc_msgSend")
573
+ func makeLiteRtConversationConfig(instructions: String?) throws -> LiteRTLM.ConversationConfig {
574
+ let sampler = try LiteRTLM.SamplerConfig(
575
+ topK: liteRtTopk,
576
+ topP: liteRtTopP,
577
+ temperature: liteRtTemperature,
578
+ seed: liteRtRandomSeed
579
+ )
580
+
581
+ let systemMessage: LiteRTLM.Message?
582
+ if let instructions, !instructions.isEmpty {
583
+ systemMessage = LiteRTLM.Message(instructions, role: .system)
584
+ } else {
585
+ systemMessage = nil
863
586
  }
864
- return unsafeBitCast(symbol, to: Function.self)
587
+
588
+ return LiteRTLM.ConversationConfig(
589
+ systemMessage: systemMessage,
590
+ samplerConfig: sampler
591
+ )
865
592
  }
866
593
 
867
- private static func requireInstanceMethod(_ selector: Selector, on cls: AnyClass) throws {
868
- guard hasInstanceMethod(cls, selector) else {
869
- throw error("ExecuTorch API selector missing: \(NSStringFromSelector(selector))")
594
+ func streamLiteRtResponse(chatId: String, message: String, conversation: LiteRTLM.Conversation, call: CAPPluginCall) {
595
+ Task {
596
+ do {
597
+ for try await chunk in conversation.sendMessageStream(LiteRTLM.Message(message)) {
598
+ let textChunk = chunk.toString
599
+ if !textChunk.isEmpty {
600
+ notifyListeners("textFromAi", data: [
601
+ "chatId": chatId,
602
+ "text": textChunk,
603
+ "isChunk": true
604
+ ])
605
+ }
606
+ }
607
+
608
+ notifyListeners("aiFinished", data: ["chatId": chatId])
609
+ call.resolve()
610
+ } catch {
611
+ notifyListeners("generationError", data: [
612
+ "chatId": chatId,
613
+ "error": error.localizedDescription
614
+ ])
615
+ call.reject("Failed to generate response: \(error.localizedDescription)")
616
+ }
870
617
  }
871
618
  }
619
+ }
620
+ #endif
872
621
 
873
- private static func hasInstanceMethod(_ cls: AnyClass, _ selector: Selector) -> Bool {
874
- class_getInstanceMethod(cls, selector) != nil
622
+ #if COCOAPODS || canImport(MediaPipeTasksGenAI)
623
+ private extension LLMPlugin {
624
+ func makeMediaPipeSessionOptions() -> LlmInference.Session.Options {
625
+ let options = LlmInference.Session.Options()
626
+ options.topk = mediaPipeTopk
627
+ options.temperature = mediaPipeTemperature
628
+ options.randomSeed = mediaPipeRandomSeed
629
+ return options
875
630
  }
876
631
 
877
- private static func findClass(_ name: String) -> AnyClass? {
878
- NSClassFromString(name) ?? NSClassFromString("ExecuTorchLLM.\(name)")
879
- }
632
+ func streamMediaPipeResponse(chatId: String, message: String, session: LlmInference.Session, call: CAPPluginCall) {
633
+ Task {
634
+ do {
635
+ try session.addQueryChunk(inputText: message)
636
+ let resultStream = session.generateResponseAsync()
637
+
638
+ for try await partialResult in resultStream {
639
+ notifyListeners("textFromAi", data: [
640
+ "chatId": chatId,
641
+ "text": partialResult,
642
+ "isChunk": true
643
+ ])
644
+ }
880
645
 
881
- private static func error(_ description: String) -> NSError {
882
- NSError(domain: "CapgoLLM.ExecuTorch", code: 1, userInfo: [NSLocalizedDescriptionKey: description])
646
+ notifyListeners("aiFinished", data: ["chatId": chatId])
647
+ call.resolve()
648
+ } catch {
649
+ notifyListeners("generationError", data: [
650
+ "chatId": chatId,
651
+ "error": error.localizedDescription
652
+ ])
653
+ call.reject("Failed to generate response: \(error.localizedDescription)")
654
+ }
655
+ }
883
656
  }
884
657
  }
658
+ #endif
885
659
 
886
- // Download delegate class to handle progress updates
887
660
  class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
888
661
  weak var plugin: CAPPlugin?
889
662
 
@@ -894,7 +667,6 @@ class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
894
667
  func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
895
668
  didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
896
669
  totalBytesExpectedToWrite: Int64) {
897
-
898
670
  if totalBytesExpectedToWrite > 0 {
899
671
  let progress = Int((Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100)
900
672
 
@@ -908,7 +680,6 @@ class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
908
680
 
909
681
  func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
910
682
  didFinishDownloadingTo location: URL) {
911
- // Handled in the completion handler
683
+ // Handled in the completion handler.
912
684
  }
913
-
914
685
  }