@capgo/capacitor-llm 8.0.19 → 8.1.0

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.
@@ -1,18 +1,15 @@
1
1
  package ee.forgr.capgo_llm;
2
2
 
3
- import com.getcapacitor.JSArray;
4
3
  import com.getcapacitor.JSObject;
5
4
  import com.getcapacitor.Plugin;
6
5
  import com.getcapacitor.PluginCall;
7
6
  import com.getcapacitor.PluginMethod;
8
7
  import com.getcapacitor.annotation.CapacitorPlugin;
9
- import java.util.ArrayList;
10
- import java.util.List;
11
8
 
12
9
  @CapacitorPlugin(name = "CapgoLLM")
13
10
  public class LLMPlugin extends Plugin {
14
11
 
15
- private final String pluginVersion = "8.0.19";
12
+ private final String pluginVersion = "8.1.0";
16
13
 
17
14
  private LLM llm;
18
15
 
@@ -44,34 +41,41 @@ public class LLMPlugin extends Plugin {
44
41
  return;
45
42
  }
46
43
 
47
- llm.sendMessage(
48
- chatId,
49
- message,
50
- new LLM.MessageCallback() {
51
- @Override
52
- public void onTextReceived(String chatId, String text, boolean isChunk) {
53
- JSObject event = new JSObject();
54
- event.put("text", text);
55
- event.put("chatId", chatId);
56
- event.put("isChunk", isChunk);
57
- notifyListeners("textFromAi", event);
58
- }
59
-
60
- @Override
61
- public void onComplete(String chatId) {
62
- JSObject event = new JSObject();
63
- event.put("chatId", chatId);
64
- notifyListeners("aiFinished", event);
65
- }
44
+ try {
45
+ llm.sendMessage(
46
+ chatId,
47
+ message,
48
+ new LLM.MessageCallback() {
49
+ @Override
50
+ public void onTextReceived(String chatId, String text, boolean isChunk) {
51
+ JSObject event = new JSObject();
52
+ event.put("text", text);
53
+ event.put("chatId", chatId);
54
+ event.put("isChunk", isChunk);
55
+ notifyListeners("textFromAi", event);
56
+ }
57
+
58
+ @Override
59
+ public void onComplete(String chatId) {
60
+ JSObject event = new JSObject();
61
+ event.put("chatId", chatId);
62
+ notifyListeners("aiFinished", event);
63
+ }
66
64
 
67
- @Override
68
- public void onError(String error) {
69
- call.reject("Failed to send message", error);
65
+ @Override
66
+ public void onError(String error) {
67
+ JSObject event = new JSObject();
68
+ event.put("chatId", chatId);
69
+ event.put("error", error);
70
+ notifyListeners("generationError", event);
71
+ }
70
72
  }
71
- }
72
- );
73
+ );
73
74
 
74
- call.resolve();
75
+ call.resolve();
76
+ } catch (IllegalStateException exception) {
77
+ call.reject(exception.getMessage());
78
+ }
75
79
  }
76
80
 
77
81
  @PluginMethod
@@ -97,35 +101,19 @@ public class LLMPlugin extends Plugin {
97
101
  }
98
102
 
99
103
  // Extract model parameters
100
- String engine = call.getString("engine", "auto");
101
- String modelType = call.getString("modelType");
102
- String tokenizerPath = call.getString("tokenizerPath");
103
104
  Integer maxTokens = call.getInt("maxTokens", 2048);
104
- Integer sequenceLength = call.getInt("sequenceLength", maxTokens);
105
105
  Integer topk = call.getInt("topk", 40);
106
106
  Float temperature = call.getFloat("temperature", 0.8f);
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
- }
107
+ Integer randomSeed = call.getInt("randomSeed", 0);
108
+ String modelType = call.getString("modelType");
118
109
 
119
110
  llm.setModel(
120
111
  path,
121
- engine,
122
112
  modelType,
123
- tokenizerPath,
124
- specialTokens,
125
113
  maxTokens,
126
- sequenceLength,
127
114
  topk,
128
115
  temperature,
116
+ randomSeed,
129
117
  new LLM.ModelLoadCallback() {
130
118
  @Override
131
119
  public void onSuccess() {
@@ -204,21 +192,6 @@ public class LLMPlugin extends Plugin {
204
192
  .start();
205
193
  }
206
194
 
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
-
222
195
  private void downloadFile(String urlString, java.io.File outputFile, ProgressCallback callback) throws Exception {
223
196
  java.net.URL url = new java.net.URL(urlString);
224
197
  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, provide a MediaPipe model, or set engine to \"executorch\"\n- Android: Path to a MediaPipe or ExecuTorch model file (in assets or files directory)",
84
+ "docs": "Sets the model configuration\n- iOS: Use \"Apple Intelligence\" as path for the system model. Custom LiteRT-LM `.litertlm` models are supported on iOS only when this plugin is integrated through Swift Package Manager.\n- Android: Use \"Gemini Nano\" for the AICore system model on supported devices where it is already available. Prefer LiteRT-LM `.litertlm` bundles for custom models; legacy MediaPipe `.task` models are still supported\n- Web: Provide a web-ready model asset for `@mediapipe/tasks-genai` such as Gemma 4 `*-web.task`",
85
85
  "complexTypes": [
86
86
  "ModelOptions"
87
87
  ],
@@ -187,6 +187,42 @@
187
187
  ],
188
188
  "slug": "addlisteneraifinished-"
189
189
  },
190
+ {
191
+ "name": "addListener",
192
+ "signature": "(eventName: 'generationError', listenerFunc: (event: GenerationErrorEvent) => void) => Promise<{ remove: () => Promise<void>; }>",
193
+ "parameters": [
194
+ {
195
+ "name": "eventName",
196
+ "docs": "- Event name 'generationError'",
197
+ "type": "'generationError'"
198
+ },
199
+ {
200
+ "name": "listenerFunc",
201
+ "docs": "- Callback function for generation errors",
202
+ "type": "(event: GenerationErrorEvent) => void"
203
+ }
204
+ ],
205
+ "returns": "Promise<{ remove: () => Promise<void>; }>",
206
+ "tags": [
207
+ {
208
+ "name": "param",
209
+ "text": "eventName - Event name 'generationError'"
210
+ },
211
+ {
212
+ "name": "param",
213
+ "text": "listenerFunc - Callback function for generation errors"
214
+ },
215
+ {
216
+ "name": "returns",
217
+ "text": "Promise with remove function to unsubscribe"
218
+ }
219
+ ],
220
+ "docs": "Adds a listener for generation failures that happen after streaming starts",
221
+ "complexTypes": [
222
+ "GenerationErrorEvent"
223
+ ],
224
+ "slug": "addlistenergenerationerror-"
225
+ },
190
226
  {
191
227
  "name": "addListener",
192
228
  "signature": "(eventName: 'downloadProgress', listenerFunc: (event: DownloadProgressEvent) => void) => Promise<{ remove: () => Promise<void>; }>",
@@ -293,45 +329,24 @@
293
329
  {
294
330
  "name": "ModelOptions",
295
331
  "slug": "modeloptions",
296
- "docs": "Model configuration options",
332
+ "docs": "Model configuration options\nOnly `path` is required. All other properties are optional overrides.",
297
333
  "tags": [],
298
334
  "methods": [],
299
335
  "properties": [
300
336
  {
301
337
  "name": "path",
302
338
  "tags": [],
303
- "docs": "Model path or \"Apple Intelligence\" for iOS system model",
339
+ "docs": "Model path, \"Apple Intelligence\" for the Apple system model on iOS, or \"Gemini Nano\" for the Android AICore system model when already available on the device. On iOS, custom `.litertlm` models require the plugin to be integrated through Swift Package Manager. Gemma 4 examples use `.litertlm` on mobile and `*-web.task` on web.",
304
340
  "complexTypes": [],
305
341
  "type": "string"
306
342
  },
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
- },
314
343
  {
315
344
  "name": "modelType",
316
345
  "tags": [],
317
- "docs": "Model file type/extension (e.g., \"task\", \"bin\", \"litertlm\"). If not provided, will be extracted from path.",
346
+ "docs": "Optional. Model file type/extension (for example `task`, `bin`, `litertlm`, or `gemini-nano`). If not provided, it is extracted from the path. Use `litertlm` on iOS only in SwiftPM integrations.",
318
347
  "complexTypes": [],
319
348
  "type": "string | undefined"
320
349
  },
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
- },
335
350
  {
336
351
  "name": "maxTokens",
337
352
  "tags": [],
@@ -339,13 +354,6 @@
339
354
  "complexTypes": [],
340
355
  "type": "number | undefined"
341
356
  },
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
- },
349
357
  {
350
358
  "name": "topk",
351
359
  "tags": [],
@@ -363,7 +371,7 @@
363
371
  {
364
372
  "name": "randomSeed",
365
373
  "tags": [],
366
- "docs": "Random seed for generation",
374
+ "docs": "Optional. Random seed for generation.",
367
375
  "complexTypes": [],
368
376
  "type": "number | undefined"
369
377
  }
@@ -395,7 +403,7 @@
395
403
  {
396
404
  "name": "DownloadModelOptions",
397
405
  "slug": "downloadmodeloptions",
398
- "docs": "Options for downloading a model",
406
+ "docs": "Options for downloading a model\nOnly `url` is required. `companionUrl` and `filename` are optional.",
399
407
  "tags": [],
400
408
  "methods": [],
401
409
  "properties": [
@@ -409,7 +417,7 @@
409
417
  {
410
418
  "name": "companionUrl",
411
419
  "tags": [],
412
- "docs": "Optional: URL of companion file (e.g., .litertlm for Android)",
420
+ "docs": "Optional: URL of a companion file for legacy model formats",
413
421
  "complexTypes": [],
414
422
  "type": "string | undefined"
415
423
  },
@@ -468,6 +476,29 @@
468
476
  }
469
477
  ]
470
478
  },
479
+ {
480
+ "name": "GenerationErrorEvent",
481
+ "slug": "generationerrorevent",
482
+ "docs": "Event data for generation failures\n`chatId` is optional and may be omitted when the failure is not tied to a specific chat.",
483
+ "tags": [],
484
+ "methods": [],
485
+ "properties": [
486
+ {
487
+ "name": "chatId",
488
+ "tags": [],
489
+ "docs": "Optional. The chat session ID that failed, when available.",
490
+ "complexTypes": [],
491
+ "type": "string | undefined"
492
+ },
493
+ {
494
+ "name": "error",
495
+ "tags": [],
496
+ "docs": "Error message describing the failure",
497
+ "complexTypes": [],
498
+ "type": "string"
499
+ }
500
+ ]
501
+ },
471
502
  {
472
503
  "name": "DownloadProgressEvent",
473
504
  "slug": "downloadprogressevent",
@@ -28,8 +28,9 @@ export interface LLMPlugin {
28
28
  }>;
29
29
  /**
30
30
  * Sets the model configuration
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)
31
+ * - iOS: Use "Apple Intelligence" as path for the system model. Custom LiteRT-LM `.litertlm` models are supported on iOS only when this plugin is integrated through Swift Package Manager.
32
+ * - Android: Use "Gemini Nano" for the AICore system model on supported devices where it is already available. Prefer LiteRT-LM `.litertlm` bundles for custom models; legacy MediaPipe `.task` models are still supported
33
+ * - Web: Provide a web-ready model asset for `@mediapipe/tasks-genai` such as Gemma 4 `*-web.task`
33
34
  * @param options - The model configuration
34
35
  * @returns Promise that resolves when model is loaded
35
36
  */
@@ -60,6 +61,15 @@ export interface LLMPlugin {
60
61
  addListener(eventName: 'aiFinished', listenerFunc: (event: AiFinishedEvent) => void): Promise<{
61
62
  remove: () => Promise<void>;
62
63
  }>;
64
+ /**
65
+ * Adds a listener for generation failures that happen after streaming starts
66
+ * @param eventName - Event name 'generationError'
67
+ * @param listenerFunc - Callback function for generation errors
68
+ * @returns Promise with remove function to unsubscribe
69
+ */
70
+ addListener(eventName: 'generationError', listenerFunc: (event: GenerationErrorEvent) => void): Promise<{
71
+ remove: () => Promise<void>;
72
+ }>;
63
73
  /**
64
74
  * Adds a listener for model download progress events
65
75
  * @param eventName - Event name 'downloadProgress'
@@ -112,13 +122,24 @@ export interface AiFinishedEvent {
112
122
  /** The chat session ID that finished */
113
123
  chatId: string;
114
124
  }
125
+ /**
126
+ * Event data for generation failures
127
+ * `chatId` is optional and may be omitted when the failure is not tied to a specific chat.
128
+ */
129
+ export interface GenerationErrorEvent {
130
+ /** Optional. The chat session ID that failed, when available. */
131
+ chatId?: string;
132
+ /** Error message describing the failure */
133
+ error: string;
134
+ }
115
135
  /**
116
136
  * Options for downloading a model
137
+ * Only `url` is required. `companionUrl` and `filename` are optional.
117
138
  */
118
139
  export interface DownloadModelOptions {
119
140
  /** URL of the model file to download */
120
141
  url: string;
121
- /** Optional: URL of companion file (e.g., .litertlm for Android) */
142
+ /** Optional: URL of a companion file for legacy model formats */
122
143
  companionUrl?: string;
123
144
  /** Optional: Custom filename (defaults to filename from URL) */
124
145
  filename?: string;
@@ -152,32 +173,19 @@ export interface ReadinessChangeEvent {
152
173
  }
153
174
  /**
154
175
  * Model configuration options
176
+ * Only `path` is required. All other properties are optional overrides.
155
177
  */
156
178
  export interface ModelOptions {
157
- /** Model path or "Apple Intelligence" for iOS system model */
179
+ /** Model path, "Apple Intelligence" for the Apple system model on iOS, or "Gemini Nano" for the Android AICore system model when already available on the device. On iOS, custom `.litertlm` models require the plugin to be integrated through Swift Package Manager. Gemma 4 examples use `.litertlm` on mobile and `*-web.task` on web. */
158
180
  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';
167
- /** Model file type/extension (e.g., "task", "bin", "litertlm"). If not provided, will be extracted from path. */
181
+ /** Optional. Model file type/extension (for example `task`, `bin`, `litertlm`, or `gemini-nano`). If not provided, it is extracted from the path. Use `litertlm` on iOS only in SwiftPM integrations. */
168
182
  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[];
173
183
  /** Maximum number of tokens the model handles */
174
184
  maxTokens?: number;
175
- /** Sequence length for ExecuTorch generation. Defaults to maxTokens when omitted. */
176
- sequenceLength?: number;
177
185
  /** Number of tokens the model considers at each step */
178
186
  topk?: number;
179
187
  /** Amount of randomness in generation (0.0-1.0) */
180
188
  temperature?: number;
181
- /** Random seed for generation */
189
+ /** Optional. Random seed for generation. */
182
190
  randomSeed?: number;
183
191
  }
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * LLM Plugin interface for interacting with on-device language models\n */\nexport interface LLMPlugin {\n /**\n * Creates a new chat session\n * @returns Promise with chat id and optional instructions\n */\n createChat(): Promise<{ id: string; instructions?: string }>;\n\n /**\n * Sends a message to the AI in a specific chat session\n * @param options - The chat id and message to send\n * @returns Promise that resolves when message is sent\n */\n sendMessage(options: { chatId: string; message: string }): Promise<void>;\n\n /**\n * Gets the readiness status of the LLM\n * @returns Promise with readiness status string\n */\n getReadiness(): Promise<{ readiness: string }>;\n\n /**\n * 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)\n * @param options - The model configuration\n * @returns Promise that resolves when model is loaded\n */\n setModel(options: ModelOptions): Promise<void>;\n\n /**\n * Downloads a model from a URL and saves it to the appropriate location\n * - iOS: Downloads to the app's documents directory\n * - Android: Downloads to the app's files directory\n * @param options - The download configuration\n * @returns Promise with download result including the saved path\n */\n downloadModel(options: DownloadModelOptions): Promise<DownloadModelResult>;\n\n /**\n * Adds a listener for text received from AI\n * @param eventName - Event name 'textFromAi'\n * @param listenerFunc - Callback function for text events\n * @returns Promise with remove function to unsubscribe\n */\n addListener(\n eventName: 'textFromAi',\n listenerFunc: (event: TextFromAiEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Adds a listener for AI completion events\n * @param eventName - Event name 'aiFinished'\n * @param listenerFunc - Callback function for finish events\n * @returns Promise with remove function to unsubscribe\n */\n addListener(\n eventName: 'aiFinished',\n listenerFunc: (event: AiFinishedEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Adds a listener for model download progress events\n * @param eventName - Event name 'downloadProgress'\n * @param listenerFunc - Callback function for progress events\n * @returns Promise with remove function to unsubscribe\n */\n addListener(\n eventName: 'downloadProgress',\n listenerFunc: (event: DownloadProgressEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Adds a listener for readiness status changes\n * @param eventName - Event name 'readinessChange'\n * @param listenerFunc - Callback function for readiness events\n * @returns Promise with remove function to unsubscribe\n */\n addListener(\n eventName: 'readinessChange',\n listenerFunc: (event: ReadinessChangeEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Get the native Capacitor plugin version.\n *\n * @returns Promise that resolves with the plugin version\n * @throws Error if getting the version fails\n * @since 1.0.0\n * @example\n * ```typescript\n * const { version } = await CapgoLLM.getPluginVersion();\n * console.log('Plugin version:', version);\n * ```\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\n\n/**\n * Event data for text received from AI\n */\nexport interface TextFromAiEvent {\n /** The text content from AI - this is an incremental chunk, not the full text */\n text: string;\n /** The chat session ID */\n chatId: string;\n /** Whether this is a complete chunk (true) or partial streaming data (false) */\n isChunk?: boolean;\n}\n\n/**\n * Event data for AI completion\n */\nexport interface AiFinishedEvent {\n /** The chat session ID that finished */\n chatId: string;\n}\n\n/**\n * Options for downloading a model\n */\nexport interface DownloadModelOptions {\n /** URL of the model file to download */\n url: string;\n /** Optional: URL of companion file (e.g., .litertlm for Android) */\n companionUrl?: string;\n /** Optional: Custom filename (defaults to filename from URL) */\n filename?: string;\n}\n\n/**\n * Result of model download\n */\nexport interface DownloadModelResult {\n /** Path where the model was saved */\n path: string;\n /** Path where the companion file was saved (if applicable) */\n companionPath?: string;\n}\n\n/**\n * Event data for download progress\n */\nexport interface DownloadProgressEvent {\n /** Percentage of download completed (0-100) */\n progress: number;\n /** Total bytes to download */\n totalBytes?: number;\n /** Bytes downloaded so far */\n downloadedBytes?: number;\n}\n\n/**\n * Event data for readiness status changes\n */\nexport interface ReadinessChangeEvent {\n /** The readiness status */\n readiness: string;\n}\n\n/**\n * Model configuration options\n */\nexport interface ModelOptions {\n /** Model path or \"Apple Intelligence\" for iOS system model */\n path: string;\n /**\n * 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\n */\n engine?: 'auto' | 'apple' | 'mediapipe' | 'executorch';\n /** Model file type/extension (e.g., \"task\", \"bin\", \"litertlm\"). If not provided, will be extracted from path. */\n modelType?: string;\n /** Tokenizer path for ExecuTorch models. Required when engine is \"executorch\". */\n tokenizerPath?: string;\n /** Optional special tokens passed to iOS ExecuTorch tokenizers. */\n specialTokens?: string[];\n /** Maximum number of tokens the model handles */\n maxTokens?: number;\n /** Sequence length for ExecuTorch generation. Defaults to maxTokens when omitted. */\n sequenceLength?: number;\n /** Number of tokens the model considers at each step */\n topk?: number;\n /** Amount of randomness in generation (0.0-1.0) */\n temperature?: number;\n /** Random seed for generation */\n randomSeed?: number;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * LLM Plugin interface for interacting with on-device language models\n */\nexport interface LLMPlugin {\n /**\n * Creates a new chat session\n * @returns Promise with chat id and optional instructions\n */\n createChat(): Promise<{ id: string; instructions?: string }>;\n\n /**\n * Sends a message to the AI in a specific chat session\n * @param options - The chat id and message to send\n * @returns Promise that resolves when message is sent\n */\n sendMessage(options: { chatId: string; message: string }): Promise<void>;\n\n /**\n * Gets the readiness status of the LLM\n * @returns Promise with readiness status string\n */\n getReadiness(): Promise<{ readiness: string }>;\n\n /**\n * Sets the model configuration\n * - iOS: Use \"Apple Intelligence\" as path for the system model. Custom LiteRT-LM `.litertlm` models are supported on iOS only when this plugin is integrated through Swift Package Manager.\n * - Android: Use \"Gemini Nano\" for the AICore system model on supported devices where it is already available. Prefer LiteRT-LM `.litertlm` bundles for custom models; legacy MediaPipe `.task` models are still supported\n * - Web: Provide a web-ready model asset for `@mediapipe/tasks-genai` such as Gemma 4 `*-web.task`\n * @param options - The model configuration\n * @returns Promise that resolves when model is loaded\n */\n setModel(options: ModelOptions): Promise<void>;\n\n /**\n * Downloads a model from a URL and saves it to the appropriate location\n * - iOS: Downloads to the app's documents directory\n * - Android: Downloads to the app's files directory\n * @param options - The download configuration\n * @returns Promise with download result including the saved path\n */\n downloadModel(options: DownloadModelOptions): Promise<DownloadModelResult>;\n\n /**\n * Adds a listener for text received from AI\n * @param eventName - Event name 'textFromAi'\n * @param listenerFunc - Callback function for text events\n * @returns Promise with remove function to unsubscribe\n */\n addListener(\n eventName: 'textFromAi',\n listenerFunc: (event: TextFromAiEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Adds a listener for AI completion events\n * @param eventName - Event name 'aiFinished'\n * @param listenerFunc - Callback function for finish events\n * @returns Promise with remove function to unsubscribe\n */\n addListener(\n eventName: 'aiFinished',\n listenerFunc: (event: AiFinishedEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Adds a listener for generation failures that happen after streaming starts\n * @param eventName - Event name 'generationError'\n * @param listenerFunc - Callback function for generation errors\n * @returns Promise with remove function to unsubscribe\n */\n addListener(\n eventName: 'generationError',\n listenerFunc: (event: GenerationErrorEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Adds a listener for model download progress events\n * @param eventName - Event name 'downloadProgress'\n * @param listenerFunc - Callback function for progress events\n * @returns Promise with remove function to unsubscribe\n */\n addListener(\n eventName: 'downloadProgress',\n listenerFunc: (event: DownloadProgressEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Adds a listener for readiness status changes\n * @param eventName - Event name 'readinessChange'\n * @param listenerFunc - Callback function for readiness events\n * @returns Promise with remove function to unsubscribe\n */\n addListener(\n eventName: 'readinessChange',\n listenerFunc: (event: ReadinessChangeEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Get the native Capacitor plugin version.\n *\n * @returns Promise that resolves with the plugin version\n * @throws Error if getting the version fails\n * @since 1.0.0\n * @example\n * ```typescript\n * const { version } = await CapgoLLM.getPluginVersion();\n * console.log('Plugin version:', version);\n * ```\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\n\n/**\n * Event data for text received from AI\n */\nexport interface TextFromAiEvent {\n /** The text content from AI - this is an incremental chunk, not the full text */\n text: string;\n /** The chat session ID */\n chatId: string;\n /** Whether this is a complete chunk (true) or partial streaming data (false) */\n isChunk?: boolean;\n}\n\n/**\n * Event data for AI completion\n */\nexport interface AiFinishedEvent {\n /** The chat session ID that finished */\n chatId: string;\n}\n\n/**\n * Event data for generation failures\n * `chatId` is optional and may be omitted when the failure is not tied to a specific chat.\n */\nexport interface GenerationErrorEvent {\n /** Optional. The chat session ID that failed, when available. */\n chatId?: string;\n /** Error message describing the failure */\n error: string;\n}\n\n/**\n * Options for downloading a model\n * Only `url` is required. `companionUrl` and `filename` are optional.\n */\nexport interface DownloadModelOptions {\n /** URL of the model file to download */\n url: string;\n /** Optional: URL of a companion file for legacy model formats */\n companionUrl?: string;\n /** Optional: Custom filename (defaults to filename from URL) */\n filename?: string;\n}\n\n/**\n * Result of model download\n */\nexport interface DownloadModelResult {\n /** Path where the model was saved */\n path: string;\n /** Path where the companion file was saved (if applicable) */\n companionPath?: string;\n}\n\n/**\n * Event data for download progress\n */\nexport interface DownloadProgressEvent {\n /** Percentage of download completed (0-100) */\n progress: number;\n /** Total bytes to download */\n totalBytes?: number;\n /** Bytes downloaded so far */\n downloadedBytes?: number;\n}\n\n/**\n * Event data for readiness status changes\n */\nexport interface ReadinessChangeEvent {\n /** The readiness status */\n readiness: string;\n}\n\n/**\n * Model configuration options\n * Only `path` is required. All other properties are optional overrides.\n */\nexport interface ModelOptions {\n /** Model path, \"Apple Intelligence\" for the Apple system model on iOS, or \"Gemini Nano\" for the Android AICore system model when already available on the device. On iOS, custom `.litertlm` models require the plugin to be integrated through Swift Package Manager. Gemma 4 examples use `.litertlm` on mobile and `*-web.task` on web. */\n path: string;\n /** Optional. Model file type/extension (for example `task`, `bin`, `litertlm`, or `gemini-nano`). If not provided, it is extracted from the path. Use `litertlm` on iOS only in SwiftPM integrations. */\n modelType?: string;\n /** Maximum number of tokens the model handles */\n maxTokens?: number;\n /** Number of tokens the model considers at each step */\n topk?: number;\n /** Amount of randomness in generation (0.0-1.0) */\n temperature?: number;\n /** Optional. Random seed for generation. */\n randomSeed?: number;\n}\n"]}
package/dist/esm/web.d.ts CHANGED
@@ -4,6 +4,8 @@ export declare class CapgoLLMWeb extends WebPlugin implements LLMPlugin {
4
4
  private llm;
5
5
  private chatSessions;
6
6
  private readiness;
7
+ private modelPath;
8
+ private modelType;
7
9
  getReadiness(): Promise<{
8
10
  readiness: string;
9
11
  }>;
@@ -20,4 +22,8 @@ export declare class CapgoLLMWeb extends WebPlugin implements LLMPlugin {
20
22
  getPluginVersion(): Promise<{
21
23
  version: string;
22
24
  }>;
25
+ private closeCurrentModel;
26
+ private resolveModelType;
27
+ private buildPrompt;
28
+ private usesGemmaChatTemplate;
23
29
  }
package/dist/esm/web.js CHANGED
@@ -1,10 +1,3 @@
1
- var __asyncValues = (this && this.__asyncValues) || function (o) {
2
- if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
3
- var m = o[Symbol.asyncIterator], i;
4
- return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
5
- function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
6
- function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
7
- };
8
1
  import { WebPlugin } from '@capacitor/core';
9
2
  import { FilesetResolver, LlmInference } from '@mediapipe/tasks-genai';
10
3
  export class CapgoLLMWeb extends WebPlugin {
@@ -13,6 +6,8 @@ export class CapgoLLMWeb extends WebPlugin {
13
6
  this.llm = null;
14
7
  this.chatSessions = new Map();
15
8
  this.readiness = 'not-loaded';
9
+ this.modelPath = '';
10
+ this.modelType = 'task';
16
11
  }
17
12
  async getReadiness() {
18
13
  return { readiness: this.readiness };
@@ -26,11 +21,13 @@ export class CapgoLLMWeb extends WebPlugin {
26
21
  id: chatId,
27
22
  llm: this.llm,
28
23
  isActive: true,
24
+ modelPath: this.modelPath,
25
+ modelType: this.modelType,
26
+ history: [],
29
27
  });
30
28
  return { id: chatId };
31
29
  }
32
30
  async sendMessage(options) {
33
- var _a, e_1, _b, _c;
34
31
  const session = this.chatSessions.get(options.chatId);
35
32
  if (!session) {
36
33
  throw new Error(`Chat session ${options.chatId} not found`);
@@ -38,29 +35,31 @@ export class CapgoLLMWeb extends WebPlugin {
38
35
  if (!session.isActive) {
39
36
  throw new Error(`Chat session ${options.chatId} is not active`);
40
37
  }
38
+ let hasStreamed = false;
39
+ let responseText = '';
41
40
  try {
42
- // Generate response using MediaPipe GenAI streaming API
43
- const response = session.llm.generateResponseStream(options.message);
44
- try {
45
- for (var _d = true, response_1 = __asyncValues(response), response_1_1; response_1_1 = await response_1.next(), _a = response_1_1.done, !_a; _d = true) {
46
- _c = response_1_1.value;
47
- _d = false;
48
- const partialResponse = _c;
49
- // Send incremental text
50
- this.notifyListeners('textFromAi', {
51
- text: partialResponse,
52
- chatId: options.chatId,
53
- isChunk: true,
54
- });
41
+ const prompt = this.buildPrompt(session, options.message);
42
+ const finalResponse = await session.llm.generateResponse(prompt, (partialResponse, done) => {
43
+ if (done || !partialResponse) {
44
+ return;
55
45
  }
46
+ hasStreamed = true;
47
+ responseText += partialResponse;
48
+ this.notifyListeners('textFromAi', {
49
+ text: partialResponse,
50
+ chatId: options.chatId,
51
+ isChunk: true,
52
+ });
53
+ });
54
+ if (!hasStreamed && finalResponse) {
55
+ responseText = finalResponse;
56
+ this.notifyListeners('textFromAi', {
57
+ text: finalResponse,
58
+ chatId: options.chatId,
59
+ isChunk: true,
60
+ });
56
61
  }
57
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
58
- finally {
59
- try {
60
- if (!_d && !_a && (_b = response_1.return)) await _b.call(response_1);
61
- }
62
- finally { if (e_1) throw e_1.error; }
63
- }
62
+ session.history.push({ role: 'user', content: options.message }, { role: 'model', content: responseText || finalResponse });
64
63
  // Notify completion
65
64
  this.notifyListeners('aiFinished', {
66
65
  chatId: options.chatId,
@@ -68,22 +67,20 @@ export class CapgoLLMWeb extends WebPlugin {
68
67
  }
69
68
  catch (error) {
70
69
  console.error('Error generating response:', error);
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ if (hasStreamed) {
72
+ this.notifyListeners('generationError', {
73
+ chatId: options.chatId,
74
+ error: message,
75
+ });
76
+ return;
77
+ }
71
78
  throw error;
72
79
  }
73
80
  }
74
81
  async setModel(options) {
75
- var _a;
76
- const engine = (_a = options.engine) !== null && _a !== void 0 ? _a : 'auto';
77
- const normalizedPath = options.path.toLowerCase();
78
- const wantsApple = engine === 'apple' || (engine === 'auto' && normalizedPath === 'apple intelligence');
79
- const wantsExecuTorch = engine === 'executorch' || (engine === 'auto' && (options.tokenizerPath || normalizedPath.endsWith('.pte')));
80
- if (wantsApple) {
81
- throw new Error('Apple Intelligence is only available on native iOS.');
82
- }
83
- if (wantsExecuTorch) {
84
- throw new Error('ExecuTorch is only available on native iOS and Android.');
85
- }
86
82
  try {
83
+ this.closeCurrentModel();
87
84
  // Update readiness
88
85
  this.readiness = 'loading';
89
86
  this.notifyListeners('readinessChange', { readiness: this.readiness });
@@ -100,11 +97,15 @@ export class CapgoLLMWeb extends WebPlugin {
100
97
  const genai = await FilesetResolver.forGenAiTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-genai@latest/wasm');
101
98
  // Create LLM instance
102
99
  this.llm = await LlmInference.createFromOptions(genai, config);
100
+ this.modelPath = options.path;
101
+ this.modelType = this.resolveModelType(options);
102
+ this.chatSessions.clear();
103
103
  // Update readiness
104
104
  this.readiness = 'ready';
105
105
  this.notifyListeners('readinessChange', { readiness: this.readiness });
106
106
  }
107
107
  catch (error) {
108
+ this.closeCurrentModel();
108
109
  this.readiness = 'error';
109
110
  this.notifyListeners('readinessChange', { readiness: this.readiness });
110
111
  throw error;
@@ -168,5 +169,33 @@ export class CapgoLLMWeb extends WebPlugin {
168
169
  async getPluginVersion() {
169
170
  return { version: 'web' };
170
171
  }
172
+ closeCurrentModel() {
173
+ var _a;
174
+ this.chatSessions.clear();
175
+ (_a = this.llm) === null || _a === void 0 ? void 0 : _a.close();
176
+ this.llm = null;
177
+ this.modelPath = '';
178
+ this.modelType = 'task';
179
+ }
180
+ resolveModelType(options) {
181
+ var _a;
182
+ if ((_a = options.modelType) === null || _a === void 0 ? void 0 : _a.trim()) {
183
+ return options.modelType.trim().toLowerCase();
184
+ }
185
+ const extension = options.path.split('.').pop();
186
+ return extension ? extension.toLowerCase() : 'task';
187
+ }
188
+ buildPrompt(session, message) {
189
+ if (!this.usesGemmaChatTemplate(session)) {
190
+ return message;
191
+ }
192
+ const history = [...session.history, { role: 'user', content: message }];
193
+ return `${history
194
+ .map((turn) => `<start_of_turn>${turn.role}\n${turn.content}<end_of_turn>`)
195
+ .join('\n')}\n<start_of_turn>model\n`;
196
+ }
197
+ usesGemmaChatTemplate(session) {
198
+ return /gemma/i.test(session.modelPath) || session.modelType === 'litertlm';
199
+ }
171
200
  }
172
201
  //# sourceMappingURL=web.js.map