expo-ai-kit 0.4.1 → 0.6.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.
- package/README.md +51 -89
- package/android/src/main/java/expo/modules/aikit/ExpoAiKitModule.kt +26 -13
- package/android/src/main/java/expo/modules/aikit/GemmaInferenceClient.kt +47 -2
- package/build/ExpoAiKitModule.d.ts +12 -3
- package/build/ExpoAiKitModule.d.ts.map +1 -1
- package/build/ExpoAiKitModule.js.map +1 -1
- package/build/index.d.ts +73 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +338 -34
- package/build/index.js.map +1 -1
- package/build/models.js +6 -6
- package/build/models.js.map +1 -1
- package/build/structured.d.ts +36 -0
- package/build/structured.d.ts.map +1 -0
- package/build/structured.js +190 -0
- package/build/structured.js.map +1 -0
- package/build/types.d.ts +128 -2
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/ios/ExpoAiKit.podspec +11 -5
- package/ios/ExpoAiKitModule.swift +255 -95
- package/ios/GemmaInferenceClient.swift +408 -0
- package/ios/Vendor/LiteRTLM/Benchmark.swift +83 -0
- package/ios/Vendor/LiteRTLM/Capabilities.swift +41 -0
- package/ios/Vendor/LiteRTLM/Config.swift +172 -0
- package/ios/Vendor/LiteRTLM/Conversation.swift +450 -0
- package/ios/Vendor/LiteRTLM/Engine.swift +208 -0
- package/ios/Vendor/LiteRTLM/ExperimentalFlags.swift +142 -0
- package/ios/Vendor/LiteRTLM/LICENSE +201 -0
- package/ios/Vendor/LiteRTLM/LiteRTLMError.swift +156 -0
- package/ios/Vendor/LiteRTLM/Message.swift +225 -0
- package/ios/Vendor/LiteRTLM/Tool.swift +291 -0
- package/ios/Vendor/LiteRTLM/ToolManager.swift +152 -0
- package/package.json +9 -3
- package/scripts/install-litertlm.sh +63 -0
- package/src/ExpoAiKitModule.ts +25 -3
- package/src/index.ts +415 -58
- package/src/models.ts +6 -6
- package/src/structured.ts +202 -0
- package/src/types.ts +150 -1
- package/ios/.xcode.env +0 -11
package/build/index.js
CHANGED
|
@@ -1,13 +1,112 @@
|
|
|
1
1
|
import ExpoAiKitModule from './ExpoAiKitModule';
|
|
2
2
|
import { Platform } from 'react-native';
|
|
3
3
|
import { ModelError, } from './types';
|
|
4
|
+
import { buildSchemaInstruction, buildSchemaRepair, extractJson, validateAgainstSchema, REPAIR_INVALID_JSON, } from './structured';
|
|
4
5
|
import { MODEL_REGISTRY, getRegistryEntry } from './models';
|
|
5
6
|
export * from './types';
|
|
6
7
|
export * from './models';
|
|
7
8
|
const DEFAULT_SYSTEM_PROMPT = 'You are a helpful, friendly assistant. Answer the user directly and concisely.';
|
|
9
|
+
const DEFAULT_OBJECT_SYSTEM_PROMPT = 'You output structured data as JSON. Follow the provided JSON Schema exactly.';
|
|
8
10
|
let streamIdCounter = 0;
|
|
9
11
|
function generateSessionId() {
|
|
10
|
-
return `
|
|
12
|
+
return `gen_${Date.now()}_${++streamIdCounter}`;
|
|
13
|
+
}
|
|
14
|
+
// The set of codes the native layer encodes in error messages as "CODE:modelId:reason".
|
|
15
|
+
const KNOWN_ERROR_CODES = new Set([
|
|
16
|
+
'MODEL_NOT_FOUND',
|
|
17
|
+
'MODEL_NOT_DOWNLOADED',
|
|
18
|
+
'DOWNLOAD_FAILED',
|
|
19
|
+
'DOWNLOAD_CORRUPT',
|
|
20
|
+
'DOWNLOAD_STORAGE_FULL',
|
|
21
|
+
'DOWNLOAD_CANCELLED',
|
|
22
|
+
'INFERENCE_OOM',
|
|
23
|
+
'INFERENCE_FAILED',
|
|
24
|
+
'INFERENCE_BUSY',
|
|
25
|
+
'INFERENCE_CANCELLED',
|
|
26
|
+
'MODEL_LOAD_FAILED',
|
|
27
|
+
'DEVICE_NOT_SUPPORTED',
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* Normalize an error from the native layer into a {@link ModelError}.
|
|
31
|
+
*
|
|
32
|
+
* The native modules format failures as "CODE:modelId:reason" (see the
|
|
33
|
+
* GemmaError/GemmaInferenceClient contract). Expo surfaces that string as the
|
|
34
|
+
* error's message, so we parse it here and rethrow a typed ModelError with a
|
|
35
|
+
* reliable `.code` and `.modelId`. Anything unrecognized becomes UNKNOWN.
|
|
36
|
+
*/
|
|
37
|
+
function toModelError(e) {
|
|
38
|
+
if (e instanceof ModelError)
|
|
39
|
+
throw e;
|
|
40
|
+
const message = String(e?.message ?? e ?? '');
|
|
41
|
+
const match = /^([A-Z_]+):([^:]*):([\s\S]*)$/.exec(message);
|
|
42
|
+
if (match && KNOWN_ERROR_CODES.has(match[1])) {
|
|
43
|
+
throw new ModelError(match[1], match[2], match[3]);
|
|
44
|
+
}
|
|
45
|
+
throw new ModelError('UNKNOWN', '', message);
|
|
46
|
+
}
|
|
47
|
+
/** Run a native promise, normalizing any rejection into a ModelError. */
|
|
48
|
+
async function wrapNative(run) {
|
|
49
|
+
try {
|
|
50
|
+
return await run();
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
toModelError(e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Single-flight inference guard
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// On-device models are backed by a single native context + KV cache that is not
|
|
60
|
+
// safe for concurrent decodes (interleaving can corrupt the cache and crash the
|
|
61
|
+
// native side). JS is single-threaded, so a synchronous check-and-set of this
|
|
62
|
+
// flag before any `await` is race-free. The flag is shared by sendMessage and
|
|
63
|
+
// streamMessage and is held until the *native* call settles — not until an
|
|
64
|
+
// early abort — so a detached-but-still-running generation still blocks a new one.
|
|
65
|
+
let inferenceInFlight = false;
|
|
66
|
+
function acquireInference() {
|
|
67
|
+
if (inferenceInFlight) {
|
|
68
|
+
throw new ModelError('INFERENCE_BUSY', '', 'A generation is already in flight. Wait for it to finish, or stop the active stream first.');
|
|
69
|
+
}
|
|
70
|
+
inferenceInFlight = true;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Map the public GenerationConfig to the native shape, dropping undefined fields
|
|
74
|
+
* and validating ranges up front so callers get a clear error instead of an
|
|
75
|
+
* opaque native MODEL_LOAD_FAILED from the sampler.
|
|
76
|
+
*/
|
|
77
|
+
function toNativeGeneration(g) {
|
|
78
|
+
const out = {};
|
|
79
|
+
if (g?.temperature != null) {
|
|
80
|
+
if (g.temperature < 0) {
|
|
81
|
+
throw new Error('generation.temperature must be >= 0');
|
|
82
|
+
}
|
|
83
|
+
out.temperature = g.temperature;
|
|
84
|
+
}
|
|
85
|
+
if (g?.topK != null) {
|
|
86
|
+
if (!Number.isInteger(g.topK) || g.topK <= 0) {
|
|
87
|
+
throw new Error('generation.topK must be a positive integer');
|
|
88
|
+
}
|
|
89
|
+
out.topK = g.topK;
|
|
90
|
+
}
|
|
91
|
+
if (g?.topP != null) {
|
|
92
|
+
if (g.topP < 0 || g.topP > 1) {
|
|
93
|
+
throw new Error('generation.topP must be within [0, 1]');
|
|
94
|
+
}
|
|
95
|
+
out.topP = g.topP;
|
|
96
|
+
}
|
|
97
|
+
if (g?.seed != null) {
|
|
98
|
+
if (!Number.isInteger(g.seed)) {
|
|
99
|
+
throw new Error('generation.seed must be an integer');
|
|
100
|
+
}
|
|
101
|
+
out.seed = g.seed;
|
|
102
|
+
}
|
|
103
|
+
if (g?.maxTokens != null) {
|
|
104
|
+
if (!Number.isInteger(g.maxTokens) || g.maxTokens <= 0) {
|
|
105
|
+
throw new Error('generation.maxTokens must be a positive integer');
|
|
106
|
+
}
|
|
107
|
+
out.maxTokens = g.maxTokens;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
11
110
|
}
|
|
12
111
|
// ============================================================================
|
|
13
112
|
// Inference API
|
|
@@ -64,12 +163,58 @@ export async function sendMessage(messages, options) {
|
|
|
64
163
|
if (!messages || messages.length === 0) {
|
|
65
164
|
throw new Error('messages array cannot be empty');
|
|
66
165
|
}
|
|
166
|
+
if (options?.signal?.aborted) {
|
|
167
|
+
throw new ModelError('INFERENCE_CANCELLED', '', 'Aborted before start');
|
|
168
|
+
}
|
|
67
169
|
// Determine system prompt: use from messages array if present, else options, else default
|
|
68
170
|
const hasSystemMessage = messages.some((m) => m.role === 'system');
|
|
69
171
|
const systemPrompt = hasSystemMessage
|
|
70
172
|
? '' // Native will extract from messages
|
|
71
173
|
: options?.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
72
|
-
|
|
174
|
+
acquireInference(); // throws INFERENCE_BUSY if a generation is already running
|
|
175
|
+
const sessionId = generateSessionId();
|
|
176
|
+
// Hold the single-flight flag until the NATIVE call settles — even if the
|
|
177
|
+
// caller aborts early — because the model may keep computing in the background.
|
|
178
|
+
const native = ExpoAiKitModule.sendMessage(messages, systemPrompt, sessionId);
|
|
179
|
+
const release = () => {
|
|
180
|
+
inferenceInFlight = false;
|
|
181
|
+
};
|
|
182
|
+
native.then(release, release);
|
|
183
|
+
const signal = options?.signal;
|
|
184
|
+
if (!signal) {
|
|
185
|
+
try {
|
|
186
|
+
return await native;
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
toModelError(e);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Race the native result against the abort signal. On abort we unblock the
|
|
193
|
+
// caller immediately and best-effort ask native to cancel; the flag stays
|
|
194
|
+
// held (via `release` above) until the native call actually finishes.
|
|
195
|
+
return await new Promise((resolve, reject) => {
|
|
196
|
+
let done = false;
|
|
197
|
+
const finish = (action) => {
|
|
198
|
+
if (done)
|
|
199
|
+
return;
|
|
200
|
+
done = true;
|
|
201
|
+
signal.removeEventListener('abort', onAbort);
|
|
202
|
+
action();
|
|
203
|
+
};
|
|
204
|
+
function onAbort() {
|
|
205
|
+
ExpoAiKitModule.stopStreaming(sessionId).catch(() => { });
|
|
206
|
+
finish(() => reject(new ModelError('INFERENCE_CANCELLED', '', 'Aborted by caller')));
|
|
207
|
+
}
|
|
208
|
+
signal.addEventListener('abort', onAbort);
|
|
209
|
+
native.then((r) => finish(() => resolve(r)), (e) => finish(() => {
|
|
210
|
+
try {
|
|
211
|
+
toModelError(e);
|
|
212
|
+
}
|
|
213
|
+
catch (me) {
|
|
214
|
+
reject(me);
|
|
215
|
+
}
|
|
216
|
+
}));
|
|
217
|
+
});
|
|
73
218
|
}
|
|
74
219
|
/**
|
|
75
220
|
* Stream messages to the on-device LLM and receive progressive token updates.
|
|
@@ -118,45 +263,160 @@ export function streamMessage(messages, onToken, options) {
|
|
|
118
263
|
stop: () => { },
|
|
119
264
|
};
|
|
120
265
|
}
|
|
266
|
+
if (inferenceInFlight) {
|
|
267
|
+
return {
|
|
268
|
+
promise: Promise.reject(new ModelError('INFERENCE_BUSY', '', 'A generation is already in flight. Stop the active stream first.')),
|
|
269
|
+
stop: () => { },
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
inferenceInFlight = true; // set synchronously — race-free with other JS
|
|
121
273
|
const sessionId = generateSessionId();
|
|
122
|
-
let finalText = '';
|
|
123
|
-
let stopped = false;
|
|
124
274
|
// Determine system prompt: use from messages array if present, else options, else default
|
|
125
275
|
const hasSystemMessage = messages.some((m) => m.role === 'system');
|
|
126
276
|
const systemPrompt = hasSystemMessage
|
|
127
277
|
? '' // Native will extract from messages
|
|
128
278
|
: options?.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
279
|
+
let finalText = '';
|
|
280
|
+
let settled = false;
|
|
281
|
+
let subscription;
|
|
282
|
+
let resolveOuter;
|
|
283
|
+
let rejectOuter;
|
|
284
|
+
// Settle exactly once: remove the listener and release the single-flight flag.
|
|
285
|
+
const settle = (action) => {
|
|
286
|
+
if (settled)
|
|
287
|
+
return;
|
|
288
|
+
settled = true;
|
|
289
|
+
subscription?.remove();
|
|
290
|
+
inferenceInFlight = false;
|
|
291
|
+
action();
|
|
292
|
+
};
|
|
129
293
|
const promise = new Promise((resolve, reject) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
294
|
+
resolveOuter = resolve;
|
|
295
|
+
rejectOuter = reject;
|
|
296
|
+
});
|
|
297
|
+
subscription = ExpoAiKitModule.addListener('onStreamToken', (event) => {
|
|
298
|
+
if (event.sessionId !== sessionId)
|
|
299
|
+
return;
|
|
300
|
+
finalText = event.accumulatedText;
|
|
301
|
+
onToken(event);
|
|
302
|
+
if (event.isDone)
|
|
303
|
+
settle(() => resolveOuter({ text: finalText }));
|
|
304
|
+
});
|
|
305
|
+
ExpoAiKitModule.startStreaming(messages, systemPrompt, sessionId).catch((error) => {
|
|
306
|
+
settle(() => {
|
|
307
|
+
try {
|
|
308
|
+
toModelError(error);
|
|
309
|
+
}
|
|
310
|
+
catch (me) {
|
|
311
|
+
rejectOuter(me);
|
|
142
312
|
}
|
|
143
|
-
});
|
|
144
|
-
// Start streaming on native side
|
|
145
|
-
ExpoAiKitModule.startStreaming(messages, systemPrompt, sessionId).catch((error) => {
|
|
146
|
-
subscription.remove();
|
|
147
|
-
reject(error);
|
|
148
313
|
});
|
|
149
314
|
});
|
|
150
315
|
const stop = () => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Ignore errors when stopping
|
|
156
|
-
});
|
|
316
|
+
// Best-effort native cancel (native also emits a terminal isDone on cancel),
|
|
317
|
+
// but resolve immediately with the text so far so `promise` can never hang.
|
|
318
|
+
ExpoAiKitModule.stopStreaming(sessionId).catch(() => { });
|
|
319
|
+
settle(() => resolveOuter({ text: finalText }));
|
|
157
320
|
};
|
|
158
321
|
return { promise, stop };
|
|
159
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Generate a typed object instead of free text.
|
|
325
|
+
*
|
|
326
|
+
* You describe the shape you want with a JSON Schema. expo-ai-kit appends a
|
|
327
|
+
* strict instruction to the system prompt, runs the on-device model, extracts
|
|
328
|
+
* the JSON from its output (tolerating prose and ```json fences), validates it
|
|
329
|
+
* against the schema, and — on a parse error or schema mismatch — feeds the
|
|
330
|
+
* error back and re-prompts up to `maxRepairAttempts` times.
|
|
331
|
+
*
|
|
332
|
+
* Works on every backend (Apple Foundation Models, ML Kit, Gemma) because it is
|
|
333
|
+
* orchestrated over {@link sendMessage}: it honors the same single-flight guard,
|
|
334
|
+
* `AbortSignal`, and `systemPrompt` semantics. Keep schemas small and shallow —
|
|
335
|
+
* on-device models follow flat shapes far more reliably than deeply nested ones.
|
|
336
|
+
*
|
|
337
|
+
* @param messages - The conversation, same shape as {@link sendMessage}.
|
|
338
|
+
* @param schema - A JSON Schema describing the desired result.
|
|
339
|
+
* @param options - Optional settings (systemPrompt, signal, maxRepairAttempts).
|
|
340
|
+
* @returns `{ object, text }` — the validated value and the raw output.
|
|
341
|
+
* @throws {ModelError} INFERENCE_FAILED if no schema-valid JSON is produced
|
|
342
|
+
* after the repair attempts. Also propagates INFERENCE_BUSY / INFERENCE_CANCELLED
|
|
343
|
+
* from the underlying generation.
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* ```ts
|
|
347
|
+
* type Recipe = { title: string; minutes: number; ingredients: string[] };
|
|
348
|
+
*
|
|
349
|
+
* const { object } = await generateObject<Recipe>(
|
|
350
|
+
* [{ role: 'user', content: 'A quick weeknight pasta.' }],
|
|
351
|
+
* {
|
|
352
|
+
* type: 'object',
|
|
353
|
+
* properties: {
|
|
354
|
+
* title: { type: 'string' },
|
|
355
|
+
* minutes: { type: 'integer' },
|
|
356
|
+
* ingredients: { type: 'array', items: { type: 'string' } },
|
|
357
|
+
* },
|
|
358
|
+
* required: ['title', 'minutes', 'ingredients'],
|
|
359
|
+
* },
|
|
360
|
+
* );
|
|
361
|
+
* object.title; // typed Recipe
|
|
362
|
+
* ```
|
|
363
|
+
*/
|
|
364
|
+
export async function generateObject(messages, schema, options) {
|
|
365
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
366
|
+
throw new ModelError('DEVICE_NOT_SUPPORTED', '', 'generateObject is only available on iOS and Android');
|
|
367
|
+
}
|
|
368
|
+
if (!messages || messages.length === 0) {
|
|
369
|
+
throw new Error('messages array cannot be empty');
|
|
370
|
+
}
|
|
371
|
+
if (!schema || typeof schema !== 'object') {
|
|
372
|
+
throw new Error('schema must be a JSON Schema object');
|
|
373
|
+
}
|
|
374
|
+
const maxRepairAttempts = Math.max(0, options?.maxRepairAttempts ?? 2);
|
|
375
|
+
const instruction = buildSchemaInstruction(schema);
|
|
376
|
+
// Inject the schema instruction. If the caller supplied a system message we
|
|
377
|
+
// append to it (sendMessage reads system from the array); otherwise we carry
|
|
378
|
+
// the instruction via the systemPrompt option, which sendMessage applies when
|
|
379
|
+
// the array has no system message — including on the repair turns we append.
|
|
380
|
+
const sysIdx = messages.findIndex((m) => m.role === 'system');
|
|
381
|
+
let working;
|
|
382
|
+
let systemPrompt;
|
|
383
|
+
if (sysIdx >= 0) {
|
|
384
|
+
working = messages.map((m, i) => i === sysIdx ? { role: m.role, content: `${m.content}\n\n${instruction}` } : m);
|
|
385
|
+
systemPrompt = undefined; // the array carries the system message
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
working = [...messages];
|
|
389
|
+
systemPrompt = `${options?.systemPrompt ?? DEFAULT_OBJECT_SYSTEM_PROMPT}\n\n${instruction}`;
|
|
390
|
+
}
|
|
391
|
+
let lastText = '';
|
|
392
|
+
for (let attempt = 0; attempt <= maxRepairAttempts; attempt++) {
|
|
393
|
+
const { text } = await sendMessage(working, { systemPrompt, signal: options?.signal });
|
|
394
|
+
lastText = text;
|
|
395
|
+
const parsed = extractJson(text);
|
|
396
|
+
if (parsed.ok) {
|
|
397
|
+
const errors = validateAgainstSchema(parsed.value, schema);
|
|
398
|
+
if (errors.length === 0) {
|
|
399
|
+
return { object: parsed.value, text };
|
|
400
|
+
}
|
|
401
|
+
if (attempt < maxRepairAttempts) {
|
|
402
|
+
working = [
|
|
403
|
+
...working,
|
|
404
|
+
{ role: 'assistant', content: text },
|
|
405
|
+
{ role: 'user', content: buildSchemaRepair(errors) },
|
|
406
|
+
];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else if (attempt < maxRepairAttempts) {
|
|
410
|
+
working = [
|
|
411
|
+
...working,
|
|
412
|
+
{ role: 'assistant', content: text },
|
|
413
|
+
{ role: 'user', content: REPAIR_INVALID_JSON },
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
throw new ModelError('INFERENCE_FAILED', getActiveModel(), `generateObject: model did not return schema-valid JSON after ${maxRepairAttempts + 1} attempt(s). ` +
|
|
418
|
+
`Last output: ${lastText.slice(0, 200)}`);
|
|
419
|
+
}
|
|
160
420
|
// ============================================================================
|
|
161
421
|
// Model Management API
|
|
162
422
|
// ============================================================================
|
|
@@ -194,8 +454,10 @@ export async function getDownloadableModels() {
|
|
|
194
454
|
catch {
|
|
195
455
|
// Native call unavailable -- default to 0 (all models will show meetsRequirements: false)
|
|
196
456
|
}
|
|
197
|
-
return platformModels.map((entry) => {
|
|
198
|
-
|
|
457
|
+
return Promise.all(platformModels.map(async (entry) => {
|
|
458
|
+
// Await: on iOS this bridges as a Promise (reads actor state); on Android
|
|
459
|
+
// it's synchronous and awaiting a plain value is a no-op.
|
|
460
|
+
const status = await ExpoAiKitModule.getDownloadableModelStatus(entry.id);
|
|
199
461
|
return {
|
|
200
462
|
id: entry.id,
|
|
201
463
|
name: entry.name,
|
|
@@ -206,7 +468,34 @@ export async function getDownloadableModels() {
|
|
|
206
468
|
meetsRequirements: deviceRamBytes >= entry.minRamBytes,
|
|
207
469
|
status,
|
|
208
470
|
};
|
|
209
|
-
});
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Pick the best downloadable model the current device can run.
|
|
475
|
+
*
|
|
476
|
+
* Returns the most capable model (largest, by RAM requirement) whose
|
|
477
|
+
* `meetsRequirements` is true — e.g. Gemma 4 E4B on high-spec phones, falling
|
|
478
|
+
* back to E2B on more constrained ones — or `null` if the device can't run any.
|
|
479
|
+
*
|
|
480
|
+
* This is a convenience over {@link getDownloadableModels}; the caller still
|
|
481
|
+
* downloads + activates explicitly. Pass `platform` is implicit (current OS).
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* ```ts
|
|
485
|
+
* const best = await getRecommendedModel();
|
|
486
|
+
* if (best) {
|
|
487
|
+
* await downloadModel(best.id, { onProgress });
|
|
488
|
+
* await setModel(best.id);
|
|
489
|
+
* }
|
|
490
|
+
* ```
|
|
491
|
+
*/
|
|
492
|
+
export async function getRecommendedModel() {
|
|
493
|
+
const models = await getDownloadableModels();
|
|
494
|
+
const runnable = models.filter((m) => m.meetsRequirements);
|
|
495
|
+
if (runnable.length === 0)
|
|
496
|
+
return null;
|
|
497
|
+
// Higher RAM requirement ⇒ larger/more capable model. Prefer the biggest that fits.
|
|
498
|
+
return runnable.sort((a, b) => b.minRamBytes - a.minRamBytes)[0];
|
|
210
499
|
}
|
|
211
500
|
/**
|
|
212
501
|
* Download a model to the device.
|
|
@@ -251,12 +540,26 @@ export async function downloadModel(modelId, options) {
|
|
|
251
540
|
});
|
|
252
541
|
}
|
|
253
542
|
try {
|
|
254
|
-
await ExpoAiKitModule.downloadModel(modelId, entry.downloadUrl, entry.sha256);
|
|
543
|
+
await wrapNative(() => ExpoAiKitModule.downloadModel(modelId, entry.downloadUrl, entry.sha256));
|
|
255
544
|
}
|
|
256
545
|
finally {
|
|
257
546
|
subscription?.remove();
|
|
258
547
|
}
|
|
259
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* Cancel an in-flight download for a model.
|
|
551
|
+
*
|
|
552
|
+
* The in-progress {@link downloadModel} promise rejects with a
|
|
553
|
+
* DOWNLOAD_CANCELLED {@link ModelError}. No-op if the model isn't downloading.
|
|
554
|
+
*
|
|
555
|
+
* @param modelId - ID of the model whose download should be cancelled
|
|
556
|
+
*/
|
|
557
|
+
export async function cancelDownload(modelId) {
|
|
558
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
await wrapNative(() => ExpoAiKitModule.cancelDownload(modelId));
|
|
562
|
+
}
|
|
260
563
|
/**
|
|
261
564
|
* Delete a downloaded model from the device.
|
|
262
565
|
*
|
|
@@ -270,7 +573,7 @@ export async function deleteModel(modelId) {
|
|
|
270
573
|
if (!entry) {
|
|
271
574
|
throw new ModelError('MODEL_NOT_FOUND', modelId);
|
|
272
575
|
}
|
|
273
|
-
await ExpoAiKitModule.deleteModel(modelId);
|
|
576
|
+
await wrapNative(() => ExpoAiKitModule.deleteModel(modelId));
|
|
274
577
|
}
|
|
275
578
|
/**
|
|
276
579
|
* Set the active model for inference.
|
|
@@ -299,7 +602,8 @@ export async function setModel(modelId, options) {
|
|
|
299
602
|
const entry = getRegistryEntry(modelId);
|
|
300
603
|
const minRamBytes = entry?.minRamBytes ?? 0;
|
|
301
604
|
const backend = options?.backend ?? 'auto';
|
|
302
|
-
|
|
605
|
+
const generation = toNativeGeneration(options?.generation);
|
|
606
|
+
await wrapNative(() => ExpoAiKitModule.setModel(modelId, minRamBytes, backend, generation));
|
|
303
607
|
}
|
|
304
608
|
/**
|
|
305
609
|
* Get the ID of the currently active model.
|
|
@@ -316,6 +620,6 @@ export function getActiveModel() {
|
|
|
316
620
|
* No-op if no downloadable model is currently loaded.
|
|
317
621
|
*/
|
|
318
622
|
export async function unloadModel() {
|
|
319
|
-
await ExpoAiKitModule.unloadModel();
|
|
623
|
+
await wrapNative(() => ExpoAiKitModule.unloadModel());
|
|
320
624
|
}
|
|
321
625
|
//# sourceMappingURL=index.js.map
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EASL,UAAU,GAEX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE5D,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AAEzB,MAAM,qBAAqB,GACzB,gFAAgF,CAAC;AAEnF,IAAI,eAAe,GAAG,CAAC,CAAC;AACxB,SAAS,iBAAiB;IACxB,OAAO,UAAU,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;AACrD,CAAC;AAED,+EAA+E;AAC/E,gBAAgB;AAChB,+EAA+E;AAE/E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,eAAe,CAAC,WAAW,EAAE,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,QAAsB,EACtB,OAAwB;IAExB,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IACtB,CAAC;IAED,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,CAAC;IAED,0FAA0F;IAC1F,MAAM,gBAAgB,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;IACnE,MAAM,YAAY,GAAG,gBAAgB;QACnC,CAAC,CAAC,EAAE,CAAC,oCAAoC;QACzC,CAAC,CAAC,OAAO,EAAE,YAAY,IAAI,qBAAqB,CAAC;IAEnD,OAAO,eAAe,CAAC,WAAW,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,aAAa,CAC3B,QAAsB,EACtB,OAA0B,EAC1B,OAA0B;IAE1B,+BAA+B;IAC/B,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO;YACL,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;YACtC,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;SACf,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvC,OAAO;YACL,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;YACpE,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;SACf,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,iBAAiB,EAAE,CAAC;IACtC,IAAI,SAAS,GAAG,EAAE,CAAC;IACnB,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,0FAA0F;IAC1F,MAAM,gBAAgB,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;IACnE,MAAM,YAAY,GAAG,gBAAgB;QACnC,CAAC,CAAC,EAAE,CAAC,oCAAoC;QACzC,CAAC,CAAC,OAAO,EAAE,YAAY,IAAI,qBAAqB,CAAC;IAEnD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3D,6BAA6B;QAC7B,MAAM,YAAY,GAAG,eAAe,CAAC,WAAW,CAC9C,eAAe,EACf,CAAC,KAAqB,EAAE,EAAE;YACxB,uCAAuC;YACvC,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS;gBAAE,OAAO;YAE1C,SAAS,GAAG,KAAK,CAAC,eAAe,CAAC;YAElC,2BAA2B;YAC3B,OAAO,CAAC,KAAK,CAAC,CAAC;YAEf,gCAAgC;YAChC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBACjB,YAAY,CAAC,MAAM,EAAE,CAAC;gBACtB,OAAO,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC,CACF,CAAC;QAEF,iCAAiC;QACjC,eAAe,CAAC,cAAc,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC,KAAK,CACrE,CAAC,KAAK,EAAE,EAAE;YACR,YAAY,CAAC,MAAM,EAAE,CAAC;YACtB,MAAM,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,GAAG,EAAE;QAChB,IAAI,OAAO;YAAE,OAAO;QACpB,OAAO,GAAG,IAAI,CAAC;QACf,eAAe,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YAClD,8BAA8B;QAChC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,+EAA+E;AAC/E,uBAAuB;AACvB,+EAA+E;AAE/E;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,eAAe,CAAC,gBAAgB,EAAE,CAAC;AAC5C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,cAAc,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CACrD,KAAK,CAAC,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAuB,CAAC,CACpE,CAAC;IAEF,IAAI,cAAc,GAAG,CAAC,CAAC;IACvB,IAAI,CAAC;QACH,cAAc,GAAG,eAAe,CAAC,iBAAiB,EAAE,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,0FAA0F;IAC5F,CAAC;IAED,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAClC,MAAM,MAAM,GAAG,eAAe,CAAC,0BAA0B,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACpE,OAAO;YACL,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,cAAc,EAAE,KAAK,CAAC,cAAc;YACpC,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,iBAAiB,EAAE,cAAc,IAAI,KAAK,CAAC,WAAW;YACtD,MAAM;SACP,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAe,EACf,OAAqD;IAErD,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,UAAU,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAuB,CAAC,EAAE,CAAC;QACzE,MAAM,IAAI,UAAU,CAClB,sBAAsB,EACtB,OAAO,EACP,SAAS,OAAO,wBAAwB,QAAQ,CAAC,EAAE,EAAE,CACtD,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,cAAc,GAAG,eAAe,CAAC,iBAAiB,EAAE,CAAC;QAC3D,IAAI,cAAc,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;YACvC,MAAM,IAAI,UAAU,CAClB,sBAAsB,EACtB,OAAO,EACP,cAAc,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,GAAG,CAAC,0BAA0B,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,GAAG,GAAG,CAAC,IAAI,CAChH,CAAC;QACJ,CAAC;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,CAAC,YAAY,UAAU;YAAE,MAAM,CAAC,CAAC;QACrC,sDAAsD;IACxD,CAAC;IAED,IAAI,YAAwE,CAAC;IAC7E,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC;QACxB,YAAY,GAAG,eAAe,CAAC,WAAW,CACxC,oBAAoB,EACpB,CAAC,KAAK,EAAE,EAAE;YACR,IAAI,KAAK,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;gBAC9B,OAAO,CAAC,UAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACtC,CAAC;QACH,CAAC,CACF,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,eAAe,CAAC,aAAa,CACjC,OAAO,EACP,KAAK,CAAC,WAAW,EACjB,KAAK,CAAC,MAAM,CACb,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,YAAY,EAAE,MAAM,EAAE,CAAC;IACzB,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAe;IAC/C,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,UAAU,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,eAAe,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAe,EAAE,OAAyB;IACvE,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACxC,MAAM,WAAW,GAAG,KAAK,EAAE,WAAW,IAAI,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,OAAO,EAAE,OAAO,IAAI,MAAM,CAAC;IAC3C,MAAM,eAAe,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;AAChE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,eAAe,CAAC,cAAc,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,eAAe,CAAC,WAAW,EAAE,CAAC;AACtC,CAAC","sourcesContent":["import ExpoAiKitModule from './ExpoAiKitModule';\nimport { Platform } from 'react-native';\nimport {\n LLMMessage,\n LLMSendOptions,\n LLMResponse,\n LLMStreamOptions,\n LLMStreamEvent,\n LLMStreamCallback,\n BuiltInModel,\n DownloadableModel,\n ModelError,\n SetModelOptions,\n} from './types';\nimport { MODEL_REGISTRY, getRegistryEntry } from './models';\n\nexport * from './types';\nexport * from './models';\n\nconst DEFAULT_SYSTEM_PROMPT =\n 'You are a helpful, friendly assistant. Answer the user directly and concisely.';\n\nlet streamIdCounter = 0;\nfunction generateSessionId(): string {\n return `stream_${Date.now()}_${++streamIdCounter}`;\n}\n\n// ============================================================================\n// Inference API\n// ============================================================================\n\n/**\n * Check if on-device AI is available on the current device.\n * Returns false on unsupported platforms (web, etc.).\n */\nexport async function isAvailable(): Promise<boolean> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return false;\n }\n return ExpoAiKitModule.isAvailable();\n}\n\n/**\n * Send messages to the on-device LLM and get a response.\n *\n * @param messages - Array of messages representing the conversation\n * @param options - Optional settings (systemPrompt fallback)\n * @returns Promise with the generated response\n *\n * @example\n * ```ts\n * const response = await sendMessage([\n * { role: 'user', content: 'What is 2 + 2?' }\n * ]);\n * console.log(response.text); // \"4\"\n * ```\n *\n * @example\n * ```ts\n * // With system prompt\n * const response = await sendMessage(\n * [{ role: 'user', content: 'Hello!' }],\n * { systemPrompt: 'You are a pirate. Respond in pirate speak.' }\n * );\n * ```\n *\n * @example\n * ```ts\n * // Multi-turn conversation\n * const response = await sendMessage([\n * { role: 'system', content: 'You are a helpful assistant.' },\n * { role: 'user', content: 'My name is Alice.' },\n * { role: 'assistant', content: 'Nice to meet you, Alice!' },\n * { role: 'user', content: 'What is my name?' }\n * ]);\n * ```\n */\nexport async function sendMessage(\n messages: LLMMessage[],\n options?: LLMSendOptions\n): Promise<LLMResponse> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return { text: '' };\n }\n\n if (!messages || messages.length === 0) {\n throw new Error('messages array cannot be empty');\n }\n\n // Determine system prompt: use from messages array if present, else options, else default\n const hasSystemMessage = messages.some((m) => m.role === 'system');\n const systemPrompt = hasSystemMessage\n ? '' // Native will extract from messages\n : options?.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;\n\n return ExpoAiKitModule.sendMessage(messages, systemPrompt);\n}\n\n/**\n * Stream messages to the on-device LLM and receive progressive token updates.\n *\n * @param messages - Array of messages representing the conversation\n * @param onToken - Callback function called for each token/chunk received\n * @param options - Optional settings (systemPrompt fallback)\n * @returns Object with stop() function to cancel streaming and promise that resolves when complete\n *\n * @example\n * ```ts\n * // Basic streaming\n * const { promise } = streamMessage(\n * [{ role: 'user', content: 'Tell me a story' }],\n * (event) => {\n * console.log(event.token); // Each token as it arrives\n * console.log(event.accumulatedText); // Full text so far\n * }\n * );\n * await promise;\n * ```\n *\n * @example\n * ```ts\n * // With cancellation\n * const { promise, stop } = streamMessage(\n * [{ role: 'user', content: 'Write a long essay' }],\n * (event) => setText(event.accumulatedText)\n * );\n *\n * // Cancel after 5 seconds\n * setTimeout(() => stop(), 5000);\n * ```\n */\nexport function streamMessage(\n messages: LLMMessage[],\n onToken: LLMStreamCallback,\n options?: LLMStreamOptions\n): { promise: Promise<LLMResponse>; stop: () => void } {\n // Handle unsupported platforms\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return {\n promise: Promise.resolve({ text: '' }),\n stop: () => {},\n };\n }\n\n if (!messages || messages.length === 0) {\n return {\n promise: Promise.reject(new Error('messages array cannot be empty')),\n stop: () => {},\n };\n }\n\n const sessionId = generateSessionId();\n let finalText = '';\n let stopped = false;\n\n // Determine system prompt: use from messages array if present, else options, else default\n const hasSystemMessage = messages.some((m) => m.role === 'system');\n const systemPrompt = hasSystemMessage\n ? '' // Native will extract from messages\n : options?.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;\n\n const promise = new Promise<LLMResponse>((resolve, reject) => {\n // Subscribe to stream events\n const subscription = ExpoAiKitModule.addListener(\n 'onStreamToken',\n (event: LLMStreamEvent) => {\n // Only process events for this session\n if (event.sessionId !== sessionId) return;\n\n finalText = event.accumulatedText;\n\n // Call the user's callback\n onToken(event);\n\n // If done, clean up and resolve\n if (event.isDone) {\n subscription.remove();\n resolve({ text: finalText });\n }\n }\n );\n\n // Start streaming on native side\n ExpoAiKitModule.startStreaming(messages, systemPrompt, sessionId).catch(\n (error) => {\n subscription.remove();\n reject(error);\n }\n );\n });\n\n const stop = () => {\n if (stopped) return;\n stopped = true;\n ExpoAiKitModule.stopStreaming(sessionId).catch(() => {\n // Ignore errors when stopping\n });\n };\n\n return { promise, stop };\n}\n\n// ============================================================================\n// Model Management API\n// ============================================================================\n\n/**\n * Get all built-in models available on the current platform.\n *\n * Built-in models are provided by the OS and require no download.\n * On iOS this returns Apple Foundation Models; on Android, ML Kit.\n *\n * @returns Array of built-in models with availability status\n */\nexport async function getBuiltInModels(): Promise<BuiltInModel[]> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return [];\n }\n return ExpoAiKitModule.getBuiltInModels();\n}\n\n/**\n * Get all downloadable models from the registry, enriched with on-device status.\n *\n * Reads from the hardcoded MODEL_REGISTRY and queries the native layer\n * for the current download/load status of each model.\n *\n * @returns Array of downloadable models with their current status\n */\nexport async function getDownloadableModels(): Promise<DownloadableModel[]> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return [];\n }\n\n const platformModels = MODEL_REGISTRY.filter((entry) =>\n entry.supportedPlatforms.includes(Platform.OS as 'ios' | 'android')\n );\n\n let deviceRamBytes = 0;\n try {\n deviceRamBytes = ExpoAiKitModule.getDeviceRamBytes();\n } catch {\n // Native call unavailable -- default to 0 (all models will show meetsRequirements: false)\n }\n\n return platformModels.map((entry) => {\n const status = ExpoAiKitModule.getDownloadableModelStatus(entry.id);\n return {\n id: entry.id,\n name: entry.name,\n parameterCount: entry.parameterCount,\n sizeBytes: entry.sizeBytes,\n contextWindow: entry.contextWindow,\n minRamBytes: entry.minRamBytes,\n meetsRequirements: deviceRamBytes >= entry.minRamBytes,\n status,\n };\n });\n}\n\n/**\n * Download a model to the device.\n *\n * Looks up the model in the registry, validates platform support and\n * device requirements, then initiates the download with integrity verification.\n *\n * @param modelId - ID of the model to download (e.g. 'gemma-e2b')\n * @param options - Optional download configuration\n * @param options.onProgress - Callback with download progress (0-1)\n * @throws {ModelError} MODEL_NOT_FOUND if modelId is not in the registry\n * @throws {ModelError} DEVICE_NOT_SUPPORTED if platform is not supported\n * @throws {ModelError} DOWNLOAD_FAILED on network error\n * @throws {ModelError} DOWNLOAD_STORAGE_FULL if insufficient disk space\n * @throws {ModelError} DOWNLOAD_CORRUPT if SHA256 hash doesn't match\n */\nexport async function downloadModel(\n modelId: string,\n options?: { onProgress?: (progress: number) => void }\n): Promise<void> {\n const entry = getRegistryEntry(modelId);\n if (!entry) {\n throw new ModelError('MODEL_NOT_FOUND', modelId);\n }\n\n if (!entry.supportedPlatforms.includes(Platform.OS as 'ios' | 'android')) {\n throw new ModelError(\n 'DEVICE_NOT_SUPPORTED',\n modelId,\n `Model ${modelId} is not supported on ${Platform.OS}`\n );\n }\n\n try {\n const deviceRamBytes = ExpoAiKitModule.getDeviceRamBytes();\n if (deviceRamBytes < entry.minRamBytes) {\n throw new ModelError(\n 'DEVICE_NOT_SUPPORTED',\n modelId,\n `Device has ${Math.round(deviceRamBytes / 1e9)}GB RAM, model requires ${Math.round(entry.minRamBytes / 1e9)}GB`\n );\n }\n } catch (e) {\n if (e instanceof ModelError) throw e;\n // If getDeviceRamBytes is unavailable, skip the check\n }\n\n let subscription: ReturnType<typeof ExpoAiKitModule.addListener> | undefined;\n if (options?.onProgress) {\n subscription = ExpoAiKitModule.addListener(\n 'onDownloadProgress',\n (event) => {\n if (event.modelId === modelId) {\n options.onProgress!(event.progress);\n }\n }\n );\n }\n\n try {\n await ExpoAiKitModule.downloadModel(\n modelId,\n entry.downloadUrl,\n entry.sha256\n );\n } finally {\n subscription?.remove();\n }\n}\n\n/**\n * Delete a downloaded model from the device.\n *\n * If the model is currently loaded, it will be unloaded first.\n *\n * @param modelId - ID of the model to delete\n * @throws {ModelError} MODEL_NOT_FOUND if modelId is not in the registry\n */\nexport async function deleteModel(modelId: string): Promise<void> {\n const entry = getRegistryEntry(modelId);\n if (!entry) {\n throw new ModelError('MODEL_NOT_FOUND', modelId);\n }\n\n await ExpoAiKitModule.deleteModel(modelId);\n}\n\n/**\n * Set the active model for inference.\n *\n * This is the sole gatekeeper for model validity. If setModel succeeds,\n * the model is loaded and ready -- sendMessage never needs its own check.\n *\n * For downloadable models, this loads the model into memory (status\n * transitions: loading -> ready). Only one downloadable model can be\n * loaded at a time; the previous one is auto-unloaded.\n *\n * For built-in models, this simply switches the active backend.\n *\n * If setModel was never called, sendMessage uses the platform built-in\n * model (today's behavior, no error).\n *\n * @param modelId - ID of the model to activate (e.g. 'gemma-e2b', 'apple-fm', 'mlkit')\n * @param options - Optional configuration for model loading\n * @param options.backend - Hardware backend: 'auto' (default, GPU with CPU fallback), 'gpu', or 'cpu'\n * @throws {ModelError} MODEL_NOT_FOUND if modelId is invalid\n * @throws {ModelError} MODEL_NOT_DOWNLOADED if the downloadable model file is not on disk\n * @throws {ModelError} MODEL_LOAD_FAILED if loading into memory fails\n * @throws {ModelError} INFERENCE_OOM if device can't fit model in memory\n */\nexport async function setModel(modelId: string, options?: SetModelOptions): Promise<void> {\n const entry = getRegistryEntry(modelId);\n const minRamBytes = entry?.minRamBytes ?? 0;\n const backend = options?.backend ?? 'auto';\n await ExpoAiKitModule.setModel(modelId, minRamBytes, backend);\n}\n\n/**\n * Get the ID of the currently active model.\n *\n * @returns The active model ID (e.g. 'apple-fm', 'mlkit', 'gemma-e2b')\n */\nexport function getActiveModel(): string {\n return ExpoAiKitModule.getActiveModel();\n}\n\n/**\n * Explicitly unload the current downloadable model from memory.\n *\n * Frees memory and reverts to the platform built-in model.\n * No-op if no downloadable model is currently loaded.\n */\nexport async function unloadModel(): Promise<void> {\n await ExpoAiKitModule.unloadModel();\n}\n\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import ExpoAiKitModule, { type NativeGenerationConfig } from './ExpoAiKitModule';\nimport { Platform } from 'react-native';\nimport {\n LLMMessage,\n LLMSendOptions,\n LLMResponse,\n LLMStreamOptions,\n LLMStreamEvent,\n LLMStreamCallback,\n LLMStreamHandle,\n BuiltInModel,\n DownloadableModel,\n GenerationConfig,\n ModelError,\n ModelErrorCode,\n SetModelOptions,\n JSONSchema,\n GenerateObjectOptions,\n GenerateObjectResult,\n} from './types';\nimport {\n buildSchemaInstruction,\n buildSchemaRepair,\n extractJson,\n validateAgainstSchema,\n REPAIR_INVALID_JSON,\n} from './structured';\nimport { MODEL_REGISTRY, getRegistryEntry } from './models';\n\nexport * from './types';\nexport * from './models';\n\nconst DEFAULT_SYSTEM_PROMPT =\n 'You are a helpful, friendly assistant. Answer the user directly and concisely.';\n\nconst DEFAULT_OBJECT_SYSTEM_PROMPT =\n 'You output structured data as JSON. Follow the provided JSON Schema exactly.';\n\nlet streamIdCounter = 0;\nfunction generateSessionId(): string {\n return `gen_${Date.now()}_${++streamIdCounter}`;\n}\n\n// The set of codes the native layer encodes in error messages as \"CODE:modelId:reason\".\nconst KNOWN_ERROR_CODES = new Set<ModelErrorCode>([\n 'MODEL_NOT_FOUND',\n 'MODEL_NOT_DOWNLOADED',\n 'DOWNLOAD_FAILED',\n 'DOWNLOAD_CORRUPT',\n 'DOWNLOAD_STORAGE_FULL',\n 'DOWNLOAD_CANCELLED',\n 'INFERENCE_OOM',\n 'INFERENCE_FAILED',\n 'INFERENCE_BUSY',\n 'INFERENCE_CANCELLED',\n 'MODEL_LOAD_FAILED',\n 'DEVICE_NOT_SUPPORTED',\n]);\n\n/**\n * Normalize an error from the native layer into a {@link ModelError}.\n *\n * The native modules format failures as \"CODE:modelId:reason\" (see the\n * GemmaError/GemmaInferenceClient contract). Expo surfaces that string as the\n * error's message, so we parse it here and rethrow a typed ModelError with a\n * reliable `.code` and `.modelId`. Anything unrecognized becomes UNKNOWN.\n */\nfunction toModelError(e: unknown): never {\n if (e instanceof ModelError) throw e;\n const message = String((e as any)?.message ?? e ?? '');\n const match = /^([A-Z_]+):([^:]*):([\\s\\S]*)$/.exec(message);\n if (match && KNOWN_ERROR_CODES.has(match[1] as ModelErrorCode)) {\n throw new ModelError(match[1] as ModelErrorCode, match[2], match[3]);\n }\n throw new ModelError('UNKNOWN', '', message);\n}\n\n/** Run a native promise, normalizing any rejection into a ModelError. */\nasync function wrapNative<T>(run: () => Promise<T>): Promise<T> {\n try {\n return await run();\n } catch (e) {\n toModelError(e);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Single-flight inference guard\n// ---------------------------------------------------------------------------\n// On-device models are backed by a single native context + KV cache that is not\n// safe for concurrent decodes (interleaving can corrupt the cache and crash the\n// native side). JS is single-threaded, so a synchronous check-and-set of this\n// flag before any `await` is race-free. The flag is shared by sendMessage and\n// streamMessage and is held until the *native* call settles — not until an\n// early abort — so a detached-but-still-running generation still blocks a new one.\nlet inferenceInFlight = false;\n\nfunction acquireInference(): void {\n if (inferenceInFlight) {\n throw new ModelError(\n 'INFERENCE_BUSY',\n '',\n 'A generation is already in flight. Wait for it to finish, or stop the active stream first.'\n );\n }\n inferenceInFlight = true;\n}\n\n/**\n * Map the public GenerationConfig to the native shape, dropping undefined fields\n * and validating ranges up front so callers get a clear error instead of an\n * opaque native MODEL_LOAD_FAILED from the sampler.\n */\nfunction toNativeGeneration(g?: GenerationConfig): NativeGenerationConfig {\n const out: NativeGenerationConfig = {};\n if (g?.temperature != null) {\n if (g.temperature < 0) {\n throw new Error('generation.temperature must be >= 0');\n }\n out.temperature = g.temperature;\n }\n if (g?.topK != null) {\n if (!Number.isInteger(g.topK) || g.topK <= 0) {\n throw new Error('generation.topK must be a positive integer');\n }\n out.topK = g.topK;\n }\n if (g?.topP != null) {\n if (g.topP < 0 || g.topP > 1) {\n throw new Error('generation.topP must be within [0, 1]');\n }\n out.topP = g.topP;\n }\n if (g?.seed != null) {\n if (!Number.isInteger(g.seed)) {\n throw new Error('generation.seed must be an integer');\n }\n out.seed = g.seed;\n }\n if (g?.maxTokens != null) {\n if (!Number.isInteger(g.maxTokens) || g.maxTokens <= 0) {\n throw new Error('generation.maxTokens must be a positive integer');\n }\n out.maxTokens = g.maxTokens;\n }\n return out;\n}\n\n// ============================================================================\n// Inference API\n// ============================================================================\n\n/**\n * Check if on-device AI is available on the current device.\n * Returns false on unsupported platforms (web, etc.).\n */\nexport async function isAvailable(): Promise<boolean> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return false;\n }\n return ExpoAiKitModule.isAvailable();\n}\n\n/**\n * Send messages to the on-device LLM and get a response.\n *\n * @param messages - Array of messages representing the conversation\n * @param options - Optional settings (systemPrompt fallback)\n * @returns Promise with the generated response\n *\n * @example\n * ```ts\n * const response = await sendMessage([\n * { role: 'user', content: 'What is 2 + 2?' }\n * ]);\n * console.log(response.text); // \"4\"\n * ```\n *\n * @example\n * ```ts\n * // With system prompt\n * const response = await sendMessage(\n * [{ role: 'user', content: 'Hello!' }],\n * { systemPrompt: 'You are a pirate. Respond in pirate speak.' }\n * );\n * ```\n *\n * @example\n * ```ts\n * // Multi-turn conversation\n * const response = await sendMessage([\n * { role: 'system', content: 'You are a helpful assistant.' },\n * { role: 'user', content: 'My name is Alice.' },\n * { role: 'assistant', content: 'Nice to meet you, Alice!' },\n * { role: 'user', content: 'What is my name?' }\n * ]);\n * ```\n */\nexport async function sendMessage(\n messages: LLMMessage[],\n options?: LLMSendOptions\n): Promise<LLMResponse> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return { text: '' };\n }\n\n if (!messages || messages.length === 0) {\n throw new Error('messages array cannot be empty');\n }\n\n if (options?.signal?.aborted) {\n throw new ModelError('INFERENCE_CANCELLED', '', 'Aborted before start');\n }\n\n // Determine system prompt: use from messages array if present, else options, else default\n const hasSystemMessage = messages.some((m) => m.role === 'system');\n const systemPrompt = hasSystemMessage\n ? '' // Native will extract from messages\n : options?.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;\n\n acquireInference(); // throws INFERENCE_BUSY if a generation is already running\n const sessionId = generateSessionId();\n\n // Hold the single-flight flag until the NATIVE call settles — even if the\n // caller aborts early — because the model may keep computing in the background.\n const native = ExpoAiKitModule.sendMessage(messages, systemPrompt, sessionId);\n const release = () => {\n inferenceInFlight = false;\n };\n native.then(release, release);\n\n const signal = options?.signal;\n if (!signal) {\n try {\n return await native;\n } catch (e) {\n toModelError(e);\n }\n }\n\n // Race the native result against the abort signal. On abort we unblock the\n // caller immediately and best-effort ask native to cancel; the flag stays\n // held (via `release` above) until the native call actually finishes.\n return await new Promise<LLMResponse>((resolve, reject) => {\n let done = false;\n const finish = (action: () => void) => {\n if (done) return;\n done = true;\n signal.removeEventListener('abort', onAbort);\n action();\n };\n function onAbort() {\n ExpoAiKitModule.stopStreaming(sessionId).catch(() => {});\n finish(() => reject(new ModelError('INFERENCE_CANCELLED', '', 'Aborted by caller')));\n }\n signal.addEventListener('abort', onAbort);\n native.then(\n (r) => finish(() => resolve(r)),\n (e) =>\n finish(() => {\n try {\n toModelError(e);\n } catch (me) {\n reject(me);\n }\n })\n );\n });\n}\n\n/**\n * Stream messages to the on-device LLM and receive progressive token updates.\n *\n * @param messages - Array of messages representing the conversation\n * @param onToken - Callback function called for each token/chunk received\n * @param options - Optional settings (systemPrompt fallback)\n * @returns Object with stop() function to cancel streaming and promise that resolves when complete\n *\n * @example\n * ```ts\n * // Basic streaming\n * const { promise } = streamMessage(\n * [{ role: 'user', content: 'Tell me a story' }],\n * (event) => {\n * console.log(event.token); // Each token as it arrives\n * console.log(event.accumulatedText); // Full text so far\n * }\n * );\n * await promise;\n * ```\n *\n * @example\n * ```ts\n * // With cancellation\n * const { promise, stop } = streamMessage(\n * [{ role: 'user', content: 'Write a long essay' }],\n * (event) => setText(event.accumulatedText)\n * );\n *\n * // Cancel after 5 seconds\n * setTimeout(() => stop(), 5000);\n * ```\n */\nexport function streamMessage(\n messages: LLMMessage[],\n onToken: LLMStreamCallback,\n options?: LLMStreamOptions\n): LLMStreamHandle {\n // Handle unsupported platforms\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return {\n promise: Promise.resolve({ text: '' }),\n stop: () => {},\n };\n }\n\n if (!messages || messages.length === 0) {\n return {\n promise: Promise.reject(new Error('messages array cannot be empty')),\n stop: () => {},\n };\n }\n\n if (inferenceInFlight) {\n return {\n promise: Promise.reject(\n new ModelError(\n 'INFERENCE_BUSY',\n '',\n 'A generation is already in flight. Stop the active stream first.'\n )\n ),\n stop: () => {},\n };\n }\n inferenceInFlight = true; // set synchronously — race-free with other JS\n\n const sessionId = generateSessionId();\n\n // Determine system prompt: use from messages array if present, else options, else default\n const hasSystemMessage = messages.some((m) => m.role === 'system');\n const systemPrompt = hasSystemMessage\n ? '' // Native will extract from messages\n : options?.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;\n\n let finalText = '';\n let settled = false;\n let subscription: ReturnType<typeof ExpoAiKitModule.addListener> | undefined;\n let resolveOuter!: (r: LLMResponse) => void;\n let rejectOuter!: (e: unknown) => void;\n\n // Settle exactly once: remove the listener and release the single-flight flag.\n const settle = (action: () => void) => {\n if (settled) return;\n settled = true;\n subscription?.remove();\n inferenceInFlight = false;\n action();\n };\n\n const promise = new Promise<LLMResponse>((resolve, reject) => {\n resolveOuter = resolve;\n rejectOuter = reject;\n });\n\n subscription = ExpoAiKitModule.addListener(\n 'onStreamToken',\n (event: LLMStreamEvent) => {\n if (event.sessionId !== sessionId) return;\n finalText = event.accumulatedText;\n onToken(event);\n if (event.isDone) settle(() => resolveOuter({ text: finalText }));\n }\n );\n\n ExpoAiKitModule.startStreaming(messages, systemPrompt, sessionId).catch(\n (error) => {\n settle(() => {\n try {\n toModelError(error);\n } catch (me) {\n rejectOuter(me);\n }\n });\n }\n );\n\n const stop = () => {\n // Best-effort native cancel (native also emits a terminal isDone on cancel),\n // but resolve immediately with the text so far so `promise` can never hang.\n ExpoAiKitModule.stopStreaming(sessionId).catch(() => {});\n settle(() => resolveOuter({ text: finalText }));\n };\n\n return { promise, stop };\n}\n\n/**\n * Generate a typed object instead of free text.\n *\n * You describe the shape you want with a JSON Schema. expo-ai-kit appends a\n * strict instruction to the system prompt, runs the on-device model, extracts\n * the JSON from its output (tolerating prose and ```json fences), validates it\n * against the schema, and — on a parse error or schema mismatch — feeds the\n * error back and re-prompts up to `maxRepairAttempts` times.\n *\n * Works on every backend (Apple Foundation Models, ML Kit, Gemma) because it is\n * orchestrated over {@link sendMessage}: it honors the same single-flight guard,\n * `AbortSignal`, and `systemPrompt` semantics. Keep schemas small and shallow —\n * on-device models follow flat shapes far more reliably than deeply nested ones.\n *\n * @param messages - The conversation, same shape as {@link sendMessage}.\n * @param schema - A JSON Schema describing the desired result.\n * @param options - Optional settings (systemPrompt, signal, maxRepairAttempts).\n * @returns `{ object, text }` — the validated value and the raw output.\n * @throws {ModelError} INFERENCE_FAILED if no schema-valid JSON is produced\n * after the repair attempts. Also propagates INFERENCE_BUSY / INFERENCE_CANCELLED\n * from the underlying generation.\n *\n * @example\n * ```ts\n * type Recipe = { title: string; minutes: number; ingredients: string[] };\n *\n * const { object } = await generateObject<Recipe>(\n * [{ role: 'user', content: 'A quick weeknight pasta.' }],\n * {\n * type: 'object',\n * properties: {\n * title: { type: 'string' },\n * minutes: { type: 'integer' },\n * ingredients: { type: 'array', items: { type: 'string' } },\n * },\n * required: ['title', 'minutes', 'ingredients'],\n * },\n * );\n * object.title; // typed Recipe\n * ```\n */\nexport async function generateObject<T = unknown>(\n messages: LLMMessage[],\n schema: JSONSchema,\n options?: GenerateObjectOptions\n): Promise<GenerateObjectResult<T>> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n throw new ModelError(\n 'DEVICE_NOT_SUPPORTED',\n '',\n 'generateObject is only available on iOS and Android'\n );\n }\n if (!messages || messages.length === 0) {\n throw new Error('messages array cannot be empty');\n }\n if (!schema || typeof schema !== 'object') {\n throw new Error('schema must be a JSON Schema object');\n }\n\n const maxRepairAttempts = Math.max(0, options?.maxRepairAttempts ?? 2);\n const instruction = buildSchemaInstruction(schema);\n\n // Inject the schema instruction. If the caller supplied a system message we\n // append to it (sendMessage reads system from the array); otherwise we carry\n // the instruction via the systemPrompt option, which sendMessage applies when\n // the array has no system message — including on the repair turns we append.\n const sysIdx = messages.findIndex((m) => m.role === 'system');\n let working: LLMMessage[];\n let systemPrompt: string | undefined;\n if (sysIdx >= 0) {\n working = messages.map((m, i) =>\n i === sysIdx ? { role: m.role, content: `${m.content}\\n\\n${instruction}` } : m\n );\n systemPrompt = undefined; // the array carries the system message\n } else {\n working = [...messages];\n systemPrompt = `${options?.systemPrompt ?? DEFAULT_OBJECT_SYSTEM_PROMPT}\\n\\n${instruction}`;\n }\n\n let lastText = '';\n for (let attempt = 0; attempt <= maxRepairAttempts; attempt++) {\n const { text } = await sendMessage(working, { systemPrompt, signal: options?.signal });\n lastText = text;\n\n const parsed = extractJson(text);\n if (parsed.ok) {\n const errors = validateAgainstSchema(parsed.value, schema);\n if (errors.length === 0) {\n return { object: parsed.value as T, text };\n }\n if (attempt < maxRepairAttempts) {\n working = [\n ...working,\n { role: 'assistant', content: text },\n { role: 'user', content: buildSchemaRepair(errors) },\n ];\n }\n } else if (attempt < maxRepairAttempts) {\n working = [\n ...working,\n { role: 'assistant', content: text },\n { role: 'user', content: REPAIR_INVALID_JSON },\n ];\n }\n }\n\n throw new ModelError(\n 'INFERENCE_FAILED',\n getActiveModel(),\n `generateObject: model did not return schema-valid JSON after ${maxRepairAttempts + 1} attempt(s). ` +\n `Last output: ${lastText.slice(0, 200)}`\n );\n}\n\n// ============================================================================\n// Model Management API\n// ============================================================================\n\n/**\n * Get all built-in models available on the current platform.\n *\n * Built-in models are provided by the OS and require no download.\n * On iOS this returns Apple Foundation Models; on Android, ML Kit.\n *\n * @returns Array of built-in models with availability status\n */\nexport async function getBuiltInModels(): Promise<BuiltInModel[]> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return [];\n }\n return ExpoAiKitModule.getBuiltInModels();\n}\n\n/**\n * Get all downloadable models from the registry, enriched with on-device status.\n *\n * Reads from the hardcoded MODEL_REGISTRY and queries the native layer\n * for the current download/load status of each model.\n *\n * @returns Array of downloadable models with their current status\n */\nexport async function getDownloadableModels(): Promise<DownloadableModel[]> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return [];\n }\n\n const platformModels = MODEL_REGISTRY.filter((entry) =>\n entry.supportedPlatforms.includes(Platform.OS as 'ios' | 'android')\n );\n\n let deviceRamBytes = 0;\n try {\n deviceRamBytes = ExpoAiKitModule.getDeviceRamBytes();\n } catch {\n // Native call unavailable -- default to 0 (all models will show meetsRequirements: false)\n }\n\n return Promise.all(\n platformModels.map(async (entry) => {\n // Await: on iOS this bridges as a Promise (reads actor state); on Android\n // it's synchronous and awaiting a plain value is a no-op.\n const status = await ExpoAiKitModule.getDownloadableModelStatus(entry.id);\n return {\n id: entry.id,\n name: entry.name,\n parameterCount: entry.parameterCount,\n sizeBytes: entry.sizeBytes,\n contextWindow: entry.contextWindow,\n minRamBytes: entry.minRamBytes,\n meetsRequirements: deviceRamBytes >= entry.minRamBytes,\n status,\n };\n })\n );\n}\n\n/**\n * Pick the best downloadable model the current device can run.\n *\n * Returns the most capable model (largest, by RAM requirement) whose\n * `meetsRequirements` is true — e.g. Gemma 4 E4B on high-spec phones, falling\n * back to E2B on more constrained ones — or `null` if the device can't run any.\n *\n * This is a convenience over {@link getDownloadableModels}; the caller still\n * downloads + activates explicitly. Pass `platform` is implicit (current OS).\n *\n * @example\n * ```ts\n * const best = await getRecommendedModel();\n * if (best) {\n * await downloadModel(best.id, { onProgress });\n * await setModel(best.id);\n * }\n * ```\n */\nexport async function getRecommendedModel(): Promise<DownloadableModel | null> {\n const models = await getDownloadableModels();\n const runnable = models.filter((m) => m.meetsRequirements);\n if (runnable.length === 0) return null;\n // Higher RAM requirement ⇒ larger/more capable model. Prefer the biggest that fits.\n return runnable.sort((a, b) => b.minRamBytes - a.minRamBytes)[0];\n}\n\n/**\n * Download a model to the device.\n *\n * Looks up the model in the registry, validates platform support and\n * device requirements, then initiates the download with integrity verification.\n *\n * @param modelId - ID of the model to download (e.g. 'gemma-e2b')\n * @param options - Optional download configuration\n * @param options.onProgress - Callback with download progress (0-1)\n * @throws {ModelError} MODEL_NOT_FOUND if modelId is not in the registry\n * @throws {ModelError} DEVICE_NOT_SUPPORTED if platform is not supported\n * @throws {ModelError} DOWNLOAD_FAILED on network error\n * @throws {ModelError} DOWNLOAD_STORAGE_FULL if insufficient disk space\n * @throws {ModelError} DOWNLOAD_CORRUPT if SHA256 hash doesn't match\n */\nexport async function downloadModel(\n modelId: string,\n options?: { onProgress?: (progress: number) => void }\n): Promise<void> {\n const entry = getRegistryEntry(modelId);\n if (!entry) {\n throw new ModelError('MODEL_NOT_FOUND', modelId);\n }\n\n if (!entry.supportedPlatforms.includes(Platform.OS as 'ios' | 'android')) {\n throw new ModelError(\n 'DEVICE_NOT_SUPPORTED',\n modelId,\n `Model ${modelId} is not supported on ${Platform.OS}`\n );\n }\n\n try {\n const deviceRamBytes = ExpoAiKitModule.getDeviceRamBytes();\n if (deviceRamBytes < entry.minRamBytes) {\n throw new ModelError(\n 'DEVICE_NOT_SUPPORTED',\n modelId,\n `Device has ${Math.round(deviceRamBytes / 1e9)}GB RAM, model requires ${Math.round(entry.minRamBytes / 1e9)}GB`\n );\n }\n } catch (e) {\n if (e instanceof ModelError) throw e;\n // If getDeviceRamBytes is unavailable, skip the check\n }\n\n let subscription: ReturnType<typeof ExpoAiKitModule.addListener> | undefined;\n if (options?.onProgress) {\n subscription = ExpoAiKitModule.addListener(\n 'onDownloadProgress',\n (event) => {\n if (event.modelId === modelId) {\n options.onProgress!(event.progress);\n }\n }\n );\n }\n\n try {\n await wrapNative(() =>\n ExpoAiKitModule.downloadModel(modelId, entry.downloadUrl, entry.sha256)\n );\n } finally {\n subscription?.remove();\n }\n}\n\n/**\n * Cancel an in-flight download for a model.\n *\n * The in-progress {@link downloadModel} promise rejects with a\n * DOWNLOAD_CANCELLED {@link ModelError}. No-op if the model isn't downloading.\n *\n * @param modelId - ID of the model whose download should be cancelled\n */\nexport async function cancelDownload(modelId: string): Promise<void> {\n if (Platform.OS !== 'ios' && Platform.OS !== 'android') {\n return;\n }\n await wrapNative(() => ExpoAiKitModule.cancelDownload(modelId));\n}\n\n/**\n * Delete a downloaded model from the device.\n *\n * If the model is currently loaded, it will be unloaded first.\n *\n * @param modelId - ID of the model to delete\n * @throws {ModelError} MODEL_NOT_FOUND if modelId is not in the registry\n */\nexport async function deleteModel(modelId: string): Promise<void> {\n const entry = getRegistryEntry(modelId);\n if (!entry) {\n throw new ModelError('MODEL_NOT_FOUND', modelId);\n }\n\n await wrapNative(() => ExpoAiKitModule.deleteModel(modelId));\n}\n\n/**\n * Set the active model for inference.\n *\n * This is the sole gatekeeper for model validity. If setModel succeeds,\n * the model is loaded and ready -- sendMessage never needs its own check.\n *\n * For downloadable models, this loads the model into memory (status\n * transitions: loading -> ready). Only one downloadable model can be\n * loaded at a time; the previous one is auto-unloaded.\n *\n * For built-in models, this simply switches the active backend.\n *\n * If setModel was never called, sendMessage uses the platform built-in\n * model (today's behavior, no error).\n *\n * @param modelId - ID of the model to activate (e.g. 'gemma-e2b', 'apple-fm', 'mlkit')\n * @param options - Optional configuration for model loading\n * @param options.backend - Hardware backend: 'auto' (default, GPU with CPU fallback), 'gpu', or 'cpu'\n * @throws {ModelError} MODEL_NOT_FOUND if modelId is invalid\n * @throws {ModelError} MODEL_NOT_DOWNLOADED if the downloadable model file is not on disk\n * @throws {ModelError} MODEL_LOAD_FAILED if loading into memory fails\n * @throws {ModelError} INFERENCE_OOM if device can't fit model in memory\n */\nexport async function setModel(modelId: string, options?: SetModelOptions): Promise<void> {\n const entry = getRegistryEntry(modelId);\n const minRamBytes = entry?.minRamBytes ?? 0;\n const backend = options?.backend ?? 'auto';\n const generation = toNativeGeneration(options?.generation);\n await wrapNative(() =>\n ExpoAiKitModule.setModel(modelId, minRamBytes, backend, generation)\n );\n}\n\n/**\n * Get the ID of the currently active model.\n *\n * @returns The active model ID (e.g. 'apple-fm', 'mlkit', 'gemma-e2b')\n */\nexport function getActiveModel(): string {\n return ExpoAiKitModule.getActiveModel();\n}\n\n/**\n * Explicitly unload the current downloadable model from memory.\n *\n * Frees memory and reverts to the platform built-in model.\n * No-op if no downloadable model is currently loaded.\n */\nexport async function unloadModel(): Promise<void> {\n await wrapNative(() => ExpoAiKitModule.unloadModel());\n}\n\n"]}
|
package/build/models.js
CHANGED
|
@@ -12,13 +12,13 @@ export const MODEL_REGISTRY = [
|
|
|
12
12
|
parameterCount: '2.3B',
|
|
13
13
|
quantization: 'mixed-2/4/8-bit',
|
|
14
14
|
downloadUrl: 'https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm',
|
|
15
|
-
sha256: '',
|
|
16
|
-
sizeBytes:
|
|
15
|
+
sha256: '181938105e0eefd105961417e8da75903eacda102c4fce9ce90f50b97139a63c',
|
|
16
|
+
sizeBytes: 2_588_147_712, // 2.59GB (exact, HF LFS)
|
|
17
17
|
// Conservative limit for 4GB RAM devices.
|
|
18
18
|
// TODO: Benchmark during Phase 2 testing.
|
|
19
19
|
contextWindow: 8_000,
|
|
20
20
|
minRamBytes: 2_000_000_000, // 2GB — LiteRT-LM memory-maps weights, actual RSS ~1.5GB
|
|
21
|
-
supportedPlatforms: ['android'],
|
|
21
|
+
supportedPlatforms: ['ios', 'android'],
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
id: 'gemma-e4b',
|
|
@@ -26,11 +26,11 @@ export const MODEL_REGISTRY = [
|
|
|
26
26
|
parameterCount: '4.5B',
|
|
27
27
|
quantization: 'mixed-4/8-bit',
|
|
28
28
|
downloadUrl: 'https://huggingface.co/litert-community/gemma-4-E4B-it-litert-lm/resolve/main/gemma-4-E4B-it.litertlm',
|
|
29
|
-
sha256: '',
|
|
30
|
-
sizeBytes:
|
|
29
|
+
sha256: '0b2a8980ce155fd97673d8e820b4d29d9c7d99b8fa6806f425d969b145bd52e0',
|
|
30
|
+
sizeBytes: 3_659_530_240, // 3.66GB (exact, HF LFS)
|
|
31
31
|
contextWindow: 16_000,
|
|
32
32
|
minRamBytes: 3_000_000_000, // 3GB — LiteRT-LM memory-maps weights
|
|
33
|
-
supportedPlatforms: ['android'],
|
|
33
|
+
supportedPlatforms: ['ios', 'android'],
|
|
34
34
|
},
|
|
35
35
|
];
|
|
36
36
|
/**
|