@capgo/capacitor-llm 8.0.11 → 8.0.13
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 +95 -254
- 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 +406 -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.13"
|
|
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,14 @@ 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 let execuTorchNotLinkedMessage = "ExecuTorch is not linked in this iOS app. " +
|
|
82
|
+
"It is not installed by CocoaPods; add the ExecuTorch Swift Package products " +
|
|
83
|
+
"to the app target on iOS 17+ or choose another engine."
|
|
84
|
+
private var execuTorchIsGenerating = false
|
|
85
|
+
|
|
59
86
|
@objc func setModel(_ call: CAPPluginCall) {
|
|
60
87
|
guard let path = call.getString("path") else {
|
|
61
88
|
call.reject("Path is required")
|
|
@@ -74,13 +101,33 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
74
101
|
let topk = call.getInt("topk") ?? 40
|
|
75
102
|
let temperature = call.getFloat("temperature") ?? 0.8
|
|
76
103
|
let modelType = call.getString("modelType") // Optional model type parameter
|
|
104
|
+
let engine = call.getString("engine")?.lowercased() ?? "auto"
|
|
105
|
+
let tokenizerPath = call.getString("tokenizerPath")
|
|
106
|
+
let specialTokens = call.getArray("specialTokens", String.self) ?? []
|
|
107
|
+
let sequenceLength = call.getInt("sequenceLength") ?? maxTokens
|
|
108
|
+
|
|
109
|
+
guard sequenceLength > 0 else {
|
|
110
|
+
call.reject("sequenceLength must be > 0")
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
guard !isExecuTorchGenerating() else {
|
|
115
|
+
call.reject("Cannot switch model while ExecuTorch is generating. Wait for aiFinished.")
|
|
116
|
+
return
|
|
117
|
+
}
|
|
77
118
|
|
|
78
119
|
modelPath = path
|
|
120
|
+
execuTorchChats.removeAll()
|
|
121
|
+
#if !targetEnvironment(macCatalyst) && (COCOAPODS || canImport(MediaPipeTasksGenAI))
|
|
122
|
+
llmInference = nil
|
|
123
|
+
#endif
|
|
124
|
+
execuTorchRunner = nil
|
|
79
125
|
isReady = false
|
|
80
126
|
|
|
81
127
|
// Check if user wants to use Apple Intelligence
|
|
82
|
-
if path.lowercased() == "apple intelligence" {
|
|
128
|
+
if engine == "apple" || path.lowercased() == "apple intelligence" {
|
|
83
129
|
print("[CapgoLLM] Switching to Apple Intelligence")
|
|
130
|
+
execuTorchRunner = nil
|
|
84
131
|
currentModelType = .appleIntelligence
|
|
85
132
|
isReady = true
|
|
86
133
|
notifyListeners("readinessChange", data: ["readiness": "ready"])
|
|
@@ -88,7 +135,22 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
88
135
|
return
|
|
89
136
|
}
|
|
90
137
|
|
|
138
|
+
if shouldUseExecuTorch(engine: engine, path: path, tokenizerPath: tokenizerPath) {
|
|
139
|
+
currentModelType = .execuTorch
|
|
140
|
+
setExecuTorchModel(
|
|
141
|
+
call,
|
|
142
|
+
path: path,
|
|
143
|
+
tokenizerPath: tokenizerPath,
|
|
144
|
+
modelType: modelType,
|
|
145
|
+
specialTokens: specialTokens,
|
|
146
|
+
sequenceLength: sequenceLength,
|
|
147
|
+
temperature: temperature
|
|
148
|
+
)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
91
152
|
#if !targetEnvironment(macCatalyst) && canImport(MediaPipeTasksGenAI)
|
|
153
|
+
currentModelType = .mediaPipe
|
|
92
154
|
Task {
|
|
93
155
|
do {
|
|
94
156
|
let modelURL: URL
|
|
@@ -153,6 +215,8 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
153
215
|
notifyListeners("readinessChange", data: ["readiness": "ready"])
|
|
154
216
|
call.resolve()
|
|
155
217
|
} catch {
|
|
218
|
+
llmInference = nil
|
|
219
|
+
isReady = false
|
|
156
220
|
print("[CapgoLLM] Error loading model: \(error.localizedDescription)")
|
|
157
221
|
notifyListeners("readinessChange", data: ["readiness": "Failed to load model: \(error.localizedDescription)"])
|
|
158
222
|
call.reject("Failed to load model: \(error.localizedDescription)")
|
|
@@ -163,6 +227,108 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
163
227
|
#endif
|
|
164
228
|
}
|
|
165
229
|
|
|
230
|
+
private func shouldUseExecuTorch(engine: String, path: String, tokenizerPath: String?) -> Bool {
|
|
231
|
+
if path.lowercased() == "apple intelligence" {
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
if engine == "executorch" {
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
if engine == "apple" || engine == "mediapipe" {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
if tokenizerPath?.isEmpty == false {
|
|
241
|
+
return true
|
|
242
|
+
}
|
|
243
|
+
return path.lowercased().hasSuffix(".pte")
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private func setExecuTorchModel(
|
|
247
|
+
_ call: CAPPluginCall,
|
|
248
|
+
path: String,
|
|
249
|
+
tokenizerPath: String?,
|
|
250
|
+
modelType: String?,
|
|
251
|
+
specialTokens: [String],
|
|
252
|
+
sequenceLength: Int,
|
|
253
|
+
temperature: Float
|
|
254
|
+
) {
|
|
255
|
+
execuTorchRunner = nil
|
|
256
|
+
|
|
257
|
+
guard sequenceLength > 0 else {
|
|
258
|
+
call.reject("sequenceLength must be > 0")
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
guard DynamicExecuTorchRunner.isAvailable else {
|
|
263
|
+
isReady = false
|
|
264
|
+
notifyListeners("readinessChange", data: ["readiness": execuTorchNotLinkedMessage])
|
|
265
|
+
call.reject(execuTorchNotLinkedMessage)
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
guard let tokenizerPath = tokenizerPath, !tokenizerPath.isEmpty else {
|
|
270
|
+
call.reject("tokenizerPath is required for ExecuTorch models")
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
Task {
|
|
275
|
+
do {
|
|
276
|
+
let modelURL = try resolveModelURL(path: path, modelType: modelType, defaultExtension: "pte")
|
|
277
|
+
let tokenizerURL = try resolveModelURL(path: tokenizerPath, modelType: nil, defaultExtension: "model")
|
|
278
|
+
|
|
279
|
+
print("[CapgoLLM] Initializing ExecuTorch with model at: \(modelURL.path)")
|
|
280
|
+
print("[CapgoLLM] Initializing ExecuTorch with tokenizer at: \(tokenizerURL.path)")
|
|
281
|
+
|
|
282
|
+
execuTorchRunner = try DynamicExecuTorchRunner(
|
|
283
|
+
modelPath: modelURL.path,
|
|
284
|
+
tokenizerPath: tokenizerURL.path,
|
|
285
|
+
specialTokens: specialTokens,
|
|
286
|
+
sequenceLength: sequenceLength,
|
|
287
|
+
temperature: Double(temperature)
|
|
288
|
+
)
|
|
289
|
+
currentModelType = .execuTorch
|
|
290
|
+
isReady = true
|
|
291
|
+
|
|
292
|
+
notifyListeners("readinessChange", data: ["readiness": "ready"])
|
|
293
|
+
call.resolve()
|
|
294
|
+
} catch {
|
|
295
|
+
execuTorchRunner = nil
|
|
296
|
+
isReady = false
|
|
297
|
+
print("[CapgoLLM] Error loading ExecuTorch model: \(error.localizedDescription)")
|
|
298
|
+
notifyListeners("readinessChange", data: ["readiness": "Failed to load ExecuTorch model: \(error.localizedDescription)"])
|
|
299
|
+
call.reject("Failed to load ExecuTorch model: \(error.localizedDescription)")
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private func resolveModelURL(path: String, modelType: String?, defaultExtension: String) throws -> URL {
|
|
305
|
+
if path.hasPrefix("/") {
|
|
306
|
+
return URL(fileURLWithPath: path)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let fileName: String
|
|
310
|
+
let fileExtension: String
|
|
311
|
+
|
|
312
|
+
if let providedType = modelType, !providedType.isEmpty {
|
|
313
|
+
fileName = (path as NSString).deletingPathExtension
|
|
314
|
+
fileExtension = providedType
|
|
315
|
+
} else {
|
|
316
|
+
fileName = (path as NSString).deletingPathExtension
|
|
317
|
+
let extractedExtension = (path as NSString).pathExtension
|
|
318
|
+
fileExtension = extractedExtension.isEmpty ? defaultExtension : extractedExtension
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
guard let bundlePath = Bundle.main.path(forResource: fileName, ofType: fileExtension) else {
|
|
322
|
+
throw NSError(
|
|
323
|
+
domain: "CapgoLLM",
|
|
324
|
+
code: 404,
|
|
325
|
+
userInfo: [NSLocalizedDescriptionKey: "Model file not found in bundle: \(path)"]
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return URL(fileURLWithPath: bundlePath)
|
|
330
|
+
}
|
|
331
|
+
|
|
166
332
|
@objc func createChat(_ call: CAPPluginCall) {
|
|
167
333
|
#if targetEnvironment(macCatalyst)
|
|
168
334
|
call.reject(catalystNotSupportedMessage)
|
|
@@ -205,6 +371,18 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
205
371
|
#else
|
|
206
372
|
call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
|
|
207
373
|
#endif
|
|
374
|
+
|
|
375
|
+
case .execuTorch:
|
|
376
|
+
guard execuTorchRunner != nil else {
|
|
377
|
+
call.reject("ExecuTorch model not loaded")
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
let id = UUID().uuidString
|
|
381
|
+
execuTorchChats[id] = ExecuTorchChatSession()
|
|
382
|
+
print("[CapgoLLM] Created ExecuTorch chat with ID: \(id)")
|
|
383
|
+
call.resolve([
|
|
384
|
+
"id": id
|
|
385
|
+
])
|
|
208
386
|
}
|
|
209
387
|
}
|
|
210
388
|
|
|
@@ -247,6 +425,9 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
247
425
|
|
|
248
426
|
case .mediaPipe:
|
|
249
427
|
return isReady ? "ready" : "not_ready"
|
|
428
|
+
|
|
429
|
+
case .execuTorch:
|
|
430
|
+
return isReady ? "ready" : "not_ready"
|
|
250
431
|
}
|
|
251
432
|
}
|
|
252
433
|
|
|
@@ -384,7 +565,71 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
384
565
|
#else
|
|
385
566
|
call.reject("MediaPipe is not available on this platform. Please install via CocoaPods on iOS.")
|
|
386
567
|
#endif
|
|
568
|
+
|
|
569
|
+
case .execuTorch:
|
|
570
|
+
guard isReady, let runner = execuTorchRunner else {
|
|
571
|
+
call.reject("ExecuTorch model not loaded")
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
guard let chat = execuTorchChats[chatId] else {
|
|
575
|
+
call.reject("chat not found")
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
guard beginExecuTorchGeneration() else {
|
|
579
|
+
call.reject("ExecuTorch is already generating. Wait for aiFinished before sending another message.")
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
Task {
|
|
584
|
+
defer {
|
|
585
|
+
self.finishExecuTorchGeneration()
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
do {
|
|
589
|
+
let prompt = chat.buildPrompt(userMessage: message)
|
|
590
|
+
var fullResponse = ""
|
|
591
|
+
print("[CapgoLLM] Attempting to generate ExecuTorch response for message: \(message)")
|
|
592
|
+
try runner.generate(prompt) { token in
|
|
593
|
+
self.notifyListeners("textFromAi", data: [
|
|
594
|
+
"chatId": chatId,
|
|
595
|
+
"text": token,
|
|
596
|
+
"isChunk": true
|
|
597
|
+
])
|
|
598
|
+
fullResponse += token
|
|
599
|
+
}
|
|
600
|
+
chat.addTurn(userMessage: message, assistantMessage: fullResponse)
|
|
601
|
+
self.notifyListeners("aiFinished", data: ["chatId": chatId])
|
|
602
|
+
call.resolve()
|
|
603
|
+
} catch {
|
|
604
|
+
print("[CapgoLLM] ExecuTorch error details: \(error)")
|
|
605
|
+
call.reject("Failed to generate ExecuTorch response: \(error.localizedDescription)")
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private func isExecuTorchGenerating() -> Bool {
|
|
612
|
+
execuTorchGenerationLock.lock()
|
|
613
|
+
defer { execuTorchGenerationLock.unlock() }
|
|
614
|
+
return execuTorchIsGenerating
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private func beginExecuTorchGeneration() -> Bool {
|
|
618
|
+
execuTorchGenerationLock.lock()
|
|
619
|
+
defer { execuTorchGenerationLock.unlock() }
|
|
620
|
+
|
|
621
|
+
if execuTorchIsGenerating {
|
|
622
|
+
return false
|
|
387
623
|
}
|
|
624
|
+
|
|
625
|
+
execuTorchIsGenerating = true
|
|
626
|
+
return true
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private func finishExecuTorchGeneration() {
|
|
630
|
+
execuTorchGenerationLock.lock()
|
|
631
|
+
execuTorchIsGenerating = false
|
|
632
|
+
execuTorchGenerationLock.unlock()
|
|
388
633
|
}
|
|
389
634
|
|
|
390
635
|
@objc func downloadModel(_ call: CAPPluginCall) {
|
|
@@ -469,6 +714,165 @@ public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
469
714
|
}
|
|
470
715
|
}
|
|
471
716
|
|
|
717
|
+
private typealias ExecuTorchAllocFunction = @convention(c) (AnyClass, Selector) -> Unmanaged<AnyObject>
|
|
718
|
+
private typealias ExecuTorchInitRunnerFunction = @convention(c) (
|
|
719
|
+
AnyObject,
|
|
720
|
+
Selector,
|
|
721
|
+
NSString,
|
|
722
|
+
NSString,
|
|
723
|
+
NSArray
|
|
724
|
+
) -> Unmanaged<AnyObject>
|
|
725
|
+
private typealias ExecuTorchLoadFunction = @convention(c) (
|
|
726
|
+
AnyObject,
|
|
727
|
+
Selector,
|
|
728
|
+
UnsafeMutablePointer<NSError?>
|
|
729
|
+
) -> Bool
|
|
730
|
+
private typealias ExecuTorchConfigBlock = @convention(block) (NSObject) -> Void
|
|
731
|
+
private typealias ExecuTorchInitConfigFunction = @convention(c) (
|
|
732
|
+
AnyObject,
|
|
733
|
+
Selector,
|
|
734
|
+
ExecuTorchConfigBlock
|
|
735
|
+
) -> Unmanaged<AnyObject>
|
|
736
|
+
private typealias ExecuTorchTokenCallback = @convention(block) (NSString) -> Void
|
|
737
|
+
private typealias ExecuTorchGenerateFunction = @convention(c) (
|
|
738
|
+
AnyObject,
|
|
739
|
+
Selector,
|
|
740
|
+
NSString,
|
|
741
|
+
AnyObject,
|
|
742
|
+
ExecuTorchTokenCallback,
|
|
743
|
+
UnsafeMutablePointer<NSError?>
|
|
744
|
+
) -> Bool
|
|
745
|
+
private typealias ExecuTorchVoidFunction = @convention(c) (AnyObject, Selector) -> Void
|
|
746
|
+
|
|
747
|
+
private final class DynamicExecuTorchRunner {
|
|
748
|
+
private static let runnerInitSelector = NSSelectorFromString("initWithModelPath:tokenizerPath:specialTokens:")
|
|
749
|
+
private static let runnerLoadSelector = NSSelectorFromString("loadWithError:")
|
|
750
|
+
private static let runnerResetSelector = NSSelectorFromString("reset")
|
|
751
|
+
private static let runnerGenerateSelector = NSSelectorFromString("generateWithPrompt:config:tokenCallback:error:")
|
|
752
|
+
private static let configInitSelector = NSSelectorFromString("initWithBlock:")
|
|
753
|
+
|
|
754
|
+
static var isAvailable: Bool {
|
|
755
|
+
guard let runnerClass = findClass("ExecuTorchLLMTextRunner"),
|
|
756
|
+
let configClass = findClass("ExecuTorchLLMConfig") else {
|
|
757
|
+
return false
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return hasInstanceMethod(runnerClass, runnerInitSelector) &&
|
|
761
|
+
hasInstanceMethod(runnerClass, runnerLoadSelector) &&
|
|
762
|
+
hasInstanceMethod(runnerClass, runnerResetSelector) &&
|
|
763
|
+
hasInstanceMethod(runnerClass, runnerGenerateSelector) &&
|
|
764
|
+
hasInstanceMethod(configClass, configInitSelector)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private let runner: AnyObject
|
|
768
|
+
private let sequenceLength: Int
|
|
769
|
+
private let temperature: Double
|
|
770
|
+
|
|
771
|
+
init(
|
|
772
|
+
modelPath: String,
|
|
773
|
+
tokenizerPath: String,
|
|
774
|
+
specialTokens: [String],
|
|
775
|
+
sequenceLength: Int,
|
|
776
|
+
temperature: Double
|
|
777
|
+
) throws {
|
|
778
|
+
guard let runnerClass = Self.findClass("ExecuTorchLLMTextRunner") else {
|
|
779
|
+
throw Self.error("ExecuTorchLLMTextRunner was not found. Link ExecuTorch in the app target.")
|
|
780
|
+
}
|
|
781
|
+
try Self.requireInstanceMethod(Self.runnerInitSelector, on: runnerClass)
|
|
782
|
+
try Self.requireInstanceMethod(Self.runnerLoadSelector, on: runnerClass)
|
|
783
|
+
|
|
784
|
+
let alloc: ExecuTorchAllocFunction = try Self.objcMessage()
|
|
785
|
+
let initRunner: ExecuTorchInitRunnerFunction = try Self.objcMessage()
|
|
786
|
+
let load: ExecuTorchLoadFunction = try Self.objcMessage()
|
|
787
|
+
|
|
788
|
+
let allocatedRunner = alloc(runnerClass, NSSelectorFromString("alloc")).takeRetainedValue()
|
|
789
|
+
runner = initRunner(
|
|
790
|
+
allocatedRunner,
|
|
791
|
+
Self.runnerInitSelector,
|
|
792
|
+
modelPath as NSString,
|
|
793
|
+
tokenizerPath as NSString,
|
|
794
|
+
specialTokens as NSArray
|
|
795
|
+
).takeRetainedValue()
|
|
796
|
+
self.sequenceLength = sequenceLength
|
|
797
|
+
self.temperature = temperature
|
|
798
|
+
|
|
799
|
+
var loadError: NSError?
|
|
800
|
+
guard load(runner, Self.runnerLoadSelector, &loadError) else {
|
|
801
|
+
throw loadError ?? Self.error("ExecuTorch model failed to load")
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
func generate(_ prompt: String, tokenCallback: @escaping (String) -> Void) throws {
|
|
806
|
+
guard let configClass = Self.findClass("ExecuTorchLLMConfig") else {
|
|
807
|
+
throw Self.error("ExecuTorchLLMConfig was not found. Link ExecuTorch in the app target.")
|
|
808
|
+
}
|
|
809
|
+
guard let runnerClass = object_getClass(runner) else {
|
|
810
|
+
throw Self.error("ExecuTorch runner class was not found")
|
|
811
|
+
}
|
|
812
|
+
try Self.requireInstanceMethod(Self.runnerResetSelector, on: runnerClass)
|
|
813
|
+
try Self.requireInstanceMethod(Self.runnerGenerateSelector, on: runnerClass)
|
|
814
|
+
try Self.requireInstanceMethod(Self.configInitSelector, on: configClass)
|
|
815
|
+
|
|
816
|
+
let sendVoid: ExecuTorchVoidFunction = try Self.objcMessage()
|
|
817
|
+
let alloc: ExecuTorchAllocFunction = try Self.objcMessage()
|
|
818
|
+
let initConfig: ExecuTorchInitConfigFunction = try Self.objcMessage()
|
|
819
|
+
let generate: ExecuTorchGenerateFunction = try Self.objcMessage()
|
|
820
|
+
|
|
821
|
+
sendVoid(runner, Self.runnerResetSelector)
|
|
822
|
+
|
|
823
|
+
let allocatedConfig = alloc(configClass, NSSelectorFromString("alloc")).takeRetainedValue()
|
|
824
|
+
let configBlock: ExecuTorchConfigBlock = { [temperature, sequenceLength] config in
|
|
825
|
+
config.setValue(temperature, forKey: "temperature")
|
|
826
|
+
config.setValue(sequenceLength, forKey: "sequenceLength")
|
|
827
|
+
}
|
|
828
|
+
let config = initConfig(
|
|
829
|
+
allocatedConfig,
|
|
830
|
+
Self.configInitSelector,
|
|
831
|
+
configBlock
|
|
832
|
+
).takeRetainedValue()
|
|
833
|
+
|
|
834
|
+
let callback: ExecuTorchTokenCallback = { token in
|
|
835
|
+
tokenCallback(token as String)
|
|
836
|
+
}
|
|
837
|
+
var generateError: NSError?
|
|
838
|
+
guard generate(
|
|
839
|
+
runner,
|
|
840
|
+
Self.runnerGenerateSelector,
|
|
841
|
+
prompt as NSString,
|
|
842
|
+
config,
|
|
843
|
+
callback,
|
|
844
|
+
&generateError
|
|
845
|
+
) else {
|
|
846
|
+
throw generateError ?? Self.error("ExecuTorch generation failed")
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
private static func objcMessage<Function>() throws -> Function {
|
|
851
|
+
guard let handle = dlopen(nil, RTLD_NOW), let symbol = dlsym(handle, "objc_msgSend") else {
|
|
852
|
+
throw error("Unable to resolve objc_msgSend")
|
|
853
|
+
}
|
|
854
|
+
return unsafeBitCast(symbol, to: Function.self)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private static func requireInstanceMethod(_ selector: Selector, on cls: AnyClass) throws {
|
|
858
|
+
guard hasInstanceMethod(cls, selector) else {
|
|
859
|
+
throw error("ExecuTorch API selector missing: \(NSStringFromSelector(selector))")
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private static func hasInstanceMethod(_ cls: AnyClass, _ selector: Selector) -> Bool {
|
|
864
|
+
class_getInstanceMethod(cls, selector) != nil
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private static func findClass(_ name: String) -> AnyClass? {
|
|
868
|
+
NSClassFromString(name) ?? NSClassFromString("ExecuTorchLLM.\(name)")
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private static func error(_ description: String) -> NSError {
|
|
872
|
+
NSError(domain: "CapgoLLM.ExecuTorch", code: 1, userInfo: [NSLocalizedDescriptionKey: description])
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
472
876
|
// Download delegate class to handle progress updates
|
|
473
877
|
class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
474
878
|
weak var plugin: CAPPlugin?
|