expo-local-llm 0.9.0 → 0.10.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 +131 -315
- package/android/build.gradle +3 -0
- package/android/src/main/java/expo/modules/localllm/AndroidModel.kt +0 -3
- package/android/src/main/java/expo/modules/localllm/LLMSession.kt +58 -23
- package/android/src/main/java/expo/modules/localllm/SessionRuntime.kt +48 -0
- package/build/ExpoLocalLlm.types.d.ts +13 -4
- package/build/ExpoLocalLlm.types.d.ts.map +1 -1
- package/build/ExpoLocalLlm.types.js.map +1 -1
- package/build/LLMSession.d.ts.map +1 -1
- package/build/LLMSession.js +16 -8
- package/build/LLMSession.js.map +1 -1
- package/build/capabilities.d.ts.map +1 -1
- package/build/capabilities.js +7 -1
- package/build/capabilities.js.map +1 -1
- package/build/useLocalLLM.d.ts.map +1 -1
- package/build/useLocalLLM.js +14 -2
- package/build/useLocalLLM.js.map +1 -1
- package/package.json +1 -1
- package/src/ExpoLocalLlm.types.ts +14 -4
- package/src/LLMSession.ts +21 -13
- package/src/__tests__/ExpoLocalLlm.test.ts +1 -1
- package/src/__tests__/inference.typecheck.ts +7 -1
- package/src/__tests__/nativeApi.test.ts +72 -5
- package/src/__tests__/useLocalLLM.native.test.ts +159 -0
- package/src/capabilities.ts +7 -1
- package/src/useLocalLLM.ts +18 -5
package/README.md
CHANGED
|
@@ -9,28 +9,34 @@
|
|
|
9
9
|
|
|
10
10
|
Use system-provided models in Expo apps: Apple Foundation Models on iOS 26+ and Gemini Nano on Android. Generate text, stream it into a component, extract typed objects and call tools, with no API key.
|
|
11
11
|
|
|
12
|
-
The full JavaScript API measures approximately **17.
|
|
12
|
+
The full JavaScript API measures approximately **17.8 kB minified / 6.0 kB gzipped**, excluding external dependencies and native code.[^js-size]
|
|
13
13
|
|
|
14
14
|
Inference stays on the device, with no cloud fallback. Once the model is ready, generation works offline; initial model provisioning or downloads may require network access and device storage. Tool handlers you write may also access the network.
|
|
15
15
|
|
|
16
16
|
[^js-size]: Measured from the built package entry point with Bun 1.4.4: a minified browser-target ESM bundle, all public exports retained, packages external, and default `node:zlib` gzip compression. Sizes use decimal kB. This standalone JS measurement excludes Expo, React, React Native, native SDKs (including Android ML Kit), and model assets; it is not a Metro/Hermes or installed-app size measurement. Reproduce with `bun run build && bun run check:size`. CI enforces budgets of 18,000 minified bytes and 6,200 gzip bytes.
|
|
17
17
|
|
|
18
|
+
[Setup](#quick-start) · [Usage](#usage) · [API reference](docs/api.md) · [Troubleshooting](docs/troubleshooting.md) · [Changelog](CHANGELOG.md)
|
|
19
|
+
|
|
18
20
|
## At a glance
|
|
19
21
|
|
|
20
22
|
| | iOS 26+ (Foundation Models) | Android (Gemini Nano) |
|
|
21
23
|
|---|---|---|
|
|
22
|
-
| Text generation and streaming | ✅ Verified on device |
|
|
23
|
-
| Image prompts | iOS 27+, Xcode 27+, and a ready vision-capable system model |
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
| Tool calling (validated primitive arguments) | ✅ Schema-constrained |
|
|
24
|
+
| Text generation and streaming | ✅ Verified on device | ✅ Implemented |
|
|
25
|
+
| Image prompts | iOS 27+, Xcode 27+, and a ready vision-capable system model | ✅ Up to eight images; requires model vision support |
|
|
26
|
+
| Structured results (`generateObject`) | ✅ Native constraints + JS validation | ✅ JS schema validation and bounded repair |
|
|
27
|
+
| Native schema constraints | ✅ Runtime schemas | Unavailable for runtime schemas in ML Kit; automatic mode uses validation instead |
|
|
28
|
+
| Tool calling (validated primitive arguments) | ✅ Schema-constrained | ✅ Native tools; selected model must advertise function calling |
|
|
27
29
|
| Stable error codes | ✅ | ✅ |
|
|
28
30
|
| Model download | Managed by the OS | `downloadModel()` with progress |
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
**Checkmarks indicate implemented features, not hardware certification.** Android session behavior is covered by automated tests, and real SDK image requests are tested on an emulator. Gemini Nano inference has not yet been validated on eligible hardware. See the [verification record](docs/release-verification.md) for evidence and remaining device checks.
|
|
33
|
+
|
|
34
|
+
Unsupported features fail with a clear error instead of degrading silently. `getCapabilities()` reports integration support, including the path selected by `objectGenerationMode`; `getModelReadiness()` reports selected-model readiness and tool support. `structuredOutput` continues to describe native constrained output only. See [how this library is verified](#how-this-library-is-verified).
|
|
31
35
|
|
|
32
36
|
## Quick start
|
|
33
37
|
|
|
38
|
+
Use an existing Expo app and a native development build; **Expo Go is not supported**. Generation needs Apple Intelligence on iOS 26+ or an eligible Gemini Nano device on Android. A successful build does not establish device eligibility. Check the [requirements](#requirements) before choosing a test device.
|
|
39
|
+
|
|
34
40
|
```bash
|
|
35
41
|
npx expo install expo-local-llm expo-dev-client expo-build-properties
|
|
36
42
|
```
|
|
@@ -45,7 +51,7 @@ Set the deployment targets the module needs in `app.json`:
|
|
|
45
51
|
"expo-build-properties",
|
|
46
52
|
{
|
|
47
53
|
"ios": { "deploymentTarget": "16.4" },
|
|
48
|
-
|
|
54
|
+
"android": { "minSdkVersion": 26, "kotlinVersion": "2.3.0" }
|
|
49
55
|
}
|
|
50
56
|
]
|
|
51
57
|
]
|
|
@@ -57,6 +63,8 @@ Build a development app, since stock Expo Go cannot load native modules:
|
|
|
57
63
|
|
|
58
64
|
```bash
|
|
59
65
|
npx expo run:ios --device
|
|
66
|
+
# Or, for Android:
|
|
67
|
+
# npx expo run:android --device
|
|
60
68
|
```
|
|
61
69
|
|
|
62
70
|
Then stream a response:
|
|
@@ -66,28 +74,49 @@ import { Button, Text } from 'react-native';
|
|
|
66
74
|
import { useLocalLLM } from 'expo-local-llm';
|
|
67
75
|
|
|
68
76
|
export function Summary({ notes }: { notes: string }) {
|
|
69
|
-
const { availability, streamResponse, streamedText, error } = useLocalLLM({
|
|
77
|
+
const { availability, streamResponse, streamedText, isGenerating, error } = useLocalLLM({
|
|
70
78
|
instructions: 'Summarise the text in two sentences.',
|
|
71
79
|
});
|
|
72
80
|
|
|
73
|
-
|
|
81
|
+
async function summarise() {
|
|
82
|
+
if (!streamResponse || isGenerating) return;
|
|
83
|
+
try {
|
|
84
|
+
await streamResponse(notes);
|
|
85
|
+
} catch {
|
|
86
|
+
// The hook exposes the rejection through `error`, rendered below.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
return (
|
|
75
91
|
<>
|
|
76
|
-
<
|
|
77
|
-
<
|
|
92
|
+
<Text>Model: {availability}</Text>
|
|
93
|
+
<Button
|
|
94
|
+
title={isGenerating ? 'Summarising…' : 'Summarise'}
|
|
95
|
+
disabled={!streamResponse || isGenerating}
|
|
96
|
+
onPress={summarise}
|
|
97
|
+
/>
|
|
98
|
+
<Text>{error ?? streamedText}</Text>
|
|
78
99
|
</>
|
|
79
100
|
);
|
|
80
101
|
}
|
|
81
102
|
```
|
|
82
103
|
|
|
83
|
-
`streamResponse` is `undefined` until the model is ready
|
|
104
|
+
`streamResponse` is `undefined` until the model is ready. `availability` distinguishes an ineligible device, disabled Apple Intelligence, a pending download, and other states. Android apps can expose the hook's `downloadModel()` when availability is `downloadRequired`; see [readiness and downloads](docs/troubleshooting.md#model-not-ready).
|
|
105
|
+
|
|
106
|
+
The hook retains conversation history between requests. Use `reset()` for a new conversation or [`generate()`](#one-shot-generation) for independent tasks.
|
|
107
|
+
|
|
108
|
+
### Upgrading to 0.10.0
|
|
109
|
+
|
|
110
|
+
All Android apps now require **Kotlin 2.3.0+**, including text-only apps; set `kotlinVersion` as shown above or update your native Kotlin Gradle plugin. Rebuild your iOS or Android app to include the new native image and tool APIs. An EAS Update alone cannot install them.
|
|
111
|
+
|
|
112
|
+
Version 0.10.0 makes `generateObject()` automatic: iOS uses native constraints and Android uses validation with bounded repair. Pass `mode: 'constrained'` to require native constraints. Capability objects now require `objectGenerationMode`, and `experimental` is `false` for both integrations; device verification is documented separately. Hook/session `responseFormat: 'json'` remains iOS-only. See the [changelog](CHANGELOG.md).
|
|
84
113
|
|
|
85
114
|
## Requirements
|
|
86
115
|
|
|
87
116
|
| | Requirement |
|
|
88
117
|
|---|---|
|
|
89
118
|
| iOS | iOS 26+ with Apple Intelligence enabled to generate. The module loads on iOS 16.4+ and reports `notEligible` below 26. The host project must target iOS 16.4+, and building needs Xcode 26.4 or newer. |
|
|
90
|
-
| Android | `minSdkVersion` 26+ and Kotlin 2.3.0+ to compile ML Kit beta5. Generation needs
|
|
119
|
+
| Android | `minSdkVersion` 26+ and Kotlin 2.3.0+ to compile ML Kit beta5. Generation needs an eligible Gemini Nano device; query readiness on the target device before offering generation. |
|
|
91
120
|
| Expo SDK | 52+ peer range. The example app targets SDK 58 / React Native 0.88; the peer range is not a tested compatibility matrix. |
|
|
92
121
|
|
|
93
122
|
A native development build is required, built locally or with EAS Build. Installing or upgrading the package needs a new binary; EAS Update cannot add native code. An iPhone simulator can run Foundation Models when the host Mac has Apple Intelligence enabled; latency and thermals still need real hardware.
|
|
@@ -117,54 +146,15 @@ Then regenerate and rebuild the native app. This is an app-level build setting,
|
|
|
117
146
|
|
|
118
147
|
## Usage
|
|
119
148
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const {
|
|
128
|
-
availability,
|
|
129
|
-
isGenerating,
|
|
130
|
-
streamedText,
|
|
131
|
-
error,
|
|
132
|
-
respond,
|
|
133
|
-
streamResponse,
|
|
134
|
-
cancelStream,
|
|
135
|
-
downloadModel,
|
|
136
|
-
downloadProgress,
|
|
137
|
-
} = useLocalLLM({
|
|
138
|
-
instructions: 'You are a helpful assistant.',
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Methods become available after session creation and model readiness.
|
|
142
|
-
if (!streamResponse) {
|
|
143
|
-
return (
|
|
144
|
-
<Text>
|
|
145
|
-
{error ?? (availability === 'available'
|
|
146
|
-
? 'Preparing on-device AI...'
|
|
147
|
-
: `On-device AI: ${availability}`)}
|
|
148
|
-
</Text>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const handleSend = async (prompt: string) => {
|
|
153
|
-
// Streaming — updates streamedText live, resolves with the final text
|
|
154
|
-
const text = await streamResponse(prompt);
|
|
149
|
+
| Task | API | Session ownership |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| Stream into a React component or keep a conversation | `useLocalLLM()` | The hook creates and releases its session |
|
|
152
|
+
| Generate one independent response | `generate()` | Released after the request |
|
|
153
|
+
| Extract a typed object | `generateObject()` | Released after each attempt |
|
|
154
|
+
| Invoke your app's tools | `useLocalLLM({ tools })` | The hook dispatches handlers |
|
|
155
|
+
| Manage events and conversation lifetime yourself | `createLLMSession()` | You must call `release()` |
|
|
155
156
|
|
|
156
|
-
|
|
157
|
-
// const response = await respond(prompt);
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<View>
|
|
162
|
-
<Text>Status: {availability}</Text>
|
|
163
|
-
{isGenerating && <Text>{streamedText}</Text>}
|
|
164
|
-
</View>
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
```
|
|
157
|
+
The [quick start](#quick-start) shows streaming with the hook. For all options and return types, see the [API reference](docs/api.md).
|
|
168
158
|
|
|
169
159
|
### One-shot generation
|
|
170
160
|
|
|
@@ -178,7 +168,9 @@ const title = await generate('Suggest a short title for this note: ...', {
|
|
|
178
168
|
});
|
|
179
169
|
```
|
|
180
170
|
|
|
181
|
-
|
|
171
|
+
<a id="describe-an-image"></a>
|
|
172
|
+
|
|
173
|
+
### Describe or compare images
|
|
182
174
|
|
|
183
175
|
Pass a prompt object to `generate()`, `session.respond()`, `session.streamResponse()`, or the hook's `respond` / `streamResponse`:
|
|
184
176
|
|
|
@@ -192,26 +184,31 @@ const description = await generate({
|
|
|
192
184
|
});
|
|
193
185
|
```
|
|
194
186
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
Both platforms accept **up to eight images per request**, with a unique label for each image. The model's context and device resources still determine whether a particular request can complete.
|
|
198
|
-
|
|
199
|
-
iOS uses native Foundation Models image attachments and requires iOS 27+, an app built with Xcode 27+, and a ready system model that advertises vision support. Older OS versions, older builds, and models without vision reject image prompts with `ERR_IMAGE_NOT_SUPPORTED`; text prompts still work with the existing iOS 26 / Xcode 26.4 minimum. The framework handles image scaling and orientation. Completed image turns remain in the native session transcript until `reset()` or release.
|
|
187
|
+
Attach up to **eight local images** with unique labels. For a comparison:
|
|
200
188
|
|
|
201
|
-
|
|
189
|
+
```ts
|
|
190
|
+
// Both URIs point to local files already available to your app.
|
|
191
|
+
const comparison = await generate({
|
|
192
|
+
text: 'What changed between the before and after photos?',
|
|
193
|
+
images: [
|
|
194
|
+
{ uri: beforePhotoUri, label: 'before' },
|
|
195
|
+
{ uri: afterPhotoUri, label: 'after' },
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
```
|
|
202
199
|
|
|
203
|
-
|
|
200
|
+
iOS images require **iOS 27+, Xcode 27+, and a ready vision-capable model**. Android image support depends on the selected model. Use `getCapabilities().images` to inspect support; a ready text model does not guarantee vision support.
|
|
204
201
|
|
|
205
|
-
|
|
202
|
+
Images must use local `file://` URIs; download remote images first. iOS retains completed image turns in session history. Android retains images during the current request and its tool turns, but you must attach them again on a later request. See [image requirements, capabilities, and errors](docs/images.md) for URI rules, limits, and cancellation behavior.
|
|
206
203
|
|
|
207
|
-
### Extract typed data
|
|
204
|
+
### Extract typed data
|
|
208
205
|
|
|
209
|
-
`generateObject()`
|
|
206
|
+
`generateObject()` infers the result type from the schema and automatically selects the strongest available path. iOS uses native constrained decoding. Android generates text, validates every field in JavaScript, and makes at most one repair request after invalid JSON or a schema mismatch. Check `objectGenerationMode` to inspect the selected path:
|
|
210
207
|
|
|
211
208
|
```ts
|
|
212
209
|
import { generateObject, getCapabilities } from 'expo-local-llm';
|
|
213
210
|
|
|
214
|
-
if (getCapabilities().
|
|
211
|
+
if (getCapabilities().objectGenerationMode !== 'unsupported') {
|
|
215
212
|
const result = await generateObject('Classify: "Can we move the meeting to Friday?"', {
|
|
216
213
|
instructions: 'Classify the message by its primary intent.',
|
|
217
214
|
schema: {
|
|
@@ -222,7 +219,7 @@ if (getCapabilities().structuredOutputMode === 'constrained') {
|
|
|
222
219
|
}
|
|
223
220
|
```
|
|
224
221
|
|
|
225
|
-
A supported capability does not mean the model is ready, so handle rejected calls.
|
|
222
|
+
A supported capability does not mean the model is ready, so handle rejected calls. Provider failures never switch paths or trigger repair. Exhausted schema repair rejects with `StructuredOutputValidationError`.
|
|
226
223
|
|
|
227
224
|
<p align="center">
|
|
228
225
|
<img src="https://raw.githubusercontent.com/GijungKim/expo-local-llm/main/docs/structured-output.png" alt="Structured output demo — recipe JSON with constrained 'difficulty' enum on iPhone" width="320" />
|
|
@@ -230,15 +227,15 @@ A supported capability does not mean the model is ready, so handle rejected call
|
|
|
230
227
|
|
|
231
228
|
*Real device output: the model fills in a recipe schema with `difficulty` constrained to `easy | medium | hard`. The streaming demo lives at `example/App.tsx`.*
|
|
232
229
|
|
|
233
|
-
###
|
|
230
|
+
### Choose the structured-output path explicitly
|
|
234
231
|
|
|
235
|
-
|
|
232
|
+
The default `mode: 'auto'` is portable. Request `mode: 'constrained'` to require native constraints and reject when unavailable, or `mode: 'validated'` to force ordinary text generation plus complete JSON validation. Validated mode does **not** constrain decoding. `structuredOutput` and `structuredOutputMode` describe native constraints; `objectGenerationMode` describes automatic routing.
|
|
236
233
|
|
|
237
234
|
```ts
|
|
238
|
-
import { generateObject, getCapabilities } from 'expo-local-llm';
|
|
235
|
+
import { generateObject, getCapabilities, getModelReadiness } from 'expo-local-llm';
|
|
239
236
|
|
|
240
237
|
const controller = new AbortController();
|
|
241
|
-
if (getCapabilities().text) {
|
|
238
|
+
if (getCapabilities().text && (await getModelReadiness('default')).availability === 'available') {
|
|
242
239
|
const result = await generateObject('Classify: "Can we meet on Friday?"', {
|
|
243
240
|
mode: 'validated',
|
|
244
241
|
repairAttempts: 1, // default; choose 0 to disable repair
|
|
@@ -254,13 +251,13 @@ if (getCapabilities().text) {
|
|
|
254
251
|
// Call controller.abort() from your cancel action while the call is pending.
|
|
255
252
|
```
|
|
256
253
|
|
|
257
|
-
Only JSON parse or schema failures permit one additional request. Each attempt uses a fresh session with the original task, schema, instructions, generation options, selected model, and image attachment records. Native errors, refusals, guardrails, cancellation, context limits, and availability failures propagate without repair. Exhausted or disabled repair rejects with `StructuredOutputValidationError`; the helper never extracts JSON from prose, coerces values, inserts fields, or removes extra fields. Validation checks structure, not factual correctness. Both platforms still need device testing of
|
|
254
|
+
Only JSON parse or schema failures permit one additional request. Each attempt uses a fresh session with the original task, schema, instructions, generation options, selected model, and image attachment records. Native errors, refusals, guardrails, cancellation, context limits, and availability failures propagate without repair. Exhausted or disabled repair rejects with `StructuredOutputValidationError`; the helper never extracts JSON from prose, coerces values, inserts fields, or removes extra fields. Validation checks structure, not factual correctness. Both platforms still need device testing of validated mode (automatic on Android or explicit).
|
|
258
255
|
|
|
259
|
-
|
|
256
|
+
Automatic and validated modes are available only in one-shot `generateObject()`. They do not change sessions, hooks, JSON streaming, or Android's rejection of native `responseFormat: 'json'`.
|
|
260
257
|
|
|
261
258
|
### Structured output in the hook
|
|
262
259
|
|
|
263
|
-
|
|
260
|
+
On iOS, JSON mode in the hook streams a partial object, then publishes the validated result. Android callers should use one-shot [`generateObject()`](#extract-typed-data); the hook does not support automatic or validated JSON generation.
|
|
264
261
|
|
|
265
262
|
```tsx
|
|
266
263
|
const { streamResponse, streamedObject, completedObject } = useLocalLLM({
|
|
@@ -283,81 +280,59 @@ Schemas are validated before they reach native code; a malformed one throws `Sch
|
|
|
283
280
|
|
|
284
281
|
### Tool calling
|
|
285
282
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
For Android preview access, pass `androidModel: 'previewFast'` to the hook/session and check `await getModelReadiness('previewFast')`. Default selection stays STABLE + FULL; previewFast explicitly selects PREVIEW + FAST. iOS ignores this option. The hook uses the same selection for readiness, downloads, and availability events.
|
|
289
|
-
|
|
283
|
+
Use `defineTool()` to type a handler from its parameters and `useLocalLLM()` to dispatch calls. This example looks up local sample data; replace the handler with your app's data access:
|
|
290
284
|
|
|
291
285
|
```tsx
|
|
292
|
-
import {
|
|
286
|
+
import { Button, Text } from 'react-native';
|
|
293
287
|
import { defineTool, useLocalLLM } from 'expo-local-llm';
|
|
294
288
|
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
289
|
+
const stock = new Map([['trail-shoes', 12], ['rain-jacket', 0]]);
|
|
290
|
+
const lookupStock = defineTool({
|
|
291
|
+
name: 'lookupStock',
|
|
292
|
+
description: 'Look up the available quantity for a product SKU',
|
|
298
293
|
parameters: {
|
|
299
|
-
|
|
300
|
-
unit: { type: 'string', description: 'Temperature unit', enum: ['celsius', 'fahrenheit'] },
|
|
301
|
-
},
|
|
302
|
-
// Typed from `parameters`: city is a string, unit is 'celsius' | 'fahrenheit'.
|
|
303
|
-
handler: async ({ city, unit }) => {
|
|
304
|
-
const res = await fetch(
|
|
305
|
-
`https://api.example.com/weather?city=${encodeURIComponent(city)}&unit=${unit}`
|
|
306
|
-
);
|
|
307
|
-
return JSON.stringify(await res.json());
|
|
294
|
+
sku: { type: 'string', description: 'Product SKU, such as trail-shoes' },
|
|
308
295
|
},
|
|
296
|
+
handler: async ({ sku }) => JSON.stringify({ sku, quantity: stock.get(sku) ?? null }),
|
|
309
297
|
});
|
|
310
298
|
|
|
311
|
-
function
|
|
312
|
-
const { streamResponse, streamedText, activeToolCalls, error } = useLocalLLM({
|
|
313
|
-
instructions: '
|
|
314
|
-
|
|
299
|
+
export function InventoryAssistant() {
|
|
300
|
+
const { streamResponse, streamedText, isGenerating, activeToolCalls, error } = useLocalLLM({
|
|
301
|
+
instructions: 'Use lookupStock to answer inventory questions.',
|
|
302
|
+
androidModel: 'previewFast', // Explicit Android preview selection; ignored on iOS.
|
|
303
|
+
tools: [lookupStock],
|
|
315
304
|
});
|
|
316
305
|
|
|
306
|
+
async function checkStock() {
|
|
307
|
+
if (!streamResponse || isGenerating) return;
|
|
308
|
+
try {
|
|
309
|
+
await streamResponse('How many trail-shoes are available?');
|
|
310
|
+
} catch {
|
|
311
|
+
// The hook exposes failures through `error`.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
317
315
|
return (
|
|
318
|
-
|
|
319
|
-
{
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
</View>
|
|
316
|
+
<>
|
|
317
|
+
<Button title="Check stock" onPress={checkStock} disabled={!streamResponse || isGenerating} />
|
|
318
|
+
{activeToolCalls.length > 0 && <Text>Checking inventory…</Text>}
|
|
319
|
+
<Text>{error ?? streamedText}</Text>
|
|
320
|
+
</>
|
|
324
321
|
);
|
|
325
322
|
}
|
|
326
323
|
```
|
|
327
324
|
|
|
328
|
-
`
|
|
329
|
-
|
|
330
|
-
On iOS, tool arguments use the same constrained decoding as structured output: the model fills in a schema built from `parameters`, so every declared parameter arrives with its declared type and any `enum` is respected. Validate values whose meaning matters (ranges, formats) as you would any input. If a handler throws or times out, the turn rejects with `ERR_TOOL_FAILED`.
|
|
331
|
-
|
|
332
|
-
Android validates every declaration and the entire returned batch before the first handler event. Every declared parameter is required; only strings (including string enums), booleans, and numbers are accepted. Missing/extra keys, nulls, nested values, nonfinite numbers, unsafe 64-bit integers, and unknown tool names fail the turn. Android tool and parameter names must match `[A-Za-z_][A-Za-z0-9_]{0,63}`. Application-specific ranges and permissions still belong in your handler.
|
|
333
|
-
|
|
334
|
-
Non-null SDK call IDs must be unique across the entire generation. A repeated ID rejects the whole batch before any of its handlers execute. Calls without SDK IDs remain supported; the library cannot infer whether two calls with different or missing IDs represent the same intended action.
|
|
335
|
-
|
|
336
|
-
Before inference, a selected model that needs downloading or is otherwise unavailable fails with `ERR_MODEL_UNAVAILABLE`. Confirmed absence of function calling on a ready model fails with `ERR_TOOL_NOT_SUPPORTED`. Readiness query failures retain normal SDK error mapping (for example, `ERR_RATE_LIMITED` or `ERR_GENERATION_FAILED`).
|
|
337
|
-
|
|
338
|
-
Android executes calls sequentially with one outer request ID, at most **8 tool-bearing rounds** and **32 calls** per generation, and **65,536 UTF-8 bytes per result**. The default handler timeout is **30 seconds**, configurable with a finite positive `toolTimeout`. Handler rejection, timeout, invalid model calls, truncated tool turns, or limit violations end the generation with `ERR_TOOL_FAILED`; handlers are never automatically retried. Results remain opaque strings wrapped as `{ result: value }` in native function responses.
|
|
339
|
-
|
|
340
|
-
Streaming tools use ML Kit's callback API and execute only the validated complete terminal batch. Text accumulates across rounds with one outer `streamComplete`. Cancellation returns accumulated text without committing history or emitting completion. Reset, release, and tool registration changes cancel pending work; await the old generation before starting another. `registerTool()` and `unregisterTool()` preserve committed history; changing hook configuration creates a new session. Android retains at most ten whole exchanges, including each exchange's model calls and user results, so trimming cannot orphan tool protocol.
|
|
341
|
-
|
|
325
|
+
Android tool calling requires a selected model that advertises function calling. Check `await getModelReadiness('previewFast')`; both `availability === 'available'` and `tools === 'available'` are needed. Preview access requires eligible hardware and AICore Developer Preview enrollment. There is no automatic fallback to another model.
|
|
342
326
|
|
|
327
|
+
The model chooses whether to call a tool. Handlers must validate application-specific values and permissions; valid arguments alone do not authorize an action. Return strings, using `JSON.stringify()` for structured results. See [tool requirements, execution limits, and cancellation](docs/tool-calling.md).
|
|
343
328
|
|
|
344
329
|
### Handle errors
|
|
345
330
|
|
|
346
|
-
Generation failures and the library's own checks carry a stable `code` (see [Error codes](#error-codes)), so you can respond to *why* generation failed rather than parsing OS messages. The hook exposes it as `errorCode`; for a caught error, use `getLocalLLMErrorCode()`.
|
|
331
|
+
Generation failures and the library's own checks carry a stable `code` (see [Error codes](docs/api.md#error-codes)), so you can respond to *why* generation failed rather than parsing OS messages. The hook exposes it as `errorCode`; for a caught error, use `getLocalLLMErrorCode()`.
|
|
347
332
|
|
|
348
|
-
|
|
333
|
+
Cancellation through validated mode's `AbortSignal` instead rejects with `error.name === 'AbortError'`, without a library error code. Handle it separately from generation failures.
|
|
349
334
|
|
|
350
|
-
|
|
351
|
-
const { errorCode, reset } = useLocalLLM({ instructions: 'You are a helpful assistant.' });
|
|
352
|
-
|
|
353
|
-
if (errorCode === 'ERR_CONTEXT_WINDOW_EXCEEDED') {
|
|
354
|
-
// Start over, or create a new session seeded with a summary of the chat.
|
|
355
|
-
reset?.();
|
|
356
|
-
}
|
|
357
|
-
if (errorCode === 'ERR_GUARDRAIL_VIOLATION' || errorCode === 'ERR_REFUSAL') {
|
|
358
|
-
// The model declined; ask the person to rephrase.
|
|
359
|
-
}
|
|
360
|
-
```
|
|
335
|
+
A conversation can fail with `ERR_CONTEXT_WINDOW_EXCEEDED`. Offer a user-triggered `reset()` action for the hook, or start a new session with a summary. Reset clears conversation history; it is not an automatic retry.
|
|
361
336
|
|
|
362
337
|
```ts
|
|
363
338
|
import { generate, getLocalLLMErrorCode } from 'expo-local-llm';
|
|
@@ -366,190 +341,31 @@ try {
|
|
|
366
341
|
await generate('Summarise my notes');
|
|
367
342
|
} catch (error) {
|
|
368
343
|
if (error instanceof Error && getLocalLLMErrorCode(error) === 'ERR_RATE_LIMITED') {
|
|
369
|
-
//
|
|
344
|
+
// Show a retry-later message in your app.
|
|
345
|
+
} else {
|
|
346
|
+
throw error;
|
|
370
347
|
}
|
|
371
348
|
}
|
|
372
349
|
```
|
|
373
350
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
The library has no additional npm runtime dependencies, so it does not ship [Effect](https://effect.website) bindings. The error codes map directly onto a tagged error, so a wrapper takes a few lines:
|
|
377
|
-
|
|
378
|
-
```ts
|
|
379
|
-
import { Data, Effect } from 'effect';
|
|
380
|
-
import { generate, getLocalLLMErrorCode, type LocalLLMErrorCode } from 'expo-local-llm';
|
|
381
|
-
|
|
382
|
-
class LocalLLMFailure extends Data.TaggedError('LocalLLMFailure')<{
|
|
383
|
-
code: LocalLLMErrorCode | null;
|
|
384
|
-
message: string;
|
|
385
|
-
}> {}
|
|
386
|
-
|
|
387
|
-
const generateEffect = (prompt: string) =>
|
|
388
|
-
Effect.tryPromise({
|
|
389
|
-
try: () => generate(prompt),
|
|
390
|
-
catch: (error) =>
|
|
391
|
-
new LocalLLMFailure({
|
|
392
|
-
code: error instanceof Error ? getLocalLLMErrorCode(error) : null,
|
|
393
|
-
message: error instanceof Error ? error.message : 'Generation failed',
|
|
394
|
-
}),
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// Recover from one failure by its code.
|
|
398
|
-
const answer = generateEffect('Summarise my notes').pipe(
|
|
399
|
-
Effect.catchIf(
|
|
400
|
-
(failure) => failure.code === 'ERR_RATE_LIMITED',
|
|
401
|
-
() => Effect.succeed('Try again in a moment.'),
|
|
402
|
-
),
|
|
403
|
-
);
|
|
404
|
-
```
|
|
351
|
+
For an Effect wrapper, see [Using with Effect](docs/effect.md).
|
|
405
352
|
|
|
406
353
|
## API reference
|
|
407
354
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
Reports which features the loaded platform integration implements. Use this to decide whether to offer text, streaming, tools, or structured output. Check model availability separately before starting generation. A supported capability does not mean the OS model is currently ready. The `images` field additionally checks current model readiness and vision support on iOS; Android reports cached readiness and cannot promise that the device model understands images. On Android, this call reads cached availability and starts an asynchronous refresh that emits `availabilityChange`.
|
|
411
|
-
|
|
412
|
-
The function does not download models, start inference, or opt the app into a cloud service.
|
|
413
|
-
|
|
414
|
-
Returns `platform`, `backend`, `status`, boolean `text`/`streaming`/`tools`/`structuredOutput` flags, `images` (`'supported'` on a ready, vision-capable iOS backend, `'unknown'` when a current Android bridge's cached availability is `'available'`, `'unsupported'` otherwise), `structuredOutputMode` (`'constrained'` or `'unsupported'`), and `experimental`. `experimental` is `true` on Android until Gemini Nano generation has been validated on real hardware; gate production features on it if you need verified behavior.
|
|
415
|
-
|
|
416
|
-
Capability `status` is `'available'`, `'moduleUnavailable'`, `'unsupportedOS'`, or `'unsupportedPlatform'`. Here `'available'` means the integration is supported; use the hook's `availability` for current model readiness.
|
|
417
|
-
|
|
418
|
-
### `getModelReadiness(androidModel?: AndroidModel)`
|
|
419
|
-
|
|
420
|
-
Returns a promise of `{ availability, tools: 'available' | 'unavailable' | 'unknown' }`. Android queries `checkStatus()` and `getAvailableFeatures()` on the selected client; query failures are `unknown`. Feature support and model availability are separate: a downloadable model may advertise tools but cannot yet generate. The hook's readiness/download paths follow its `androidModel` option, and Android module events include `androidModel`. Direct module `getAvailability(androidModel?)` and `downloadModel(androidModel?)` accept the same selection.
|
|
421
|
-
|
|
422
|
-
`getCapabilities().tools` reports integration support, not device readiness. Older Android binaries without `getToolCallingVersion() === 1` report no tool support. Explicit preview selection fails clearly on those binaries instead of querying or downloading the default model. Rebuild after upgrading native code; an OTA JS update cannot add this support.
|
|
423
|
-
|
|
424
|
-
### `getModuleDiagnostics()`
|
|
425
|
-
|
|
426
|
-
Reports whether native code loaded, with `available`, `reason`, `message`, and `setupInstructions`. A missing module is a build/setup problem, distinct from a device that cannot run the model.
|
|
427
|
-
|
|
428
|
-
```ts
|
|
429
|
-
import { getModuleDiagnostics } from 'expo-local-llm';
|
|
430
|
-
|
|
431
|
-
const diagnostics = getModuleDiagnostics();
|
|
432
|
-
if (!diagnostics.available) {
|
|
433
|
-
console.warn(diagnostics.message);
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
### `useLocalLLM(options?)`
|
|
438
|
-
|
|
439
|
-
#### Options
|
|
440
|
-
|
|
441
|
-
| Option | Type | Description |
|
|
442
|
-
|--------|------|-------------|
|
|
443
|
-
| `instructions` | `string` | System instructions for the session |
|
|
444
|
-
| `options.temperature` | `number` | Sampling temperature |
|
|
445
|
-
| `options.maxTokens` | `number` | Max output tokens (`maxOutputTokens` on Android; `maximumResponseTokens` on iOS). Native SDK/model limits apply. |
|
|
446
|
-
| `options.topK` | `number` | Top-K sampling (maps to `.random(top:)` on iOS) |
|
|
447
|
-
| `tools` | `ToolDefinition[]` | Tools the model can invoke (iOS 26+; Android native version 1 and eligible selected model) |
|
|
448
|
-
| `androidModel` | `'default' \| 'previewFast'` | Explicit Android model selection; ignored on iOS |
|
|
449
|
-
| `toolTimeout` | `number` | Seconds before an unresolved tool call times out (default 30) |
|
|
450
|
-
| `responseFormat` | `"text" \| "json"` | Set to `"json"` for structured JSON output; requires `schema` |
|
|
451
|
-
| `schema` | `Schema` | Schema for structured output (used with `responseFormat: "json"`); see [Structured output in the hook](#structured-output-in-the-hook) |
|
|
452
|
-
| `includeSchemaInPrompt` | `boolean` | iOS 26+: include the schema definition in the prompt. Default `true`. Set `false` when you've front-loaded few-shot examples. |
|
|
453
|
-
|
|
454
|
-
#### Returns
|
|
455
|
-
|
|
456
|
-
| Property | Type | Description |
|
|
457
|
-
|----------|------|-------------|
|
|
458
|
-
| `availability` | `ModelAvailability` | Current model status |
|
|
459
|
-
| `isGenerating` | `boolean` | Whether a generation is in progress |
|
|
460
|
-
| `streamedText` | `string` | Accumulated text from current text stream |
|
|
461
|
-
| `streamedJSON` | `string` | Current partial JSON from a schema-driven stream |
|
|
462
|
-
| `streamedObject` | `DeepPartial<InferSchema<schema>> \| null` | Parsed value of `streamedJSON` (or `null` before any partial). Fields may be missing until generated. |
|
|
463
|
-
| `completedObject` | `InferSchema<schema> \| null` | The finished JSON-mode response, validated against the schema. `null` while generating and after a cancel. |
|
|
464
|
-
| `downloadProgress` | `number \| null` | Download progress 0-1 (Android only) |
|
|
465
|
-
| `error` | `string \| null` | Last error message |
|
|
466
|
-
| `errorCode` | `LocalLLMErrorCode \| null` | Code of the last error (see [Error codes](#error-codes)); `null` when there is none or it came from your own code |
|
|
467
|
-
| `activeToolCalls` | `ActiveToolCall[]` | In-flight tool calls (`{ callId, toolName }`) |
|
|
468
|
-
| `respond` | `(prompt: LLMPrompt) => Promise<string>` | Non-streaming generation. `undefined` when unavailable. |
|
|
469
|
-
| `session` | `LLMSession \| null` | The native session object. `null` before creation or when unavailable. Use the hook's generation methods for hook-managed state and tool dispatch. |
|
|
470
|
-
| `streamResponse` | `(prompt: LLMPrompt) => Promise<string>` | Streaming generation (updates `streamedText`). Resolves with the final text at stream end, or the partial text if cancelled; rejects on stream failure. `undefined` when model not `available`. |
|
|
471
|
-
| `cancelStream` | `() => Promise<void>` | Cancel the active stream; the pending `streamResponse` promise resolves with the partial text. Available whenever a session exists, even if model readiness changes. |
|
|
472
|
-
| `reset` | `() => void` | Clear the conversation transcript, keeping instructions/tools/schema/options. Also cancels any in-flight stream. `undefined` only when there is no session. |
|
|
473
|
-
| `downloadModel` | `() => Promise<void>` | Trigger model download (Android). `undefined` when unavailable. |
|
|
474
|
-
|
|
475
|
-
### `generate(prompt, config?)`
|
|
476
|
-
|
|
477
|
-
One-shot stateless generation — creates a session, responds once, and releases it. Takes the
|
|
478
|
-
same generation config as `useLocalLLM`/`createLLMSession`, except tool handlers are not supported. Use it for classification or extraction
|
|
479
|
-
calls where conversation history between calls is unwanted. Throws when the module or model
|
|
480
|
-
is unavailable (check `ExpoLocalLlmModule.getAvailability()` first if you need to gate).
|
|
355
|
+
The [complete API reference](docs/api.md) covers configuration, return types, events, errors, and session ownership.
|
|
481
356
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
`validateStructuredOutput(value, schema)` is also available for checking an already-parsed value against a valid schema. It returns `{ ok: true }` or `{ ok: false, errors }` with field paths.
|
|
495
|
-
|
|
496
|
-
### `createLLMSession(config?)`
|
|
497
|
-
|
|
498
|
-
Creates a manually owned conversation. Release it when finished:
|
|
499
|
-
|
|
500
|
-
```ts
|
|
501
|
-
import { createLLMSession } from 'expo-local-llm';
|
|
502
|
-
|
|
503
|
-
const session = createLLMSession({ instructions: 'Keep answers concise.' });
|
|
504
|
-
try {
|
|
505
|
-
const text = await session.respond('Suggest a short title for a gardening journal.');
|
|
506
|
-
} finally {
|
|
507
|
-
session.release();
|
|
508
|
-
}
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
Low-level `respond(prompt, requestId?)` and `streamResponse(prompt, requestId?)` accept an optional request identifier. Generation events carry `requestId` so consumers can discard queued events from cancelled or reset requests. Omit it to let native code generate an identifier; `useLocalLLM()` manages identifiers and filtering automatically. Use a distinct identifier for each invocation.
|
|
512
|
-
|
|
513
|
-
### Error codes
|
|
514
|
-
|
|
515
|
-
Errors thrown or rejected by the library, and `streamError` events, carry one of these codes. Messages come from the OS and may change; codes do not.
|
|
516
|
-
|
|
517
|
-
On iOS 27, Foundation Models reports some failures with error types that only the iOS 27 SDK can name. Build with Xcode 27 to classify all of them. An app built with Xcode 26.4 still gets codes for the iOS 26 error types, but reports the newer ones as `ERR_GENERATION_FAILED`.
|
|
518
|
-
|
|
519
|
-
| Code | Meaning |
|
|
520
|
-
|------|---------|
|
|
521
|
-
| `ERR_CONTEXT_WINDOW_EXCEEDED` | The conversation no longer fits the model's context window. Reset or start a new session. |
|
|
522
|
-
| `ERR_GUARDRAIL_VIOLATION` | Safety guardrails blocked the prompt or response. |
|
|
523
|
-
| `ERR_REFUSAL` | The model declined to answer. |
|
|
524
|
-
| `ERR_UNSUPPORTED_LANGUAGE` | The prompt language or device locale is not supported. |
|
|
525
|
-
| `ERR_UNSUPPORTED_REQUEST` | The request uses a capability the model or helper cannot honour. |
|
|
526
|
-
| `ERR_RATE_LIMITED` | The system is throttling generation (quota, background use, load). Retry later. |
|
|
527
|
-
| `ERR_MODEL_UNAVAILABLE` | Model assets are missing or the device cannot run the model right now. |
|
|
528
|
-
| `ERR_DECODING_FAILED` | Output could not be decoded into the requested structure. |
|
|
529
|
-
| `ERR_TIMEOUT` | Generation exceeded the system's time limit (iOS 27+). |
|
|
530
|
-
| `ERR_TOOL_FAILED` | A tool handler threw or timed out, or the hook had no handler for the tool. |
|
|
531
|
-
| `ERR_GENERATION_FAILED` | Any other generation failure. |
|
|
532
|
-
| `ERR_SESSION_BUSY` | The session already has a generation in progress. |
|
|
533
|
-
| `ERR_SESSION_INVALID` | The session was released or is not ready. |
|
|
534
|
-
| `ERR_NOT_SUPPORTED` | On-device generation is not available on this OS version. |
|
|
535
|
-
| `ERR_RESPONSE_FORMAT_NOT_SUPPORTED` | Structured output is not supported on this platform. |
|
|
536
|
-
| `ERR_TOOL_NOT_SUPPORTED` | Tool calling is not supported on this platform. |
|
|
537
|
-
| `ERR_IMAGE_INVALID` | Invalid image URI, unreadable image, invalid or duplicate label, too many images for the platform, or image rejected as invalid by ML Kit. |
|
|
538
|
-
| `ERR_IMAGE_NOT_SUPPORTED` | Image prompts are not supported by this backend. |
|
|
539
|
-
| `ERR_MODEL_DOWNLOAD_FAILED` | Android: downloading Gemini Nano failed. |
|
|
540
|
-
| `ERR_SCHEMA_INVALID` | A schema is malformed (`SchemaInvalidError`). |
|
|
541
|
-
| `ERR_STRUCTURED_OUTPUT_INVALID` | Generated JSON did not match the schema (`StructuredOutputValidationError`). |
|
|
542
|
-
| `ERR_UNAVAILABLE` | The native module is missing from this build. |
|
|
543
|
-
|
|
544
|
-
### `ModelAvailability`
|
|
545
|
-
|
|
546
|
-
`'available' | 'notEnabled' | 'notReady' | 'notEligible' | 'downloadRequired' | 'downloading' | 'unknown' | 'moduleUnavailable'`
|
|
547
|
-
|
|
548
|
-
Android readiness is checked asynchronously and may initially be `unknown`. Prefer `useLocalLLM()` for reactive readiness; low-level consumers should listen for the module's `availabilityChange` event instead of treating the first synchronous status read as final.
|
|
549
|
-
|
|
550
|
-
### `validateSchema(schema)` and `validateStructuredOutput(value, schema)`
|
|
551
|
-
|
|
552
|
-
`validateSchema` checks a schema and returns `{ ok: true }` or `{ ok: false, errors }` with field paths (missing `items`, `enum` on the wrong type, unknown `type`). `validateStructuredOutput` checks an already-parsed value against a valid schema the same way.
|
|
357
|
+
| API | Reference |
|
|
358
|
+
|---|---|
|
|
359
|
+
| <a id="getcapabilities"></a>`getCapabilities()` | [Integration and image capabilities](docs/api.md#getcapabilities) |
|
|
360
|
+
| <a id="getmodelreadinessandroidmodel-androidmodel"></a>`getModelReadiness()` | [Selected-model readiness](docs/api.md#getmodelreadinessandroidmodel-androidmodel) |
|
|
361
|
+
| <a id="getmodulediagnostics"></a>`getModuleDiagnostics()` | [Native module diagnostics](docs/api.md#getmodulediagnostics) |
|
|
362
|
+
| <a id="uselocalllmoptions"></a>`useLocalLLM()` | [Options and return values](docs/api.md#uselocalllmoptions) |
|
|
363
|
+
| <a id="generateprompt-config"></a>`generate()` | [Independent text generation](docs/api.md#generateprompt-config) |
|
|
364
|
+
| <a id="generateobjectprompt-config"></a>`generateObject()` | [Constrained and validated modes](docs/api.md#generateobjectprompt-config) |
|
|
365
|
+
| <a id="createllmsessionconfig"></a>`createLLMSession()` | [Manual session ownership](docs/api.md#createllmsessionconfig) |
|
|
366
|
+
| <a id="error-codes"></a>Error codes | [Generation and validation failures](docs/api.md#error-codes) |
|
|
367
|
+
| <a id="modelavailability"></a>`ModelAvailability` | [Readiness states](docs/api.md#modelavailability) |
|
|
368
|
+
| <a id="validateschemaschema-and-validatestructuredoutputvalue-schema"></a>Schema validators | [Validate schemas and objects](docs/api.md#validateschemaschema-and-validatestructuredoutputvalue-schema) |
|
|
553
369
|
|
|
554
370
|
## Platform differences and limitations
|
|
555
371
|
|
|
@@ -564,14 +380,14 @@ Android readiness is checked asynchronously and may initially be `unknown`. Pref
|
|
|
564
380
|
- **Context window.** Neither platform trims history to fit the model's context; Android only caps it at 10 exchanges. iOS rejects with `ERR_CONTEXT_WINDOW_EXCEEDED`; call `reset()` or start a new session.
|
|
565
381
|
- **One generation per session.** Overlapping requests reject with `ERR_SESSION_BUSY`. After cancelling, await the original promise before starting another request. Use separate sessions for independent conversations; events belong to their session.
|
|
566
382
|
- **Guardrails.** Apple's model refuses some categories of prompt, such as interpreting personal health data. A refusal arrives as `ERR_GUARDRAIL_VIOLATION` or `ERR_REFUSAL`.
|
|
567
|
-
- **Android
|
|
383
|
+
- **Android model requirements.** The Gemini Nano SDK is in beta and its API may change. Tools require selected-model feature support. Default `generateObject()` uses text generation plus JS validation and bounded repair. Native `responseFormat: 'json'` and explicit `mode: 'constrained'` remain unsupported because ML Kit only accepts compiled Kotlin types for constrained output, with no public runtime-schema entrypoint for arbitrary JS schemas.
|
|
568
384
|
- **Readiness gates generation.** `respond` and `streamResponse` need a session and `availability === 'available'`; cancelling and resetting work whenever a session exists.
|
|
569
385
|
|
|
570
386
|
## How this library is verified
|
|
571
387
|
|
|
572
|
-
- **
|
|
388
|
+
- **Code changes:** lint, unit tests, type-level tests, and a compile of a freshly generated Expo SDK 58 app on both platforms, with the iOS build using Xcode 26.4, the oldest supported toolchain. Android also runs JVM tests of the prompt assembly and error mapping.
|
|
573
389
|
- **Against the real model:** [`tools/model-check`](tools/model-check/README.md) runs the library's Foundation Models code on a Mac with Apple Intelligence: constrained tool arguments, error-code mapping and schema-constrained output. The release checklist runs it before each release.
|
|
574
|
-
- **On devices:** the [release verification record](docs/release-verification.md) lists
|
|
390
|
+
- **On devices:** the [release verification record](docs/release-verification.md) lists the device/OS, package revision, and results for each recorded run, and leaves untested scenarios marked as pending rather than assumed.
|
|
575
391
|
|
|
576
392
|
## Choosing this library
|
|
577
393
|
|
|
@@ -588,7 +404,7 @@ The [Expo Modules API](https://docs.expo.dev/modules/overview/) fits OS-provided
|
|
|
588
404
|
- **Swift and Kotlin** — both implementations use a consistent module definition style, without adding a separate inference engine.
|
|
589
405
|
- **Expo build workflows** — autolinking integrates with prebuild, local development builds, and EAS Build.
|
|
590
406
|
|
|
591
|
-
Expo
|
|
407
|
+
Expo supplies the native integration and build workflow. Model behavior and device eligibility still come from the platform backend.
|
|
592
408
|
|
|
593
409
|
Existing React Native apps can use this library after [installing Expo Modules support](https://docs.expo.dev/bare/installing-expo-modules/). Expo-first does not mean Expo-only.
|
|
594
410
|
|