@capgo/capacitor-llm 7.1.2
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/Package.swift +32 -0
- package/README.md +600 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/ee/forgr/capgo_llm/LLM.java +317 -0
- package/android/src/main/java/ee/forgr/capgo_llm/LLMPlugin.java +211 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +466 -0
- package/dist/esm/definitions.d.ts +154 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +20 -0
- package/dist/esm/web.js +156 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +170 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +172 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/LLMPlugin/LLMPlugin.swift +469 -0
- package/ios/Tests/LLMPluginTests/LLMPluginPluginTests.swift +15 -0
- package/package.json +92 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
#if canImport(FoundationModels)
|
|
4
|
+
import FoundationModels
|
|
5
|
+
#endif
|
|
6
|
+
// For CocoaPods, MediaPipeTasksGenAI should always be available
|
|
7
|
+
// For SPM, we need conditional import when SPM support is added
|
|
8
|
+
#if canImport(MediaPipeTasksGenAI)
|
|
9
|
+
import MediaPipeTasksGenAI
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Please read the Capacitor iOS Plugin Development Guide
|
|
14
|
+
* here: https://capacitorjs.com/docs/plugins/ios
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
@objc(LLMPlugin)
|
|
18
|
+
public class LLMPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
19
|
+
public let identifier = "LLMPlugin"
|
|
20
|
+
public let jsName = "CapgoLLM"
|
|
21
|
+
public var pluginMethods: [CAPPluginMethod] = [
|
|
22
|
+
CAPPluginMethod(name: "createChat", returnType: CAPPluginReturnPromise),
|
|
23
|
+
CAPPluginMethod(name: "sendMessage", returnType: CAPPluginReturnPromise),
|
|
24
|
+
CAPPluginMethod(name: "getReadiness", returnType: CAPPluginReturnPromise),
|
|
25
|
+
CAPPluginMethod(name: "setModel", returnType: CAPPluginReturnPromise),
|
|
26
|
+
CAPPluginMethod(name: "downloadModel", returnType: CAPPluginReturnPromise)
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
private enum ModelType {
|
|
30
|
+
case appleIntelligence
|
|
31
|
+
case mediaPipe
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private var currentModelType: ModelType = .appleIntelligence
|
|
35
|
+
private var isReady = false
|
|
36
|
+
private var modelPath: String?
|
|
37
|
+
|
|
38
|
+
#if canImport(FoundationModels)
|
|
39
|
+
private var appleModel: Any? = {
|
|
40
|
+
if #available(iOS 26.0, *) {
|
|
41
|
+
return SystemLanguageModel.default
|
|
42
|
+
} else {
|
|
43
|
+
return nil
|
|
44
|
+
}
|
|
45
|
+
}()
|
|
46
|
+
|
|
47
|
+
private var appleChats: [String: Any] = [:]
|
|
48
|
+
#endif
|
|
49
|
+
|
|
50
|
+
#if COCOAPODS || canImport(MediaPipeTasksGenAI)
|
|
51
|
+
private var llmInference: LlmInference?
|
|
52
|
+
#endif
|
|
53
|
+
|
|
54
|
+
@objc func setModel(_ call: CAPPluginCall) {
|
|
55
|
+
guard let path = call.getString("path") else {
|
|
56
|
+
call.reject("Path is required")
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
print("[CapgoLLM] Setting model path: \(path)")
|
|
61
|
+
|
|
62
|
+
// Extract model parameters
|
|
63
|
+
let maxTokens = call.getInt("maxTokens") ?? 2048
|
|
64
|
+
let topk = call.getInt("topk") ?? 40
|
|
65
|
+
let temperature = call.getFloat("temperature") ?? 0.8
|
|
66
|
+
let modelType = call.getString("modelType") // Optional model type parameter
|
|
67
|
+
|
|
68
|
+
modelPath = path
|
|
69
|
+
isReady = false
|
|
70
|
+
|
|
71
|
+
// Check if user wants to use Apple Intelligence
|
|
72
|
+
if path.lowercased() == "apple intelligence" {
|
|
73
|
+
print("[CapgoLLM] Switching to Apple Intelligence")
|
|
74
|
+
currentModelType = .appleIntelligence
|
|
75
|
+
isReady = true
|
|
76
|
+
notifyListeners("readinessChange", data: ["readiness": "ready"])
|
|
77
|
+
call.resolve()
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#if canImport(MediaPipeTasksGenAI)
|
|
82
|
+
Task {
|
|
83
|
+
do {
|
|
84
|
+
let modelURL: URL
|
|
85
|
+
|
|
86
|
+
// Check if it's a path within the app bundle
|
|
87
|
+
if path.hasPrefix("/") {
|
|
88
|
+
modelURL = URL(fileURLWithPath: path)
|
|
89
|
+
print("[CapgoLLM] Using absolute path: \(modelURL.path)")
|
|
90
|
+
} else {
|
|
91
|
+
// Try to find it in the app bundle
|
|
92
|
+
let fileName: String
|
|
93
|
+
let fileExtension: String
|
|
94
|
+
|
|
95
|
+
// If modelType is provided, use it as the extension
|
|
96
|
+
if let providedType = modelType, !providedType.isEmpty {
|
|
97
|
+
// Remove any extension from path and use provided type
|
|
98
|
+
fileName = (path as NSString).deletingPathExtension
|
|
99
|
+
fileExtension = providedType
|
|
100
|
+
print("[CapgoLLM] Using provided model type: \(fileExtension)")
|
|
101
|
+
} else {
|
|
102
|
+
// Extract from path
|
|
103
|
+
fileName = (path as NSString).deletingPathExtension
|
|
104
|
+
fileExtension = (path as NSString).pathExtension
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
print("[CapgoLLM] Looking for model in bundle: \(fileName) with type: \(fileExtension.isEmpty ? "bin" : fileExtension)")
|
|
108
|
+
|
|
109
|
+
// Use Bundle.main.path as shown in MediaPipe docs
|
|
110
|
+
guard let bundlePath = Bundle.main.path(forResource: fileName, ofType: fileExtension.isEmpty ? "bin" : fileExtension) else {
|
|
111
|
+
print("[CapgoLLM] Error: Model file not found in bundle: \(path)")
|
|
112
|
+
print("[CapgoLLM] Tried resource: '\(fileName)' with type: '\(fileExtension.isEmpty ? "bin" : fileExtension)'")
|
|
113
|
+
call.reject("Model file not found in bundle: \(path)")
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
modelURL = URL(fileURLWithPath: bundlePath)
|
|
117
|
+
print("[CapgoLLM] Found model in bundle at path: \(bundlePath)")
|
|
118
|
+
|
|
119
|
+
// For MediaPipe models, verify companion .litertlm file exists
|
|
120
|
+
if fileExtension == "task" {
|
|
121
|
+
let companionPath = Bundle.main.path(forResource: fileName, ofType: "litertlm")
|
|
122
|
+
if companionPath == nil {
|
|
123
|
+
print("[CapgoLLM] Warning: Companion .litertlm file not found for \(fileName)")
|
|
124
|
+
// Don't fail here as some models might not need it
|
|
125
|
+
} else {
|
|
126
|
+
print("[CapgoLLM] Found companion file at path: \(companionPath!)")
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Initialize MediaPipe LLM
|
|
132
|
+
print("[CapgoLLM] Initializing MediaPipe with model at: \(modelURL.path)")
|
|
133
|
+
let options = LlmInference.Options(modelPath: modelURL.path)
|
|
134
|
+
options.maxTokens = maxTokens
|
|
135
|
+
options.maxTopk = topk
|
|
136
|
+
|
|
137
|
+
print("[CapgoLLM] Creating LlmInference instance with maxTokens: \(maxTokens), topk: \(topk), temperature: \(temperature)")
|
|
138
|
+
llmInference = try LlmInference(options: options)
|
|
139
|
+
currentModelType = .mediaPipe
|
|
140
|
+
isReady = true
|
|
141
|
+
|
|
142
|
+
print("[CapgoLLM] Model loaded successfully")
|
|
143
|
+
notifyListeners("readinessChange", data: ["readiness": "ready"])
|
|
144
|
+
call.resolve()
|
|
145
|
+
} catch {
|
|
146
|
+
print("[CapgoLLM] Error loading model: \(error.localizedDescription)")
|
|
147
|
+
notifyListeners("readinessChange", data: ["readiness": "Failed to load model: \(error.localizedDescription)"])
|
|
148
|
+
call.reject("Failed to load model: \(error.localizedDescription)")
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
#else
|
|
152
|
+
call.reject("MediaPipe is not available. Please install via CocoaPods.")
|
|
153
|
+
#endif
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@objc func createChat(_ call: CAPPluginCall) {
|
|
157
|
+
print("[CapgoLLM] Creating chat with model type: \(currentModelType)")
|
|
158
|
+
switch currentModelType {
|
|
159
|
+
case .appleIntelligence:
|
|
160
|
+
#if canImport(FoundationModels)
|
|
161
|
+
if #available(iOS 26.0, *) {
|
|
162
|
+
let instructions = call.getString("instructions")
|
|
163
|
+
let session = LanguageModelSession(instructions: instructions)
|
|
164
|
+
let id = UUID()
|
|
165
|
+
appleChats[id.uuidString] = session
|
|
166
|
+
call.resolve([
|
|
167
|
+
"id": id.uuidString
|
|
168
|
+
])
|
|
169
|
+
} else {
|
|
170
|
+
call.reject("Apple Intelligence requires iOS 26.0 or later")
|
|
171
|
+
}
|
|
172
|
+
#else
|
|
173
|
+
call.reject("Apple Intelligence is not available on this device")
|
|
174
|
+
#endif
|
|
175
|
+
|
|
176
|
+
case .mediaPipe:
|
|
177
|
+
#if COCOAPODS || canImport(MediaPipeTasksGenAI)
|
|
178
|
+
guard llmInference != nil else {
|
|
179
|
+
print("[CapgoLLM] Error: Model not loaded when creating chat")
|
|
180
|
+
call.reject("Model not loaded")
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// MediaPipe doesn't use sessions, just return a dummy ID
|
|
185
|
+
let id = UUID().uuidString
|
|
186
|
+
print("[CapgoLLM] Created MediaPipe chat with ID: \(id)")
|
|
187
|
+
call.resolve([
|
|
188
|
+
"id": id
|
|
189
|
+
])
|
|
190
|
+
#else
|
|
191
|
+
call.reject("MediaPipe is not available. Please install via CocoaPods.")
|
|
192
|
+
#endif
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@objc func getReadiness(_ call: CAPPluginCall) {
|
|
197
|
+
let readiness = getReadinessStatus()
|
|
198
|
+
call.resolve(["readiness": readiness])
|
|
199
|
+
// Also notify listeners
|
|
200
|
+
notifyListeners("readinessChange", data: ["readiness": readiness])
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private func getReadinessStatus() -> String {
|
|
204
|
+
switch currentModelType {
|
|
205
|
+
case .appleIntelligence:
|
|
206
|
+
#if canImport(FoundationModels)
|
|
207
|
+
if #available(iOS 26.0, *), let model = appleModel as? SystemLanguageModel {
|
|
208
|
+
switch model.availability {
|
|
209
|
+
case .available:
|
|
210
|
+
return "ready"
|
|
211
|
+
case .unavailable(.deviceNotEligible):
|
|
212
|
+
return "Device is not eligible for Apple Intelligence"
|
|
213
|
+
case .unavailable(.appleIntelligenceNotEnabled):
|
|
214
|
+
return "Apple Intelligence is not enabled"
|
|
215
|
+
case .unavailable(.modelNotReady):
|
|
216
|
+
return "Model is not ready"
|
|
217
|
+
case .unavailable(let other):
|
|
218
|
+
return "Error: \(other)"
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
return "Apple Intelligence requires iOS 26.0 or later"
|
|
222
|
+
}
|
|
223
|
+
#else
|
|
224
|
+
return "Apple Intelligence is not available on this device"
|
|
225
|
+
#endif
|
|
226
|
+
|
|
227
|
+
case .mediaPipe:
|
|
228
|
+
return isReady ? "ready" : "not_ready"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@objc func sendMessage(_ call: CAPPluginCall) {
|
|
233
|
+
let chatId = call.getString("chatId", "")
|
|
234
|
+
guard let message = call.getString("message") else {
|
|
235
|
+
call.reject("message not found")
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
switch currentModelType {
|
|
240
|
+
case .appleIntelligence:
|
|
241
|
+
#if canImport(FoundationModels)
|
|
242
|
+
if #available(iOS 26.0, *), let model = appleModel as? SystemLanguageModel {
|
|
243
|
+
switch model.availability {
|
|
244
|
+
case .available:
|
|
245
|
+
break
|
|
246
|
+
case .unavailable(.deviceNotEligible):
|
|
247
|
+
call.reject("error deviceNotEligible error")
|
|
248
|
+
return
|
|
249
|
+
case .unavailable(.appleIntelligenceNotEnabled):
|
|
250
|
+
call.reject("error appleIntelligenceNotEnabled error")
|
|
251
|
+
return
|
|
252
|
+
case .unavailable(.modelNotReady):
|
|
253
|
+
call.reject("error modelNotReady error")
|
|
254
|
+
return
|
|
255
|
+
case .unavailable(let other):
|
|
256
|
+
call.reject("error \(other) error")
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
guard let chat = appleChats[chatId] as? LanguageModelSession else {
|
|
261
|
+
call.reject("chat not found")
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if chat.isResponding {
|
|
266
|
+
call.reject("chat is responding, please wait before asking new questions")
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Task {
|
|
271
|
+
do {
|
|
272
|
+
let stream = chat.streamResponse(to: message)
|
|
273
|
+
for try await chunk in stream {
|
|
274
|
+
print("[CapgoLLM] Chunk type: \(type(of: chunk))")
|
|
275
|
+
|
|
276
|
+
// Extract content from Snapshot object using Mirror reflection
|
|
277
|
+
let textChunk: String
|
|
278
|
+
let mirror = Mirror(reflecting: chunk)
|
|
279
|
+
|
|
280
|
+
// Try to find content property
|
|
281
|
+
if let contentProperty = mirror.children.first(where: { $0.label == "content" }),
|
|
282
|
+
let content = contentProperty.value as? String {
|
|
283
|
+
textChunk = content
|
|
284
|
+
print("[CapgoLLM] Extracted content: \(content)")
|
|
285
|
+
} else {
|
|
286
|
+
// Fallback: try rawContent
|
|
287
|
+
if let rawContentProperty = mirror.children.first(where: { $0.label == "rawContent" }),
|
|
288
|
+
let rawContent = rawContentProperty.value as? String {
|
|
289
|
+
textChunk = rawContent
|
|
290
|
+
print("[CapgoLLM] Extracted rawContent: \(rawContent)")
|
|
291
|
+
} else {
|
|
292
|
+
// Final fallback
|
|
293
|
+
textChunk = "\(chunk)"
|
|
294
|
+
print("[CapgoLLM] Fallback to string conversion: \(textChunk)")
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
let data: [String: Any] = [
|
|
298
|
+
"chatId": chatId as String,
|
|
299
|
+
"text": textChunk as String,
|
|
300
|
+
"isChunk": true
|
|
301
|
+
]
|
|
302
|
+
self.notifyListeners("textFromAi", data: data)
|
|
303
|
+
}
|
|
304
|
+
let finishData: [String: Any] = ["chatId": chatId as String]
|
|
305
|
+
self.notifyListeners("aiFinished", data: finishData)
|
|
306
|
+
call.resolve()
|
|
307
|
+
} catch {
|
|
308
|
+
call.reject("Failed to get response: \(error.localizedDescription)")
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
call.reject("Apple Intelligence requires iOS 26.0 or later")
|
|
313
|
+
}
|
|
314
|
+
#else
|
|
315
|
+
call.reject("Apple Intelligence is not available on this device")
|
|
316
|
+
#endif
|
|
317
|
+
|
|
318
|
+
case .mediaPipe:
|
|
319
|
+
#if COCOAPODS || canImport(MediaPipeTasksGenAI)
|
|
320
|
+
guard let inference = llmInference else {
|
|
321
|
+
call.reject("Model not loaded")
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
Task {
|
|
326
|
+
do {
|
|
327
|
+
print("[CapgoLLM] Attempting to generate streaming response for message: \(message)")
|
|
328
|
+
|
|
329
|
+
// Use native streaming API
|
|
330
|
+
let resultStream = inference.generateResponseAsync(inputText: message)
|
|
331
|
+
|
|
332
|
+
var fullResponse = ""
|
|
333
|
+
|
|
334
|
+
for try await partialResult in resultStream {
|
|
335
|
+
print("[CapgoLLM] Partial result: \(partialResult)")
|
|
336
|
+
|
|
337
|
+
// Send the chunk
|
|
338
|
+
self.notifyListeners("textFromAi", data: [
|
|
339
|
+
"chatId": chatId,
|
|
340
|
+
"text": partialResult,
|
|
341
|
+
"isChunk": true
|
|
342
|
+
])
|
|
343
|
+
|
|
344
|
+
// Accumulate for history (if needed)
|
|
345
|
+
fullResponse += partialResult
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
print("[CapgoLLM] Streaming complete. Full response: \(fullResponse)")
|
|
349
|
+
|
|
350
|
+
self.notifyListeners("aiFinished", data: ["chatId": chatId])
|
|
351
|
+
call.resolve()
|
|
352
|
+
|
|
353
|
+
} catch {
|
|
354
|
+
print("[CapgoLLM] Error details: \(error)")
|
|
355
|
+
call.reject("Failed to generate response: \(error.localizedDescription)")
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
#else
|
|
359
|
+
call.reject("MediaPipe is not available. Please install via CocoaPods.")
|
|
360
|
+
#endif
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
@objc func downloadModel(_ call: CAPPluginCall) {
|
|
365
|
+
guard let urlString = call.getString("url") else {
|
|
366
|
+
call.reject("URL is required")
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
guard let url = URL(string: urlString) else {
|
|
371
|
+
call.reject("Invalid URL")
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let filename = call.getString("filename") ?? url.lastPathComponent
|
|
376
|
+
|
|
377
|
+
// Get documents directory
|
|
378
|
+
guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
|
379
|
+
call.reject("Could not access documents directory")
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let destinationURL = documentsPath.appendingPathComponent(filename)
|
|
384
|
+
|
|
385
|
+
// Create download task
|
|
386
|
+
let session = URLSession(configuration: .default, delegate: DownloadDelegate(plugin: self), delegateQueue: nil)
|
|
387
|
+
let downloadTask = session.downloadTask(with: url) { [weak self] (tempURL, _, error) in
|
|
388
|
+
guard let self = self else { return }
|
|
389
|
+
|
|
390
|
+
if let error = error {
|
|
391
|
+
call.reject("Download failed: \(error.localizedDescription)")
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
guard let tempURL = tempURL else {
|
|
396
|
+
call.reject("Download failed: No temporary file")
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
do {
|
|
401
|
+
// Remove existing file if it exists
|
|
402
|
+
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
|
403
|
+
try FileManager.default.removeItem(at: destinationURL)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Move downloaded file to documents directory
|
|
407
|
+
try FileManager.default.moveItem(at: tempURL, to: destinationURL)
|
|
408
|
+
|
|
409
|
+
var result = [
|
|
410
|
+
"path": destinationURL.path
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
// Handle companion URL if provided (for compatibility)
|
|
414
|
+
if let companionUrlString = call.getString("companionUrl"),
|
|
415
|
+
let companionUrl = URL(string: companionUrlString) {
|
|
416
|
+
|
|
417
|
+
let companionFilename = companionUrl.lastPathComponent
|
|
418
|
+
let companionDestination = documentsPath.appendingPathComponent(companionFilename)
|
|
419
|
+
|
|
420
|
+
// Download companion file synchronously for simplicity
|
|
421
|
+
do {
|
|
422
|
+
let companionData = try Data(contentsOf: companionUrl)
|
|
423
|
+
try companionData.write(to: companionDestination)
|
|
424
|
+
result["companionPath"] = companionDestination.path
|
|
425
|
+
} catch {
|
|
426
|
+
// Log error but don't fail the whole download
|
|
427
|
+
print("Failed to download companion file: \(error)")
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
call.resolve(result)
|
|
432
|
+
|
|
433
|
+
} catch {
|
|
434
|
+
call.reject("Failed to save file: \(error.localizedDescription)")
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
downloadTask.resume()
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Download delegate class to handle progress updates
|
|
443
|
+
class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
444
|
+
weak var plugin: CAPPlugin?
|
|
445
|
+
|
|
446
|
+
init(plugin: CAPPlugin) {
|
|
447
|
+
self.plugin = plugin
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
|
|
451
|
+
didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
|
|
452
|
+
totalBytesExpectedToWrite: Int64) {
|
|
453
|
+
|
|
454
|
+
if totalBytesExpectedToWrite > 0 {
|
|
455
|
+
let progress = Int((Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100)
|
|
456
|
+
|
|
457
|
+
plugin?.notifyListeners("downloadProgress", data: [
|
|
458
|
+
"progress": progress,
|
|
459
|
+
"downloadedBytes": totalBytesWritten,
|
|
460
|
+
"totalBytes": totalBytesExpectedToWrite
|
|
461
|
+
])
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
|
|
466
|
+
didFinishDownloadingTo location: URL) {
|
|
467
|
+
// Handled in the completion handler
|
|
468
|
+
}
|
|
469
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import LLMPlugin
|
|
3
|
+
|
|
4
|
+
class LLMPluginTests: XCTestCase {
|
|
5
|
+
func testEcho() {
|
|
6
|
+
// This is an example of a functional test case for a plugin.
|
|
7
|
+
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
8
|
+
|
|
9
|
+
let plugin = LLMPlugin()
|
|
10
|
+
|
|
11
|
+
let result = plugin.echo(value: "Hello, World!")
|
|
12
|
+
|
|
13
|
+
XCTAssertEqual(result.value, "Hello, World!")
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@capgo/capacitor-llm",
|
|
3
|
+
"version": "7.1.2",
|
|
4
|
+
"description": "Adds support for LLM locally runned for Capacitor",
|
|
5
|
+
"main": "dist/plugin.cjs.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.ts",
|
|
8
|
+
"unpkg": "dist/plugin.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"android/src/main/",
|
|
11
|
+
"android/build.gradle",
|
|
12
|
+
"dist/",
|
|
13
|
+
"ios/Sources",
|
|
14
|
+
"ios/Tests",
|
|
15
|
+
"Package.swift",
|
|
16
|
+
"CapgoLLM.podspec"
|
|
17
|
+
],
|
|
18
|
+
"author": "WcaleNieWolny <michael@capgo.app>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Cap-go/llm.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Cap-go/llm/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"capacitor",
|
|
29
|
+
"plugin",
|
|
30
|
+
"native",
|
|
31
|
+
"llm",
|
|
32
|
+
"local",
|
|
33
|
+
"gemma",
|
|
34
|
+
"gemma3",
|
|
35
|
+
"gemma3-270m",
|
|
36
|
+
"gemma2-2b",
|
|
37
|
+
"apple-intelligence",
|
|
38
|
+
"gpt"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|
|
42
|
+
"verify:ios": "xcodebuild -scheme CapgoLLM -destination generic/platform=iOS",
|
|
43
|
+
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
44
|
+
"verify:web": "npm run build",
|
|
45
|
+
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|
|
46
|
+
"fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
|
|
47
|
+
"eslint": "eslint . --ext ts",
|
|
48
|
+
"prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
49
|
+
"swiftlint": "node-swiftlint",
|
|
50
|
+
"docgen": "docgen --api LLMPlugin --output-readme README.md --output-json dist/docs.json",
|
|
51
|
+
"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
|
|
52
|
+
"clean": "rimraf ./dist",
|
|
53
|
+
"watch": "tsc --watch",
|
|
54
|
+
"prepublishOnly": "npm run build"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@capacitor/android": "^7.0.0",
|
|
58
|
+
"@capacitor/core": "^7.0.0",
|
|
59
|
+
"@capacitor/docgen": "^0.3.0",
|
|
60
|
+
"@capacitor/ios": "^7.0.0",
|
|
61
|
+
"@ionic/eslint-config": "^0.4.0",
|
|
62
|
+
"@ionic/prettier-config": "^4.0.0",
|
|
63
|
+
"@ionic/swiftlint-config": "^2.0.0",
|
|
64
|
+
"@webgpu/types": "^0.1.64",
|
|
65
|
+
"eslint": "^8.57.0",
|
|
66
|
+
"google-protobuf": "^3.21.4",
|
|
67
|
+
"prettier": "^3.4.2",
|
|
68
|
+
"prettier-plugin-java": "^2.6.6",
|
|
69
|
+
"rimraf": "^6.0.1",
|
|
70
|
+
"rollup": "^4.30.1",
|
|
71
|
+
"swiftlint": "^2.0.0",
|
|
72
|
+
"typescript": "~4.1.5"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"@capacitor/core": ">=7.0.0",
|
|
76
|
+
"@mediapipe/tasks-genai": "^0.10.24"
|
|
77
|
+
},
|
|
78
|
+
"prettier": "@ionic/prettier-config",
|
|
79
|
+
"swiftlint": "@ionic/swiftlint-config",
|
|
80
|
+
"eslintConfig": {
|
|
81
|
+
"extends": "@ionic/eslint-config/recommended"
|
|
82
|
+
},
|
|
83
|
+
"capacitor": {
|
|
84
|
+
"ios": {
|
|
85
|
+
"src": "ios"
|
|
86
|
+
},
|
|
87
|
+
"android": {
|
|
88
|
+
"src": "android"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"dependencies": {}
|
|
92
|
+
}
|