expo-local-llm 0.8.0 → 0.8.1
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 +192 -209
- package/android/build/intermediates/incremental/mergeDebugAssets/merger.xml +1 -1
- package/android/build.gradle +1 -0
- package/android/proguard-rules.pro +8 -0
- package/android/src/main/java/expo/modules/localllm/LLMSession.kt +4 -0
- package/build/ExpoLocalLlm.types.d.ts +2 -2
- package/build/ExpoLocalLlm.types.d.ts.map +1 -1
- package/build/ExpoLocalLlm.types.js.map +1 -1
- package/package.json +9 -9
- package/src/ExpoLocalLlm.types.ts +2 -2
package/README.md
CHANGED
|
@@ -1,53 +1,33 @@
|
|
|
1
1
|
# expo-local-llm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/expo-local-llm)
|
|
4
|
+
[](https://github.com/GijungKim/expo-local-llm/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/GijungKim/expo-local-llm/actions/workflows/native.yml)
|
|
6
|
+
[](LICENSE)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
On-device language models for Expo apps, using the models the operating system already ships: 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 model weights to bundle, no API key, and no network connection needed to generate.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
Inference stays on the device. There is no cloud fallback; network access happens only inside tool handlers you write. The package has no runtime dependencies.
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|----------|-------------|
|
|
11
|
-
| iOS | iOS 26+ with Apple Intelligence enabled. Loads on iOS 16.4+ without crashing (returns `notEligible`). Host project must target iOS 16.4+. |
|
|
12
|
-
| Android | Host app uses `minSdkVersion` 26+. Inference requires a Gemini Nano–capable device; most Android devices, including most mid-range phones, report `notEligible`. Build, module loading, eligibility, and unsupported-feature handling are device-verified; generation is not yet validated on Gemini Nano hardware, so `getCapabilities()` reports Android as `experimental: true`. |
|
|
13
|
-
| Expo SDK | 52+ (peer range); the example targets SDK 57 / React Native 0.86. A peer range is not a tested compatibility matrix. |
|
|
12
|
+
## At a glance
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
| | iOS 26+ (Foundation Models) | Android (Gemini Nano) |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Text generation and streaming | ✅ Verified on device | ⚠️ Experimental: builds and reports eligibility; generation not yet validated on Gemini Nano hardware |
|
|
17
|
+
| Structured output (typed, schema-constrained) | ✅ Constrained decoding, validated in JS | ❌ Rejected explicitly |
|
|
18
|
+
| Tool calling (schema-constrained arguments) | ✅ | ❌ Rejected explicitly |
|
|
19
|
+
| Stable error codes | ✅ | ✅ |
|
|
20
|
+
| Model download | Managed by the OS | `downloadModel()` with progress |
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
Unsupported features fail with a clear error instead of degrading silently. `getCapabilities()` reports all of this at runtime, including `experimental: true` on Android. See [how this library is verified](#how-this-library-is-verified).
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Apps built with the iOS 27 SDK must adopt the UIKit scene lifecycle, or UIKit stops them at launch before any JavaScript runs. Expo SDK 57 apps opt in through `expo-build-properties` (Expo `57.0.23`+, `expo-build-properties` `57.0.20`+):
|
|
22
|
-
|
|
23
|
-
```json
|
|
24
|
-
{
|
|
25
|
-
"expo": {
|
|
26
|
-
"plugins": [
|
|
27
|
-
[
|
|
28
|
-
"expo-build-properties",
|
|
29
|
-
{ "ios": { "deploymentTarget": "16.4", "enableSceneSupport": true } }
|
|
30
|
-
]
|
|
31
|
-
]
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Then regenerate and rebuild the native app. This is an app-level build setting, not something this library can enable for you.
|
|
37
|
-
|
|
38
|
-
## Installation
|
|
24
|
+
## Quick start
|
|
39
25
|
|
|
40
26
|
```bash
|
|
41
|
-
npx expo install expo-local-llm expo-dev-client
|
|
27
|
+
npx expo install expo-local-llm expo-dev-client expo-build-properties
|
|
42
28
|
```
|
|
43
29
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
npx expo install expo-build-properties
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
In `app.json`, add the plugin with the deployment target:
|
|
30
|
+
Set the deployment targets the module needs in `app.json`:
|
|
51
31
|
|
|
52
32
|
```json
|
|
53
33
|
{
|
|
@@ -65,43 +45,69 @@ In `app.json`, add the plugin with the deployment target:
|
|
|
65
45
|
}
|
|
66
46
|
```
|
|
67
47
|
|
|
68
|
-
Build
|
|
48
|
+
Build a development app, since stock Expo Go cannot load native modules:
|
|
69
49
|
|
|
70
50
|
```bash
|
|
71
51
|
npx expo run:ios --device
|
|
72
|
-
# or
|
|
73
|
-
npx expo run:android --device
|
|
74
52
|
```
|
|
75
53
|
|
|
76
|
-
|
|
54
|
+
Then stream a response:
|
|
77
55
|
|
|
78
|
-
|
|
56
|
+
```tsx
|
|
57
|
+
import { Button, Text } from 'react-native';
|
|
58
|
+
import { useLocalLLM } from 'expo-local-llm';
|
|
79
59
|
|
|
80
|
-
|
|
60
|
+
export function Summary({ notes }: { notes: string }) {
|
|
61
|
+
const { availability, streamResponse, streamedText, error } = useLocalLLM({
|
|
62
|
+
instructions: 'Summarise the text in two sentences.',
|
|
63
|
+
});
|
|
81
64
|
|
|
82
|
-
|
|
65
|
+
if (!streamResponse) return <Text>{error ?? `On-device AI: ${availability}`}</Text>;
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<Button title="Summarise" onPress={() => streamResponse(notes)} />
|
|
69
|
+
<Text>{streamedText}</Text>
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
83
74
|
|
|
84
|
-
|
|
75
|
+
`streamResponse` is `undefined` until the model is ready, so the component shows why when it isn't: Apple Intelligence switched off, the model still downloading, or an ineligible device.
|
|
85
76
|
|
|
86
|
-
|
|
77
|
+
## Requirements
|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
| | Requirement |
|
|
80
|
+
|---|---|
|
|
81
|
+
| 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+. Generation needs a Gemini Nano–capable device; most Android phones, including most mid-range ones, report `notEligible`. |
|
|
83
|
+
| Expo SDK | 52+ peer range. The example app targets SDK 58 / React Native 0.88; the peer range is not a tested compatibility matrix. |
|
|
90
84
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
If the project targets an iOS version below 16.4, the build fails with `module 'ExpoLocalLlm' has a minimum deployment target of iOS 16.4`. The 16.4 floor is for compilation only; generation still needs iOS 26.
|
|
88
|
+
|
|
89
|
+
For setup failures, unavailable models and unsupported features, see [troubleshooting](docs/troubleshooting.md).
|
|
90
|
+
|
|
91
|
+
### Xcode 27 requires scene support
|
|
92
|
+
|
|
93
|
+
Apps built with the iOS 27 SDK must adopt the UIKit scene lifecycle, or UIKit stops them at launch before any JavaScript runs. Expo SDK 58 apps adopt it by default, so there is nothing to set. Expo SDK 57 apps opt in through `expo-build-properties` (Expo `57.0.23`+, `expo-build-properties` `57.0.20`+):
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"expo": {
|
|
98
|
+
"plugins": [
|
|
99
|
+
[
|
|
100
|
+
"expo-build-properties",
|
|
101
|
+
{ "ios": { "deploymentTarget": "16.4", "enableSceneSupport": true } }
|
|
102
|
+
]
|
|
103
|
+
]
|
|
104
|
+
}
|
|
101
105
|
}
|
|
102
106
|
```
|
|
103
107
|
|
|
104
|
-
|
|
108
|
+
Then regenerate and rebuild the native app. This is an app-level build setting, not something this library can enable for you.
|
|
109
|
+
|
|
110
|
+
## Usage
|
|
105
111
|
|
|
106
112
|
### Stream text in a component
|
|
107
113
|
|
|
@@ -152,71 +158,71 @@ function Chat() {
|
|
|
152
158
|
}
|
|
153
159
|
```
|
|
154
160
|
|
|
155
|
-
###
|
|
156
|
-
|
|
157
|
-
Every error from this library carries 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()`.
|
|
161
|
+
### One-shot generation
|
|
158
162
|
|
|
159
|
-
|
|
163
|
+
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:
|
|
160
164
|
|
|
161
|
-
```
|
|
162
|
-
|
|
165
|
+
```ts
|
|
166
|
+
import { generate } from 'expo-local-llm';
|
|
163
167
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
if (errorCode === 'ERR_GUARDRAIL_VIOLATION' || errorCode === 'ERR_REFUSAL') {
|
|
169
|
-
// The model declined; ask the person to rephrase.
|
|
170
|
-
}
|
|
168
|
+
const title = await generate('Suggest a short title for this note: ...', {
|
|
169
|
+
instructions: 'Reply with the title only.',
|
|
170
|
+
});
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
### Extract typed data (iOS)
|
|
174
|
+
|
|
175
|
+
`generateObject()` infers the result type from the schema, constrains the model's decoding to it, and validates the JSON before returning it:
|
|
176
|
+
|
|
173
177
|
```ts
|
|
174
|
-
import {
|
|
178
|
+
import { generateObject, getCapabilities } from 'expo-local-llm';
|
|
175
179
|
|
|
176
|
-
|
|
177
|
-
await
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
if (getCapabilities().structuredOutputMode === 'constrained') {
|
|
181
|
+
const result = await generateObject('Classify: "Can we move the meeting to Friday?"', {
|
|
182
|
+
instructions: 'Classify the message by its primary intent.',
|
|
183
|
+
schema: {
|
|
184
|
+
intent: { type: 'string', enum: ['scheduling', 'question', 'other'] },
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
// result.intent: 'scheduling' | 'question' | 'other'
|
|
182
188
|
}
|
|
183
189
|
```
|
|
184
190
|
|
|
185
|
-
|
|
191
|
+
A supported capability does not mean the model is ready, so handle rejected calls. A mismatch rejects with `StructuredOutputValidationError`; output is never repaired silently.
|
|
186
192
|
|
|
187
|
-
|
|
193
|
+
<p align="center">
|
|
194
|
+
<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" />
|
|
195
|
+
</p>
|
|
188
196
|
|
|
189
|
-
|
|
190
|
-
import { Data, Effect } from 'effect';
|
|
191
|
-
import { generate, getLocalLLMErrorCode, type LocalLLMErrorCode } from 'expo-local-llm';
|
|
197
|
+
*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`.*
|
|
192
198
|
|
|
193
|
-
|
|
194
|
-
code: LocalLLMErrorCode | null;
|
|
195
|
-
message: string;
|
|
196
|
-
}> {}
|
|
199
|
+
### Structured output in the hook
|
|
197
200
|
|
|
198
|
-
|
|
199
|
-
Effect.tryPromise({
|
|
200
|
-
try: () => generate(prompt),
|
|
201
|
-
catch: (error) =>
|
|
202
|
-
new LocalLLMFailure({
|
|
203
|
-
code: error instanceof Error ? getLocalLLMErrorCode(error) : null,
|
|
204
|
-
message: error instanceof Error ? error.message : 'Generation failed',
|
|
205
|
-
}),
|
|
206
|
-
});
|
|
201
|
+
In JSON mode the hook streams a partial object, then publishes the validated result:
|
|
207
202
|
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
203
|
+
```tsx
|
|
204
|
+
const { streamResponse, streamedObject, completedObject } = useLocalLLM({
|
|
205
|
+
responseFormat: 'json',
|
|
206
|
+
schema: {
|
|
207
|
+
name: { type: 'string', description: 'Recipe name' },
|
|
208
|
+
ingredients: { type: 'array', items: { type: 'string' } },
|
|
209
|
+
difficulty: { type: 'string', enum: ['easy', 'medium', 'hard'] },
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await streamResponse?.('Give me a pasta recipe');
|
|
214
|
+
// streamedObject: fields appear as the model writes them (every field optional)
|
|
215
|
+
// completedObject: { name: string; ingredients: string[]; difficulty: 'easy' | 'medium' | 'hard' } | null
|
|
215
216
|
```
|
|
216
217
|
|
|
217
|
-
|
|
218
|
+
`respond` and `streamResponse` validate the final JSON and reject with `StructuredOutputValidationError` (`ERR_STRUCTURED_OUTPUT_INVALID`) if it does not match. A cancelled stream keeps its partial `streamedObject` and leaves `completedObject` null. `streamedJSON` holds the raw partial JSON.
|
|
219
|
+
|
|
220
|
+
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
|
+
|
|
222
|
+
### Tool calling (iOS)
|
|
218
223
|
|
|
219
224
|
```tsx
|
|
225
|
+
import { Text, View } from 'react-native';
|
|
220
226
|
import { defineTool, useLocalLLM } from 'expo-local-llm';
|
|
221
227
|
|
|
222
228
|
const getWeather = defineTool({
|
|
@@ -256,97 +262,69 @@ function WeatherChat() {
|
|
|
256
262
|
|
|
257
263
|
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`.
|
|
258
264
|
|
|
259
|
-
###
|
|
260
|
-
|
|
261
|
-
iOS 26+ uses Apple's constrained decoding (`DynamicGenerationSchema`) for schema-driven output.
|
|
262
|
-
Successful generation produces structured JSON; requests can still fail or be refused.
|
|
263
|
-
Android structured output is unsupported and rejected explicitly, rather than silently generating ordinary text.
|
|
265
|
+
### Handle errors
|
|
264
266
|
|
|
265
|
-
|
|
266
|
-
<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" />
|
|
267
|
-
</p>
|
|
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()`.
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
Foundation Models has a small context window, so a long conversation eventually fails with `ERR_CONTEXT_WINDOW_EXCEEDED`. On iOS the library never drops turns to make room (Android replays only the last 10 exchanges); decide what your app should do:
|
|
270
270
|
|
|
271
271
|
```tsx
|
|
272
|
-
const {
|
|
273
|
-
responseFormat: 'json',
|
|
274
|
-
schema: {
|
|
275
|
-
name: { type: 'string', description: 'Recipe name' },
|
|
276
|
-
ingredients: {
|
|
277
|
-
type: 'array',
|
|
278
|
-
items: { type: 'string' },
|
|
279
|
-
description: 'List of ingredients',
|
|
280
|
-
},
|
|
281
|
-
steps: {
|
|
282
|
-
type: 'array',
|
|
283
|
-
items: { type: 'string' },
|
|
284
|
-
description: 'Cooking steps',
|
|
285
|
-
},
|
|
286
|
-
difficulty: {
|
|
287
|
-
type: 'string',
|
|
288
|
-
enum: ['easy', 'medium', 'hard'],
|
|
289
|
-
},
|
|
290
|
-
},
|
|
291
|
-
});
|
|
272
|
+
const { errorCode, reset } = useLocalLLM({ instructions: 'You are a helpful assistant.' });
|
|
292
273
|
|
|
293
|
-
if (
|
|
294
|
-
|
|
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.
|
|
295
280
|
}
|
|
296
|
-
// completedObject is typed from the schema and validated against it:
|
|
297
|
-
// { name: string; ingredients: string[]; steps: string[]; difficulty: 'easy' | 'medium' | 'hard' } | null
|
|
298
281
|
```
|
|
299
282
|
|
|
300
|
-
In JSON mode, `respond` and `streamResponse` validate the final JSON against the schema and reject with `StructuredOutputValidationError` (`ERR_STRUCTURED_OUTPUT_INVALID`) if it does not match.
|
|
301
|
-
|
|
302
|
-
For one-shot structured calls (classification, extraction) where you don't want conversation
|
|
303
|
-
history accumulating between calls, use `generate()` instead of a session:
|
|
304
|
-
|
|
305
283
|
```ts
|
|
306
|
-
import { generate } from 'expo-local-llm';
|
|
284
|
+
import { generate, getLocalLLMErrorCode } from 'expo-local-llm';
|
|
307
285
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
286
|
+
try {
|
|
287
|
+
await generate('Summarise my notes');
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error instanceof Error && getLocalLLMErrorCode(error) === 'ERR_RATE_LIMITED') {
|
|
290
|
+
// The system is throttling on-device generation; retry later.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
313
293
|
```
|
|
314
294
|
|
|
315
|
-
|
|
316
|
-
partial as a JSON string) and `streamedObject` (the parsed value, typed with every field optional).
|
|
317
|
-
When the stream completes, the validated result lands in `completedObject`:
|
|
318
|
-
|
|
319
|
-
```tsx
|
|
320
|
-
const { streamResponse, streamedObject, completedObject } = useLocalLLM({
|
|
321
|
-
responseFormat: 'json',
|
|
322
|
-
schema: { /* ... */ },
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
await streamResponse?.('Give me a pasta recipe');
|
|
326
|
-
// streamedObject updates as the model fills in fields;
|
|
327
|
-
// completedObject is set once the whole object has validated.
|
|
328
|
-
```
|
|
295
|
+
#### Using with Effect
|
|
329
296
|
|
|
330
|
-
|
|
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:
|
|
331
298
|
|
|
332
|
-
|
|
299
|
+
```ts
|
|
300
|
+
import { Data, Effect } from 'effect';
|
|
301
|
+
import { generate, getLocalLLMErrorCode, type LocalLLMErrorCode } from 'expo-local-llm';
|
|
333
302
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
303
|
+
class LocalLLMFailure extends Data.TaggedError('LocalLLMFailure')<{
|
|
304
|
+
code: LocalLLMErrorCode | null;
|
|
305
|
+
message: string;
|
|
306
|
+
}> {}
|
|
337
307
|
|
|
338
|
-
|
|
339
|
-
|
|
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
|
+
});
|
|
340
317
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
+
);
|
|
347
325
|
```
|
|
348
326
|
|
|
349
|
-
## API
|
|
327
|
+
## API reference
|
|
350
328
|
|
|
351
329
|
### `getCapabilities()`
|
|
352
330
|
|
|
@@ -384,7 +362,7 @@ if (!diagnostics.available) {
|
|
|
384
362
|
| `tools` | `ToolDefinition[]` | Tools the model can invoke (iOS 26+ only) |
|
|
385
363
|
| `toolTimeout` | `number` | Seconds before an unresolved tool call times out (default 30) |
|
|
386
364
|
| `responseFormat` | `"text" \| "json"` | Set to `"json"` for structured JSON output; requires `schema` |
|
|
387
|
-
| `schema` | `Schema` | Schema for structured output (used with `responseFormat: "json"`)
|
|
365
|
+
| `schema` | `Schema` | Schema for structured output (used with `responseFormat: "json"`); see [Structured output in the hook](#structured-output-in-the-hook) |
|
|
388
366
|
| `includeSchemaInPrompt` | `boolean` | iOS 26+: include the schema definition in the prompt. Default `true`. Set `false` when you've front-loaded few-shot examples. |
|
|
389
367
|
|
|
390
368
|
#### Returns
|
|
@@ -459,7 +437,7 @@ On iOS 27, Foundation Models reports some failures with error types that only th
|
|
|
459
437
|
| `ERR_MODEL_UNAVAILABLE` | Model assets are missing or the device cannot run the model right now. |
|
|
460
438
|
| `ERR_DECODING_FAILED` | Output could not be decoded into the requested structure. |
|
|
461
439
|
| `ERR_TIMEOUT` | Generation exceeded the system's time limit (iOS 27+). |
|
|
462
|
-
| `ERR_TOOL_FAILED` | A tool handler threw
|
|
440
|
+
| `ERR_TOOL_FAILED` | A tool handler threw or timed out, or the hook had no handler for the tool. |
|
|
463
441
|
| `ERR_GENERATION_FAILED` | Any other generation failure. |
|
|
464
442
|
| `ERR_SESSION_BUSY` | The session already has a generation in progress. |
|
|
465
443
|
| `ERR_SESSION_INVALID` | The session was released or is not ready. |
|
|
@@ -477,28 +455,38 @@ On iOS 27, Foundation Models reports some failures with error types that only th
|
|
|
477
455
|
|
|
478
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.
|
|
479
457
|
|
|
480
|
-
|
|
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.
|
|
461
|
+
|
|
462
|
+
## Platform differences and limitations
|
|
481
463
|
|
|
482
464
|
| Concern | iOS | Android |
|
|
483
|
-
|
|
484
|
-
| System instructions | Native `LanguageModelSession(instructions:)` | Prepended
|
|
485
|
-
|
|
|
486
|
-
| Model availability | Built
|
|
487
|
-
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
-
|
|
493
|
-
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
-
|
|
498
|
-
-
|
|
499
|
-
-
|
|
500
|
-
|
|
501
|
-
##
|
|
465
|
+
|---|---|---|
|
|
466
|
+
| System instructions | Native `LanguageModelSession(instructions:)` | Prepended to the prompt |
|
|
467
|
+
| Conversation history | Kept by the native session | Replayed in the prompt: the last 10 exchanges, with role markers in conversation text neutralised |
|
|
468
|
+
| Model availability | Built into the OS | May need `downloadModel()` |
|
|
469
|
+
| Token limits | OS and model dependent | SDK and model dependent |
|
|
470
|
+
|
|
471
|
+
- **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
|
+
- **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
|
+
- **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 is experimental.** The Gemini Nano SDK is in beta and its API may change. Structured output and tools are rejected at session creation.
|
|
475
|
+
- **Readiness gates generation.** `respond` and `streamResponse` need a session and `availability === 'available'`; cancelling and resetting work whenever a session exists.
|
|
476
|
+
|
|
477
|
+
## How this library is verified
|
|
478
|
+
|
|
479
|
+
- **Every pull request:** 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
|
+
- **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 what has been checked on an iPhone 15 Pro, a simulator and a Galaxy A17, and leaves untested scenarios marked as pending rather than assumed.
|
|
482
|
+
|
|
483
|
+
## Choosing this library
|
|
484
|
+
|
|
485
|
+
Choose `expo-local-llm` for a small, Expo-first API around system-provided language models: short text generation, classification, extraction, and streaming interfaces on supported devices. You do not select or distribute model weights.
|
|
486
|
+
|
|
487
|
+
If you need downloadable models, embeddings, speech, or an AI SDK provider interface, evaluate a broader runtime such as [React Native AI](https://github.com/callstackincubator/ai). Check its current provider documentation for platform requirements. This library does not implement those capabilities or automatically route requests to a cloud model.
|
|
488
|
+
|
|
489
|
+
### Why an Expo module?
|
|
502
490
|
|
|
503
491
|
The [Expo Modules API](https://docs.expo.dev/modules/overview/) fits OS-provided AI APIs well:
|
|
504
492
|
|
|
@@ -511,16 +499,11 @@ Expo does not improve model quality or remove device eligibility requirements. I
|
|
|
511
499
|
|
|
512
500
|
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.
|
|
513
501
|
|
|
514
|
-
##
|
|
515
|
-
|
|
516
|
-
Choose `expo-local-llm` for a small, Expo-first API around system-provided language models: short text generation, classification, extraction, and streaming interfaces on supported devices. You do not select or distribute model weights.
|
|
517
|
-
|
|
518
|
-
If you need downloadable models, embeddings, speech, or an AI SDK provider interface, evaluate a broader runtime such as [React Native AI](https://github.com/callstackincubator/ai). Check its current provider documentation for platform requirements. This library does not implement those capabilities or automatically route requests to a cloud model.
|
|
519
|
-
|
|
520
|
-
## Built with expo-local-llm
|
|
502
|
+
## Used by
|
|
521
503
|
|
|
522
|
-
- [
|
|
523
|
-
- [
|
|
504
|
+
- [Angular Native](https://github.com/ng-native/ng-native): wraps it as the `LanguageModel` service in `@ng-native/expo`
|
|
505
|
+
- [PulseID](https://apps.apple.com/us/app/pulseid-heart-rate-camera/id6754331991): heart rate camera
|
|
506
|
+
- [Teamfit Tactics](https://apps.apple.com/us/app/teamfit-tactics-tft-fitness/id6757195493): TFT fitness
|
|
524
507
|
|
|
525
508
|
*Using this library? Open a PR to add your app.*
|
|
526
509
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/swoleknee/chingu/expo-local-llm/example/node_modules/expo-local-llm/android/src/main/assets"/></dataSet><dataSet config="debug" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/swoleknee/chingu/expo-local-llm/example/node_modules/expo-local-llm/android/src/debug/assets"/></dataSet
|
|
2
|
+
<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/swoleknee/chingu/expo-local-llm/example/node_modules/expo-local-llm/android/src/main/assets"/></dataSet><dataSet config="debug" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/swoleknee/chingu/expo-local-llm/example/node_modules/expo-local-llm/android/src/debug/assets"/></dataSet></merger>
|
package/android/build.gradle
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Consumer rules applied to apps that minify release builds (the default from
|
|
2
|
+
# Expo SDK 58). ML Kit discovers its components by instantiating the
|
|
3
|
+
# registrars named in its manifest metadata through reflection. ML Kit's own
|
|
4
|
+
# rules do not keep their no-argument constructors, so R8 removes them and
|
|
5
|
+
# ML Kit never initialises: availability checks then fail on every device.
|
|
6
|
+
-keep class * implements com.google.firebase.components.ComponentRegistrar {
|
|
7
|
+
<init>();
|
|
8
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package expo.modules.localllm
|
|
2
2
|
|
|
3
|
+
import android.util.Log
|
|
3
4
|
import com.google.mlkit.genai.common.DownloadStatus
|
|
4
5
|
import com.google.mlkit.genai.common.FeatureStatus
|
|
5
6
|
import com.google.mlkit.genai.prompt.GenerateContentRequest
|
|
@@ -48,6 +49,8 @@ class LLMSession(
|
|
|
48
49
|
private var disposed = false
|
|
49
50
|
|
|
50
51
|
companion object {
|
|
52
|
+
private const val LOG_TAG = "ExpoLocalLlm"
|
|
53
|
+
|
|
51
54
|
@Volatile
|
|
52
55
|
private var cachedAvailability = ModelAvailability.unknown
|
|
53
56
|
private val availabilityMutex = Mutex()
|
|
@@ -69,6 +72,7 @@ class LLMSession(
|
|
|
69
72
|
throw e
|
|
70
73
|
} catch (e: Exception) {
|
|
71
74
|
// SDK/API failures do not prove that the device is ineligible.
|
|
75
|
+
Log.w(LOG_TAG, "Gemini Nano availability check failed", e)
|
|
72
76
|
ModelAvailability.unknown
|
|
73
77
|
} finally {
|
|
74
78
|
// Generation.getClient() creates an independent engine-backed client.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type ModelAvailability = "available" | "moduleUnavailable" | "notEnabled" | "notReady" | "notEligible" | "downloadRequired" | "downloading" | "unknown";
|
|
2
2
|
/**
|
|
3
|
-
* Stable codes carried by
|
|
3
|
+
* Stable codes carried by generation failures and the library's own checks, on both platforms.
|
|
4
4
|
* Branch on these rather than on messages, which come from the OS and change.
|
|
5
5
|
* Read one from any caught error with `getLocalLLMErrorCode`.
|
|
6
6
|
*/
|
|
@@ -23,7 +23,7 @@ export type LocalLLMErrorCode =
|
|
|
23
23
|
| "ERR_DECODING_FAILED"
|
|
24
24
|
/** Generation took longer than the system allows. */
|
|
25
25
|
| "ERR_TIMEOUT"
|
|
26
|
-
/** A tool call failed: the handler threw
|
|
26
|
+
/** A tool call failed: the handler threw or timed out, or the hook had no handler for it. */
|
|
27
27
|
| "ERR_TOOL_FAILED"
|
|
28
28
|
/** Any other generation failure. */
|
|
29
29
|
| "ERR_GENERATION_FAILED"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoLocalLlm.types.d.ts","sourceRoot":"","sources":["../src/ExpoLocalLlm.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,mBAAmB,GACnB,YAAY,GACZ,UAAU,GACV,aAAa,GACb,kBAAkB,GAClB,aAAa,GACb,SAAS,CAAC;AAEd;;;;GAIG;AACH,MAAM,MAAM,iBAAiB;AAC3B,gGAAgG;AAC9F,6BAA6B;AAC/B,2EAA2E;GACzE,yBAAyB;AAC3B,oCAAoC;GAClC,aAAa;AACf,gFAAgF;GAC9E,0BAA0B;AAC5B,+EAA+E;GAC7E,yBAAyB;AAC3B,yFAAyF;GACvF,kBAAkB;AACpB,4EAA4E;GAC1E,uBAAuB;AACzB,4EAA4E;GAC1E,qBAAqB;AACvB,qDAAqD;GACnD,aAAa;AACf,
|
|
1
|
+
{"version":3,"file":"ExpoLocalLlm.types.d.ts","sourceRoot":"","sources":["../src/ExpoLocalLlm.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoLocalLlm.types.js","sourceRoot":"","sources":["../src/ExpoLocalLlm.types.ts"],"names":[],"mappings":"","sourcesContent":["export type ModelAvailability =\n | \"available\"\n | \"moduleUnavailable\"\n | \"notEnabled\"\n | \"notReady\"\n | \"notEligible\"\n | \"downloadRequired\"\n | \"downloading\"\n | \"unknown\";\n\n/**\n * Stable codes carried by
|
|
1
|
+
{"version":3,"file":"ExpoLocalLlm.types.js","sourceRoot":"","sources":["../src/ExpoLocalLlm.types.ts"],"names":[],"mappings":"","sourcesContent":["export type ModelAvailability =\n | \"available\"\n | \"moduleUnavailable\"\n | \"notEnabled\"\n | \"notReady\"\n | \"notEligible\"\n | \"downloadRequired\"\n | \"downloading\"\n | \"unknown\";\n\n/**\n * Stable codes carried by generation failures and the library's own checks, on both platforms.\n * Branch on these rather than on messages, which come from the OS and change.\n * Read one from any caught error with `getLocalLLMErrorCode`.\n */\nexport type LocalLLMErrorCode =\n /** The conversation no longer fits the model's context window. Reset or start a new session. */\n | \"ERR_CONTEXT_WINDOW_EXCEEDED\"\n /** The platform's safety guardrails blocked the prompt or the response. */\n | \"ERR_GUARDRAIL_VIOLATION\"\n /** The model declined to answer. */\n | \"ERR_REFUSAL\"\n /** The prompt's language or the device locale is not supported by the model. */\n | \"ERR_UNSUPPORTED_LANGUAGE\"\n /** The request uses a schema feature or capability the model cannot honour. */\n | \"ERR_UNSUPPORTED_REQUEST\"\n /** The system is throttling generation (quota, background use, or load). Retry later. */\n | \"ERR_RATE_LIMITED\"\n /** The model's assets are missing or the device cannot run it right now. */\n | \"ERR_MODEL_UNAVAILABLE\"\n /** The model's output could not be decoded into the requested structure. */\n | \"ERR_DECODING_FAILED\"\n /** Generation took longer than the system allows. */\n | \"ERR_TIMEOUT\"\n /** A tool call failed: the handler threw or timed out, or the hook had no handler for it. */\n | \"ERR_TOOL_FAILED\"\n /** Any other generation failure. */\n | \"ERR_GENERATION_FAILED\"\n /** This session already has a generation in progress. */\n | \"ERR_SESSION_BUSY\"\n /** The session was released or never initialised. */\n | \"ERR_SESSION_INVALID\"\n /** On-device generation is not available on this OS version. */\n | \"ERR_NOT_SUPPORTED\"\n /** `responseFormat: \"json\"` is not supported on this platform. */\n | \"ERR_RESPONSE_FORMAT_NOT_SUPPORTED\"\n /** Tool calling is not supported on this platform. */\n | \"ERR_TOOL_NOT_SUPPORTED\"\n /** Android only: downloading Gemini Nano failed. */\n | \"ERR_MODEL_DOWNLOAD_FAILED\"\n /** The schema passed to a session or `generateObject` is malformed. */\n | \"ERR_SCHEMA_INVALID\"\n /** The generated JSON did not match the schema. */\n | \"ERR_STRUCTURED_OUTPUT_INVALID\"\n /** The native module is missing from this build (Expo Go, or not rebuilt). */\n | \"ERR_UNAVAILABLE\";\n\nexport type GenerationOptions = {\n temperature?: number;\n maxTokens?: number;\n topK?: number;\n};\n\n// Tool calling types\n\nexport type ToolParameterType = \"string\" | \"number\" | \"boolean\";\n\nexport type ToolParameter = {\n type: ToolParameterType;\n description: string;\n /** Allowed values for a `\"string\"` parameter. */\n enum?: readonly string[];\n};\n\nexport type ToolParameters = Record<string, ToolParameter>;\n\nexport type InferToolParameter<Parameter extends ToolParameter> =\n Parameter extends {\n type: \"string\";\n enum: readonly (infer Value extends string)[];\n }\n ? Value\n : Parameter extends { type: \"string\" }\n ? string\n : Parameter extends { type: \"number\" }\n ? number\n : Parameter extends { type: \"boolean\" }\n ? boolean\n : never;\n\n/**\n * Arguments the model supplied for a tool call. On iOS the model's decoding is\n * constrained to the tool's declared `parameters`, so every declared field is\n * present with its declared type and enum values are respected. Handlers\n * should still validate values that carry meaning (ranges, formats), as they\n * would for any other input.\n */\nexport type InferToolArguments<Parameters extends ToolParameters> = {\n -readonly [Key in keyof Parameters]: InferToolParameter<Parameters[Key]>;\n};\n\n/** Tool arguments when the tool's parameters are not known statically. */\nexport type ToolArguments = InferToolArguments<ToolParameters>;\n\n/**\n * A tool the model can call. Declare tools with `defineTool()` to have the\n * handler's arguments typed from `parameters`.\n */\nexport type ToolDefinition<Parameters extends ToolParameters = ToolParameters> =\n {\n name: string;\n description: string;\n parameters: Parameters;\n // Method syntax keeps a tool with specific parameters assignable to the\n // general `ToolDefinition` that `tools` arrays hold.\n handler(args: InferToolArguments<Parameters>): Promise<string>;\n };\n\nexport type ToolConfig = {\n name: string;\n description: string;\n parameters: ToolParameters;\n};\n\nexport type ToolCallEvent = {\n requestId?: string;\n callId: string;\n toolName: string;\n arguments: ToolArguments;\n};\n\nexport type ActiveToolCall = {\n callId: string;\n toolName: string;\n};\n\n// Structured output types\n\nexport type ResponseFormat = \"text\" | \"json\";\n\nexport type SchemaField =\n | { type: \"string\"; description?: string; enum?: readonly string[] }\n | { type: \"number\" | \"integer\" | \"boolean\"; description?: string }\n | { type: \"array\"; description?: string; items: SchemaField }\n | {\n type: \"object\";\n description?: string;\n properties: Record<string, SchemaField>;\n };\n\nexport type Schema = Record<string, SchemaField>;\n\nexport type InferSchemaField<Field extends SchemaField> = Field extends {\n type: \"string\";\n enum: readonly (infer Value extends string)[];\n}\n ? Value\n : Field extends { type: \"string\" }\n ? string\n : Field extends { type: \"number\" | \"integer\" }\n ? number\n : Field extends { type: \"boolean\" }\n ? boolean\n : Field extends { type: \"array\"; items: infer Item extends SchemaField }\n ? InferSchemaField<Item>[]\n : Field extends {\n type: \"object\";\n properties: infer Properties extends Schema;\n }\n ? InferSchema<Properties>\n : never;\n\nexport type InferSchema<Definition extends Schema> = {\n -readonly [Key in keyof Definition]: InferSchemaField<Definition[Key]>;\n};\n\n/**\n * An object still being generated: any field, at any depth, may be missing\n * until the model reaches it.\n */\nexport type DeepPartial<Value> = Value extends readonly (infer Item)[]\n ? DeepPartial<Item>[]\n : Value extends object\n ? { [Key in keyof Value]?: DeepPartial<Value[Key]> }\n : Value;\n\nexport type GenerateObjectConfig<Definition extends Schema> = Omit<\n SessionConfig,\n \"responseFormat\" | \"schema\" | \"tools\"\n> & {\n schema: Definition;\n /** One-shot helpers cannot service native tool-call events. */\n tools?: never;\n};\n\nexport type NativeModuleDiagnostics =\n | {\n available: true;\n reason: null;\n message: null;\n setupInstructions: readonly [];\n }\n | {\n available: false;\n reason: \"moduleUnavailable\";\n message: string;\n setupInstructions: readonly string[];\n };\n\nexport type LocalLLMCapabilities = {\n platform: \"ios\" | \"android\" | \"unsupported\";\n backend: \"appleFoundationModels\" | \"geminiNano\" | null;\n status:\n | \"available\"\n | \"moduleUnavailable\"\n | \"unsupportedOS\"\n | \"unsupportedPlatform\";\n text: boolean;\n streaming: boolean;\n tools: boolean;\n structuredOutput: boolean;\n structuredOutputMode: \"constrained\" | \"unsupported\";\n /**\n * True where generation has not been verified on real hardware. Android is\n * experimental until Gemini Nano generation is validated on a device.\n */\n experimental: boolean;\n};\n\nexport type SessionConfig = {\n instructions?: string;\n options?: GenerationOptions;\n tools?: ToolDefinition[];\n toolTimeout?: number;\n responseFormat?: ResponseFormat;\n schema?: Schema;\n includeSchemaInPrompt?: boolean;\n};\n\nexport type TokenEvent = {\n requestId?: string;\n token: string;\n accumulated: string;\n};\n\nexport type StreamCompleteEvent = {\n requestId?: string;\n text: string;\n};\n\nexport type PartialEvent = {\n requestId?: string;\n json: string;\n complete: boolean;\n};\n\nexport type StreamErrorEvent = {\n requestId?: string;\n error: string;\n code: LocalLLMErrorCode;\n};\n\nexport type DownloadProgress = {\n progress: number;\n};\n\nexport type LLMSessionEvents = {\n token: (event: TokenEvent) => void;\n partial: (event: PartialEvent) => void;\n streamComplete: (event: StreamCompleteEvent) => void;\n streamError: (event: StreamErrorEvent) => void;\n toolCall: (event: ToolCallEvent) => void;\n};\n\nexport type ExpoLocalLlmModuleEvents = {\n downloadProgress: (event: DownloadProgress) => void;\n availabilityChange: (event: { availability: ModelAvailability }) => void;\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-local-llm",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Expo module for on-device LLM inference via Apple Foundation Models (iOS) and Gemini Nano (Android)",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -48,19 +48,19 @@
|
|
|
48
48
|
"homepage": "https://github.com/GijungKim/expo-local-llm#readme",
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@oxlint/plugins": "1.88.0",
|
|
51
|
-
"@react-native/jest-preset": "0.
|
|
52
|
-
"@types/react": "~19.
|
|
51
|
+
"@react-native/jest-preset": "0.88.0",
|
|
52
|
+
"@types/react": "~19.3.0",
|
|
53
53
|
"@types/react-test-renderer": "19.1.0",
|
|
54
54
|
"eslint": "10.11.0",
|
|
55
|
-
"expo": "~
|
|
55
|
+
"expo": "~58.0.11",
|
|
56
56
|
"expo-module-scripts": "^56.0.2",
|
|
57
|
-
"expo-modules-core": "~
|
|
58
|
-
"jest-expo": "~
|
|
57
|
+
"expo-modules-core": "~58.0.19",
|
|
58
|
+
"jest-expo": "~58.0.11",
|
|
59
59
|
"knip": "6.41.0",
|
|
60
60
|
"oxlint": "1.88.0",
|
|
61
|
-
"react": "19.
|
|
62
|
-
"react-native": "0.
|
|
63
|
-
"react-test-renderer": "19.
|
|
61
|
+
"react": "19.3.0",
|
|
62
|
+
"react-native": "0.88.0",
|
|
63
|
+
"react-test-renderer": "19.3.0",
|
|
64
64
|
"typescript": "6.0.3"
|
|
65
65
|
},
|
|
66
66
|
"jest": {
|
|
@@ -9,7 +9,7 @@ export type ModelAvailability =
|
|
|
9
9
|
| "unknown";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Stable codes carried by
|
|
12
|
+
* Stable codes carried by generation failures and the library's own checks, on both platforms.
|
|
13
13
|
* Branch on these rather than on messages, which come from the OS and change.
|
|
14
14
|
* Read one from any caught error with `getLocalLLMErrorCode`.
|
|
15
15
|
*/
|
|
@@ -32,7 +32,7 @@ export type LocalLLMErrorCode =
|
|
|
32
32
|
| "ERR_DECODING_FAILED"
|
|
33
33
|
/** Generation took longer than the system allows. */
|
|
34
34
|
| "ERR_TIMEOUT"
|
|
35
|
-
/** A tool call failed: the handler threw
|
|
35
|
+
/** A tool call failed: the handler threw or timed out, or the hook had no handler for it. */
|
|
36
36
|
| "ERR_TOOL_FAILED"
|
|
37
37
|
/** Any other generation failure. */
|
|
38
38
|
| "ERR_GENERATION_FAILED"
|