@capgo/capacitor-llm 8.0.19 → 8.1.1

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.1"
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,24 @@ 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")
83
+ let backend = call.getString("backend")
128
84
 
129
85
  modelPath = path
130
- execuTorchChats.removeAll()
131
- #if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
86
+ isReady = false
87
+ clearChatSessions()
88
+ #if canImport(LiteRTLM)
89
+ liteRtEngine = nil
90
+ #endif
91
+ #if COCOAPODS || canImport(MediaPipeTasksGenAI)
132
92
  llmInference = nil
133
93
  #endif
134
- execuTorchRunner = nil
135
- isReady = false
136
94
 
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
95
+ if path.lowercased() == "apple intelligence" {
141
96
  currentModelType = .appleIntelligence
142
97
  isReady = true
143
98
  notifyListeners("readinessChange", data: ["readiness": "ready"])
@@ -145,218 +100,84 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
145
100
  return
146
101
  }
147
102
 
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)")
103
+ let resolvedModelType = resolveModelType(path: path, modelType: modelType)
104
+ switch resolveModelKind(path: path, modelType: modelType) {
105
+ case .appleIntelligence:
106
+ currentModelType = .appleIntelligence
107
+ isReady = true
108
+ notifyListeners("readinessChange", data: ["readiness": "ready"])
109
+ call.resolve()
190
110
 
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
- }
111
+ case .liteRtLm:
112
+ #if canImport(LiteRTLM)
113
+ Task {
114
+ do {
115
+ let modelURL = try resolveModelURL(path: path, modelType: modelType)
116
+ let engine = try await initializeLiteRtEngine(
117
+ modelURL: modelURL,
118
+ maxTokens: maxTokens,
119
+ backend: backend
120
+ )
121
+ liteRtEngine = engine
122
+ liteRtTopk = topk
123
+ liteRtTemperature = temperature
124
+ liteRtRandomSeed = randomSeed
125
+ currentModelType = .liteRtLm
126
+ isReady = true
127
+ notifyListeners("readinessChange", data: ["readiness": "ready"])
128
+ call.resolve()
129
+ } catch {
130
+ let message = "Failed to load model: \(error.localizedDescription)"
131
+ notifyListeners("readinessChange", data: ["readiness": message])
132
+ call.reject(message)
211
133
  }
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
134
  }
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
- }
135
+ #else
136
+ let message = resolvedModelType == "litertlm"
137
+ ? "LiteRT-LM on iOS is available only when this plugin is integrated through Swift Package Manager."
138
+ : "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."
139
+ call.reject(message)
140
+ #endif
283
141
 
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)")
142
+ case .mediaPipe:
143
+ #if canImport(MediaPipeTasksGenAI)
144
+ Task {
145
+ do {
146
+ let modelURL = try resolveModelURL(path: path, modelType: modelType)
147
+ let options = LlmInference.Options(modelPath: modelURL.path)
148
+ options.maxTokens = maxTokens
149
+ options.maxTopk = topk
150
+
151
+ llmInference = try LlmInference(options: options)
152
+ mediaPipeTopk = topk
153
+ mediaPipeTemperature = temperature
154
+ mediaPipeRandomSeed = randomSeed
155
+ currentModelType = .mediaPipe
156
+ isReady = true
157
+ notifyListeners("readinessChange", data: ["readiness": "ready"])
158
+ call.resolve()
159
+ } catch {
160
+ let message = "Failed to load model: \(error.localizedDescription)"
161
+ notifyListeners("readinessChange", data: ["readiness": message])
162
+ call.reject(message)
163
+ }
310
164
  }
165
+ #else
166
+ call.reject("MediaPipe is not available. For iOS custom .litertlm models, integrate the plugin through Swift Package Manager to use LiteRT-LM.")
167
+ #endif
311
168
  }
312
169
  }
313
170
 
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
171
  @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
172
  switch currentModelType {
350
173
  case .appleIntelligence:
351
174
  #if canImport(FoundationModels)
352
175
  if #available(iOS 26.0, *) {
353
176
  let instructions = call.getString("instructions")
354
177
  let session = LanguageModelSession(instructions: instructions)
355
- let id = UUID()
356
- appleChats[id.uuidString] = session
357
- call.resolve([
358
- "id": id.uuidString
359
- ])
178
+ let id = UUID().uuidString
179
+ appleChats[id] = session
180
+ call.resolve(["id": id])
360
181
  } else {
361
182
  call.reject("Apple Intelligence requires iOS 26.0 or later")
362
183
  }
@@ -364,48 +185,52 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
364
185
  call.reject("Apple Intelligence is not available on this device")
365
186
  #endif
366
187
 
188
+ case .liteRtLm:
189
+ #if canImport(LiteRTLM)
190
+ Task {
191
+ do {
192
+ guard let engine = liteRtEngine else {
193
+ call.reject("LiteRT-LM engine not loaded")
194
+ return
195
+ }
196
+
197
+ let conversationConfig = try makeLiteRtConversationConfig(instructions: call.getString("instructions"))
198
+ let conversation = try await engine.createConversation(with: conversationConfig)
199
+ let id = UUID().uuidString
200
+ liteRtChats[id] = conversation
201
+ call.resolve(["id": id])
202
+ } catch {
203
+ call.reject("Failed to create LiteRT-LM chat: \(error.localizedDescription)")
204
+ }
205
+ }
206
+ #else
207
+ call.reject("LiteRT-LM on iOS is available only when this plugin is integrated through Swift Package Manager.")
208
+ #endif
209
+
367
210
  case .mediaPipe:
368
- #if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
369
- guard llmInference != nil else {
370
- print("[CapgoLLM] Error: Model not loaded when creating chat")
211
+ #if COCOAPODS || canImport(MediaPipeTasksGenAI)
212
+ guard let inference = llmInference else {
371
213
  call.reject("Model not loaded")
372
214
  return
373
215
  }
374
216
 
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
- ])
217
+ do {
218
+ let session = try LlmInference.Session(llmInference: inference, options: makeMediaPipeSessionOptions())
219
+ let id = UUID().uuidString
220
+ mediaPipeChats[id] = session
221
+ call.resolve(["id": id])
222
+ } catch {
223
+ call.reject("Failed to create chat: \(error.localizedDescription)")
224
+ }
381
225
  #else
382
- call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
226
+ call.reject("MediaPipe is not available. Please install via CocoaPods.")
383
227
  #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
228
  }
397
229
  }
398
230
 
399
231
  @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
232
  let readiness = getReadinessStatus()
407
233
  call.resolve(["readiness": readiness])
408
- // Also notify listeners
409
234
  notifyListeners("readinessChange", data: ["readiness": readiness])
410
235
  }
411
236
 
@@ -426,27 +251,18 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
426
251
  case .unavailable(let other):
427
252
  return "Error: \(other)"
428
253
  }
429
- } else {
430
- return "Apple Intelligence requires iOS 26.0 or later"
431
254
  }
255
+ return "Apple Intelligence requires iOS 26.0 or later"
432
256
  #else
433
257
  return "Apple Intelligence is not available on this device"
434
258
  #endif
435
259
 
436
- case .mediaPipe:
437
- return isReady ? "ready" : "not_ready"
438
-
439
- case .execuTorch:
260
+ case .liteRtLm, .mediaPipe:
440
261
  return isReady ? "ready" : "not_ready"
441
262
  }
442
263
  }
443
264
 
444
265
  @objc func sendMessage(_ call: CAPPluginCall) {
445
- #if targetEnvironment(macCatalyst)
446
- call.reject(catalystNotSupportedMessage)
447
- return
448
- #endif
449
-
450
266
  let chatId = call.getString("chatId", "")
451
267
  guard let message = call.getString("message") else {
452
268
  call.reject("message not found")
@@ -457,23 +273,7 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
457
273
  case .appleIntelligence:
458
274
  #if canImport(FoundationModels)
459
275
  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
-
276
+ guard validateAppleAvailability(model: model, call: call) else { return }
477
277
  guard let chat = appleChats[chatId] as? LanguageModelSession else {
478
278
  call.reject("chat not found")
479
279
  return
@@ -484,47 +284,7 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
484
284
  return
485
285
  }
486
286
 
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
- }
287
+ streamAppleResponse(chatId: chatId, message: message, chat: chat, call: call)
528
288
  } else {
529
289
  call.reject("Apple Intelligence requires iOS 26.0 or later")
530
290
  }
@@ -532,114 +292,35 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
532
292
  call.reject("Apple Intelligence is not available on this device")
533
293
  #endif
534
294
 
535
- case .mediaPipe:
536
- #if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
537
- guard let inference = llmInference else {
538
- call.reject("Model not loaded")
295
+ case .liteRtLm:
296
+ #if canImport(LiteRTLM)
297
+ guard let conversation = liteRtChats[chatId] else {
298
+ call.reject("chat not found")
539
299
  return
540
300
  }
541
301
 
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
- }
302
+ streamLiteRtResponse(chatId: chatId, message: message, conversation: conversation, call: call)
575
303
  #else
576
- call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
304
+ call.reject("LiteRT-LM on iOS is available only when this plugin is integrated through Swift Package Manager.")
577
305
  #endif
578
306
 
579
- case .execuTorch:
580
- guard isReady, let runner = execuTorchRunner else {
581
- call.reject("ExecuTorch model not loaded")
307
+ case .mediaPipe:
308
+ #if COCOAPODS || canImport(MediaPipeTasksGenAI)
309
+ guard llmInference != nil else {
310
+ call.reject("Model not loaded")
582
311
  return
583
312
  }
584
- guard let chat = execuTorchChats[chatId] else {
313
+
314
+ guard let session = mediaPipeChats[chatId] else {
585
315
  call.reject("chat not found")
586
316
  return
587
317
  }
588
- guard beginExecuTorchGeneration() else {
589
- call.reject("ExecuTorch is already generating. Wait for aiFinished before sending another message.")
590
- return
591
- }
592
-
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
318
 
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
319
+ streamMediaPipeResponse(chatId: chatId, message: message, session: session, call: call)
320
+ #else
321
+ call.reject("MediaPipe is not available. Please install via CocoaPods.")
322
+ #endif
633
323
  }
634
-
635
- execuTorchIsGenerating = true
636
- return true
637
- }
638
-
639
- private func finishExecuTorchGeneration() {
640
- execuTorchGenerationLock.lock()
641
- execuTorchIsGenerating = false
642
- execuTorchGenerationLock.unlock()
643
324
  }
644
325
 
645
326
  @objc func downloadModel(_ call: CAPPluginCall) {
@@ -655,19 +336,14 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
655
336
 
656
337
  let filename = call.getString("filename") ?? url.lastPathComponent
657
338
 
658
- // Get documents directory
659
339
  guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
660
340
  call.reject("Could not access documents directory")
661
341
  return
662
342
  }
663
343
 
664
344
  let destinationURL = documentsPath.appendingPathComponent(filename)
665
-
666
- // Create download task
667
345
  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
-
346
+ let downloadTask = session.downloadTask(with: url) { tempURL, _, error in
671
347
  if let error = error {
672
348
  call.reject("Download failed: \(error.localizedDescription)")
673
349
  return
@@ -679,38 +355,31 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
679
355
  }
680
356
 
681
357
  do {
682
- // Remove existing file if it exists
683
358
  if FileManager.default.fileExists(atPath: destinationURL.path) {
684
359
  try FileManager.default.removeItem(at: destinationURL)
685
360
  }
686
361
 
687
- // Move downloaded file to documents directory
688
362
  try FileManager.default.moveItem(at: tempURL, to: destinationURL)
689
363
 
690
364
  var result = [
691
365
  "path": destinationURL.path
692
366
  ]
693
367
 
694
- // Handle companion URL if provided (for compatibility)
695
368
  if let companionUrlString = call.getString("companionUrl"),
696
369
  let companionUrl = URL(string: companionUrlString) {
697
-
698
370
  let companionFilename = companionUrl.lastPathComponent
699
371
  let companionDestination = documentsPath.appendingPathComponent(companionFilename)
700
372
 
701
- // Download companion file synchronously for simplicity
702
373
  do {
703
374
  let companionData = try Data(contentsOf: companionUrl)
704
375
  try companionData.write(to: companionDestination)
705
376
  result["companionPath"] = companionDestination.path
706
377
  } catch {
707
- // Log error but don't fail the whole download
708
378
  print("Failed to download companion file: \(error)")
709
379
  }
710
380
  }
711
381
 
712
382
  call.resolve(result)
713
-
714
383
  } catch {
715
384
  call.reject("Failed to save file: \(error.localizedDescription)")
716
385
  }
@@ -722,168 +391,286 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
722
391
  @objc func getPluginVersion(_ call: CAPPluginCall) {
723
392
  call.resolve(["version": self.pluginVersion])
724
393
  }
725
- }
726
394
 
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
395
+ private func clearChatSessions() {
396
+ #if canImport(FoundationModels)
397
+ appleChats.removeAll()
398
+ #endif
399
+ #if canImport(LiteRTLM)
400
+ liteRtChats.removeAll()
401
+ #endif
402
+ #if COCOAPODS || canImport(MediaPipeTasksGenAI)
403
+ mediaPipeChats.removeAll()
404
+ #endif
405
+ }
406
+
407
+ private func resolveModelKind(path: String, modelType: String?) -> ModelType {
408
+ let resolvedModelType = resolveModelType(path: path, modelType: modelType)
409
+ switch resolvedModelType {
410
+ case "litertlm":
411
+ return .liteRtLm
412
+ case "task":
413
+ return .mediaPipe
414
+ default:
415
+ #if canImport(LiteRTLM)
416
+ return .liteRtLm
417
+ #else
418
+ return .mediaPipe
419
+ #endif
768
420
  }
421
+ }
769
422
 
770
- return hasInstanceMethod(runnerClass, runnerInitSelector) &&
771
- hasInstanceMethod(runnerClass, runnerLoadSelector) &&
772
- hasInstanceMethod(runnerClass, runnerResetSelector) &&
773
- hasInstanceMethod(runnerClass, runnerGenerateSelector) &&
774
- hasInstanceMethod(configClass, configInitSelector)
423
+ private func resolveModelType(path: String, modelType: String?) -> String {
424
+ if let modelType, !modelType.isEmpty {
425
+ return stripURLSuffix(modelType).lowercased()
426
+ }
427
+
428
+ let normalizedPath = stripURLSuffix(path)
429
+ let pathExtension = (normalizedPath as NSString).pathExtension
430
+ return pathExtension.lowercased()
775
431
  }
776
432
 
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.")
433
+ private func stripURLSuffix(_ value: String) -> String {
434
+ let queryIndex = value.firstIndex(of: "?")
435
+ let fragmentIndex = value.firstIndex(of: "#")
436
+ let cutoff = [queryIndex, fragmentIndex].compactMap { $0 }.min() ?? value.endIndex
437
+ return String(value[..<cutoff])
438
+ }
439
+
440
+ private func resolveModelURL(path: String, modelType: String?) throws -> URL {
441
+ if path.hasPrefix("/") {
442
+ return URL(fileURLWithPath: path)
790
443
  }
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")
444
+
445
+ let fileName: String
446
+ let fileExtension: String
447
+
448
+ if let modelType, !modelType.isEmpty {
449
+ fileName = (path as NSString).deletingPathExtension
450
+ fileExtension = stripURLSuffix(modelType)
451
+ } else {
452
+ let normalizedPath = stripURLSuffix(path)
453
+ fileName = (normalizedPath as NSString).deletingPathExtension
454
+ fileExtension = (normalizedPath as NSString).pathExtension
812
455
  }
456
+
457
+ let fallbackExtension = fileExtension.isEmpty ? "bin" : fileExtension
458
+ guard let bundlePath = Bundle.main.path(forResource: fileName, ofType: fallbackExtension) else {
459
+ throw NSError(
460
+ domain: "CapgoLLM",
461
+ code: 404,
462
+ userInfo: [NSLocalizedDescriptionKey: "Model file not found in bundle: \(path)"]
463
+ )
464
+ }
465
+
466
+ return URL(fileURLWithPath: bundlePath)
813
467
  }
468
+ }
814
469
 
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.")
470
+ #if canImport(FoundationModels)
471
+ private extension LLMPlugin {
472
+ @available(iOS 26.0, *)
473
+ func validateAppleAvailability(model: SystemLanguageModel, call: CAPPluginCall) -> Bool {
474
+ switch model.availability {
475
+ case .available:
476
+ return true
477
+ case .unavailable(.deviceNotEligible):
478
+ call.reject("error deviceNotEligible error")
479
+ case .unavailable(.appleIntelligenceNotEnabled):
480
+ call.reject("error appleIntelligenceNotEnabled error")
481
+ case .unavailable(.modelNotReady):
482
+ call.reject("error modelNotReady error")
483
+ case .unavailable(let other):
484
+ call.reject("error \(other) error")
818
485
  }
819
- guard let runnerClass = object_getClass(runner) else {
820
- throw Self.error("ExecuTorch runner class was not found")
486
+
487
+ return false
488
+ }
489
+
490
+ @available(iOS 26.0, *)
491
+ func streamAppleResponse(chatId: String, message: String, chat: LanguageModelSession, call: CAPPluginCall) {
492
+ Task {
493
+ do {
494
+ let stream = chat.streamResponse(to: message)
495
+ for try await chunk in stream {
496
+ let textChunk = extractAppleTextChunk(from: chunk)
497
+ notifyListeners("textFromAi", data: [
498
+ "chatId": chatId,
499
+ "text": textChunk,
500
+ "isChunk": true
501
+ ])
502
+ }
503
+ notifyListeners("aiFinished", data: ["chatId": chatId])
504
+ call.resolve()
505
+ } catch {
506
+ notifyListeners("generationError", data: [
507
+ "chatId": chatId,
508
+ "error": error.localizedDescription
509
+ ])
510
+ call.reject("Failed to get response: \(error.localizedDescription)")
511
+ }
821
512
  }
822
- try Self.requireInstanceMethod(Self.runnerResetSelector, on: runnerClass)
823
- try Self.requireInstanceMethod(Self.runnerGenerateSelector, on: runnerClass)
824
- try Self.requireInstanceMethod(Self.configInitSelector, on: configClass)
513
+ }
825
514
 
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()
515
+ @available(iOS 26.0, *)
516
+ func extractAppleTextChunk(from chunk: Any) -> String {
517
+ let mirror = Mirror(reflecting: chunk)
830
518
 
831
- sendVoid(runner, Self.runnerResetSelector)
519
+ if let contentProperty = mirror.children.first(where: { $0.label == "content" }),
520
+ let content = contentProperty.value as? String {
521
+ return content
522
+ }
832
523
 
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")
524
+ if let rawContentProperty = mirror.children.first(where: { $0.label == "rawContent" }),
525
+ let rawContent = rawContentProperty.value as? String {
526
+ return rawContent
837
527
  }
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)
528
+
529
+ return "\(chunk)"
530
+ }
531
+ }
532
+ #endif
533
+
534
+ #if canImport(LiteRTLM)
535
+ private extension LLMPlugin {
536
+ func initializeLiteRtEngine(
537
+ modelURL: URL,
538
+ maxTokens: Int,
539
+ backend: String?
540
+ ) async throws -> LiteRTLM.Engine {
541
+ guard let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
542
+ throw NSError(
543
+ domain: "CapgoLLM",
544
+ code: 500,
545
+ userInfo: [NSLocalizedDescriptionKey: "Could not access the iOS caches directory for LiteRT-LM."]
546
+ )
846
547
  }
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")
548
+
549
+ let cacheDir = cacheDirectory.path
550
+ let makeConfig = { (selectedBackend: LiteRTLM.Backend) throws -> LiteRTLM.EngineConfig in
551
+ try LiteRTLM.EngineConfig(
552
+ modelPath: modelURL.path,
553
+ backend: selectedBackend,
554
+ maxNumTokens: maxTokens,
555
+ cacheDir: cacheDir
556
+ )
857
557
  }
858
- }
859
558
 
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")
559
+ let configs: [LiteRTLM.EngineConfig]
560
+ switch backend?.lowercased() {
561
+ case "cpu":
562
+ configs = [try makeConfig(.cpu())]
563
+ case "gpu":
564
+ configs = [try makeConfig(.gpu), try makeConfig(.cpu())]
565
+ default:
566
+ configs = [try makeConfig(.cpu()), try makeConfig(.gpu)]
567
+ }
568
+
569
+ var lastError: Error?
570
+ for config in configs {
571
+ let engine = LiteRTLM.Engine(engineConfig: config)
572
+ do {
573
+ try await engine.initialize()
574
+ return engine
575
+ } catch {
576
+ lastError = error
577
+ }
863
578
  }
864
- return unsafeBitCast(symbol, to: Function.self)
579
+
580
+ throw lastError ?? NSError(
581
+ domain: "CapgoLLM",
582
+ code: 500,
583
+ userInfo: [NSLocalizedDescriptionKey: "Failed to initialize LiteRT-LM on iOS."]
584
+ )
865
585
  }
866
586
 
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))")
587
+ func makeLiteRtConversationConfig(instructions: String?) throws -> LiteRTLM.ConversationConfig {
588
+ let sampler = try LiteRTLM.SamplerConfig(
589
+ topK: liteRtTopk,
590
+ topP: liteRtTopP,
591
+ temperature: liteRtTemperature,
592
+ seed: liteRtRandomSeed
593
+ )
594
+
595
+ let systemMessage: LiteRTLM.Message?
596
+ if let instructions, !instructions.isEmpty {
597
+ systemMessage = LiteRTLM.Message(instructions, role: .system)
598
+ } else {
599
+ systemMessage = nil
870
600
  }
601
+
602
+ return LiteRTLM.ConversationConfig(
603
+ systemMessage: systemMessage,
604
+ samplerConfig: sampler
605
+ )
871
606
  }
872
607
 
873
- private static func hasInstanceMethod(_ cls: AnyClass, _ selector: Selector) -> Bool {
874
- class_getInstanceMethod(cls, selector) != nil
608
+ func streamLiteRtResponse(chatId: String, message: String, conversation: LiteRTLM.Conversation, call: CAPPluginCall) {
609
+ Task {
610
+ do {
611
+ for try await chunk in conversation.sendMessageStream(LiteRTLM.Message(message)) {
612
+ let textChunk = chunk.toString
613
+ if !textChunk.isEmpty {
614
+ notifyListeners("textFromAi", data: [
615
+ "chatId": chatId,
616
+ "text": textChunk,
617
+ "isChunk": true
618
+ ])
619
+ }
620
+ }
621
+
622
+ notifyListeners("aiFinished", data: ["chatId": chatId])
623
+ call.resolve()
624
+ } catch {
625
+ notifyListeners("generationError", data: [
626
+ "chatId": chatId,
627
+ "error": error.localizedDescription
628
+ ])
629
+ call.reject("Failed to generate response: \(error.localizedDescription)")
630
+ }
631
+ }
875
632
  }
633
+ }
634
+ #endif
876
635
 
877
- private static func findClass(_ name: String) -> AnyClass? {
878
- NSClassFromString(name) ?? NSClassFromString("ExecuTorchLLM.\(name)")
636
+ #if COCOAPODS || canImport(MediaPipeTasksGenAI)
637
+ private extension LLMPlugin {
638
+ func makeMediaPipeSessionOptions() -> LlmInference.Session.Options {
639
+ let options = LlmInference.Session.Options()
640
+ options.topk = mediaPipeTopk
641
+ options.temperature = mediaPipeTemperature
642
+ options.randomSeed = mediaPipeRandomSeed
643
+ return options
879
644
  }
880
645
 
881
- private static func error(_ description: String) -> NSError {
882
- NSError(domain: "CapgoLLM.ExecuTorch", code: 1, userInfo: [NSLocalizedDescriptionKey: description])
646
+ func streamMediaPipeResponse(chatId: String, message: String, session: LlmInference.Session, call: CAPPluginCall) {
647
+ Task {
648
+ do {
649
+ try session.addQueryChunk(inputText: message)
650
+ let resultStream = session.generateResponseAsync()
651
+
652
+ for try await partialResult in resultStream {
653
+ notifyListeners("textFromAi", data: [
654
+ "chatId": chatId,
655
+ "text": partialResult,
656
+ "isChunk": true
657
+ ])
658
+ }
659
+
660
+ notifyListeners("aiFinished", data: ["chatId": chatId])
661
+ call.resolve()
662
+ } catch {
663
+ notifyListeners("generationError", data: [
664
+ "chatId": chatId,
665
+ "error": error.localizedDescription
666
+ ])
667
+ call.reject("Failed to generate response: \(error.localizedDescription)")
668
+ }
669
+ }
883
670
  }
884
671
  }
672
+ #endif
885
673
 
886
- // Download delegate class to handle progress updates
887
674
  class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
888
675
  weak var plugin: CAPPlugin?
889
676
 
@@ -894,7 +681,6 @@ class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
894
681
  func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
895
682
  didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
896
683
  totalBytesExpectedToWrite: Int64) {
897
-
898
684
  if totalBytesExpectedToWrite > 0 {
899
685
  let progress = Int((Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100)
900
686
 
@@ -908,7 +694,6 @@ class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
908
694
 
909
695
  func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
910
696
  didFinishDownloadingTo location: URL) {
911
- // Handled in the completion handler
697
+ // Handled in the completion handler.
912
698
  }
913
-
914
699
  }