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