expo-local-llm 0.3.1 → 0.4.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 +6 -5
- package/build/ExpoLocalLlmModule.js +1 -1
- package/build/ExpoLocalLlmModule.js.map +1 -1
- package/build/useLocalLLM.d.ts.map +1 -1
- package/build/useLocalLLM.js +19 -16
- package/build/useLocalLLM.js.map +1 -1
- package/build/validateSchema.js.map +1 -1
- package/ios/ExpoLocalLlm.podspec +1 -1
- package/package.json +7 -8
- package/src/__tests__/ExpoLocalLlm.test.ts +3 -3
- package/src/__tests__/useLocalLLM.test.ts +2 -2
- package/src/__tests__/validateSchema.test.ts +6 -4
- package/src/useLocalLLM.ts +45 -37
- package/src/validateSchema.ts +1 -1
package/README.md
CHANGED
|
@@ -6,8 +6,9 @@ Expo module for on-device LLM inference. Wraps Apple Foundation Models (iOS 26+)
|
|
|
6
6
|
|
|
7
7
|
| Platform | Requirement |
|
|
8
8
|
|----------|-------------|
|
|
9
|
-
| iOS | iOS 26+ with Apple Intelligence enabled. Loads on iOS 16+ without crashing (returns `notEligible`). Host project must target iOS 16.
|
|
9
|
+
| iOS | iOS 26+ with Apple Intelligence enabled. Loads on iOS 16.4+ without crashing (returns `notEligible`). Host project must target iOS 16.4+. |
|
|
10
10
|
| Android | Device with Gemini Nano support (Pixel 8+, Galaxy S25+). Model may require download. |
|
|
11
|
+
| Expo SDK | 52+ (peer requirement). Tested against SDK 56 and SDK 57 (React Native 0.86). |
|
|
11
12
|
|
|
12
13
|
## Installation
|
|
13
14
|
|
|
@@ -15,7 +16,7 @@ Expo module for on-device LLM inference. Wraps Apple Foundation Models (iOS 26+)
|
|
|
15
16
|
npm install expo-local-llm
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
This module requires **iOS 16.
|
|
19
|
+
This module requires **iOS 16.4+** as a compile target. Expo SDK 56+ defaults to 16.4 (the same as the module's floor), but if you're on an older SDK or have customized the deployment target, raise it via [`expo-build-properties`](https://docs.expo.dev/versions/latest/sdk/build-properties/):
|
|
19
20
|
|
|
20
21
|
```bash
|
|
21
22
|
npx expo install expo-build-properties
|
|
@@ -29,7 +30,7 @@ In `app.json`, add the plugin with the deployment target:
|
|
|
29
30
|
"plugins": [
|
|
30
31
|
[
|
|
31
32
|
"expo-build-properties",
|
|
32
|
-
{ "ios": { "deploymentTarget": "16.
|
|
33
|
+
{ "ios": { "deploymentTarget": "16.4" } }
|
|
33
34
|
]
|
|
34
35
|
]
|
|
35
36
|
}
|
|
@@ -42,7 +43,7 @@ Then prebuild:
|
|
|
42
43
|
npx expo prebuild --clean
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
>
|
|
46
|
+
> If your project targets an iOS version below 16.4, you'll hit `compiling for iOS X.Y, but module 'ExpoLocalLlm' has a minimum deployment target of iOS 16.4` at build time. (Apple Intelligence itself still requires iOS 26+ at runtime — the 16.4 floor is just for compilation; the module returns `notEligible` on iOS 16.4–25.)
|
|
46
47
|
|
|
47
48
|
## Usage
|
|
48
49
|
|
|
@@ -269,7 +270,7 @@ a bare React Native library. Here's why:
|
|
|
269
270
|
If you're on bare React Native without Expo, this module won't work — you'd need
|
|
270
271
|
to add `expo-modules-core` as a dependency or use a different library.
|
|
271
272
|
|
|
272
|
-
## How is this different from React Native AI? (as of
|
|
273
|
+
## How is this different from React Native AI? (as of June 2026)
|
|
273
274
|
|
|
274
275
|
[React Native AI](https://github.com/callstackincubator/ai) (formerly `@callstack/ai`)
|
|
275
276
|
is a Vercel AI SDK-compatible collection of on-device AI primitives, modularized
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoLocalLlmModule.js","sourceRoot":"","sources":["../src/ExpoLocalLlmModule.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"ExpoLocalLlmModule.js","sourceRoot":"","sources":["../src/ExpoLocalLlmModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAUtE,IAAI,YAA2C,CAAC;AAEhD,IAAI,CAAC;IACH,YAAY,GAAG,mBAAmB,CAAyB,cAAc,CAAC,CAAC;AAC7E,CAAC;AAAC,MAAM,CAAC;IACP,YAAY,GAAG,IAAI,CAAC;AACtB,CAAC;AAED,eAAe,YAAY,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo-modules-core\";\n\nimport type { ExpoLocalLlmModuleEvents } from \"./ExpoLocalLlm.types\";\n\ndeclare class ExpoLocalLlmModuleType extends NativeModule<ExpoLocalLlmModuleEvents> {\n getAvailability(): string;\n downloadModel(): Promise<void>;\n LLMSession: any;\n}\n\nlet nativeModule: ExpoLocalLlmModuleType | null;\n\ntry {\n nativeModule = requireNativeModule<ExpoLocalLlmModuleType>(\"ExpoLocalLlm\");\n} catch {\n nativeModule = null;\n}\n\nexport default nativeModule;\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLocalLLM.d.ts","sourceRoot":"","sources":["../src/useLocalLLM.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,aAAa,EAQb,cAAc,EACf,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAoB,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,KAAK,kBAAkB,GAAG,aAAa,CAAC;AAExC,KAAK,iBAAiB,GAAG;IACvB,YAAY,EAAE,iBAAiB,CAAC;IAChC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC,CAAC;AAEF,wBAAgB,WAAW,CACzB,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,
|
|
1
|
+
{"version":3,"file":"useLocalLLM.d.ts","sourceRoot":"","sources":["../src/useLocalLLM.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,aAAa,EAQb,cAAc,EACf,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAoB,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,KAAK,kBAAkB,GAAG,aAAa,CAAC;AAExC,KAAK,iBAAiB,GAAG;IACvB,YAAY,EAAE,iBAAiB,CAAC;IAChC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC,CAAC;AAEF,wBAAgB,WAAW,CACzB,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CAgSnB"}
|
package/build/useLocalLLM.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback, useMemo, useRef } from "react";
|
|
2
2
|
import ExpoLocalLlmModule from "./ExpoLocalLlmModule";
|
|
3
|
-
import { createLLMSession } from "./LLMSession";
|
|
3
|
+
import { createLLMSession, LLMSession } from "./LLMSession";
|
|
4
4
|
export function useLocalLLM(options = {}) {
|
|
5
|
-
const [availability, setAvailability] = useState(
|
|
5
|
+
const [availability, setAvailability] = useState(() => ExpoLocalLlmModule
|
|
6
|
+
? ExpoLocalLlmModule.getAvailability()
|
|
7
|
+
: "unknown");
|
|
6
8
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
7
9
|
const [streamedText, setStreamedText] = useState("");
|
|
8
10
|
const [streamedJSON, setStreamedJSON] = useState("");
|
|
9
11
|
const [streamedObject, setStreamedObject] = useState(null);
|
|
10
12
|
const [downloadProgress, setDownloadProgress] = useState(null);
|
|
11
|
-
const [
|
|
13
|
+
const [runtimeError, setRuntimeError] = useState(null);
|
|
12
14
|
const [activeToolCalls, setActiveToolCalls] = useState([]);
|
|
13
15
|
// Keep a ref to the current tool handlers so the event listener
|
|
14
16
|
// always sees the latest handlers without needing to recreate the session.
|
|
@@ -42,23 +44,24 @@ export function useLocalLLM(options = {}) {
|
|
|
42
44
|
toolsFingerprint,
|
|
43
45
|
]);
|
|
44
46
|
const isJSONMode = options.responseFormat === "json" && !!options.schema;
|
|
45
|
-
const
|
|
47
|
+
const sessionResult = useMemo(() => {
|
|
46
48
|
if (!ExpoLocalLlmModule)
|
|
47
|
-
return null;
|
|
49
|
+
return { session: null, creationError: null };
|
|
48
50
|
try {
|
|
49
|
-
|
|
50
|
-
setError(null);
|
|
51
|
-
return s;
|
|
51
|
+
return { session: createLLMSession(stableConfig), creationError: null };
|
|
52
52
|
}
|
|
53
53
|
catch (e) {
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
return {
|
|
55
|
+
session: null,
|
|
56
|
+
creationError: e.message ?? "Failed to create LLM session",
|
|
57
|
+
};
|
|
56
58
|
}
|
|
57
59
|
}, [stableConfig]);
|
|
60
|
+
const session = sessionResult.session;
|
|
61
|
+
const error = sessionResult.creationError ?? runtimeError;
|
|
58
62
|
useEffect(() => {
|
|
59
63
|
if (!ExpoLocalLlmModule)
|
|
60
64
|
return;
|
|
61
|
-
setAvailability(ExpoLocalLlmModule.getAvailability());
|
|
62
65
|
const subs = [];
|
|
63
66
|
subs.push(ExpoLocalLlmModule.addListener("downloadProgress", (event) => {
|
|
64
67
|
setDownloadProgress(event.progress);
|
|
@@ -95,7 +98,7 @@ export function useLocalLLM(options = {}) {
|
|
|
95
98
|
setIsGenerating(false);
|
|
96
99
|
}));
|
|
97
100
|
subs.push(session.addListener("streamError", (event) => {
|
|
98
|
-
|
|
101
|
+
setRuntimeError(event.error);
|
|
99
102
|
setIsGenerating(false);
|
|
100
103
|
}));
|
|
101
104
|
// Tool call event listener
|
|
@@ -155,14 +158,14 @@ export function useLocalLLM(options = {}) {
|
|
|
155
158
|
const respond = useCallback(async (prompt) => {
|
|
156
159
|
if (!session)
|
|
157
160
|
throw new Error("Session not available");
|
|
158
|
-
|
|
161
|
+
setRuntimeError(null);
|
|
159
162
|
setIsGenerating(true);
|
|
160
163
|
try {
|
|
161
164
|
const result = await session.respond(prompt);
|
|
162
165
|
return result;
|
|
163
166
|
}
|
|
164
167
|
catch (e) {
|
|
165
|
-
|
|
168
|
+
setRuntimeError(e.message);
|
|
166
169
|
throw e;
|
|
167
170
|
}
|
|
168
171
|
finally {
|
|
@@ -172,7 +175,7 @@ export function useLocalLLM(options = {}) {
|
|
|
172
175
|
const streamResponse = useCallback(async (prompt) => {
|
|
173
176
|
if (!session)
|
|
174
177
|
throw new Error("Session not available");
|
|
175
|
-
|
|
178
|
+
setRuntimeError(null);
|
|
176
179
|
setStreamedText("");
|
|
177
180
|
setStreamedJSON("");
|
|
178
181
|
setStreamedObject(null);
|
|
@@ -181,7 +184,7 @@ export function useLocalLLM(options = {}) {
|
|
|
181
184
|
await session.streamResponse(prompt);
|
|
182
185
|
}
|
|
183
186
|
catch (e) {
|
|
184
|
-
|
|
187
|
+
setRuntimeError(e.message);
|
|
185
188
|
setIsGenerating(false);
|
|
186
189
|
throw e;
|
|
187
190
|
}
|
package/build/useLocalLLM.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLocalLLM.js","sourceRoot":"","sources":["../src/useLocalLLM.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAc1E,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAc,MAAM,cAAc,CAAC;AAoB5D,MAAM,UAAU,WAAW,CACzB,UAA8B,EAAE;IAEhC,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GACnC,QAAQ,CAAoB,SAAS,CAAC,CAAC;IACzC,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IACrD,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IACrD,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAAU,IAAI,CAAC,CAAC;IACpE,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAC9E,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IACxD,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAmB,EAAE,CAAC,CAAC;IAE7E,gEAAgE;IAChE,2EAA2E;IAC3E,MAAM,eAAe,GAAG,MAAM,CAC5B,IAAI,GAAG,EAAE,CACV,CAAC;IAEF,2CAA2C;IAC3C,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAqC,CAAC;QAC9D,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAClB,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;gBACjC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;YACxC,CAAC;QACH,CAAC;QACD,eAAe,CAAC,OAAO,GAAG,QAAQ,CAAC;IACrC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAEpB,4DAA4D;IAC5D,sFAAsF;IACtF,MAAM,gBAAgB,GAAG,OAAO,CAC9B,GAAG,EAAE,CACH,OAAO,CAAC,KAAK;QACX,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,WAAW,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC;SACzE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EACpB,CAAC,OAAO,CAAC,KAAK,CAAC,CAChB,CAAC;IAEF,8DAA8D;IAC9D,MAAM,iBAAiB,GAAG,OAAO,CAC/B,GAAG,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAC5D,CAAC,OAAO,CAAC,MAAM,CAAC,CACjB,CAAC;IAEF,MAAM,YAAY,GAAG,OAAO,CAC1B,GAAG,EAAE,CAAC,OAAO,EACb;QACE,OAAO,CAAC,YAAY;QACpB,OAAO,CAAC,OAAO,EAAE,WAAW;QAC5B,OAAO,CAAC,OAAO,EAAE,SAAS;QAC1B,OAAO,CAAC,OAAO,EAAE,IAAI;QACrB,OAAO,CAAC,WAAW;QACnB,OAAO,CAAC,cAAc;QACtB,OAAO,CAAC,qBAAqB;QAC7B,iBAAiB;QACjB,gBAAgB;KACjB,CACF,CAAC;IAEF,MAAM,UAAU,GAAG,OAAO,CAAC,cAAc,KAAK,MAAM,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IAEzE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE;QAC3B,IAAI,CAAC,kBAAkB;YAAE,OAAO,IAAI,CAAC;QACrC,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,gBAAgB,CAAC,YAAY,CAAC,CAAC;YACzC,QAAQ,CAAC,IAAI,CAAC,CAAC;YACf,OAAO,CAAC,CAAC;QACX,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,QAAQ,CAAC,CAAC,CAAC,OAAO,IAAI,8BAA8B,CAAC,CAAC;YACtD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IAEnB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,kBAAkB;YAAE,OAAO;QAEhC,eAAe,CAAC,kBAAkB,CAAC,eAAe,EAAuB,CAAC,CAAC;QAE3E,MAAM,IAAI,GAAyB,EAAE,CAAC;QAEtC,IAAI,CAAC,IAAI,CACP,kBAAkB,CAAC,WAAW,CAC5B,kBAAkB,EAClB,CAAC,KAAuB,EAAE,EAAE;YAC1B,mBAAmB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACtC,CAAC,CACF,CACF,CAAC;QACF,IAAI,CAAC,IAAI,CACP,kBAAkB,CAAC,WAAW,CAAC,oBAAoB,EAAE,CAAC,KAAK,EAAE,EAAE;YAC7D,eAAe,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QACtC,CAAC,CAAC,CACH,CAAC;QAEF,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,IAAI,CACP,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,KAAiB,EAAE,EAAE;gBACjD,eAAe,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YACrC,CAAC,CAAC,CACH,CAAC;YACF,IAAI,CAAC,IAAI,CACP,OAAO,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC,KAAmB,EAAE,EAAE;gBACrD,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC5B,IAAI,CAAC;oBACH,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC5C,CAAC;gBAAC,MAAM,CAAC;oBACP,oEAAoE;gBACtE,CAAC;YACH,CAAC,CAAC,CACH,CAAC;YACF,IAAI,CAAC,IAAI,CACP,OAAO,CAAC,WAAW,CAAC,gBAAgB,EAAE,CAAC,KAA0B,EAAE,EAAE;gBACnE,IAAI,UAAU,EAAE,CAAC;oBACf,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBAC5B,IAAI,CAAC;wBACH,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;oBAC5C,CAAC;oBAAC,MAAM,CAAC;wBACP,sEAAsE;oBACxE,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC9B,CAAC;gBACD,eAAe,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC,CAAC,CACH,CAAC;YACF,IAAI,CAAC,IAAI,CACP,OAAO,CAAC,WAAW,CAAC,aAAa,EAAE,CAAC,KAAuB,EAAE,EAAE;gBAC7D,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBACtB,eAAe,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC,CAAC,CACH,CAAC;YAEF,2BAA2B;YAC3B,IAAI,CAAC,IAAI,CACP,OAAO,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC,KAAoB,EAAE,EAAE;gBACvD,MAAM,UAAU,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACtE,kBAAkB,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;gBAEpD,MAAM,gBAAgB,GAAG,GAAG,EAAE;oBAC5B,kBAAkB,CAAC,CAAC,IAAI,EAAE,EAAE,CAC1B,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC,CAC9C,CAAC;gBACJ,CAAC,CAAC;gBAEF,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBAC5D,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,gBAAgB,EAAE,CAAC;oBACnB,IAAI,CAAC;wBACH,OAAO,CAAC,cAAc,CACpB,KAAK,CAAC,MAAM,EACZ,mCAAmC,KAAK,CAAC,QAAQ,EAAE,CACpD,CAAC;oBACJ,CAAC;oBAAC,MAAM,CAAC;wBACP,yCAAyC;oBAC3C,CAAC;oBACD,OAAO;gBACT,CAAC;gBAED,IAAI,MAAuB,CAAC;gBAC5B,IAAI,CAAC;oBACH,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;gBACpC,CAAC;gBAAC,OAAO,CAAM,EAAE,CAAC;oBAChB,yDAAyD;oBACzD,gBAAgB,EAAE,CAAC;oBACnB,IAAI,CAAC;wBACH,OAAO,CAAC,cAAc,CACpB,KAAK,CAAC,MAAM,EACZ,CAAC,CAAC,OAAO,IAAI,qBAAqB,CACnC,CAAC;oBACJ,CAAC;oBAAC,MAAM,CAAC;wBACP,yCAAyC;oBAC3C,CAAC;oBACD,OAAO;gBACT,CAAC;gBAED,MAAM;qBACH,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;oBACd,gBAAgB,EAAE,CAAC;oBACnB,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;gBAC/C,CAAC,CAAC;qBACD,KAAK,CAAC,CAAC,CAAM,EAAE,EAAE;oBAChB,gBAAgB,EAAE,CAAC;oBACnB,IAAI,CAAC;wBACH,OAAO,CAAC,cAAc,CACpB,KAAK,CAAC,MAAM,EACZ,CAAC,CAAC,OAAO,IAAI,qBAAqB,CACnC,CAAC;oBACJ,CAAC;oBAAC,MAAM,CAAC;wBACP,yCAAyC;oBAC3C,CAAC;gBACH,CAAC,CAAC,CAAC;YACP,CAAC,CAAC,CACH,CAAC;QACJ,CAAC;QAED,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;YAChC,OAAO,EAAE,OAAO,EAAE,CAAC;QACrB,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAEd,MAAM,OAAO,GAAG,WAAW,CACzB,KAAK,EAAE,MAAc,EAAmB,EAAE;QACxC,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;QACvD,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,eAAe,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAC7C,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YACpB,MAAM,CAAC,CAAC;QACV,CAAC;gBAAS,CAAC;YACT,eAAe,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;IACH,CAAC,EACD,CAAC,OAAO,CAAC,CACV,CAAC;IAEF,MAAM,cAAc,GAAG,WAAW,CAChC,KAAK,EAAE,MAAc,EAAiB,EAAE;QACtC,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;QACvD,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,eAAe,CAAC,EAAE,CAAC,CAAC;QACpB,eAAe,CAAC,EAAE,CAAC,CAAC;QACpB,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACxB,eAAe,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACvC,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YACpB,eAAe,CAAC,KAAK,CAAC,CAAC;YACvB,MAAM,CAAC,CAAC;QACV,CAAC;IACH,CAAC,EACD,CAAC,OAAO,CAAC,CACV,CAAC;IAEF,MAAM,YAAY,GAAG,WAAW,CAAC,KAAK,IAAmB,EAAE;QACzD,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;QAC7B,eAAe,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAEd,MAAM,aAAa,GAAG,WAAW,CAAC,KAAK,IAAmB,EAAE;QAC1D,IAAI,CAAC,kBAAkB;YAAE,OAAO;QAChC,MAAM,kBAAkB,CAAC,aAAa,EAAE,CAAC;IAC3C,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,OAAO;YACL,YAAY,EAAE,aAAa;YAC3B,OAAO,EAAE,IAAI;YACb,YAAY,EAAE,KAAK;YACnB,YAAY,EAAE,EAAE;YAChB,YAAY,EAAE,EAAE;YAChB,cAAc,EAAE,IAAI;YACpB,gBAAgB,EAAE,IAAI;YACtB,KAAK,EAAE,IAAI;YACX,eAAe,EAAE,EAAE;SACpB,CAAC;IACJ,CAAC;IAED,OAAO;QACL,YAAY;QACZ,OAAO;QACP,YAAY;QACZ,YAAY;QACZ,YAAY;QACZ,cAAc;QACd,gBAAgB;QAChB,KAAK;QACL,eAAe;QACf,OAAO,EAAE,OAAO,IAAI,YAAY,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;QACtE,cAAc,EACZ,OAAO,IAAI,YAAY,KAAK,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS;QACtE,YAAY,EACV,OAAO,IAAI,YAAY,KAAK,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS;QACpE,aAAa;KACd,CAAC;AACJ,CAAC","sourcesContent":["import { useEffect, useState, useCallback, useMemo, useRef } from \"react\";\n\nimport type {\n ModelAvailability,\n SessionConfig,\n TokenEvent,\n PartialEvent,\n StreamCompleteEvent,\n StreamErrorEvent,\n DownloadProgress,\n ToolDefinition,\n ToolCallEvent,\n ActiveToolCall,\n} from \"./ExpoLocalLlm.types\";\nimport ExpoLocalLlmModule from \"./ExpoLocalLlmModule\";\nimport { createLLMSession, LLMSession } from \"./LLMSession\";\n\ntype UseLocalLLMOptions = SessionConfig;\n\ntype UseLocalLLMResult = {\n availability: ModelAvailability;\n session: LLMSession | null;\n isGenerating: boolean;\n streamedText: string;\n streamedJSON: string;\n streamedObject: unknown;\n downloadProgress: number | null;\n error: string | null;\n activeToolCalls: ActiveToolCall[];\n respond?: (prompt: string) => Promise<string>;\n streamResponse?: (prompt: string) => Promise<void>;\n cancelStream?: () => Promise<void>;\n downloadModel?: () => Promise<void>;\n};\n\nexport function useLocalLLM(\n options: UseLocalLLMOptions = {}\n): UseLocalLLMResult {\n const [availability, setAvailability] =\n useState<ModelAvailability>(\"unknown\");\n const [isGenerating, setIsGenerating] = useState(false);\n const [streamedText, setStreamedText] = useState(\"\");\n const [streamedJSON, setStreamedJSON] = useState(\"\");\n const [streamedObject, setStreamedObject] = useState<unknown>(null);\n const [downloadProgress, setDownloadProgress] = useState<number | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [activeToolCalls, setActiveToolCalls] = useState<ActiveToolCall[]>([]);\n\n // Keep a ref to the current tool handlers so the event listener\n // always sees the latest handlers without needing to recreate the session.\n const toolHandlersRef = useRef<Map<string, ToolDefinition[\"handler\"]>>(\n new Map()\n );\n\n // Update handler ref whenever tools change\n useEffect(() => {\n const handlers = new Map<string, ToolDefinition[\"handler\"]>();\n if (options.tools) {\n for (const tool of options.tools) {\n handlers.set(tool.name, tool.handler);\n }\n }\n toolHandlersRef.current = handlers;\n }, [options.tools]);\n\n // Stabilize options to avoid unnecessary session recreation\n // Include tool names/descriptions so session recreates when tools change structurally\n const toolsFingerprint = useMemo(\n () =>\n options.tools\n ?.map((t) => `${t.name}:${t.description}:${JSON.stringify(t.parameters)}`)\n .join(\"|\") ?? \"\",\n [options.tools]\n );\n\n // Fingerprint the schema so session recreates when it changes\n const schemaFingerprint = useMemo(\n () => (options.schema ? JSON.stringify(options.schema) : \"\"),\n [options.schema]\n );\n\n const stableConfig = useMemo(\n () => options,\n [\n options.instructions,\n options.options?.temperature,\n options.options?.maxTokens,\n options.options?.topK,\n options.toolTimeout,\n options.responseFormat,\n options.includeSchemaInPrompt,\n schemaFingerprint,\n toolsFingerprint,\n ]\n );\n\n const isJSONMode = options.responseFormat === \"json\" && !!options.schema;\n\n const session = useMemo(() => {\n if (!ExpoLocalLlmModule) return null;\n try {\n const s = createLLMSession(stableConfig);\n setError(null);\n return s;\n } catch (e: any) {\n setError(e.message ?? \"Failed to create LLM session\");\n return null;\n }\n }, [stableConfig]);\n\n useEffect(() => {\n if (!ExpoLocalLlmModule) return;\n\n setAvailability(ExpoLocalLlmModule.getAvailability() as ModelAvailability);\n\n const subs: { remove(): void }[] = [];\n\n subs.push(\n ExpoLocalLlmModule.addListener(\n \"downloadProgress\",\n (event: DownloadProgress) => {\n setDownloadProgress(event.progress);\n }\n )\n );\n subs.push(\n ExpoLocalLlmModule.addListener(\"availabilityChange\", (event) => {\n setAvailability(event.availability);\n })\n );\n\n if (session) {\n subs.push(\n session.addListener(\"token\", (event: TokenEvent) => {\n setStreamedText(event.accumulated);\n })\n );\n subs.push(\n session.addListener(\"partial\", (event: PartialEvent) => {\n setStreamedJSON(event.json);\n try {\n setStreamedObject(JSON.parse(event.json));\n } catch {\n // Partial may not be parseable yet; keep the previous parsed value.\n }\n })\n );\n subs.push(\n session.addListener(\"streamComplete\", (event: StreamCompleteEvent) => {\n if (isJSONMode) {\n setStreamedJSON(event.text);\n try {\n setStreamedObject(JSON.parse(event.text));\n } catch {\n // Final JSON should always parse; leave previous value if it doesn't.\n }\n } else {\n setStreamedText(event.text);\n }\n setIsGenerating(false);\n })\n );\n subs.push(\n session.addListener(\"streamError\", (event: StreamErrorEvent) => {\n setError(event.error);\n setIsGenerating(false);\n })\n );\n\n // Tool call event listener\n subs.push(\n session.addListener(\"toolCall\", (event: ToolCallEvent) => {\n const activeCall = { callId: event.callId, toolName: event.toolName };\n setActiveToolCalls((prev) => [...prev, activeCall]);\n\n const removeActiveCall = () => {\n setActiveToolCalls((prev) =>\n prev.filter((c) => c.callId !== event.callId)\n );\n };\n\n const handler = toolHandlersRef.current.get(event.toolName);\n if (!handler) {\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n `No handler registered for tool: ${event.toolName}`\n );\n } catch {\n // Ignore if session is already torn down\n }\n return;\n }\n\n let result: Promise<string>;\n try {\n result = handler(event.arguments);\n } catch (e: any) {\n // Handler threw synchronously before returning a promise\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n e.message || \"Tool handler failed\"\n );\n } catch {\n // Ignore if session is already torn down\n }\n return;\n }\n\n result\n .then((value) => {\n removeActiveCall();\n session.resolveToolCall(event.callId, value);\n })\n .catch((e: any) => {\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n e.message || \"Tool handler failed\"\n );\n } catch {\n // Ignore if session is already torn down\n }\n });\n })\n );\n }\n\n return () => {\n subs.forEach((s) => s.remove());\n session?.release();\n };\n }, [session]);\n\n const respond = useCallback(\n async (prompt: string): Promise<string> => {\n if (!session) throw new Error(\"Session not available\");\n setError(null);\n setIsGenerating(true);\n try {\n const result = await session.respond(prompt);\n return result;\n } catch (e: any) {\n setError(e.message);\n throw e;\n } finally {\n setIsGenerating(false);\n }\n },\n [session]\n );\n\n const streamResponse = useCallback(\n async (prompt: string): Promise<void> => {\n if (!session) throw new Error(\"Session not available\");\n setError(null);\n setStreamedText(\"\");\n setStreamedJSON(\"\");\n setStreamedObject(null);\n setIsGenerating(true);\n try {\n await session.streamResponse(prompt);\n } catch (e: any) {\n setError(e.message);\n setIsGenerating(false);\n throw e;\n }\n },\n [session]\n );\n\n const cancelStream = useCallback(async (): Promise<void> => {\n if (!session) return;\n await session.cancelStream();\n setIsGenerating(false);\n }, [session]);\n\n const downloadModel = useCallback(async (): Promise<void> => {\n if (!ExpoLocalLlmModule) return;\n await ExpoLocalLlmModule.downloadModel();\n }, []);\n\n if (!ExpoLocalLlmModule) {\n return {\n availability: \"notEligible\",\n session: null,\n isGenerating: false,\n streamedText: \"\",\n streamedJSON: \"\",\n streamedObject: null,\n downloadProgress: null,\n error: null,\n activeToolCalls: [],\n };\n }\n\n return {\n availability,\n session,\n isGenerating,\n streamedText,\n streamedJSON,\n streamedObject,\n downloadProgress,\n error,\n activeToolCalls,\n respond: session && availability === \"available\" ? respond : undefined,\n streamResponse:\n session && availability === \"available\" ? streamResponse : undefined,\n cancelStream:\n session && availability === \"available\" ? cancelStream : undefined,\n downloadModel,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"useLocalLLM.js","sourceRoot":"","sources":["../src/useLocalLLM.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useState, useCallback, useMemo, useRef } from \"react\";\n\nimport type {\n ModelAvailability,\n SessionConfig,\n TokenEvent,\n PartialEvent,\n StreamCompleteEvent,\n StreamErrorEvent,\n DownloadProgress,\n ToolDefinition,\n ToolCallEvent,\n ActiveToolCall,\n} from \"./ExpoLocalLlm.types\";\nimport ExpoLocalLlmModule from \"./ExpoLocalLlmModule\";\nimport { createLLMSession, LLMSession } from \"./LLMSession\";\n\ntype UseLocalLLMOptions = SessionConfig;\n\ntype UseLocalLLMResult = {\n availability: ModelAvailability;\n session: LLMSession | null;\n isGenerating: boolean;\n streamedText: string;\n streamedJSON: string;\n streamedObject: unknown;\n downloadProgress: number | null;\n error: string | null;\n activeToolCalls: ActiveToolCall[];\n respond?: (prompt: string) => Promise<string>;\n streamResponse?: (prompt: string) => Promise<void>;\n cancelStream?: () => Promise<void>;\n downloadModel?: () => Promise<void>;\n};\n\nexport function useLocalLLM(\n options: UseLocalLLMOptions = {},\n): UseLocalLLMResult {\n const [availability, setAvailability] = useState<ModelAvailability>(() =>\n ExpoLocalLlmModule\n ? (ExpoLocalLlmModule.getAvailability() as ModelAvailability)\n : \"unknown\",\n );\n const [isGenerating, setIsGenerating] = useState(false);\n const [streamedText, setStreamedText] = useState(\"\");\n const [streamedJSON, setStreamedJSON] = useState(\"\");\n const [streamedObject, setStreamedObject] = useState<unknown>(null);\n const [downloadProgress, setDownloadProgress] = useState<number | null>(null);\n const [runtimeError, setRuntimeError] = useState<string | null>(null);\n const [activeToolCalls, setActiveToolCalls] = useState<ActiveToolCall[]>([]);\n\n // Keep a ref to the current tool handlers so the event listener\n // always sees the latest handlers without needing to recreate the session.\n const toolHandlersRef = useRef<Map<string, ToolDefinition[\"handler\"]>>(\n new Map(),\n );\n\n // Update handler ref whenever tools change\n useEffect(() => {\n const handlers = new Map<string, ToolDefinition[\"handler\"]>();\n if (options.tools) {\n for (const tool of options.tools) {\n handlers.set(tool.name, tool.handler);\n }\n }\n toolHandlersRef.current = handlers;\n }, [options.tools]);\n\n // Stabilize options to avoid unnecessary session recreation\n // Include tool names/descriptions so session recreates when tools change structurally\n const toolsFingerprint = useMemo(\n () =>\n options.tools\n ?.map(\n (t) => `${t.name}:${t.description}:${JSON.stringify(t.parameters)}`,\n )\n .join(\"|\") ?? \"\",\n [options.tools],\n );\n\n // Fingerprint the schema so session recreates when it changes\n const schemaFingerprint = useMemo(\n () => (options.schema ? JSON.stringify(options.schema) : \"\"),\n [options.schema],\n );\n\n const stableConfig = useMemo(\n () => options,\n [\n options.instructions,\n options.options?.temperature,\n options.options?.maxTokens,\n options.options?.topK,\n options.toolTimeout,\n options.responseFormat,\n options.includeSchemaInPrompt,\n schemaFingerprint,\n toolsFingerprint,\n ],\n );\n\n const isJSONMode = options.responseFormat === \"json\" && !!options.schema;\n\n const sessionResult = useMemo<{\n session: LLMSession | null;\n creationError: string | null;\n }>(() => {\n if (!ExpoLocalLlmModule) return { session: null, creationError: null };\n try {\n return { session: createLLMSession(stableConfig), creationError: null };\n } catch (e: any) {\n return {\n session: null,\n creationError: e.message ?? \"Failed to create LLM session\",\n };\n }\n }, [stableConfig]);\n const session = sessionResult.session;\n const error = sessionResult.creationError ?? runtimeError;\n\n useEffect(() => {\n if (!ExpoLocalLlmModule) return;\n\n const subs: { remove(): void }[] = [];\n\n subs.push(\n ExpoLocalLlmModule.addListener(\n \"downloadProgress\",\n (event: DownloadProgress) => {\n setDownloadProgress(event.progress);\n },\n ),\n );\n subs.push(\n ExpoLocalLlmModule.addListener(\"availabilityChange\", (event) => {\n setAvailability(event.availability);\n }),\n );\n\n if (session) {\n subs.push(\n session.addListener(\"token\", (event: TokenEvent) => {\n setStreamedText(event.accumulated);\n }),\n );\n subs.push(\n session.addListener(\"partial\", (event: PartialEvent) => {\n setStreamedJSON(event.json);\n try {\n setStreamedObject(JSON.parse(event.json));\n } catch {\n // Partial may not be parseable yet; keep the previous parsed value.\n }\n }),\n );\n subs.push(\n session.addListener(\"streamComplete\", (event: StreamCompleteEvent) => {\n if (isJSONMode) {\n setStreamedJSON(event.text);\n try {\n setStreamedObject(JSON.parse(event.text));\n } catch {\n // Final JSON should always parse; leave previous value if it doesn't.\n }\n } else {\n setStreamedText(event.text);\n }\n setIsGenerating(false);\n }),\n );\n subs.push(\n session.addListener(\"streamError\", (event: StreamErrorEvent) => {\n setRuntimeError(event.error);\n setIsGenerating(false);\n }),\n );\n\n // Tool call event listener\n subs.push(\n session.addListener(\"toolCall\", (event: ToolCallEvent) => {\n const activeCall = { callId: event.callId, toolName: event.toolName };\n setActiveToolCalls((prev) => [...prev, activeCall]);\n\n const removeActiveCall = () => {\n setActiveToolCalls((prev) =>\n prev.filter((c) => c.callId !== event.callId),\n );\n };\n\n const handler = toolHandlersRef.current.get(event.toolName);\n if (!handler) {\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n `No handler registered for tool: ${event.toolName}`,\n );\n } catch {\n // Ignore if session is already torn down\n }\n return;\n }\n\n let result: Promise<string>;\n try {\n result = handler(event.arguments);\n } catch (e: any) {\n // Handler threw synchronously before returning a promise\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n e.message || \"Tool handler failed\",\n );\n } catch {\n // Ignore if session is already torn down\n }\n return;\n }\n\n result\n .then((value) => {\n removeActiveCall();\n session.resolveToolCall(event.callId, value);\n })\n .catch((e: any) => {\n removeActiveCall();\n try {\n session.rejectToolCall(\n event.callId,\n e.message || \"Tool handler failed\",\n );\n } catch {\n // Ignore if session is already torn down\n }\n });\n }),\n );\n }\n\n return () => {\n subs.forEach((s) => s.remove());\n session?.release();\n };\n }, [session]);\n\n const respond = useCallback(\n async (prompt: string): Promise<string> => {\n if (!session) throw new Error(\"Session not available\");\n setRuntimeError(null);\n setIsGenerating(true);\n try {\n const result = await session.respond(prompt);\n return result;\n } catch (e: any) {\n setRuntimeError(e.message);\n throw e;\n } finally {\n setIsGenerating(false);\n }\n },\n [session],\n );\n\n const streamResponse = useCallback(\n async (prompt: string): Promise<void> => {\n if (!session) throw new Error(\"Session not available\");\n setRuntimeError(null);\n setStreamedText(\"\");\n setStreamedJSON(\"\");\n setStreamedObject(null);\n setIsGenerating(true);\n try {\n await session.streamResponse(prompt);\n } catch (e: any) {\n setRuntimeError(e.message);\n setIsGenerating(false);\n throw e;\n }\n },\n [session],\n );\n\n const cancelStream = useCallback(async (): Promise<void> => {\n if (!session) return;\n await session.cancelStream();\n setIsGenerating(false);\n }, [session]);\n\n const downloadModel = useCallback(async (): Promise<void> => {\n if (!ExpoLocalLlmModule) return;\n await ExpoLocalLlmModule.downloadModel();\n }, []);\n\n if (!ExpoLocalLlmModule) {\n return {\n availability: \"notEligible\",\n session: null,\n isGenerating: false,\n streamedText: \"\",\n streamedJSON: \"\",\n streamedObject: null,\n downloadProgress: null,\n error: null,\n activeToolCalls: [],\n };\n }\n\n return {\n availability,\n session,\n isGenerating,\n streamedText,\n streamedJSON,\n streamedObject,\n downloadProgress,\n error,\n activeToolCalls,\n respond: session && availability === \"available\" ? respond : undefined,\n streamResponse:\n session && availability === \"available\" ? streamResponse : undefined,\n cancelStream:\n session && availability === \"available\" ? cancelStream : undefined,\n downloadModel,\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateSchema.js","sourceRoot":"","sources":["../src/validateSchema.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Schema } from \"./ExpoLocalLlm.types\";\n\nexport type SchemaValidationError = {\n path: string;\n message: string;\n};\n\nexport type SchemaValidationResult =\n | { ok: true }\n | { ok: false; errors: SchemaValidationError[] };\n\nexport class SchemaInvalidError extends Error {\n readonly errors: SchemaValidationError[];\n constructor(errors: SchemaValidationError[]) {\n const summary = errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n super(`Invalid schema:\\n${summary}`);\n this.name = \"SchemaInvalidError\";\n this.errors = errors;\n }\n}\n\nconst ALLOWED_TYPES = new Set([\n \"string\",\n \"number\",\n \"integer\",\n \"boolean\",\n \"array\",\n \"object\",\n]);\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction validateField(\n field: unknown,\n path: string,\n errors: SchemaValidationError[]
|
|
1
|
+
{"version":3,"file":"validateSchema.js","sourceRoot":"","sources":["../src/validateSchema.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Schema } from \"./ExpoLocalLlm.types\";\n\nexport type SchemaValidationError = {\n path: string;\n message: string;\n};\n\nexport type SchemaValidationResult =\n | { ok: true }\n | { ok: false; errors: SchemaValidationError[] };\n\nexport class SchemaInvalidError extends Error {\n readonly errors: SchemaValidationError[];\n constructor(errors: SchemaValidationError[]) {\n const summary = errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n super(`Invalid schema:\\n${summary}`);\n this.name = \"SchemaInvalidError\";\n this.errors = errors;\n }\n}\n\nconst ALLOWED_TYPES = new Set([\n \"string\",\n \"number\",\n \"integer\",\n \"boolean\",\n \"array\",\n \"object\",\n]);\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction validateField(\n field: unknown,\n path: string,\n errors: SchemaValidationError[],\n): void {\n if (!isPlainObject(field)) {\n errors.push({ path, message: \"field must be an object\" });\n return;\n }\n\n const type = field.type;\n if (typeof type !== \"string\" || !ALLOWED_TYPES.has(type)) {\n errors.push({\n path,\n message: `type must be one of: string, number, integer, boolean, array, object (got: ${JSON.stringify(type)})`,\n });\n return;\n }\n\n if (\"enum\" in field) {\n if (type !== \"string\") {\n errors.push({\n path,\n message: `enum is only valid on type 'string' (got type: '${type}')`,\n });\n } else if (!Array.isArray(field.enum) || field.enum.length === 0) {\n errors.push({\n path,\n message: \"enum must be a non-empty array of strings\",\n });\n } else if (!field.enum.every((v) => typeof v === \"string\")) {\n errors.push({\n path,\n message: \"enum values must all be strings\",\n });\n }\n }\n\n if (type === \"array\") {\n if (!(\"items\" in field)) {\n errors.push({ path, message: \"type 'array' requires 'items'\" });\n } else {\n validateField(field.items, `${path}.items`, errors);\n }\n }\n\n if (type === \"object\") {\n if (!(\"properties\" in field)) {\n errors.push({ path, message: \"type 'object' requires 'properties'\" });\n } else if (!isPlainObject(field.properties)) {\n errors.push({\n path: `${path}.properties`,\n message: \"properties must be an object\",\n });\n } else {\n const entries = Object.entries(field.properties);\n if (entries.length === 0) {\n errors.push({\n path: `${path}.properties`,\n message: \"properties must be non-empty\",\n });\n } else {\n for (const [key, subField] of entries) {\n validateField(subField, `${path}.${key}`, errors);\n }\n }\n }\n }\n}\n\nexport function validateSchema(schema: unknown): SchemaValidationResult {\n const errors: SchemaValidationError[] = [];\n\n if (!isPlainObject(schema)) {\n errors.push({ path: \"$\", message: \"schema must be an object\" });\n return { ok: false, errors };\n }\n\n const entries = Object.entries(schema);\n if (entries.length === 0) {\n errors.push({ path: \"$\", message: \"schema must be a non-empty object\" });\n return { ok: false, errors };\n }\n\n for (const [key, field] of entries) {\n validateField(field, key, errors);\n }\n\n return errors.length === 0 ? { ok: true } : { ok: false, errors };\n}\n\n// Re-export Schema so callers building schemas at runtime have one import surface.\nexport type { Schema };\n"]}
|
package/ios/ExpoLocalLlm.podspec
CHANGED
|
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
|
|
|
10
10
|
s.license = package['license']
|
|
11
11
|
s.author = package['author']
|
|
12
12
|
s.homepage = package['homepage']
|
|
13
|
-
s.platforms = { :ios => '16.
|
|
13
|
+
s.platforms = { :ios => '16.4' }
|
|
14
14
|
s.swift_version = '5.9'
|
|
15
15
|
s.source = { git: '' }
|
|
16
16
|
s.static_framework = true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-local-llm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"clean": "expo-module clean",
|
|
17
17
|
"lint": "expo-module lint",
|
|
18
18
|
"test": "expo-module test",
|
|
19
|
-
"prepare": "expo-module
|
|
19
|
+
"prepare": "chmod +x node_modules/expo-module-scripts/bin/expo-module-* 2>/dev/null || true",
|
|
20
20
|
"prepublishOnly": "expo-module prepublishOnly",
|
|
21
21
|
"expo-module": "expo-module",
|
|
22
22
|
"open:ios": "xed example/ios",
|
|
@@ -42,19 +42,18 @@
|
|
|
42
42
|
"author": "GijungKim <tonykim970@gmail.com>",
|
|
43
43
|
"license": "MIT",
|
|
44
44
|
"homepage": "https://github.com/GijungKim/expo-local-llm#readme",
|
|
45
|
-
"dependencies": {},
|
|
46
45
|
"devDependencies": {
|
|
47
|
-
"@types/react": "~19.
|
|
48
|
-
"expo
|
|
49
|
-
"expo": "^
|
|
50
|
-
"
|
|
46
|
+
"@types/react": "~19.2.14",
|
|
47
|
+
"expo": "^57.0.0",
|
|
48
|
+
"expo-module-scripts": "^56.0.2",
|
|
49
|
+
"jest-expo": "~57.0.1",
|
|
50
|
+
"react-native": "0.86.0"
|
|
51
51
|
},
|
|
52
52
|
"jest": {
|
|
53
53
|
"preset": "jest-expo/node"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"expo": ">=52.0.0",
|
|
57
|
-
"expo-modules-core": ">=2.0.0",
|
|
58
57
|
"react": ">=18.0.0",
|
|
59
58
|
"react-native": ">=0.76.0"
|
|
60
59
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LLMSession, createLLMSession, ExpoLocalLlmModule } from "../index";
|
|
2
|
+
|
|
1
3
|
jest.mock("expo-modules-core", () => ({
|
|
2
4
|
requireNativeModule: jest.fn(() => {
|
|
3
5
|
throw new Error("Native module not available");
|
|
@@ -11,8 +13,6 @@ jest.mock("expo-modules-core", () => ({
|
|
|
11
13
|
},
|
|
12
14
|
}));
|
|
13
15
|
|
|
14
|
-
import { LLMSession, createLLMSession, ExpoLocalLlmModule } from "../index";
|
|
15
|
-
|
|
16
16
|
describe("ExpoLocalLlm", () => {
|
|
17
17
|
describe("exports", () => {
|
|
18
18
|
it("exports LLMSession class", () => {
|
|
@@ -37,7 +37,7 @@ describe("ExpoLocalLlm", () => {
|
|
|
37
37
|
|
|
38
38
|
it("throws with module name in message", () => {
|
|
39
39
|
expect(() => createLLMSession({ instructions: "test" })).toThrow(
|
|
40
|
-
/ExpoLocalLlm
|
|
40
|
+
/ExpoLocalLlm/,
|
|
41
41
|
);
|
|
42
42
|
});
|
|
43
43
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import ExpoLocalLlmModule from "../ExpoLocalLlmModule";
|
|
2
|
+
|
|
1
3
|
jest.mock("expo-modules-core", () => ({
|
|
2
4
|
requireNativeModule: jest.fn(() => {
|
|
3
5
|
throw new Error("Native module not available");
|
|
@@ -15,8 +17,6 @@ jest.mock("react", () => ({
|
|
|
15
17
|
...jest.requireActual("react"),
|
|
16
18
|
}));
|
|
17
19
|
|
|
18
|
-
import ExpoLocalLlmModule from "../ExpoLocalLlmModule";
|
|
19
|
-
|
|
20
20
|
describe("useLocalLLM prerequisites", () => {
|
|
21
21
|
it("native module is null in test environment", () => {
|
|
22
22
|
expect(ExpoLocalLlmModule).toBeNull();
|
|
@@ -3,7 +3,9 @@ import { validateSchema, SchemaInvalidError } from "../validateSchema";
|
|
|
3
3
|
describe("validateSchema", () => {
|
|
4
4
|
describe("root", () => {
|
|
5
5
|
it("ok with a non-empty schema", () => {
|
|
6
|
-
expect(validateSchema({ name: { type: "string" } })).toEqual({
|
|
6
|
+
expect(validateSchema({ name: { type: "string" } })).toEqual({
|
|
7
|
+
ok: true,
|
|
8
|
+
});
|
|
7
9
|
});
|
|
8
10
|
|
|
9
11
|
it("rejects empty schema", () => {
|
|
@@ -65,7 +67,7 @@ describe("validateSchema", () => {
|
|
|
65
67
|
expect(
|
|
66
68
|
validateSchema({
|
|
67
69
|
difficulty: { type: "string", enum: ["easy", "medium", "hard"] },
|
|
68
|
-
})
|
|
70
|
+
}),
|
|
69
71
|
).toEqual({ ok: true });
|
|
70
72
|
});
|
|
71
73
|
|
|
@@ -128,7 +130,7 @@ describe("validateSchema", () => {
|
|
|
128
130
|
properties: { name: { type: "string" } },
|
|
129
131
|
},
|
|
130
132
|
},
|
|
131
|
-
})
|
|
133
|
+
}),
|
|
132
134
|
).toEqual({ ok: true });
|
|
133
135
|
});
|
|
134
136
|
});
|
|
@@ -197,7 +199,7 @@ describe("validateSchema", () => {
|
|
|
197
199
|
type: "string",
|
|
198
200
|
enum: ["easy", "medium", "hard"],
|
|
199
201
|
},
|
|
200
|
-
})
|
|
202
|
+
}),
|
|
201
203
|
).toEqual({ ok: true });
|
|
202
204
|
});
|
|
203
205
|
});
|
package/src/useLocalLLM.ts
CHANGED
|
@@ -34,22 +34,25 @@ type UseLocalLLMResult = {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
export function useLocalLLM(
|
|
37
|
-
options: UseLocalLLMOptions = {}
|
|
37
|
+
options: UseLocalLLMOptions = {},
|
|
38
38
|
): UseLocalLLMResult {
|
|
39
|
-
const [availability, setAvailability] =
|
|
40
|
-
|
|
39
|
+
const [availability, setAvailability] = useState<ModelAvailability>(() =>
|
|
40
|
+
ExpoLocalLlmModule
|
|
41
|
+
? (ExpoLocalLlmModule.getAvailability() as ModelAvailability)
|
|
42
|
+
: "unknown",
|
|
43
|
+
);
|
|
41
44
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
42
45
|
const [streamedText, setStreamedText] = useState("");
|
|
43
46
|
const [streamedJSON, setStreamedJSON] = useState("");
|
|
44
47
|
const [streamedObject, setStreamedObject] = useState<unknown>(null);
|
|
45
48
|
const [downloadProgress, setDownloadProgress] = useState<number | null>(null);
|
|
46
|
-
const [
|
|
49
|
+
const [runtimeError, setRuntimeError] = useState<string | null>(null);
|
|
47
50
|
const [activeToolCalls, setActiveToolCalls] = useState<ActiveToolCall[]>([]);
|
|
48
51
|
|
|
49
52
|
// Keep a ref to the current tool handlers so the event listener
|
|
50
53
|
// always sees the latest handlers without needing to recreate the session.
|
|
51
54
|
const toolHandlersRef = useRef<Map<string, ToolDefinition["handler"]>>(
|
|
52
|
-
new Map()
|
|
55
|
+
new Map(),
|
|
53
56
|
);
|
|
54
57
|
|
|
55
58
|
// Update handler ref whenever tools change
|
|
@@ -68,15 +71,17 @@ export function useLocalLLM(
|
|
|
68
71
|
const toolsFingerprint = useMemo(
|
|
69
72
|
() =>
|
|
70
73
|
options.tools
|
|
71
|
-
?.map(
|
|
74
|
+
?.map(
|
|
75
|
+
(t) => `${t.name}:${t.description}:${JSON.stringify(t.parameters)}`,
|
|
76
|
+
)
|
|
72
77
|
.join("|") ?? "",
|
|
73
|
-
[options.tools]
|
|
78
|
+
[options.tools],
|
|
74
79
|
);
|
|
75
80
|
|
|
76
81
|
// Fingerprint the schema so session recreates when it changes
|
|
77
82
|
const schemaFingerprint = useMemo(
|
|
78
83
|
() => (options.schema ? JSON.stringify(options.schema) : ""),
|
|
79
|
-
[options.schema]
|
|
84
|
+
[options.schema],
|
|
80
85
|
);
|
|
81
86
|
|
|
82
87
|
const stableConfig = useMemo(
|
|
@@ -91,28 +96,31 @@ export function useLocalLLM(
|
|
|
91
96
|
options.includeSchemaInPrompt,
|
|
92
97
|
schemaFingerprint,
|
|
93
98
|
toolsFingerprint,
|
|
94
|
-
]
|
|
99
|
+
],
|
|
95
100
|
);
|
|
96
101
|
|
|
97
102
|
const isJSONMode = options.responseFormat === "json" && !!options.schema;
|
|
98
103
|
|
|
99
|
-
const
|
|
100
|
-
|
|
104
|
+
const sessionResult = useMemo<{
|
|
105
|
+
session: LLMSession | null;
|
|
106
|
+
creationError: string | null;
|
|
107
|
+
}>(() => {
|
|
108
|
+
if (!ExpoLocalLlmModule) return { session: null, creationError: null };
|
|
101
109
|
try {
|
|
102
|
-
|
|
103
|
-
setError(null);
|
|
104
|
-
return s;
|
|
110
|
+
return { session: createLLMSession(stableConfig), creationError: null };
|
|
105
111
|
} catch (e: any) {
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
return {
|
|
113
|
+
session: null,
|
|
114
|
+
creationError: e.message ?? "Failed to create LLM session",
|
|
115
|
+
};
|
|
108
116
|
}
|
|
109
117
|
}, [stableConfig]);
|
|
118
|
+
const session = sessionResult.session;
|
|
119
|
+
const error = sessionResult.creationError ?? runtimeError;
|
|
110
120
|
|
|
111
121
|
useEffect(() => {
|
|
112
122
|
if (!ExpoLocalLlmModule) return;
|
|
113
123
|
|
|
114
|
-
setAvailability(ExpoLocalLlmModule.getAvailability() as ModelAvailability);
|
|
115
|
-
|
|
116
124
|
const subs: { remove(): void }[] = [];
|
|
117
125
|
|
|
118
126
|
subs.push(
|
|
@@ -120,20 +128,20 @@ export function useLocalLLM(
|
|
|
120
128
|
"downloadProgress",
|
|
121
129
|
(event: DownloadProgress) => {
|
|
122
130
|
setDownloadProgress(event.progress);
|
|
123
|
-
}
|
|
124
|
-
)
|
|
131
|
+
},
|
|
132
|
+
),
|
|
125
133
|
);
|
|
126
134
|
subs.push(
|
|
127
135
|
ExpoLocalLlmModule.addListener("availabilityChange", (event) => {
|
|
128
136
|
setAvailability(event.availability);
|
|
129
|
-
})
|
|
137
|
+
}),
|
|
130
138
|
);
|
|
131
139
|
|
|
132
140
|
if (session) {
|
|
133
141
|
subs.push(
|
|
134
142
|
session.addListener("token", (event: TokenEvent) => {
|
|
135
143
|
setStreamedText(event.accumulated);
|
|
136
|
-
})
|
|
144
|
+
}),
|
|
137
145
|
);
|
|
138
146
|
subs.push(
|
|
139
147
|
session.addListener("partial", (event: PartialEvent) => {
|
|
@@ -143,7 +151,7 @@ export function useLocalLLM(
|
|
|
143
151
|
} catch {
|
|
144
152
|
// Partial may not be parseable yet; keep the previous parsed value.
|
|
145
153
|
}
|
|
146
|
-
})
|
|
154
|
+
}),
|
|
147
155
|
);
|
|
148
156
|
subs.push(
|
|
149
157
|
session.addListener("streamComplete", (event: StreamCompleteEvent) => {
|
|
@@ -158,13 +166,13 @@ export function useLocalLLM(
|
|
|
158
166
|
setStreamedText(event.text);
|
|
159
167
|
}
|
|
160
168
|
setIsGenerating(false);
|
|
161
|
-
})
|
|
169
|
+
}),
|
|
162
170
|
);
|
|
163
171
|
subs.push(
|
|
164
172
|
session.addListener("streamError", (event: StreamErrorEvent) => {
|
|
165
|
-
|
|
173
|
+
setRuntimeError(event.error);
|
|
166
174
|
setIsGenerating(false);
|
|
167
|
-
})
|
|
175
|
+
}),
|
|
168
176
|
);
|
|
169
177
|
|
|
170
178
|
// Tool call event listener
|
|
@@ -175,7 +183,7 @@ export function useLocalLLM(
|
|
|
175
183
|
|
|
176
184
|
const removeActiveCall = () => {
|
|
177
185
|
setActiveToolCalls((prev) =>
|
|
178
|
-
prev.filter((c) => c.callId !== event.callId)
|
|
186
|
+
prev.filter((c) => c.callId !== event.callId),
|
|
179
187
|
);
|
|
180
188
|
};
|
|
181
189
|
|
|
@@ -185,7 +193,7 @@ export function useLocalLLM(
|
|
|
185
193
|
try {
|
|
186
194
|
session.rejectToolCall(
|
|
187
195
|
event.callId,
|
|
188
|
-
`No handler registered for tool: ${event.toolName}
|
|
196
|
+
`No handler registered for tool: ${event.toolName}`,
|
|
189
197
|
);
|
|
190
198
|
} catch {
|
|
191
199
|
// Ignore if session is already torn down
|
|
@@ -202,7 +210,7 @@ export function useLocalLLM(
|
|
|
202
210
|
try {
|
|
203
211
|
session.rejectToolCall(
|
|
204
212
|
event.callId,
|
|
205
|
-
e.message || "Tool handler failed"
|
|
213
|
+
e.message || "Tool handler failed",
|
|
206
214
|
);
|
|
207
215
|
} catch {
|
|
208
216
|
// Ignore if session is already torn down
|
|
@@ -220,13 +228,13 @@ export function useLocalLLM(
|
|
|
220
228
|
try {
|
|
221
229
|
session.rejectToolCall(
|
|
222
230
|
event.callId,
|
|
223
|
-
e.message || "Tool handler failed"
|
|
231
|
+
e.message || "Tool handler failed",
|
|
224
232
|
);
|
|
225
233
|
} catch {
|
|
226
234
|
// Ignore if session is already torn down
|
|
227
235
|
}
|
|
228
236
|
});
|
|
229
|
-
})
|
|
237
|
+
}),
|
|
230
238
|
);
|
|
231
239
|
}
|
|
232
240
|
|
|
@@ -239,25 +247,25 @@ export function useLocalLLM(
|
|
|
239
247
|
const respond = useCallback(
|
|
240
248
|
async (prompt: string): Promise<string> => {
|
|
241
249
|
if (!session) throw new Error("Session not available");
|
|
242
|
-
|
|
250
|
+
setRuntimeError(null);
|
|
243
251
|
setIsGenerating(true);
|
|
244
252
|
try {
|
|
245
253
|
const result = await session.respond(prompt);
|
|
246
254
|
return result;
|
|
247
255
|
} catch (e: any) {
|
|
248
|
-
|
|
256
|
+
setRuntimeError(e.message);
|
|
249
257
|
throw e;
|
|
250
258
|
} finally {
|
|
251
259
|
setIsGenerating(false);
|
|
252
260
|
}
|
|
253
261
|
},
|
|
254
|
-
[session]
|
|
262
|
+
[session],
|
|
255
263
|
);
|
|
256
264
|
|
|
257
265
|
const streamResponse = useCallback(
|
|
258
266
|
async (prompt: string): Promise<void> => {
|
|
259
267
|
if (!session) throw new Error("Session not available");
|
|
260
|
-
|
|
268
|
+
setRuntimeError(null);
|
|
261
269
|
setStreamedText("");
|
|
262
270
|
setStreamedJSON("");
|
|
263
271
|
setStreamedObject(null);
|
|
@@ -265,12 +273,12 @@ export function useLocalLLM(
|
|
|
265
273
|
try {
|
|
266
274
|
await session.streamResponse(prompt);
|
|
267
275
|
} catch (e: any) {
|
|
268
|
-
|
|
276
|
+
setRuntimeError(e.message);
|
|
269
277
|
setIsGenerating(false);
|
|
270
278
|
throw e;
|
|
271
279
|
}
|
|
272
280
|
},
|
|
273
|
-
[session]
|
|
281
|
+
[session],
|
|
274
282
|
);
|
|
275
283
|
|
|
276
284
|
const cancelStream = useCallback(async (): Promise<void> => {
|
package/src/validateSchema.ts
CHANGED
|
@@ -35,7 +35,7 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
|
35
35
|
function validateField(
|
|
36
36
|
field: unknown,
|
|
37
37
|
path: string,
|
|
38
|
-
errors: SchemaValidationError[]
|
|
38
|
+
errors: SchemaValidationError[],
|
|
39
39
|
): void {
|
|
40
40
|
if (!isPlainObject(field)) {
|
|
41
41
|
errors.push({ path, message: "field must be an object" });
|