@capgo/capacitor-llm 8.0.11 → 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/README.md +86 -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 +401 -2
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
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
|
|
@@ -16,7 +18,7 @@ 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"
|
|
20
22
|
#if targetEnvironment(macCatalyst)
|
|
21
23
|
private let catalystNotSupportedMessage = "CapgoLLM is not available on Mac Catalyst. Use an iOS/iPadOS target instead."
|
|
22
24
|
#endif
|
|
@@ -34,6 +36,23 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
34
36
|
private enum ModelType {
|
|
35
37
|
case appleIntelligence
|
|
36
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
|
+
}
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
private var currentModelType: ModelType = .appleIntelligence
|
|
@@ -56,6 +75,11 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
56
75
|
private var llmInference: LlmInference?
|
|
57
76
|
#endif
|
|
58
77
|
|
|
78
|
+
private var execuTorchRunner: DynamicExecuTorchRunner?
|
|
79
|
+
private var execuTorchChats: [String: ExecuTorchChatSession] = [:]
|
|
80
|
+
private let execuTorchGenerationLock = NSLock()
|
|
81
|
+
private var execuTorchIsGenerating = false
|
|
82
|
+
|
|
59
83
|
@objc func setModel(_ call: CAPPluginCall) {
|
|
60
84
|
guard let path = call.getString("path") else {
|
|
61
85
|
call.reject("Path is required")
|
|
@@ -74,13 +98,33 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
74
98
|
let topk = call.getInt("topk") ?? 40
|
|
75
99
|
let temperature = call.getFloat("temperature") ?? 0.8
|
|
76
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
|
+
}
|
|
77
115
|
|
|
78
116
|
modelPath = path
|
|
117
|
+
execuTorchChats.removeAll()
|
|
118
|
+
#if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
|
|
119
|
+
llmInference = nil
|
|
120
|
+
#endif
|
|
121
|
+
execuTorchRunner = nil
|
|
79
122
|
isReady = false
|
|
80
123
|
|
|
81
124
|
// Check if user wants to use Apple Intelligence
|
|
82
|
-
if path.lowercased() == "apple intelligence" {
|
|
125
|
+
if engine == "apple" || path.lowercased() == "apple intelligence" {
|
|
83
126
|
print("[CapgoLLM] Switching to Apple Intelligence")
|
|
127
|
+
execuTorchRunner = nil
|
|
84
128
|
currentModelType = .appleIntelligence
|
|
85
129
|
isReady = true
|
|
86
130
|
notifyListeners("readinessChange", data: ["readiness": "ready"])
|
|
@@ -88,7 +132,22 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
88
132
|
return
|
|
89
133
|
}
|
|
90
134
|
|
|
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
|
+
|
|
91
149
|
#if !targetEnvironment(macCatalyst) && canImport(MediaPipeTasksGenAI)
|
|
150
|
+
currentModelType = .mediaPipe
|
|
92
151
|
Task {
|
|
93
152
|
do {
|
|
94
153
|
let modelURL: URL
|
|
@@ -153,6 +212,8 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
153
212
|
notifyListeners("readinessChange", data: ["readiness": "ready"])
|
|
154
213
|
call.resolve()
|
|
155
214
|
} catch {
|
|
215
|
+
llmInference = nil
|
|
216
|
+
isReady = false
|
|
156
217
|
print("[CapgoLLM] Error loading model: \(error.localizedDescription)")
|
|
157
218
|
notifyListeners("readinessChange", data: ["readiness": "Failed to load model: \(error.localizedDescription)"])
|
|
158
219
|
call.reject("Failed to load model: \(error.localizedDescription)")
|
|
@@ -163,6 +224,106 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
163
224
|
#endif
|
|
164
225
|
}
|
|
165
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
|
+
|
|
166
327
|
@objc func createChat(_ call: CAPPluginCall) {
|
|
167
328
|
#if targetEnvironment(macCatalyst)
|
|
168
329
|
call.reject(catalystNotSupportedMessage)
|
|
@@ -205,6 +366,18 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
205
366
|
#else
|
|
206
367
|
call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
|
|
207
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
|
+
])
|
|
208
381
|
}
|
|
209
382
|
}
|
|
210
383
|
|
|
@@ -247,6 +420,9 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
247
420
|
|
|
248
421
|
case .mediaPipe:
|
|
249
422
|
return isReady ? "ready" : "not_ready"
|
|
423
|
+
|
|
424
|
+
case .execuTorch:
|
|
425
|
+
return isReady ? "ready" : "not_ready"
|
|
250
426
|
}
|
|
251
427
|
}
|
|
252
428
|
|
|
@@ -384,7 +560,71 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
384
560
|
#else
|
|
385
561
|
call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
|
|
386
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
|
|
387
618
|
}
|
|
619
|
+
|
|
620
|
+
execuTorchIsGenerating = true
|
|
621
|
+
return true
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private func finishExecuTorchGeneration() {
|
|
625
|
+
execuTorchGenerationLock.lock()
|
|
626
|
+
execuTorchIsGenerating = false
|
|
627
|
+
execuTorchGenerationLock.unlock()
|
|
388
628
|
}
|
|
389
629
|
|
|
390
630
|
@objc func downloadModel(_ call: CAPPluginCall) {
|
|
@@ -469,6 +709,165 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
469
709
|
}
|
|
470
710
|
}
|
|
471
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
|
+
|
|
472
871
|
// Download delegate class to handle progress updates
|
|
473
872
|
class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
474
873
|
weak var plugin: CAPPlugin?
|