@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.
@@ -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 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
- public void setModel(String path, Integer maxTokens, Integer topk, Float temperature, ModelLoadCallback callback) {
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
- isReady = false;
46
-
47
- // If LLM was already initialized, clean it up
48
- if (llmInference != null) {
49
- llmInference = null;
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 path: " + path + ", maxTokens: " + maxTokens + ", topk: " + topk + ", temperature: " + temperature
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
- String actualPath = modelPath;
70
-
71
- // Debug logging
72
- android.util.Log.d("LLM", "Original model path: " + modelPath);
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
- java.io.InputStream is = context.getAssets().open(assetPath);
144
- java.io.FileOutputStream fos = new java.io.FileOutputStream(destFile);
145
- byte[] buffer = new byte[1024];
146
- int length;
147
- while ((length = is.read(buffer)) > 0) {
148
- fos.write(buffer, 0, length);
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 || llmInference == null) {
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(topk)
186
- .setTemperature(temperature)
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(llmInference, sessionOptions);
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
- // Process any remaining content when done
248
- if (done && buffer.length() > 0) {
249
- String remaining = buffer.toString();
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
- // For Gemma with MediaPipe, just return the message as-is
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.11";
15
+ private final String pluginVersion = "8.0.12";
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
- Integer randomSeed = call.getInt("randomSeed", 101);
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 provide path to MediaPipe model\n- Android: Path to a MediaPipe model file (in assets or files directory)",
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 provide path to MediaPipe model
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) */