expo-local-llm 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,19 +94,71 @@ function WeatherChat() {
94
94
 
95
95
  ### Structured Output
96
96
 
97
+ iOS 26+ uses Apple's constrained decoding (`DynamicGenerationSchema`) — output is guaranteed to be
98
+ a structurally-valid JSON object matching the schema. Android falls back to instruction-based
99
+ guidance.
100
+
101
+ <p align="center">
102
+ <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" />
103
+ </p>
104
+
105
+ *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`.*
106
+
97
107
  ```tsx
98
108
  const { respond } = useLocalLLM({
99
109
  responseFormat: 'json',
100
110
  schema: {
101
111
  name: { type: 'string', description: 'Recipe name' },
102
- ingredients: { type: 'array', description: 'List of ingredients' },
103
- steps: { type: 'array', description: 'Cooking steps' },
112
+ ingredients: {
113
+ type: 'array',
114
+ items: { type: 'string' },
115
+ description: 'List of ingredients',
116
+ },
117
+ steps: {
118
+ type: 'array',
119
+ items: { type: 'string' },
120
+ description: 'Cooking steps',
121
+ },
122
+ difficulty: {
123
+ type: 'string',
124
+ enum: ['easy', 'medium', 'hard'],
125
+ },
104
126
  },
105
127
  });
106
128
 
107
129
  const recipe = JSON.parse(await respond('Give me a pasta recipe'));
108
130
  ```
109
131
 
132
+ Streaming with a schema yields progressively-filled snapshots via `streamedJSON` (the current
133
+ partial as a JSON string) and `streamedObject` (the parsed value):
134
+
135
+ ```tsx
136
+ const { streamResponse, streamedObject } = useLocalLLM({
137
+ responseFormat: 'json',
138
+ schema: { /* ... */ },
139
+ });
140
+
141
+ await streamResponse('Give me a pasta recipe');
142
+ // streamedObject updates as the model fills in fields
143
+ ```
144
+
145
+ #### Schema validation
146
+
147
+ `createLLMSession` validates the schema before crossing to native and throws `SchemaInvalidError`
148
+ with per-field paths if anything is malformed (missing `items`, `enum` on the wrong type, unknown
149
+ `type`, etc.). You can also call `validateSchema()` directly when building schemas at runtime:
150
+
151
+ ```ts
152
+ import { validateSchema } from 'expo-local-llm';
153
+
154
+ const result = validateSchema(mySchema);
155
+ if (!result.ok) {
156
+ for (const err of result.errors) {
157
+ console.warn(`${err.path}: ${err.message}`);
158
+ }
159
+ }
160
+ ```
161
+
110
162
  ## API
111
163
 
112
164
  ### `useLocalLLM(options?)`
@@ -122,7 +174,8 @@ const recipe = JSON.parse(await respond('Give me a pasta recipe'));
122
174
  | `tools` | `ToolDefinition[]` | Tools the model can invoke (iOS 26+ only) |
123
175
  | `toolTimeout` | `number` | Seconds before an unresolved tool call times out (default 30) |
124
176
  | `responseFormat` | `"text" \| "json"` | Set to `"json"` for structured JSON output |
125
- | `schema` | `Record<string, any>` | JSON schema for structured output (used with `responseFormat: "json"`) |
177
+ | `schema` | `Schema` | Schema for structured output (used with `responseFormat: "json"`) — see Structured Output section |
178
+ | `includeSchemaInPrompt` | `boolean` | iOS 26+: include the schema definition in the prompt. Default `true`. Set `false` when you've front-loaded few-shot examples. |
126
179
 
127
180
  #### Returns
128
181
 
@@ -130,7 +183,9 @@ const recipe = JSON.parse(await respond('Give me a pasta recipe'));
130
183
  |----------|------|-------------|
131
184
  | `availability` | `ModelAvailability` | Current model status |
132
185
  | `isGenerating` | `boolean` | Whether a generation is in progress |
133
- | `streamedText` | `string` | Accumulated text from current stream |
186
+ | `streamedText` | `string` | Accumulated text from current text stream |
187
+ | `streamedJSON` | `string` | Current partial JSON from a schema-driven stream |
188
+ | `streamedObject` | `unknown` | Parsed value of `streamedJSON` (or `null` before any partial) |
134
189
  | `downloadProgress` | `number \| null` | Download progress 0-1 (Android only) |
135
190
  | `error` | `string \| null` | Last error message |
136
191
  | `activeToolCalls` | `ActiveToolCall[]` | In-flight tool calls (`{ callId, toolName }`) |
@@ -161,7 +216,8 @@ const recipe = JSON.parse(await respond('Give me a pasta recipe'));
161
216
  - **iOS**: Apple's Foundation Model may refuse certain categories of prompts (e.g. personal health data interpretation) due to built-in safety guardrails.
162
217
  - Methods (`respond`, `streamResponse`, `cancelStream`) are only defined when `availability === 'available'`.
163
218
  - Tool calling is iOS 26+ only. Android will throw at session creation if tools are passed.
164
- - Structured output uses instruction-based JSON guidance, not Apple's constrained decoding.
219
+ - Structured output on Android uses instruction-based JSON guidance. iOS 26+ uses constrained
220
+ decoding via `DynamicGenerationSchema`.
165
221
 
166
222
  ## Why an Expo module?
167
223
 
@@ -201,7 +257,7 @@ Llama/GGUF, MLC) with Vercel AI SDK integration, generative UI, and DevTools.
201
257
  | **Model management** | None — the OS handles it | You configure/download models |
202
258
  | **Bundle size impact** | Near zero | Depends on backend + model weights |
203
259
  | **Tool calling** | Yes (iOS 26+) | Yes |
204
- | **Structured output** | Yes (instruction-based) | Yes |
260
+ | **Structured output** | Yes — constrained decoding on iOS 26+ | Yes |
205
261
  | **Expo native module** | Yes — `expo install` and go | Bare React Native setup |
206
262
  | **Dependencies** | None | Vercel AI SDK, Jotai, backend runtimes |
207
263
 
@@ -213,6 +269,13 @@ to configure, no weights to bundle, and it works on both iOS and Android.
213
269
  Phi-3, Mistral), want Vercel AI SDK compatibility, or need the generative UI
214
270
  and DevTools ecosystem.
215
271
 
272
+ ## Built with expo-local-llm
273
+
274
+ - [PulseID](https://apps.apple.com/us/app/pulseid-heart-rate-camera/id6754331991) — Heart rate camera
275
+ - [Teamfit Tactics](https://apps.apple.com/us/app/teamfit-tactics-tft-fitness/id6757195493) — TFT fitness
276
+
277
+ *Using this library? Open a PR to add your app.*
278
+
216
279
  ## License
217
280
 
218
281
  MIT
@@ -31,13 +31,31 @@ export type ActiveToolCall = {
31
31
  toolName: string;
32
32
  };
33
33
  export type ResponseFormat = "text" | "json";
34
+ export type SchemaField = {
35
+ type: "string";
36
+ description?: string;
37
+ enum?: string[];
38
+ } | {
39
+ type: "number" | "integer" | "boolean";
40
+ description?: string;
41
+ } | {
42
+ type: "array";
43
+ description?: string;
44
+ items: SchemaField;
45
+ } | {
46
+ type: "object";
47
+ description?: string;
48
+ properties: Record<string, SchemaField>;
49
+ };
50
+ export type Schema = Record<string, SchemaField>;
34
51
  export type SessionConfig = {
35
52
  instructions?: string;
36
53
  options?: GenerationOptions;
37
54
  tools?: ToolDefinition[];
38
55
  toolTimeout?: number;
39
56
  responseFormat?: ResponseFormat;
40
- schema?: Record<string, any>;
57
+ schema?: Schema;
58
+ includeSchemaInPrompt?: boolean;
41
59
  };
42
60
  export type TokenEvent = {
43
61
  token: string;
@@ -46,6 +64,10 @@ export type TokenEvent = {
46
64
  export type StreamCompleteEvent = {
47
65
  text: string;
48
66
  };
67
+ export type PartialEvent = {
68
+ json: string;
69
+ complete: boolean;
70
+ };
49
71
  export type StreamErrorEvent = {
50
72
  error: string;
51
73
  };
@@ -54,6 +76,7 @@ export type DownloadProgress = {
54
76
  };
55
77
  export type LLMSessionEvents = {
56
78
  token: (event: TokenEvent) => void;
79
+ partial: (event: PartialEvent) => void;
57
80
  streamComplete: (event: StreamCompleteEvent) => void;
58
81
  streamError: (event: StreamErrorEvent) => void;
59
82
  toolCall: (event: ToolCallEvent) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoLocalLlm.types.d.ts","sourceRoot":"","sources":["../src/ExpoLocalLlm.types.ts"],"names":[],"mappings":"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"}
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 | \"notEnabled\"\n | \"notReady\"\n | \"notEligible\"\n | \"downloadRequired\"\n | \"downloading\"\n | \"unknown\";\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 enum?: string[];\n};\n\nexport type ToolDefinition = {\n name: string;\n description: string;\n parameters: Record<string, ToolParameter>;\n handler: (args: Record<string, any>) => Promise<string>;\n};\n\nexport type ToolConfig = {\n name: string;\n description: string;\n parameters: Record<string, ToolParameter>;\n};\n\nexport type ToolCallEvent = {\n callId: string;\n toolName: string;\n arguments: Record<string, any>;\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 SessionConfig = {\n instructions?: string;\n options?: GenerationOptions;\n tools?: ToolDefinition[];\n toolTimeout?: number;\n responseFormat?: ResponseFormat;\n schema?: Record<string, any>;\n};\n\nexport type TokenEvent = {\n token: string;\n accumulated: string;\n};\n\nexport type StreamCompleteEvent = {\n text: string;\n};\n\nexport type StreamErrorEvent = {\n error: string;\n};\n\nexport type DownloadProgress = {\n progress: number;\n};\n\nexport type LLMSessionEvents = {\n token: (event: TokenEvent) => 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"]}
1
+ {"version":3,"file":"ExpoLocalLlm.types.js","sourceRoot":"","sources":["../src/ExpoLocalLlm.types.ts"],"names":[],"mappings":"","sourcesContent":["export type ModelAvailability =\n | \"available\"\n | \"notEnabled\"\n | \"notReady\"\n | \"notEligible\"\n | \"downloadRequired\"\n | \"downloading\"\n | \"unknown\";\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 enum?: string[];\n};\n\nexport type ToolDefinition = {\n name: string;\n description: string;\n parameters: Record<string, ToolParameter>;\n handler: (args: Record<string, any>) => Promise<string>;\n};\n\nexport type ToolConfig = {\n name: string;\n description: string;\n parameters: Record<string, ToolParameter>;\n};\n\nexport type ToolCallEvent = {\n callId: string;\n toolName: string;\n arguments: Record<string, any>;\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?: 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 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 token: string;\n accumulated: string;\n};\n\nexport type StreamCompleteEvent = {\n text: string;\n};\n\nexport type PartialEvent = {\n json: string;\n complete: boolean;\n};\n\nexport type StreamErrorEvent = {\n error: string;\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"]}
@@ -1 +1 @@
1
- {"version":3,"file":"LLMSession.d.ts","sourceRoot":"","sources":["../src/LLMSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,UAAU,EACX,MAAM,sBAAsB,CAAC;AAG9B,qBAAa,UAAW,SAAQ,YAAY,CAAC,gBAAgB,CAAC;IAG5D,OAAO,EAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,cAAc,EAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,YAAY,EAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,YAAY,EAAG,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC5C,cAAc,EAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,eAAe,EAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,cAAc,EAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,aAAkB,GAAG,UAAU,CAWvE"}
1
+ {"version":3,"file":"LLMSession.d.ts","sourceRoot":"","sources":["../src/LLMSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,UAAU,EACX,MAAM,sBAAsB,CAAC;AAI9B,qBAAa,UAAW,SAAQ,YAAY,CAAC,gBAAgB,CAAC;IAG5D,OAAO,EAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,cAAc,EAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,YAAY,EAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,YAAY,EAAG,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC5C,cAAc,EAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,eAAe,EAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,cAAc,EAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,aAAkB,GAAG,UAAU,CAkBvE"}
@@ -1,5 +1,6 @@
1
1
  import { SharedObject, UnavailabilityError } from "expo-modules-core";
2
2
  import ExpoLocalLlmModule from "./ExpoLocalLlmModule";
3
+ import { validateSchema, SchemaInvalidError } from "./validateSchema";
3
4
  export class LLMSession extends SharedObject {
4
5
  // Methods are implemented natively via the Class() DSL.
5
6
  // TypeScript declarations provide type safety only.
@@ -15,6 +16,12 @@ export function createLLMSession(config = {}) {
15
16
  if (!ExpoLocalLlmModule) {
16
17
  throw new UnavailabilityError("ExpoLocalLlm", "createLLMSession");
17
18
  }
19
+ if (config.schema) {
20
+ const result = validateSchema(config.schema);
21
+ if (!result.ok) {
22
+ throw new SchemaInvalidError(result.errors);
23
+ }
24
+ }
18
25
  // Strip handler functions before passing to native — native doesn't need them
19
26
  const nativeConfig = {
20
27
  ...config,
@@ -1 +1 @@
1
- {"version":3,"file":"LLMSession.js","sourceRoot":"","sources":["../src/LLMSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAOtE,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AAEtD,MAAM,OAAO,UAAW,SAAQ,YAA8B;IAC5D,wDAAwD;IACxD,oDAAoD;IACpD,OAAO,CAAuC;IAC9C,cAAc,CAAqC;IACnD,YAAY,CAAuB;IACnC,YAAY,CAAgC;IAC5C,cAAc,CAA0B;IACxC,eAAe,CAA4C;IAC3D,cAAc,CAA2C;CAC1D;AAED,MAAM,UAAU,gBAAgB,CAAC,SAAwB,EAAE;IACzD,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,MAAM,IAAI,mBAAmB,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;IACpE,CAAC;IAED,8EAA8E;IAC9E,MAAM,YAAY,GAAG;QACnB,GAAG,MAAM;QACT,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;KACzD,CAAC;IACF,OAAO,IAAI,kBAAkB,CAAC,UAAU,CAAC,YAAY,CAAe,CAAC;AACvE,CAAC","sourcesContent":["import { SharedObject, UnavailabilityError } from \"expo-modules-core\";\n\nimport type {\n SessionConfig,\n LLMSessionEvents,\n ToolConfig,\n} from \"./ExpoLocalLlm.types\";\nimport ExpoLocalLlmModule from \"./ExpoLocalLlmModule\";\n\nexport class LLMSession extends SharedObject<LLMSessionEvents> {\n // Methods are implemented natively via the Class() DSL.\n // TypeScript declarations provide type safety only.\n respond!: (prompt: string) => Promise<string>;\n streamResponse!: (prompt: string) => Promise<void>;\n cancelStream!: () => Promise<void>;\n registerTool!: (config: ToolConfig) => void;\n unregisterTool!: (name: string) => void;\n resolveToolCall!: (callId: string, result: string) => void;\n rejectToolCall!: (callId: string, error: string) => void;\n}\n\nexport function createLLMSession(config: SessionConfig = {}): LLMSession {\n if (!ExpoLocalLlmModule) {\n throw new UnavailabilityError(\"ExpoLocalLlm\", \"createLLMSession\");\n }\n\n // Strip handler functions before passing to native — native doesn't need them\n const nativeConfig = {\n ...config,\n tools: config.tools?.map(({ handler, ...rest }) => rest),\n };\n return new ExpoLocalLlmModule.LLMSession(nativeConfig) as LLMSession;\n}\n"]}
1
+ {"version":3,"file":"LLMSession.js","sourceRoot":"","sources":["../src/LLMSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAOtE,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,MAAM,OAAO,UAAW,SAAQ,YAA8B;IAC5D,wDAAwD;IACxD,oDAAoD;IACpD,OAAO,CAAuC;IAC9C,cAAc,CAAqC;IACnD,YAAY,CAAuB;IACnC,YAAY,CAAgC;IAC5C,cAAc,CAA0B;IACxC,eAAe,CAA4C;IAC3D,cAAc,CAA2C;CAC1D;AAED,MAAM,UAAU,gBAAgB,CAAC,SAAwB,EAAE;IACzD,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,MAAM,IAAI,mBAAmB,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;IACpE,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,MAAM,IAAI,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,MAAM,YAAY,GAAG;QACnB,GAAG,MAAM;QACT,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;KACzD,CAAC;IACF,OAAO,IAAI,kBAAkB,CAAC,UAAU,CAAC,YAAY,CAAe,CAAC;AACvE,CAAC","sourcesContent":["import { SharedObject, UnavailabilityError } from \"expo-modules-core\";\n\nimport type {\n SessionConfig,\n LLMSessionEvents,\n ToolConfig,\n} from \"./ExpoLocalLlm.types\";\nimport ExpoLocalLlmModule from \"./ExpoLocalLlmModule\";\nimport { validateSchema, SchemaInvalidError } from \"./validateSchema\";\n\nexport class LLMSession extends SharedObject<LLMSessionEvents> {\n // Methods are implemented natively via the Class() DSL.\n // TypeScript declarations provide type safety only.\n respond!: (prompt: string) => Promise<string>;\n streamResponse!: (prompt: string) => Promise<void>;\n cancelStream!: () => Promise<void>;\n registerTool!: (config: ToolConfig) => void;\n unregisterTool!: (name: string) => void;\n resolveToolCall!: (callId: string, result: string) => void;\n rejectToolCall!: (callId: string, error: string) => void;\n}\n\nexport function createLLMSession(config: SessionConfig = {}): LLMSession {\n if (!ExpoLocalLlmModule) {\n throw new UnavailabilityError(\"ExpoLocalLlm\", \"createLLMSession\");\n }\n\n if (config.schema) {\n const result = validateSchema(config.schema);\n if (!result.ok) {\n throw new SchemaInvalidError(result.errors);\n }\n }\n\n // Strip handler functions before passing to native — native doesn't need them\n const nativeConfig = {\n ...config,\n tools: config.tools?.map(({ handler, ...rest }) => rest),\n };\n return new ExpoLocalLlmModule.LLMSession(nativeConfig) as LLMSession;\n}\n"]}
package/build/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { useLocalLLM } from "./useLocalLLM";
2
2
  export { LLMSession, createLLMSession } from "./LLMSession";
3
3
  export { default as ExpoLocalLlmModule } from "./ExpoLocalLlmModule";
4
+ export { validateSchema, SchemaInvalidError, type SchemaValidationError, type SchemaValidationResult, } from "./validateSchema";
4
5
  export * from "./ExpoLocalLlm.types";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrE,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,cAAc,sBAAsB,CAAC"}
package/build/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { useLocalLLM } from "./useLocalLLM";
2
2
  export { LLMSession, createLLMSession } from "./LLMSession";
3
3
  export { default as ExpoLocalLlmModule } from "./ExpoLocalLlmModule";
4
+ export { validateSchema, SchemaInvalidError, } from "./validateSchema";
4
5
  export * from "./ExpoLocalLlm.types";
5
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrE,cAAc,sBAAsB,CAAC","sourcesContent":["export { useLocalLLM } from \"./useLocalLLM\";\nexport { LLMSession, createLLMSession } from \"./LLMSession\";\nexport { default as ExpoLocalLlmModule } from \"./ExpoLocalLlmModule\";\nexport * from \"./ExpoLocalLlm.types\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EACL,cAAc,EACd,kBAAkB,GAGnB,MAAM,kBAAkB,CAAC;AAC1B,cAAc,sBAAsB,CAAC","sourcesContent":["export { useLocalLLM } from \"./useLocalLLM\";\nexport { LLMSession, createLLMSession } from \"./LLMSession\";\nexport { default as ExpoLocalLlmModule } from \"./ExpoLocalLlmModule\";\nexport {\n validateSchema,\n SchemaInvalidError,\n type SchemaValidationError,\n type SchemaValidationResult,\n} from \"./validateSchema\";\nexport * from \"./ExpoLocalLlm.types\";\n"]}
@@ -6,6 +6,8 @@ type UseLocalLLMResult = {
6
6
  session: LLMSession | null;
7
7
  isGenerating: boolean;
8
8
  streamedText: string;
9
+ streamedJSON: string;
10
+ streamedObject: unknown;
9
11
  downloadProgress: number | null;
10
12
  error: string | null;
11
13
  activeToolCalls: ActiveToolCall[];
@@ -1 +1 @@
1
- {"version":3,"file":"useLocalLLM.d.ts","sourceRoot":"","sources":["../src/useLocalLLM.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,aAAa,EAOb,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,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,CA0PnB"}
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,CAwRnB"}
@@ -5,6 +5,8 @@ export function useLocalLLM(options = {}) {
5
5
  const [availability, setAvailability] = useState("unknown");
6
6
  const [isGenerating, setIsGenerating] = useState(false);
7
7
  const [streamedText, setStreamedText] = useState("");
8
+ const [streamedJSON, setStreamedJSON] = useState("");
9
+ const [streamedObject, setStreamedObject] = useState(null);
8
10
  const [downloadProgress, setDownloadProgress] = useState(null);
9
11
  const [error, setError] = useState(null);
10
12
  const [activeToolCalls, setActiveToolCalls] = useState([]);
@@ -35,9 +37,11 @@ export function useLocalLLM(options = {}) {
35
37
  options.options?.topK,
36
38
  options.toolTimeout,
37
39
  options.responseFormat,
40
+ options.includeSchemaInPrompt,
38
41
  schemaFingerprint,
39
42
  toolsFingerprint,
40
43
  ]);
44
+ const isJSONMode = options.responseFormat === "json" && !!options.schema;
41
45
  const session = useMemo(() => {
42
46
  if (!ExpoLocalLlmModule)
43
47
  return null;
@@ -66,8 +70,28 @@ export function useLocalLLM(options = {}) {
66
70
  subs.push(session.addListener("token", (event) => {
67
71
  setStreamedText(event.accumulated);
68
72
  }));
73
+ subs.push(session.addListener("partial", (event) => {
74
+ setStreamedJSON(event.json);
75
+ try {
76
+ setStreamedObject(JSON.parse(event.json));
77
+ }
78
+ catch {
79
+ // Partial may not be parseable yet; keep the previous parsed value.
80
+ }
81
+ }));
69
82
  subs.push(session.addListener("streamComplete", (event) => {
70
- setStreamedText(event.text);
83
+ if (isJSONMode) {
84
+ setStreamedJSON(event.text);
85
+ try {
86
+ setStreamedObject(JSON.parse(event.text));
87
+ }
88
+ catch {
89
+ // Final JSON should always parse; leave previous value if it doesn't.
90
+ }
91
+ }
92
+ else {
93
+ setStreamedText(event.text);
94
+ }
71
95
  setIsGenerating(false);
72
96
  }));
73
97
  subs.push(session.addListener("streamError", (event) => {
@@ -150,6 +174,8 @@ export function useLocalLLM(options = {}) {
150
174
  throw new Error("Session not available");
151
175
  setError(null);
152
176
  setStreamedText("");
177
+ setStreamedJSON("");
178
+ setStreamedObject(null);
153
179
  setIsGenerating(true);
154
180
  try {
155
181
  await session.streamResponse(prompt);
@@ -177,6 +203,8 @@ export function useLocalLLM(options = {}) {
177
203
  session: null,
178
204
  isGenerating: false,
179
205
  streamedText: "",
206
+ streamedJSON: "",
207
+ streamedObject: null,
180
208
  downloadProgress: null,
181
209
  error: null,
182
210
  activeToolCalls: [],
@@ -187,6 +215,8 @@ export function useLocalLLM(options = {}) {
187
215
  session,
188
216
  isGenerating,
189
217
  streamedText,
218
+ streamedJSON,
219
+ streamedObject,
190
220
  downloadProgress,
191
221
  error,
192
222
  activeToolCalls,
@@ -1 +1 @@
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 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 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 [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 schemaFingerprint,\n toolsFingerprint,\n ]\n );\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(\"streamComplete\", (event: StreamCompleteEvent) => {\n setStreamedText(event.text);\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 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 downloadProgress: null,\n error: null,\n activeToolCalls: [],\n };\n }\n\n return {\n availability,\n session,\n isGenerating,\n streamedText,\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] =\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"]}
@@ -0,0 +1,18 @@
1
+ import type { Schema } from "./ExpoLocalLlm.types";
2
+ export type SchemaValidationError = {
3
+ path: string;
4
+ message: string;
5
+ };
6
+ export type SchemaValidationResult = {
7
+ ok: true;
8
+ } | {
9
+ ok: false;
10
+ errors: SchemaValidationError[];
11
+ };
12
+ export declare class SchemaInvalidError extends Error {
13
+ readonly errors: SchemaValidationError[];
14
+ constructor(errors: SchemaValidationError[]);
15
+ }
16
+ export declare function validateSchema(schema: unknown): SchemaValidationResult;
17
+ export type { Schema };
18
+ //# sourceMappingURL=validateSchema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateSchema.d.ts","sourceRoot":"","sources":["../src/validateSchema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,qBAAqB,EAAE,CAAA;CAAE,CAAC;AAEnD,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC;gBAC7B,MAAM,EAAE,qBAAqB,EAAE;CAM5C;AAqFD,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,sBAAsB,CAmBtE;AAGD,YAAY,EAAE,MAAM,EAAE,CAAC"}
@@ -0,0 +1,104 @@
1
+ export class SchemaInvalidError extends Error {
2
+ errors;
3
+ constructor(errors) {
4
+ const summary = errors.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
5
+ super(`Invalid schema:\n${summary}`);
6
+ this.name = "SchemaInvalidError";
7
+ this.errors = errors;
8
+ }
9
+ }
10
+ const ALLOWED_TYPES = new Set([
11
+ "string",
12
+ "number",
13
+ "integer",
14
+ "boolean",
15
+ "array",
16
+ "object",
17
+ ]);
18
+ function isPlainObject(v) {
19
+ return typeof v === "object" && v !== null && !Array.isArray(v);
20
+ }
21
+ function validateField(field, path, errors) {
22
+ if (!isPlainObject(field)) {
23
+ errors.push({ path, message: "field must be an object" });
24
+ return;
25
+ }
26
+ const type = field.type;
27
+ if (typeof type !== "string" || !ALLOWED_TYPES.has(type)) {
28
+ errors.push({
29
+ path,
30
+ message: `type must be one of: string, number, integer, boolean, array, object (got: ${JSON.stringify(type)})`,
31
+ });
32
+ return;
33
+ }
34
+ if ("enum" in field) {
35
+ if (type !== "string") {
36
+ errors.push({
37
+ path,
38
+ message: `enum is only valid on type 'string' (got type: '${type}')`,
39
+ });
40
+ }
41
+ else if (!Array.isArray(field.enum) || field.enum.length === 0) {
42
+ errors.push({
43
+ path,
44
+ message: "enum must be a non-empty array of strings",
45
+ });
46
+ }
47
+ else if (!field.enum.every((v) => typeof v === "string")) {
48
+ errors.push({
49
+ path,
50
+ message: "enum values must all be strings",
51
+ });
52
+ }
53
+ }
54
+ if (type === "array") {
55
+ if (!("items" in field)) {
56
+ errors.push({ path, message: "type 'array' requires 'items'" });
57
+ }
58
+ else {
59
+ validateField(field.items, `${path}.items`, errors);
60
+ }
61
+ }
62
+ if (type === "object") {
63
+ if (!("properties" in field)) {
64
+ errors.push({ path, message: "type 'object' requires 'properties'" });
65
+ }
66
+ else if (!isPlainObject(field.properties)) {
67
+ errors.push({
68
+ path: `${path}.properties`,
69
+ message: "properties must be an object",
70
+ });
71
+ }
72
+ else {
73
+ const entries = Object.entries(field.properties);
74
+ if (entries.length === 0) {
75
+ errors.push({
76
+ path: `${path}.properties`,
77
+ message: "properties must be non-empty",
78
+ });
79
+ }
80
+ else {
81
+ for (const [key, subField] of entries) {
82
+ validateField(subField, `${path}.${key}`, errors);
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ export function validateSchema(schema) {
89
+ const errors = [];
90
+ if (!isPlainObject(schema)) {
91
+ errors.push({ path: "$", message: "schema must be an object" });
92
+ return { ok: false, errors };
93
+ }
94
+ const entries = Object.entries(schema);
95
+ if (entries.length === 0) {
96
+ errors.push({ path: "$", message: "schema must be a non-empty object" });
97
+ return { ok: false, errors };
98
+ }
99
+ for (const [key, field] of entries) {
100
+ validateField(field, key, errors);
101
+ }
102
+ return errors.length === 0 ? { ok: true } : { ok: false, errors };
103
+ }
104
+ //# sourceMappingURL=validateSchema.js.map
@@ -0,0 +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[]\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"]}