Version not found. Please check the version and try again.
@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
|
@@ -4,20 +4,33 @@ import android.content.Context;
|
|
|
4
4
|
import com.google.mediapipe.tasks.genai.llminference.LlmInference;
|
|
5
5
|
import com.google.mediapipe.tasks.genai.llminference.LlmInference.LlmInferenceOptions;
|
|
6
6
|
import java.util.HashMap;
|
|
7
|
+
import java.util.List;
|
|
8
|
+
import java.util.Locale;
|
|
7
9
|
import java.util.Map;
|
|
8
10
|
import java.util.UUID;
|
|
9
11
|
import java.util.concurrent.Executor;
|
|
10
12
|
import java.util.concurrent.Executors;
|
|
13
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
14
|
+
import org.pytorch.executorch.extension.llm.LlmCallback;
|
|
15
|
+
import org.pytorch.executorch.extension.llm.LlmModule;
|
|
11
16
|
|
|
12
17
|
public class LLM {
|
|
13
18
|
|
|
19
|
+
private enum Engine {
|
|
20
|
+
MEDIAPIPE,
|
|
21
|
+
EXECUTORCH
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
private static LLM instance;
|
|
15
|
-
private LlmInference
|
|
25
|
+
private LlmInference mediaPipeInference;
|
|
26
|
+
private LlmModule executorchModule;
|
|
16
27
|
private Map<String, ChatSession> chatSessions;
|
|
17
28
|
private boolean isReady = false;
|
|
18
29
|
private Context context;
|
|
19
30
|
private Executor executor;
|
|
20
31
|
private String modelPath = null;
|
|
32
|
+
private String tokenizerPath = null;
|
|
33
|
+
private Engine currentEngine = Engine.MEDIAPIPE;
|
|
21
34
|
|
|
22
35
|
private LLM(Context context) {
|
|
23
36
|
this.context = context;
|
|
@@ -34,28 +47,105 @@ public class LLM {
|
|
|
34
47
|
|
|
35
48
|
// Model parameters
|
|
36
49
|
private Integer maxTokens = 2048;
|
|
50
|
+
private Integer sequenceLength = 2048;
|
|
37
51
|
private Integer topk = 40;
|
|
38
52
|
private Float temperature = 0.1f;
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
private String modelType = null;
|
|
54
|
+
|
|
55
|
+
public void setModel(
|
|
56
|
+
String path,
|
|
57
|
+
String engine,
|
|
58
|
+
String modelType,
|
|
59
|
+
String tokenizerPath,
|
|
60
|
+
List<String> specialTokens,
|
|
61
|
+
Integer maxTokens,
|
|
62
|
+
Integer sequenceLength,
|
|
63
|
+
Integer topk,
|
|
64
|
+
Float temperature,
|
|
65
|
+
ModelLoadCallback callback
|
|
66
|
+
) {
|
|
41
67
|
this.modelPath = path;
|
|
68
|
+
this.tokenizerPath = tokenizerPath;
|
|
69
|
+
this.modelType = modelType;
|
|
42
70
|
this.maxTokens = maxTokens;
|
|
71
|
+
this.sequenceLength = sequenceLength != null ? sequenceLength : maxTokens;
|
|
43
72
|
this.topk = topk;
|
|
44
73
|
this.temperature = temperature;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
try {
|
|
75
|
+
this.currentEngine = resolveEngine(engine, path, tokenizerPath);
|
|
76
|
+
} catch (IllegalArgumentException e) {
|
|
77
|
+
isReady = false;
|
|
78
|
+
if (callback != null) {
|
|
79
|
+
callback.onError(e.getMessage());
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
50
82
|
}
|
|
83
|
+
isReady = false;
|
|
51
84
|
|
|
52
85
|
android.util.Log.d(
|
|
53
86
|
"LLM",
|
|
54
|
-
"setModel called with
|
|
87
|
+
"setModel called with engine: " +
|
|
88
|
+
currentEngine +
|
|
89
|
+
", path: " +
|
|
90
|
+
path +
|
|
91
|
+
", tokenizerPath: " +
|
|
92
|
+
tokenizerPath +
|
|
93
|
+
", maxTokens: " +
|
|
94
|
+
maxTokens +
|
|
95
|
+
", sequenceLength: " +
|
|
96
|
+
this.sequenceLength +
|
|
97
|
+
", topk: " +
|
|
98
|
+
topk +
|
|
99
|
+
", temperature: " +
|
|
100
|
+
temperature +
|
|
101
|
+
", specialTokens: " +
|
|
102
|
+
(specialTokens == null ? 0 : specialTokens.size())
|
|
55
103
|
);
|
|
56
104
|
initializeModel(callback);
|
|
57
105
|
}
|
|
58
106
|
|
|
107
|
+
private Engine resolveEngine(String engine, String path, String tokenizerPath) {
|
|
108
|
+
String normalizedEngine = engine == null ? "auto" : engine.toLowerCase(Locale.US);
|
|
109
|
+
if ("executorch".equals(normalizedEngine)) {
|
|
110
|
+
return Engine.EXECUTORCH;
|
|
111
|
+
}
|
|
112
|
+
if ("mediapipe".equals(normalizedEngine)) {
|
|
113
|
+
return Engine.MEDIAPIPE;
|
|
114
|
+
}
|
|
115
|
+
if ("apple".equals(normalizedEngine)) {
|
|
116
|
+
throw new IllegalArgumentException("Apple Intelligence is only available on iOS");
|
|
117
|
+
}
|
|
118
|
+
if (tokenizerPath != null && !tokenizerPath.isEmpty()) {
|
|
119
|
+
return Engine.EXECUTORCH;
|
|
120
|
+
}
|
|
121
|
+
if (path != null && path.toLowerCase(Locale.US).endsWith(".pte")) {
|
|
122
|
+
return Engine.EXECUTORCH;
|
|
123
|
+
}
|
|
124
|
+
return Engine.MEDIAPIPE;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private void releaseCurrentModel() {
|
|
128
|
+
if (mediaPipeInference != null) {
|
|
129
|
+
try {
|
|
130
|
+
mediaPipeInference.close();
|
|
131
|
+
} catch (Exception e) {
|
|
132
|
+
android.util.Log.w("LLM", "Failed to close MediaPipe inference: " + e.getMessage());
|
|
133
|
+
} finally {
|
|
134
|
+
mediaPipeInference = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (executorchModule != null) {
|
|
139
|
+
try {
|
|
140
|
+
executorchModule.stop();
|
|
141
|
+
} catch (Exception e) {
|
|
142
|
+
android.util.Log.w("LLM", "Failed to stop ExecuTorch module: " + e.getMessage());
|
|
143
|
+
} finally {
|
|
144
|
+
executorchModule = null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
59
149
|
private void initializeModel(ModelLoadCallback callback) {
|
|
60
150
|
if (modelPath == null) {
|
|
61
151
|
if (callback != null) {
|
|
@@ -66,64 +156,12 @@ public class LLM {
|
|
|
66
156
|
|
|
67
157
|
executor.execute(() -> {
|
|
68
158
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// For /android_asset/ paths, we need to handle them specially
|
|
75
|
-
if (modelPath.startsWith("/android_asset/")) {
|
|
76
|
-
// Extract the relative path from assets
|
|
77
|
-
String assetPath = modelPath.substring("/android_asset/".length());
|
|
78
|
-
android.util.Log.d("LLM", "Asset path: " + assetPath);
|
|
79
|
-
|
|
80
|
-
// Check if the asset exists
|
|
81
|
-
try {
|
|
82
|
-
java.io.InputStream is = context.getAssets().open(assetPath);
|
|
83
|
-
is.close();
|
|
84
|
-
android.util.Log.d("LLM", "Asset exists: " + assetPath);
|
|
85
|
-
|
|
86
|
-
// For MediaPipe, we need to copy the asset to a file
|
|
87
|
-
java.io.File cacheDir = context.getCacheDir();
|
|
88
|
-
java.io.File modelFile = new java.io.File(cacheDir, assetPath);
|
|
89
|
-
|
|
90
|
-
// Create parent directories if needed
|
|
91
|
-
modelFile.getParentFile().mkdirs();
|
|
92
|
-
|
|
93
|
-
// Copy asset to cache
|
|
94
|
-
copyAssetToFile(assetPath, modelFile);
|
|
95
|
-
actualPath = modelFile.getAbsolutePath();
|
|
96
|
-
android.util.Log.d("LLM", "Copied asset to: " + actualPath);
|
|
97
|
-
|
|
98
|
-
// Also check for companion .litertlm file
|
|
99
|
-
String litertlmPath = assetPath.replace(".task", ".litertlm");
|
|
100
|
-
try {
|
|
101
|
-
java.io.InputStream litertlmIs = context.getAssets().open(litertlmPath);
|
|
102
|
-
litertlmIs.close();
|
|
103
|
-
java.io.File litertlmFile = new java.io.File(cacheDir, litertlmPath);
|
|
104
|
-
copyAssetToFile(litertlmPath, litertlmFile);
|
|
105
|
-
android.util.Log.d("LLM", "Also copied companion file: " + litertlmFile.getAbsolutePath());
|
|
106
|
-
} catch (Exception e) {
|
|
107
|
-
android.util.Log.d("LLM", "No companion .litertlm file found");
|
|
108
|
-
}
|
|
109
|
-
} catch (Exception e) {
|
|
110
|
-
android.util.Log.e("LLM", "Asset not found: " + assetPath, e);
|
|
111
|
-
throw new RuntimeException("Asset not found: " + assetPath);
|
|
112
|
-
}
|
|
159
|
+
releaseCurrentModel();
|
|
160
|
+
if (currentEngine == Engine.EXECUTORCH) {
|
|
161
|
+
initializeExecutorchModel();
|
|
162
|
+
} else {
|
|
163
|
+
initializeMediaPipeModel();
|
|
113
164
|
}
|
|
114
|
-
|
|
115
|
-
android.util.Log.d("LLM", "Final model path: " + actualPath);
|
|
116
|
-
|
|
117
|
-
// Create options for LLM inference
|
|
118
|
-
LlmInferenceOptions.Builder optionsBuilder = LlmInferenceOptions.builder()
|
|
119
|
-
.setModelPath(actualPath)
|
|
120
|
-
.setMaxTokens(maxTokens)
|
|
121
|
-
.setMaxTopK(topk);
|
|
122
|
-
|
|
123
|
-
LlmInferenceOptions options = optionsBuilder.build();
|
|
124
|
-
|
|
125
|
-
// Initialize the LLM inference
|
|
126
|
-
llmInference = LlmInference.createFromOptions(context, options);
|
|
127
165
|
isReady = true;
|
|
128
166
|
|
|
129
167
|
if (callback != null) {
|
|
@@ -139,16 +177,109 @@ public class LLM {
|
|
|
139
177
|
});
|
|
140
178
|
}
|
|
141
179
|
|
|
180
|
+
private void initializeMediaPipeModel() throws Exception {
|
|
181
|
+
String actualPath = resolveModelPath(modelPath, true);
|
|
182
|
+
android.util.Log.d("LLM", "Final MediaPipe model path: " + actualPath);
|
|
183
|
+
|
|
184
|
+
LlmInferenceOptions.Builder optionsBuilder = LlmInferenceOptions.builder()
|
|
185
|
+
.setModelPath(actualPath)
|
|
186
|
+
.setMaxTokens(maxTokens)
|
|
187
|
+
.setMaxTopK(topk);
|
|
188
|
+
|
|
189
|
+
LlmInferenceOptions options = optionsBuilder.build();
|
|
190
|
+
mediaPipeInference = LlmInference.createFromOptions(context, options);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private void initializeExecutorchModel() throws Exception {
|
|
194
|
+
if (tokenizerPath == null || tokenizerPath.isEmpty()) {
|
|
195
|
+
throw new RuntimeException("tokenizerPath is required for ExecuTorch models");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
String actualModelPath = resolveModelPath(modelPath, false);
|
|
199
|
+
String actualTokenizerPath = resolveModelPath(tokenizerPath, false);
|
|
200
|
+
int executorModelType = resolveExecutorchModelType(modelType);
|
|
201
|
+
|
|
202
|
+
android.util.Log.d("LLM", "Final ExecuTorch model path: " + actualModelPath);
|
|
203
|
+
android.util.Log.d("LLM", "Final ExecuTorch tokenizer path: " + actualTokenizerPath);
|
|
204
|
+
|
|
205
|
+
executorchModule = new LlmModule(executorModelType, actualModelPath, actualTokenizerPath, temperature);
|
|
206
|
+
executorchModule.load();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private int resolveExecutorchModelType(String modelType) {
|
|
210
|
+
if (modelType == null) {
|
|
211
|
+
return LlmModule.MODEL_TYPE_TEXT;
|
|
212
|
+
}
|
|
213
|
+
String normalized = modelType.toLowerCase(Locale.US);
|
|
214
|
+
if ("text-vision".equals(normalized) || "vision".equals(normalized)) {
|
|
215
|
+
return LlmModule.MODEL_TYPE_TEXT_VISION;
|
|
216
|
+
}
|
|
217
|
+
if ("multimodal".equals(normalized)) {
|
|
218
|
+
return LlmModule.MODEL_TYPE_MULTIMODAL;
|
|
219
|
+
}
|
|
220
|
+
return LlmModule.MODEL_TYPE_TEXT;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private String resolveModelPath(String path, boolean copyMediaPipeCompanion) throws Exception {
|
|
224
|
+
android.util.Log.d("LLM", "Original model path: " + path);
|
|
225
|
+
|
|
226
|
+
if (!path.startsWith("/android_asset/")) {
|
|
227
|
+
return path;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
String assetPath = path.substring("/android_asset/".length());
|
|
231
|
+
android.util.Log.d("LLM", "Asset path: " + assetPath);
|
|
232
|
+
|
|
233
|
+
try (java.io.InputStream is = context.getAssets().open(assetPath)) {
|
|
234
|
+
android.util.Log.d("LLM", "Asset exists: " + assetPath);
|
|
235
|
+
} catch (Exception e) {
|
|
236
|
+
android.util.Log.e("LLM", "Asset not found: " + assetPath, e);
|
|
237
|
+
throw new RuntimeException("Asset not found: " + assetPath);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
java.io.File cacheDir = context.getCacheDir();
|
|
241
|
+
java.io.File modelFile = new java.io.File(cacheDir, assetPath);
|
|
242
|
+
java.io.File parent = modelFile.getParentFile();
|
|
243
|
+
if (parent != null) {
|
|
244
|
+
parent.mkdirs();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
copyAssetToFile(assetPath, modelFile);
|
|
248
|
+
android.util.Log.d("LLM", "Copied asset to: " + modelFile.getAbsolutePath());
|
|
249
|
+
|
|
250
|
+
if (copyMediaPipeCompanion && assetPath.endsWith(".task")) {
|
|
251
|
+
copyOptionalCompanion(assetPath, cacheDir);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return modelFile.getAbsolutePath();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private void copyOptionalCompanion(String assetPath, java.io.File cacheDir) {
|
|
258
|
+
String litertlmPath = assetPath.replace(".task", ".litertlm");
|
|
259
|
+
try (java.io.InputStream litertlmIs = context.getAssets().open(litertlmPath)) {
|
|
260
|
+
java.io.File litertlmFile = new java.io.File(cacheDir, litertlmPath);
|
|
261
|
+
java.io.File parent = litertlmFile.getParentFile();
|
|
262
|
+
if (parent != null) {
|
|
263
|
+
parent.mkdirs();
|
|
264
|
+
}
|
|
265
|
+
copyAssetToFile(litertlmPath, litertlmFile);
|
|
266
|
+
android.util.Log.d("LLM", "Also copied companion file: " + litertlmFile.getAbsolutePath());
|
|
267
|
+
} catch (Exception e) {
|
|
268
|
+
android.util.Log.d("LLM", "No companion .litertlm file found");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
142
272
|
private void copyAssetToFile(String assetPath, java.io.File destFile) throws Exception {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
273
|
+
try (
|
|
274
|
+
java.io.InputStream is = context.getAssets().open(assetPath);
|
|
275
|
+
java.io.FileOutputStream fos = new java.io.FileOutputStream(destFile)
|
|
276
|
+
) {
|
|
277
|
+
byte[] buffer = new byte[4096];
|
|
278
|
+
int length;
|
|
279
|
+
while ((length = is.read(buffer)) > 0) {
|
|
280
|
+
fos.write(buffer, 0, length);
|
|
281
|
+
}
|
|
149
282
|
}
|
|
150
|
-
fos.close();
|
|
151
|
-
is.close();
|
|
152
283
|
}
|
|
153
284
|
|
|
154
285
|
public String createChat() {
|
|
@@ -165,11 +296,86 @@ public class LLM {
|
|
|
165
296
|
return;
|
|
166
297
|
}
|
|
167
298
|
|
|
168
|
-
if (!isReady
|
|
299
|
+
if (!isReady) {
|
|
169
300
|
callback.onError("Model not ready");
|
|
170
301
|
return;
|
|
171
302
|
}
|
|
172
303
|
|
|
304
|
+
if (currentEngine == Engine.EXECUTORCH) {
|
|
305
|
+
sendExecutorchMessage(chatId, message, session, callback);
|
|
306
|
+
} else {
|
|
307
|
+
sendMediaPipeMessage(chatId, message, session, callback);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private void sendExecutorchMessage(String chatId, String message, ChatSession session, MessageCallback callback) {
|
|
312
|
+
final LlmModule module = executorchModule;
|
|
313
|
+
final int generationSequenceLength = sequenceLength;
|
|
314
|
+
final float generationTemperature = temperature;
|
|
315
|
+
|
|
316
|
+
if (module == null) {
|
|
317
|
+
callback.onError("ExecuTorch model not ready");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
executor.execute(() -> {
|
|
322
|
+
try {
|
|
323
|
+
session.addMessage("user", message);
|
|
324
|
+
String fullPrompt = session.buildPrompt(message);
|
|
325
|
+
AtomicBoolean completed = new AtomicBoolean(false);
|
|
326
|
+
StringBuilder fullResponse = new StringBuilder();
|
|
327
|
+
|
|
328
|
+
module.resetContext();
|
|
329
|
+
int errorCode = module.generate(
|
|
330
|
+
fullPrompt,
|
|
331
|
+
generationSequenceLength,
|
|
332
|
+
new LlmCallback() {
|
|
333
|
+
@Override
|
|
334
|
+
public void onResult(String result) {
|
|
335
|
+
if (result == null || result.isEmpty()) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
callback.onTextReceived(chatId, result, true);
|
|
339
|
+
fullResponse.append(result);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@Override
|
|
343
|
+
public void onStats(String stats) {
|
|
344
|
+
if (completed.compareAndSet(false, true)) {
|
|
345
|
+
session.addMessage("assistant", fullResponse.toString());
|
|
346
|
+
callback.onComplete(chatId);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
false,
|
|
351
|
+
generationTemperature,
|
|
352
|
+
0,
|
|
353
|
+
0
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (errorCode != 0) {
|
|
357
|
+
completed.set(true);
|
|
358
|
+
callback.onError("ExecuTorch error " + errorCode);
|
|
359
|
+
} else if (completed.compareAndSet(false, true)) {
|
|
360
|
+
session.addMessage("assistant", fullResponse.toString());
|
|
361
|
+
callback.onComplete(chatId);
|
|
362
|
+
}
|
|
363
|
+
} catch (Exception e) {
|
|
364
|
+
callback.onError(e.getMessage());
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private void sendMediaPipeMessage(String chatId, String message, ChatSession session, MessageCallback callback) {
|
|
370
|
+
final LlmInference inference = mediaPipeInference;
|
|
371
|
+
final int sessionTopk = topk;
|
|
372
|
+
final float sessionTemperature = temperature;
|
|
373
|
+
|
|
374
|
+
if (inference == null) {
|
|
375
|
+
callback.onError("MediaPipe model not ready");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
173
379
|
executor.execute(() -> {
|
|
174
380
|
try {
|
|
175
381
|
// Add user message to history
|
|
@@ -182,12 +388,12 @@ public class LLM {
|
|
|
182
388
|
// Create a session with proper options
|
|
183
389
|
com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession.LlmInferenceSessionOptions sessionOptions =
|
|
184
390
|
com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession.LlmInferenceSessionOptions.builder()
|
|
185
|
-
.setTopK(
|
|
186
|
-
.setTemperature(
|
|
391
|
+
.setTopK(sessionTopk)
|
|
392
|
+
.setTemperature(sessionTemperature)
|
|
187
393
|
.build();
|
|
188
394
|
|
|
189
395
|
com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession inferenceSession =
|
|
190
|
-
com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession.createFromOptions(
|
|
396
|
+
com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession.createFromOptions(inference, sessionOptions);
|
|
191
397
|
|
|
192
398
|
// Add the query
|
|
193
399
|
inferenceSession.addQueryChunk(fullPrompt);
|
|
@@ -199,6 +405,7 @@ public class LLM {
|
|
|
199
405
|
new com.google.mediapipe.tasks.genai.llminference.ProgressListener<String>() {
|
|
200
406
|
private StringBuilder buffer = new StringBuilder();
|
|
201
407
|
private boolean hasStarted = false;
|
|
408
|
+
private final AtomicBoolean completed = new AtomicBoolean(false);
|
|
202
409
|
|
|
203
410
|
@Override
|
|
204
411
|
public void run(String partialResult, boolean done) {
|
|
@@ -244,19 +451,17 @@ public class LLM {
|
|
|
244
451
|
fullResponse.append(chunk);
|
|
245
452
|
}
|
|
246
453
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (!remaining.isEmpty()) {
|
|
454
|
+
if (done && completed.compareAndSet(false, true)) {
|
|
455
|
+
if (buffer.length() > 0) {
|
|
456
|
+
String remaining = buffer.toString();
|
|
251
457
|
callback.onTextReceived(chatId, remaining, true);
|
|
252
458
|
fullResponse.append(remaining);
|
|
459
|
+
buffer = new StringBuilder();
|
|
253
460
|
}
|
|
254
461
|
|
|
255
|
-
// Add complete response to history
|
|
256
462
|
session.addMessage("assistant", fullResponse.toString());
|
|
257
463
|
callback.onComplete(chatId);
|
|
258
464
|
|
|
259
|
-
// Close the session
|
|
260
465
|
try {
|
|
261
466
|
inferenceSession.close();
|
|
262
467
|
} catch (Exception e) {
|
|
@@ -310,8 +515,7 @@ public class LLM {
|
|
|
310
515
|
}
|
|
311
516
|
|
|
312
517
|
String buildPrompt(String newMessage) {
|
|
313
|
-
|
|
314
|
-
return newMessage;
|
|
518
|
+
return history.toString();
|
|
315
519
|
}
|
|
316
520
|
}
|
|
317
521
|
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
package ee.forgr.capgo_llm;
|
|
2
2
|
|
|
3
|
+
import com.getcapacitor.JSArray;
|
|
3
4
|
import com.getcapacitor.JSObject;
|
|
4
5
|
import com.getcapacitor.Plugin;
|
|
5
6
|
import com.getcapacitor.PluginCall;
|
|
6
7
|
import com.getcapacitor.PluginMethod;
|
|
7
8
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
9
|
+
import java.util.ArrayList;
|
|
10
|
+
import java.util.List;
|
|
8
11
|
|
|
9
12
|
@CapacitorPlugin(name = "CapgoLLM")
|
|
10
13
|
public class LLMPlugin extends Plugin {
|
|
11
14
|
|
|
12
|
-
private final String pluginVersion = "8.0.
|
|
15
|
+
private final String pluginVersion = "8.0.13";
|
|
13
16
|
|
|
14
17
|
private LLM llm;
|
|
15
18
|
|
|
@@ -94,14 +97,33 @@ public class LLMPlugin extends Plugin {
|
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
// Extract model parameters
|
|
100
|
+
String engine = call.getString("engine", "auto");
|
|
101
|
+
String modelType = call.getString("modelType");
|
|
102
|
+
String tokenizerPath = call.getString("tokenizerPath");
|
|
97
103
|
Integer maxTokens = call.getInt("maxTokens", 2048);
|
|
104
|
+
Integer sequenceLength = call.getInt("sequenceLength", maxTokens);
|
|
98
105
|
Integer topk = call.getInt("topk", 40);
|
|
99
106
|
Float temperature = call.getFloat("temperature", 0.8f);
|
|
100
|
-
|
|
107
|
+
List<String> specialTokens = getStringList(call.getArray("specialTokens"));
|
|
108
|
+
|
|
109
|
+
if (sequenceLength <= 0) {
|
|
110
|
+
call.reject("sequenceLength must be > 0");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!specialTokens.isEmpty()) {
|
|
115
|
+
call.reject("specialTokens are only supported by ExecuTorch on iOS");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
101
118
|
|
|
102
119
|
llm.setModel(
|
|
103
120
|
path,
|
|
121
|
+
engine,
|
|
122
|
+
modelType,
|
|
123
|
+
tokenizerPath,
|
|
124
|
+
specialTokens,
|
|
104
125
|
maxTokens,
|
|
126
|
+
sequenceLength,
|
|
105
127
|
topk,
|
|
106
128
|
temperature,
|
|
107
129
|
new LLM.ModelLoadCallback() {
|
|
@@ -182,6 +204,21 @@ public class LLMPlugin extends Plugin {
|
|
|
182
204
|
.start();
|
|
183
205
|
}
|
|
184
206
|
|
|
207
|
+
private List<String> getStringList(JSArray values) {
|
|
208
|
+
List<String> result = new ArrayList<>();
|
|
209
|
+
if (values == null) {
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (int i = 0; i < values.length(); i++) {
|
|
214
|
+
String value = values.optString(i, null);
|
|
215
|
+
if (value != null) {
|
|
216
|
+
result.add(value);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
185
222
|
private void downloadFile(String urlString, java.io.File outputFile, ProgressCallback callback) throws Exception {
|
|
186
223
|
java.net.URL url = new java.net.URL(urlString);
|
|
187
224
|
java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();
|
package/dist/docs.json
CHANGED
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"text": "Promise that resolves when model is loaded"
|
|
82
82
|
}
|
|
83
83
|
],
|
|
84
|
-
"docs": "Sets the model configuration\n- iOS: Use \"Apple Intelligence\" as path for system model, or
|
|
84
|
+
"docs": "Sets the model configuration\n- iOS: Use \"Apple Intelligence\" as path for system model, provide a MediaPipe model, or set engine to \"executorch\"\n- Android: Path to a MediaPipe or ExecuTorch model file (in assets or files directory)",
|
|
85
85
|
"complexTypes": [
|
|
86
86
|
"ModelOptions"
|
|
87
87
|
],
|
|
@@ -304,6 +304,13 @@
|
|
|
304
304
|
"complexTypes": [],
|
|
305
305
|
"type": "string"
|
|
306
306
|
},
|
|
307
|
+
{
|
|
308
|
+
"name": "engine",
|
|
309
|
+
"tags": [],
|
|
310
|
+
"docs": "Runtime engine to use.\n- \"auto\": uses Apple Intelligence on iOS when path is \"Apple Intelligence\", ExecuTorch for `.pte` or tokenizerPath, otherwise MediaPipe\n- \"apple\": iOS Foundation Models / Apple Intelligence\n- \"mediapipe\": MediaPipe GenAI `.task` models\n- \"executorch\": ExecuTorch `.pte` models with a tokenizer file",
|
|
311
|
+
"complexTypes": [],
|
|
312
|
+
"type": "'auto' | 'apple' | 'mediapipe' | 'executorch' | undefined"
|
|
313
|
+
},
|
|
307
314
|
{
|
|
308
315
|
"name": "modelType",
|
|
309
316
|
"tags": [],
|
|
@@ -311,6 +318,20 @@
|
|
|
311
318
|
"complexTypes": [],
|
|
312
319
|
"type": "string | undefined"
|
|
313
320
|
},
|
|
321
|
+
{
|
|
322
|
+
"name": "tokenizerPath",
|
|
323
|
+
"tags": [],
|
|
324
|
+
"docs": "Tokenizer path for ExecuTorch models. Required when engine is \"executorch\".",
|
|
325
|
+
"complexTypes": [],
|
|
326
|
+
"type": "string | undefined"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
"name": "specialTokens",
|
|
330
|
+
"tags": [],
|
|
331
|
+
"docs": "Optional special tokens passed to iOS ExecuTorch tokenizers.",
|
|
332
|
+
"complexTypes": [],
|
|
333
|
+
"type": "string[] | undefined"
|
|
334
|
+
},
|
|
314
335
|
{
|
|
315
336
|
"name": "maxTokens",
|
|
316
337
|
"tags": [],
|
|
@@ -318,6 +339,13 @@
|
|
|
318
339
|
"complexTypes": [],
|
|
319
340
|
"type": "number | undefined"
|
|
320
341
|
},
|
|
342
|
+
{
|
|
343
|
+
"name": "sequenceLength",
|
|
344
|
+
"tags": [],
|
|
345
|
+
"docs": "Sequence length for ExecuTorch generation. Defaults to maxTokens when omitted.",
|
|
346
|
+
"complexTypes": [],
|
|
347
|
+
"type": "number | undefined"
|
|
348
|
+
},
|
|
321
349
|
{
|
|
322
350
|
"name": "topk",
|
|
323
351
|
"tags": [],
|
|
@@ -28,8 +28,8 @@ export interface LLMPlugin {
|
|
|
28
28
|
}>;
|
|
29
29
|
/**
|
|
30
30
|
* Sets the model configuration
|
|
31
|
-
* - iOS: Use "Apple Intelligence" as path for system model, or
|
|
32
|
-
* - Android: Path to a MediaPipe model file (in assets or files directory)
|
|
31
|
+
* - iOS: Use "Apple Intelligence" as path for system model, provide a MediaPipe model, or set engine to "executorch"
|
|
32
|
+
* - Android: Path to a MediaPipe or ExecuTorch model file (in assets or files directory)
|
|
33
33
|
* @param options - The model configuration
|
|
34
34
|
* @returns Promise that resolves when model is loaded
|
|
35
35
|
*/
|
|
@@ -156,10 +156,24 @@ export interface ReadinessChangeEvent {
|
|
|
156
156
|
export interface ModelOptions {
|
|
157
157
|
/** Model path or "Apple Intelligence" for iOS system model */
|
|
158
158
|
path: string;
|
|
159
|
+
/**
|
|
160
|
+
* Runtime engine to use.
|
|
161
|
+
* - "auto": uses Apple Intelligence on iOS when path is "Apple Intelligence", ExecuTorch for `.pte` or tokenizerPath, otherwise MediaPipe
|
|
162
|
+
* - "apple": iOS Foundation Models / Apple Intelligence
|
|
163
|
+
* - "mediapipe": MediaPipe GenAI `.task` models
|
|
164
|
+
* - "executorch": ExecuTorch `.pte` models with a tokenizer file
|
|
165
|
+
*/
|
|
166
|
+
engine?: 'auto' | 'apple' | 'mediapipe' | 'executorch';
|
|
159
167
|
/** Model file type/extension (e.g., "task", "bin", "litertlm"). If not provided, will be extracted from path. */
|
|
160
168
|
modelType?: string;
|
|
169
|
+
/** Tokenizer path for ExecuTorch models. Required when engine is "executorch". */
|
|
170
|
+
tokenizerPath?: string;
|
|
171
|
+
/** Optional special tokens passed to iOS ExecuTorch tokenizers. */
|
|
172
|
+
specialTokens?: string[];
|
|
161
173
|
/** Maximum number of tokens the model handles */
|
|
162
174
|
maxTokens?: number;
|
|
175
|
+
/** Sequence length for ExecuTorch generation. Defaults to maxTokens when omitted. */
|
|
176
|
+
sequenceLength?: number;
|
|
163
177
|
/** Number of tokens the model considers at each step */
|
|
164
178
|
topk?: number;
|
|
165
179
|
/** Amount of randomness in generation (0.0-1.0) */
|