expo-local-llm 0.8.1 → 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 +179 -270
- package/android/build.gradle +11 -4
- package/android/src/main/java/expo/modules/localllm/AndroidModel.kt +73 -0
- package/android/src/main/java/expo/modules/localllm/ConversationHistory.kt +50 -3
- package/android/src/main/java/expo/modules/localllm/Exceptions.kt +15 -3
- package/android/src/main/java/expo/modules/localllm/ExpoLocalLlmModule.kt +28 -35
- package/android/src/main/java/expo/modules/localllm/GenerationLifecycle.kt +76 -0
- package/android/src/main/java/expo/modules/localllm/ImageDecoder.kt +164 -0
- package/android/src/main/java/expo/modules/localllm/ImagePrompt.kt +86 -0
- package/android/src/main/java/expo/modules/localllm/LLMSession.kt +350 -125
- package/android/src/main/java/expo/modules/localllm/NativeTools.kt +229 -0
- package/android/src/main/java/expo/modules/localllm/Records.kt +21 -0
- package/android/src/main/java/expo/modules/localllm/SessionRuntime.kt +48 -0
- package/build/ExpoLocalLlm.types.d.ts +68 -4
- package/build/ExpoLocalLlm.types.d.ts.map +1 -1
- package/build/ExpoLocalLlm.types.js.map +1 -1
- package/build/ExpoLocalLlmModule.d.ts +7 -3
- package/build/ExpoLocalLlmModule.d.ts.map +1 -1
- package/build/ExpoLocalLlmModule.js.map +1 -1
- package/build/LLMSession.d.ts +5 -5
- package/build/LLMSession.d.ts.map +1 -1
- package/build/LLMSession.js +41 -4
- package/build/LLMSession.js.map +1 -1
- package/build/capabilities.d.ts +4 -1
- package/build/capabilities.d.ts.map +1 -1
- package/build/capabilities.js +71 -2
- package/build/capabilities.js.map +1 -1
- package/build/defineTool.d.ts.map +1 -1
- package/build/defineTool.js.map +1 -1
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +2 -0
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/useLocalLLM.d.ts +3 -3
- package/build/useLocalLLM.d.ts.map +1 -1
- package/build/useLocalLLM.js +61 -14
- package/build/useLocalLLM.js.map +1 -1
- package/build/validateSchema.d.ts +1 -0
- package/build/validateSchema.d.ts.map +1 -1
- package/build/validateSchema.js +59 -0
- package/build/validateSchema.js.map +1 -1
- package/build/validatedOutput.d.ts +4 -0
- package/build/validatedOutput.d.ts.map +1 -0
- package/build/validatedOutput.js +87 -0
- package/build/validatedOutput.js.map +1 -0
- package/ios/Exceptions.swift +20 -0
- package/ios/ExpoLocalLlmModule.swift +13 -4
- package/ios/FoundationModelBridge.swift +102 -18
- package/ios/LLMSession.swift +10 -4
- package/ios/Records.swift +162 -0
- package/package.json +23 -14
- package/src/ExpoLocalLlm.types.ts +77 -5
- package/src/ExpoLocalLlmModule.ts +12 -3
- package/src/LLMSession.ts +72 -8
- package/src/__tests__/ExpoLocalLlm.test.ts +1 -1
- package/src/__tests__/inference.typecheck.ts +77 -4
- package/src/__tests__/nativeApi.test.ts +747 -8
- package/src/__tests__/useLocalLLM.native.test.ts +357 -0
- package/src/capabilities.ts +87 -2
- package/src/errors.ts +2 -0
- package/src/index.ts +5 -1
- package/src/useLocalLLM.ts +77 -22
- package/src/validateSchema.ts +61 -1
- package/src/validatedOutput.ts +129 -0
- package/android/build/generated/source/buildConfig/debug/expo/modules/localllm/BuildConfig.java +0 -10
- package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +0 -7
- package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json +0 -18
- package/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties +0 -6
- package/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json +0 -1
- package/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar +0 -0
- package/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt +0 -0
- package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +0 -1
- package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +0 -2
- package/android/build/intermediates/incremental/mergeDebugAssets/merger.xml +0 -2
- package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +0 -2
- package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +0 -2
- package/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt +0 -2
- package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +0 -7
- package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +0 -7
- package/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json +0 -1
- package/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt +0 -1
- package/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt +0 -1
- package/android/build/outputs/logs/manifest-merger-debug-report.txt +0 -14
- package/android/src/test/java/expo/modules/localllm/ConversationHistoryTest.kt +0 -96
- package/android/src/test/java/expo/modules/localllm/GenerationErrorsTest.kt +0 -48
package/README.md
CHANGED
|
@@ -5,24 +5,38 @@
|
|
|
5
5
|
[](https://github.com/GijungKim/expo-local-llm/actions/workflows/native.yml)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
On-device
|
|
8
|
+
**On-device AI. No bundled model weights. No additional npm runtime dependencies.**
|
|
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
|
+
|
|
12
|
+
The full JavaScript API measures approximately **17.8 kB minified / 6.0 kB gzipped**, excluding external dependencies and native code.[^js-size]
|
|
13
|
+
|
|
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
|
+
|
|
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
|
+
|
|
18
|
+
[Setup](#quick-start) · [Usage](#usage) · [API reference](docs/api.md) · [Troubleshooting](docs/troubleshooting.md) · [Changelog](CHANGELOG.md)
|
|
11
19
|
|
|
12
20
|
## At a glance
|
|
13
21
|
|
|
14
22
|
| | iOS 26+ (Foundation Models) | Android (Gemini Nano) |
|
|
15
23
|
|---|---|---|
|
|
16
|
-
| Text generation and streaming | ✅ Verified on device |
|
|
17
|
-
|
|
|
18
|
-
|
|
|
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 |
|
|
19
29
|
| Stable error codes | ✅ | ✅ |
|
|
20
30
|
| Model download | Managed by the OS | `downloadModel()` with progress |
|
|
21
31
|
|
|
22
|
-
|
|
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).
|
|
23
35
|
|
|
24
36
|
## Quick start
|
|
25
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
|
+
|
|
26
40
|
```bash
|
|
27
41
|
npx expo install expo-local-llm expo-dev-client expo-build-properties
|
|
28
42
|
```
|
|
@@ -37,7 +51,7 @@ Set the deployment targets the module needs in `app.json`:
|
|
|
37
51
|
"expo-build-properties",
|
|
38
52
|
{
|
|
39
53
|
"ios": { "deploymentTarget": "16.4" },
|
|
40
|
-
"android": { "minSdkVersion": 26 }
|
|
54
|
+
"android": { "minSdkVersion": 26, "kotlinVersion": "2.3.0" }
|
|
41
55
|
}
|
|
42
56
|
]
|
|
43
57
|
]
|
|
@@ -49,6 +63,8 @@ Build a development app, since stock Expo Go cannot load native modules:
|
|
|
49
63
|
|
|
50
64
|
```bash
|
|
51
65
|
npx expo run:ios --device
|
|
66
|
+
# Or, for Android:
|
|
67
|
+
# npx expo run:android --device
|
|
52
68
|
```
|
|
53
69
|
|
|
54
70
|
Then stream a response:
|
|
@@ -58,28 +74,49 @@ import { Button, Text } from 'react-native';
|
|
|
58
74
|
import { useLocalLLM } from 'expo-local-llm';
|
|
59
75
|
|
|
60
76
|
export function Summary({ notes }: { notes: string }) {
|
|
61
|
-
const { availability, streamResponse, streamedText, error } = useLocalLLM({
|
|
77
|
+
const { availability, streamResponse, streamedText, isGenerating, error } = useLocalLLM({
|
|
62
78
|
instructions: 'Summarise the text in two sentences.',
|
|
63
79
|
});
|
|
64
80
|
|
|
65
|
-
|
|
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
|
+
|
|
66
90
|
return (
|
|
67
91
|
<>
|
|
68
|
-
<
|
|
69
|
-
<
|
|
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>
|
|
70
99
|
</>
|
|
71
100
|
);
|
|
72
101
|
}
|
|
73
102
|
```
|
|
74
103
|
|
|
75
|
-
`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).
|
|
76
113
|
|
|
77
114
|
## Requirements
|
|
78
115
|
|
|
79
116
|
| | Requirement |
|
|
80
117
|
|---|---|
|
|
81
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. |
|
|
82
|
-
| Android | `minSdkVersion` 26
|
|
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. |
|
|
83
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. |
|
|
84
121
|
|
|
85
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.
|
|
@@ -109,75 +146,69 @@ Then regenerate and rebuild the native app. This is an app-level build setting,
|
|
|
109
146
|
|
|
110
147
|
## Usage
|
|
111
148
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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()` |
|
|
117
156
|
|
|
118
|
-
|
|
119
|
-
const {
|
|
120
|
-
availability,
|
|
121
|
-
isGenerating,
|
|
122
|
-
streamedText,
|
|
123
|
-
error,
|
|
124
|
-
respond,
|
|
125
|
-
streamResponse,
|
|
126
|
-
cancelStream,
|
|
127
|
-
downloadModel,
|
|
128
|
-
downloadProgress,
|
|
129
|
-
} = useLocalLLM({
|
|
130
|
-
instructions: 'You are a helpful assistant.',
|
|
131
|
-
});
|
|
157
|
+
The [quick start](#quick-start) shows streaming with the hook. For all options and return types, see the [API reference](docs/api.md).
|
|
132
158
|
|
|
133
|
-
|
|
134
|
-
if (!streamResponse) {
|
|
135
|
-
return (
|
|
136
|
-
<Text>
|
|
137
|
-
{error ?? (availability === 'available'
|
|
138
|
-
? 'Preparing on-device AI...'
|
|
139
|
-
: `On-device AI: ${availability}`)}
|
|
140
|
-
</Text>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
159
|
+
### One-shot generation
|
|
143
160
|
|
|
144
|
-
|
|
145
|
-
// Streaming — updates streamedText live, resolves with the final text
|
|
146
|
-
const text = await streamResponse(prompt);
|
|
161
|
+
For a single call with no conversation history, such as classifying or rewriting one message, use `generate()`. It creates a session, responds once and releases it:
|
|
147
162
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
};
|
|
163
|
+
```ts
|
|
164
|
+
import { generate } from 'expo-local-llm';
|
|
151
165
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
{isGenerating && <Text>{streamedText}</Text>}
|
|
156
|
-
</View>
|
|
157
|
-
);
|
|
158
|
-
}
|
|
166
|
+
const title = await generate('Suggest a short title for this note: ...', {
|
|
167
|
+
instructions: 'Reply with the title only.',
|
|
168
|
+
});
|
|
159
169
|
```
|
|
160
170
|
|
|
161
|
-
|
|
171
|
+
<a id="describe-an-image"></a>
|
|
162
172
|
|
|
163
|
-
|
|
173
|
+
### Describe or compare images
|
|
174
|
+
|
|
175
|
+
Pass a prompt object to `generate()`, `session.respond()`, `session.streamResponse()`, or the hook's `respond` / `streamResponse`:
|
|
164
176
|
|
|
165
177
|
```ts
|
|
166
178
|
import { generate } from 'expo-local-llm';
|
|
167
179
|
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
// photoUri is a local file:// URI, for example from your app's cache.
|
|
181
|
+
const description = await generate({
|
|
182
|
+
text: 'Describe the photo in one sentence.',
|
|
183
|
+
images: [{ uri: photoUri, label: 'photo' }],
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Attach up to **eight local images** with unique labels. For a comparison:
|
|
188
|
+
|
|
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
|
+
],
|
|
170
197
|
});
|
|
171
198
|
```
|
|
172
199
|
|
|
173
|
-
|
|
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.
|
|
201
|
+
|
|
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.
|
|
174
203
|
|
|
175
|
-
|
|
204
|
+
### Extract typed data
|
|
205
|
+
|
|
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:
|
|
176
207
|
|
|
177
208
|
```ts
|
|
178
209
|
import { generateObject, getCapabilities } from 'expo-local-llm';
|
|
179
210
|
|
|
180
|
-
if (getCapabilities().
|
|
211
|
+
if (getCapabilities().objectGenerationMode !== 'unsupported') {
|
|
181
212
|
const result = await generateObject('Classify: "Can we move the meeting to Friday?"', {
|
|
182
213
|
instructions: 'Classify the message by its primary intent.',
|
|
183
214
|
schema: {
|
|
@@ -188,7 +219,7 @@ if (getCapabilities().structuredOutputMode === 'constrained') {
|
|
|
188
219
|
}
|
|
189
220
|
```
|
|
190
221
|
|
|
191
|
-
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`.
|
|
192
223
|
|
|
193
224
|
<p align="center">
|
|
194
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" />
|
|
@@ -196,9 +227,37 @@ A supported capability does not mean the model is ready, so handle rejected call
|
|
|
196
227
|
|
|
197
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`.*
|
|
198
229
|
|
|
230
|
+
### Choose the structured-output path explicitly
|
|
231
|
+
|
|
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.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { generateObject, getCapabilities, getModelReadiness } from 'expo-local-llm';
|
|
236
|
+
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
if (getCapabilities().text && (await getModelReadiness('default')).availability === 'available') {
|
|
239
|
+
const result = await generateObject('Classify: "Can we meet on Friday?"', {
|
|
240
|
+
mode: 'validated',
|
|
241
|
+
repairAttempts: 1, // default; choose 0 to disable repair
|
|
242
|
+
signal: controller.signal,
|
|
243
|
+
androidModel: 'default',
|
|
244
|
+
schema: {
|
|
245
|
+
intent: { type: 'string', enum: ['scheduling', 'question', 'other'] },
|
|
246
|
+
dates: { type: 'array', items: { type: 'string' } },
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
// result.intent: 'scheduling' | 'question' | 'other'; result.dates: string[]
|
|
250
|
+
}
|
|
251
|
+
// Call controller.abort() from your cancel action while the call is pending.
|
|
252
|
+
```
|
|
253
|
+
|
|
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).
|
|
255
|
+
|
|
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'`.
|
|
257
|
+
|
|
199
258
|
### Structured output in the hook
|
|
200
259
|
|
|
201
|
-
|
|
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.
|
|
202
261
|
|
|
203
262
|
```tsx
|
|
204
263
|
const { streamResponse, streamedObject, completedObject } = useLocalLLM({
|
|
@@ -219,66 +278,61 @@ await streamResponse?.('Give me a pasta recipe');
|
|
|
219
278
|
|
|
220
279
|
Schemas are validated before they reach native code; a malformed one throws `SchemaInvalidError` with per-field paths. Call `validateSchema()` yourself when building schemas at runtime.
|
|
221
280
|
|
|
222
|
-
### Tool calling
|
|
281
|
+
### Tool calling
|
|
282
|
+
|
|
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:
|
|
223
284
|
|
|
224
285
|
```tsx
|
|
225
|
-
import {
|
|
286
|
+
import { Button, Text } from 'react-native';
|
|
226
287
|
import { defineTool, useLocalLLM } from 'expo-local-llm';
|
|
227
288
|
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
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',
|
|
231
293
|
parameters: {
|
|
232
|
-
|
|
233
|
-
unit: { type: 'string', description: 'Temperature unit', enum: ['celsius', 'fahrenheit'] },
|
|
234
|
-
},
|
|
235
|
-
// Typed from `parameters`: city is a string, unit is 'celsius' | 'fahrenheit'.
|
|
236
|
-
handler: async ({ city, unit }) => {
|
|
237
|
-
const res = await fetch(
|
|
238
|
-
`https://api.example.com/weather?city=${encodeURIComponent(city)}&unit=${unit}`
|
|
239
|
-
);
|
|
240
|
-
return JSON.stringify(await res.json());
|
|
294
|
+
sku: { type: 'string', description: 'Product SKU, such as trail-shoes' },
|
|
241
295
|
},
|
|
296
|
+
handler: async ({ sku }) => JSON.stringify({ sku, quantity: stock.get(sku) ?? null }),
|
|
242
297
|
});
|
|
243
298
|
|
|
244
|
-
function
|
|
245
|
-
const { streamResponse, streamedText, activeToolCalls, error } = useLocalLLM({
|
|
246
|
-
instructions: '
|
|
247
|
-
|
|
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],
|
|
248
304
|
});
|
|
249
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
|
+
|
|
250
315
|
return (
|
|
251
|
-
|
|
252
|
-
{
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
</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
|
+
</>
|
|
257
321
|
);
|
|
258
322
|
}
|
|
259
323
|
```
|
|
260
324
|
|
|
261
|
-
`
|
|
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.
|
|
262
326
|
|
|
263
|
-
|
|
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).
|
|
264
328
|
|
|
265
329
|
### Handle errors
|
|
266
330
|
|
|
267
|
-
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()`.
|
|
268
332
|
|
|
269
|
-
|
|
333
|
+
Cancellation through validated mode's `AbortSignal` instead rejects with `error.name === 'AbortError'`, without a library error code. Handle it separately from generation failures.
|
|
270
334
|
|
|
271
|
-
|
|
272
|
-
const { errorCode, reset } = useLocalLLM({ instructions: 'You are a helpful assistant.' });
|
|
273
|
-
|
|
274
|
-
if (errorCode === 'ERR_CONTEXT_WINDOW_EXCEEDED') {
|
|
275
|
-
// Start over, or create a new session seeded with a summary of the chat.
|
|
276
|
-
reset?.();
|
|
277
|
-
}
|
|
278
|
-
if (errorCode === 'ERR_GUARDRAIL_VIOLATION' || errorCode === 'ERR_REFUSAL') {
|
|
279
|
-
// The model declined; ask the person to rephrase.
|
|
280
|
-
}
|
|
281
|
-
```
|
|
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.
|
|
282
336
|
|
|
283
337
|
```ts
|
|
284
338
|
import { generate, getLocalLLMErrorCode } from 'expo-local-llm';
|
|
@@ -287,177 +341,31 @@ try {
|
|
|
287
341
|
await generate('Summarise my notes');
|
|
288
342
|
} catch (error) {
|
|
289
343
|
if (error instanceof Error && getLocalLLMErrorCode(error) === 'ERR_RATE_LIMITED') {
|
|
290
|
-
//
|
|
344
|
+
// Show a retry-later message in your app.
|
|
345
|
+
} else {
|
|
346
|
+
throw error;
|
|
291
347
|
}
|
|
292
348
|
}
|
|
293
349
|
```
|
|
294
350
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
The library has no 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:
|
|
298
|
-
|
|
299
|
-
```ts
|
|
300
|
-
import { Data, Effect } from 'effect';
|
|
301
|
-
import { generate, getLocalLLMErrorCode, type LocalLLMErrorCode } from 'expo-local-llm';
|
|
302
|
-
|
|
303
|
-
class LocalLLMFailure extends Data.TaggedError('LocalLLMFailure')<{
|
|
304
|
-
code: LocalLLMErrorCode | null;
|
|
305
|
-
message: string;
|
|
306
|
-
}> {}
|
|
307
|
-
|
|
308
|
-
const generateEffect = (prompt: string) =>
|
|
309
|
-
Effect.tryPromise({
|
|
310
|
-
try: () => generate(prompt),
|
|
311
|
-
catch: (error) =>
|
|
312
|
-
new LocalLLMFailure({
|
|
313
|
-
code: error instanceof Error ? getLocalLLMErrorCode(error) : null,
|
|
314
|
-
message: error instanceof Error ? error.message : 'Generation failed',
|
|
315
|
-
}),
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Recover from one failure by its code.
|
|
319
|
-
const answer = generateEffect('Summarise my notes').pipe(
|
|
320
|
-
Effect.catchIf(
|
|
321
|
-
(failure) => failure.code === 'ERR_RATE_LIMITED',
|
|
322
|
-
() => Effect.succeed('Try again in a moment.'),
|
|
323
|
-
),
|
|
324
|
-
);
|
|
325
|
-
```
|
|
351
|
+
For an Effect wrapper, see [Using with Effect](docs/effect.md).
|
|
326
352
|
|
|
327
353
|
## API reference
|
|
328
354
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
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.
|
|
332
|
-
|
|
333
|
-
The function does not download models, start inference, or opt the app into a cloud service.
|
|
334
|
-
|
|
335
|
-
Returns `platform`, `backend`, `status`, boolean `text`/`streaming`/`tools`/`structuredOutput` flags, `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.
|
|
336
|
-
|
|
337
|
-
Capability `status` is `'available'`, `'moduleUnavailable'`, `'unsupportedOS'`, or `'unsupportedPlatform'`. Here `'available'` means the integration is supported; use the hook's `availability` for current model readiness.
|
|
338
|
-
|
|
339
|
-
### `getModuleDiagnostics()`
|
|
340
|
-
|
|
341
|
-
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.
|
|
342
|
-
|
|
343
|
-
```ts
|
|
344
|
-
import { getModuleDiagnostics } from 'expo-local-llm';
|
|
345
|
-
|
|
346
|
-
const diagnostics = getModuleDiagnostics();
|
|
347
|
-
if (!diagnostics.available) {
|
|
348
|
-
console.warn(diagnostics.message);
|
|
349
|
-
}
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### `useLocalLLM(options?)`
|
|
353
|
-
|
|
354
|
-
#### Options
|
|
355
|
-
|
|
356
|
-
| Option | Type | Description |
|
|
357
|
-
|--------|------|-------------|
|
|
358
|
-
| `instructions` | `string` | System instructions for the session |
|
|
359
|
-
| `options.temperature` | `number` | Sampling temperature |
|
|
360
|
-
| `options.maxTokens` | `number` | Max output tokens (`maxOutputTokens` on Android; `maximumResponseTokens` on iOS). Native SDK/model limits apply. |
|
|
361
|
-
| `options.topK` | `number` | Top-K sampling (maps to `.random(top:)` on iOS) |
|
|
362
|
-
| `tools` | `ToolDefinition[]` | Tools the model can invoke (iOS 26+ only) |
|
|
363
|
-
| `toolTimeout` | `number` | Seconds before an unresolved tool call times out (default 30) |
|
|
364
|
-
| `responseFormat` | `"text" \| "json"` | Set to `"json"` for structured JSON output; requires `schema` |
|
|
365
|
-
| `schema` | `Schema` | Schema for structured output (used with `responseFormat: "json"`); see [Structured output in the hook](#structured-output-in-the-hook) |
|
|
366
|
-
| `includeSchemaInPrompt` | `boolean` | iOS 26+: include the schema definition in the prompt. Default `true`. Set `false` when you've front-loaded few-shot examples. |
|
|
355
|
+
The [complete API reference](docs/api.md) covers configuration, return types, events, errors, and session ownership.
|
|
367
356
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
|
371
|
-
|
|
372
|
-
|
|
|
373
|
-
|
|
|
374
|
-
|
|
|
375
|
-
|
|
|
376
|
-
|
|
|
377
|
-
|
|
|
378
|
-
|
|
|
379
|
-
|
|
|
380
|
-
| `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 |
|
|
381
|
-
| `activeToolCalls` | `ActiveToolCall[]` | In-flight tool calls (`{ callId, toolName }`) |
|
|
382
|
-
| `respond` | `(prompt: string) => Promise<string>` | Non-streaming generation. `undefined` when unavailable. |
|
|
383
|
-
| `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. |
|
|
384
|
-
| `streamResponse` | `(prompt: string) => 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`. |
|
|
385
|
-
| `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. |
|
|
386
|
-
| `reset` | `() => void` | Clear the conversation transcript, keeping instructions/tools/schema/options. Also cancels any in-flight stream. `undefined` only when there is no session. |
|
|
387
|
-
| `downloadModel` | `() => Promise<void>` | Trigger model download (Android). `undefined` when unavailable. |
|
|
388
|
-
|
|
389
|
-
### `generate(prompt, config?)`
|
|
390
|
-
|
|
391
|
-
One-shot stateless generation — creates a session, responds once, and releases it. Takes the
|
|
392
|
-
same generation config as `useLocalLLM`/`createLLMSession`, except tool handlers are not supported. Use it for classification or extraction
|
|
393
|
-
calls where conversation history between calls is unwanted. Throws when the module or model
|
|
394
|
-
is unavailable (check `ExpoLocalLlmModule.getAvailability()` first if you need to gate).
|
|
395
|
-
|
|
396
|
-
Use `useLocalLLM()` for automatic tool dispatch. Low-level `createLLMSession()` consumers must subscribe to `toolCall` and resolve or reject each call themselves; passing handlers in its configuration does not install those listeners.
|
|
397
|
-
|
|
398
|
-
### `generateObject(prompt, config)`
|
|
399
|
-
|
|
400
|
-
One-shot structured generation with a required `schema`. Returns an object whose type is inferred from the schema, after validating every required field, primitive type, enum, array item, and nested object. Unexpected properties are rejected. Use inline schema literals or `as const satisfies Schema` to preserve enum literal types.
|
|
401
|
-
|
|
402
|
-
This helper requires constrained structured output support (currently iOS 26+). It releases its session on success and failure. Generation errors and model refusals still reject the call; schema validation establishes structure, not factual accuracy.
|
|
403
|
-
|
|
404
|
-
`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.
|
|
405
|
-
|
|
406
|
-
### `createLLMSession(config?)`
|
|
407
|
-
|
|
408
|
-
Creates a manually owned conversation. Release it when finished:
|
|
409
|
-
|
|
410
|
-
```ts
|
|
411
|
-
import { createLLMSession } from 'expo-local-llm';
|
|
412
|
-
|
|
413
|
-
const session = createLLMSession({ instructions: 'Keep answers concise.' });
|
|
414
|
-
try {
|
|
415
|
-
const text = await session.respond('Suggest a short title for a gardening journal.');
|
|
416
|
-
} finally {
|
|
417
|
-
session.release();
|
|
418
|
-
}
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
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.
|
|
422
|
-
|
|
423
|
-
### Error codes
|
|
424
|
-
|
|
425
|
-
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.
|
|
426
|
-
|
|
427
|
-
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`.
|
|
428
|
-
|
|
429
|
-
| Code | Meaning |
|
|
430
|
-
|------|---------|
|
|
431
|
-
| `ERR_CONTEXT_WINDOW_EXCEEDED` | The conversation no longer fits the model's context window. Reset or start a new session. |
|
|
432
|
-
| `ERR_GUARDRAIL_VIOLATION` | Safety guardrails blocked the prompt or response. |
|
|
433
|
-
| `ERR_REFUSAL` | The model declined to answer. |
|
|
434
|
-
| `ERR_UNSUPPORTED_LANGUAGE` | The prompt language or device locale is not supported. |
|
|
435
|
-
| `ERR_UNSUPPORTED_REQUEST` | The request uses a capability the model or helper cannot honour. |
|
|
436
|
-
| `ERR_RATE_LIMITED` | The system is throttling generation (quota, background use, load). Retry later. |
|
|
437
|
-
| `ERR_MODEL_UNAVAILABLE` | Model assets are missing or the device cannot run the model right now. |
|
|
438
|
-
| `ERR_DECODING_FAILED` | Output could not be decoded into the requested structure. |
|
|
439
|
-
| `ERR_TIMEOUT` | Generation exceeded the system's time limit (iOS 27+). |
|
|
440
|
-
| `ERR_TOOL_FAILED` | A tool handler threw or timed out, or the hook had no handler for the tool. |
|
|
441
|
-
| `ERR_GENERATION_FAILED` | Any other generation failure. |
|
|
442
|
-
| `ERR_SESSION_BUSY` | The session already has a generation in progress. |
|
|
443
|
-
| `ERR_SESSION_INVALID` | The session was released or is not ready. |
|
|
444
|
-
| `ERR_NOT_SUPPORTED` | On-device generation is not available on this OS version. |
|
|
445
|
-
| `ERR_RESPONSE_FORMAT_NOT_SUPPORTED` | Structured output is not supported on this platform. |
|
|
446
|
-
| `ERR_TOOL_NOT_SUPPORTED` | Tool calling is not supported on this platform. |
|
|
447
|
-
| `ERR_MODEL_DOWNLOAD_FAILED` | Android: downloading Gemini Nano failed. |
|
|
448
|
-
| `ERR_SCHEMA_INVALID` | A schema is malformed (`SchemaInvalidError`). |
|
|
449
|
-
| `ERR_STRUCTURED_OUTPUT_INVALID` | Generated JSON did not match the schema (`StructuredOutputValidationError`). |
|
|
450
|
-
| `ERR_UNAVAILABLE` | The native module is missing from this build. |
|
|
451
|
-
|
|
452
|
-
### `ModelAvailability`
|
|
453
|
-
|
|
454
|
-
`'available' | 'notEnabled' | 'notReady' | 'notEligible' | 'downloadRequired' | 'downloading' | 'unknown' | 'moduleUnavailable'`
|
|
455
|
-
|
|
456
|
-
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.
|
|
457
|
-
|
|
458
|
-
### `validateSchema(schema)` and `validateStructuredOutput(value, schema)`
|
|
459
|
-
|
|
460
|
-
`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) |
|
|
461
369
|
|
|
462
370
|
## Platform differences and limitations
|
|
463
371
|
|
|
@@ -467,18 +375,19 @@ Android readiness is checked asynchronously and may initially be `unknown`. Pref
|
|
|
467
375
|
| Conversation history | Kept by the native session | Replayed in the prompt: the last 10 exchanges, with role markers in conversation text neutralised |
|
|
468
376
|
| Model availability | Built into the OS | May need `downloadModel()` |
|
|
469
377
|
| Token limits | OS and model dependent | SDK and model dependent |
|
|
378
|
+
| Image prompts | Up to eight local images per request on iOS 27+ with a ready vision-capable model and an Xcode 27+ build; retained in native history | Up to eight local images per request; device model support is unknown |
|
|
470
379
|
|
|
471
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.
|
|
472
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.
|
|
473
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`.
|
|
474
|
-
- **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.
|
|
475
384
|
- **Readiness gates generation.** `respond` and `streamResponse` need a session and `availability === 'available'`; cancelling and resetting work whenever a session exists.
|
|
476
385
|
|
|
477
386
|
## How this library is verified
|
|
478
387
|
|
|
479
|
-
- **
|
|
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.
|
|
480
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.
|
|
481
|
-
- **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.
|
|
482
391
|
|
|
483
392
|
## Choosing this library
|
|
484
393
|
|
|
@@ -495,7 +404,7 @@ The [Expo Modules API](https://docs.expo.dev/modules/overview/) fits OS-provided
|
|
|
495
404
|
- **Swift and Kotlin** — both implementations use a consistent module definition style, without adding a separate inference engine.
|
|
496
405
|
- **Expo build workflows** — autolinking integrates with prebuild, local development builds, and EAS Build.
|
|
497
406
|
|
|
498
|
-
Expo
|
|
407
|
+
Expo supplies the native integration and build workflow. Model behavior and device eligibility still come from the platform backend.
|
|
499
408
|
|
|
500
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.
|
|
501
410
|
|