expo-ai-kit 0.5.0 → 0.6.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.
@@ -0,0 +1,190 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Pure helpers for generateObject().
3
+ //
4
+ // This module is deliberately free of any native-module import so its logic can
5
+ // be unit-tested in plain Node. generateObject() (in index.ts) orchestrates the
6
+ // inference call + repair loop on top of these.
7
+ // ---------------------------------------------------------------------------
8
+ /** Build the instruction appended to the system prompt to elicit schema-shaped JSON. */
9
+ export function buildSchemaInstruction(schema) {
10
+ return [
11
+ 'You must respond with a single JSON value that strictly conforms to this JSON Schema:',
12
+ '',
13
+ JSON.stringify(schema),
14
+ '',
15
+ 'Rules:',
16
+ '- Output ONLY the JSON value. No prose, no explanation, no markdown code fences.',
17
+ '- Include every property listed under "required".',
18
+ '- Use the exact property names and value types defined by the schema.',
19
+ ].join('\n');
20
+ }
21
+ /** Follow-up prompt when the model returned something that could not be parsed as JSON. */
22
+ export const REPAIR_INVALID_JSON = 'Your previous response was not valid JSON. Respond again with ONLY a single valid ' +
23
+ 'JSON value that conforms to the schema — no prose, no markdown code fences.';
24
+ /** Follow-up prompt when the model returned valid JSON that violated the schema. */
25
+ export function buildSchemaRepair(errors) {
26
+ const detail = errors.slice(0, 8).join('; ');
27
+ return (`Your previous JSON did not match the schema: ${detail}. ` +
28
+ 'Respond again with ONLY the corrected JSON value — no prose, no markdown code fences.');
29
+ }
30
+ /**
31
+ * Best-effort extraction of a JSON value from model output.
32
+ *
33
+ * On-device models often wrap JSON in a ```json fence or add a sentence of prose
34
+ * around it. We try, in order: a fenced block, the trimmed whole string, then the
35
+ * first balanced {...}/[...] block found anywhere in the text.
36
+ */
37
+ export function extractJson(text) {
38
+ if (!text)
39
+ return { ok: false };
40
+ const candidates = [];
41
+ const fence = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
42
+ if (fence && fence[1])
43
+ candidates.push(fence[1]);
44
+ candidates.push(text);
45
+ for (const raw of candidates) {
46
+ const trimmed = raw.trim();
47
+ const direct = tryParse(trimmed);
48
+ if (direct.ok)
49
+ return direct;
50
+ const sliced = sliceBalanced(trimmed);
51
+ if (sliced != null) {
52
+ const p = tryParse(sliced);
53
+ if (p.ok)
54
+ return p;
55
+ }
56
+ }
57
+ return { ok: false };
58
+ }
59
+ function tryParse(s) {
60
+ if (!s)
61
+ return { ok: false };
62
+ try {
63
+ return { ok: true, value: JSON.parse(s) };
64
+ }
65
+ catch {
66
+ return { ok: false };
67
+ }
68
+ }
69
+ /**
70
+ * Return the first balanced `{...}` or `[...]` substring, or null if none.
71
+ * Tracks string state so braces inside JSON strings are not counted.
72
+ */
73
+ export function sliceBalanced(text) {
74
+ const start = text.search(/[{[]/);
75
+ if (start < 0)
76
+ return null;
77
+ const open = text[start];
78
+ const close = open === '{' ? '}' : ']';
79
+ let depth = 0;
80
+ let inStr = false;
81
+ let escaped = false;
82
+ for (let i = start; i < text.length; i++) {
83
+ const ch = text[i];
84
+ if (inStr) {
85
+ if (escaped)
86
+ escaped = false;
87
+ else if (ch === '\\')
88
+ escaped = true;
89
+ else if (ch === '"')
90
+ inStr = false;
91
+ continue;
92
+ }
93
+ if (ch === '"')
94
+ inStr = true;
95
+ else if (ch === open)
96
+ depth++;
97
+ else if (ch === close) {
98
+ depth--;
99
+ if (depth === 0)
100
+ return text.slice(start, i + 1);
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ /**
106
+ * Validate `value` against the supported subset of `schema`.
107
+ * Returns a list of human-readable error strings (empty ⇒ valid).
108
+ *
109
+ * Intentionally lenient: unknown keywords and extra object properties are
110
+ * allowed. The goal is to catch structural mistakes worth re-prompting over
111
+ * (wrong type, missing required field), not full JSON Schema conformance.
112
+ */
113
+ export function validateAgainstSchema(value, schema) {
114
+ const errors = [];
115
+ validateInto(value, schema, 'value', errors);
116
+ return errors;
117
+ }
118
+ function validateInto(value, schema, path, errors) {
119
+ if (!schema || typeof schema !== 'object')
120
+ return;
121
+ // enum is authoritative — when present, the value must be one of its members.
122
+ if (Array.isArray(schema.enum)) {
123
+ if (!schema.enum.some((e) => jsonEqual(e, value))) {
124
+ errors.push(`${path} must be one of ${JSON.stringify(schema.enum)}`);
125
+ }
126
+ return;
127
+ }
128
+ const types = normalizeTypes(schema);
129
+ if (types && !types.some((t) => matchesType(value, t))) {
130
+ errors.push(`${path} must be of type ${types.join(' | ')}`);
131
+ return; // don't descend into a value of the wrong shape
132
+ }
133
+ if (isPlainObject(value)) {
134
+ if (Array.isArray(schema.required)) {
135
+ for (const key of schema.required) {
136
+ if (!(key in value))
137
+ errors.push(`${path}.${key} is required`);
138
+ }
139
+ }
140
+ if (schema.properties) {
141
+ for (const [key, sub] of Object.entries(schema.properties)) {
142
+ if (key in value)
143
+ validateInto(value[key], sub, `${path}.${key}`, errors);
144
+ }
145
+ }
146
+ }
147
+ if (Array.isArray(value) && schema.items) {
148
+ value.forEach((item, i) => validateInto(item, schema.items, `${path}[${i}]`, errors));
149
+ }
150
+ }
151
+ /** Resolve the declared type(s), inferring object/array from properties/items. */
152
+ function normalizeTypes(schema) {
153
+ const t = schema.type;
154
+ if (typeof t === 'string')
155
+ return [t];
156
+ if (Array.isArray(t))
157
+ return t;
158
+ if (schema.properties)
159
+ return ['object'];
160
+ if (schema.items)
161
+ return ['array'];
162
+ return null;
163
+ }
164
+ function matchesType(value, type) {
165
+ switch (type) {
166
+ case 'object':
167
+ return isPlainObject(value);
168
+ case 'array':
169
+ return Array.isArray(value);
170
+ case 'string':
171
+ return typeof value === 'string';
172
+ case 'number':
173
+ return typeof value === 'number' && Number.isFinite(value);
174
+ case 'integer':
175
+ return typeof value === 'number' && Number.isInteger(value);
176
+ case 'boolean':
177
+ return typeof value === 'boolean';
178
+ case 'null':
179
+ return value === null;
180
+ default:
181
+ return true;
182
+ }
183
+ }
184
+ function isPlainObject(value) {
185
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
186
+ }
187
+ function jsonEqual(a, b) {
188
+ return a === b || JSON.stringify(a) === JSON.stringify(b);
189
+ }
190
+ //# sourceMappingURL=structured.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured.js","sourceRoot":"","sources":["../src/structured.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JSONSchema, JSONSchemaType } from './types';\n\n// ---------------------------------------------------------------------------\n// Pure helpers for generateObject().\n//\n// This module is deliberately free of any native-module import so its logic can\n// be unit-tested in plain Node. generateObject() (in index.ts) orchestrates the\n// inference call + repair loop on top of these.\n// ---------------------------------------------------------------------------\n\n/** Build the instruction appended to the system prompt to elicit schema-shaped JSON. */\nexport function buildSchemaInstruction(schema: JSONSchema): string {\n return [\n 'You must respond with a single JSON value that strictly conforms to this JSON Schema:',\n '',\n JSON.stringify(schema),\n '',\n 'Rules:',\n '- Output ONLY the JSON value. No prose, no explanation, no markdown code fences.',\n '- Include every property listed under \"required\".',\n '- Use the exact property names and value types defined by the schema.',\n ].join('\\n');\n}\n\n/** Follow-up prompt when the model returned something that could not be parsed as JSON. */\nexport const REPAIR_INVALID_JSON =\n 'Your previous response was not valid JSON. Respond again with ONLY a single valid ' +\n 'JSON value that conforms to the schema — no prose, no markdown code fences.';\n\n/** Follow-up prompt when the model returned valid JSON that violated the schema. */\nexport function buildSchemaRepair(errors: string[]): string {\n const detail = errors.slice(0, 8).join('; ');\n return (\n `Your previous JSON did not match the schema: ${detail}. ` +\n 'Respond again with ONLY the corrected JSON value — no prose, no markdown code fences.'\n );\n}\n\nexport type ParseResult = { ok: true; value: unknown } | { ok: false };\n\n/**\n * Best-effort extraction of a JSON value from model output.\n *\n * On-device models often wrap JSON in a ```json fence or add a sentence of prose\n * around it. We try, in order: a fenced block, the trimmed whole string, then the\n * first balanced {...}/[...] block found anywhere in the text.\n */\nexport function extractJson(text: string): ParseResult {\n if (!text) return { ok: false };\n\n const candidates: string[] = [];\n const fence = /```(?:json)?\\s*([\\s\\S]*?)```/i.exec(text);\n if (fence && fence[1]) candidates.push(fence[1]);\n candidates.push(text);\n\n for (const raw of candidates) {\n const trimmed = raw.trim();\n const direct = tryParse(trimmed);\n if (direct.ok) return direct;\n\n const sliced = sliceBalanced(trimmed);\n if (sliced != null) {\n const p = tryParse(sliced);\n if (p.ok) return p;\n }\n }\n return { ok: false };\n}\n\nfunction tryParse(s: string): ParseResult {\n if (!s) return { ok: false };\n try {\n return { ok: true, value: JSON.parse(s) };\n } catch {\n return { ok: false };\n }\n}\n\n/**\n * Return the first balanced `{...}` or `[...]` substring, or null if none.\n * Tracks string state so braces inside JSON strings are not counted.\n */\nexport function sliceBalanced(text: string): string | null {\n const start = text.search(/[{[]/);\n if (start < 0) return null;\n\n const open = text[start];\n const close = open === '{' ? '}' : ']';\n let depth = 0;\n let inStr = false;\n let escaped = false;\n\n for (let i = start; i < text.length; i++) {\n const ch = text[i];\n if (inStr) {\n if (escaped) escaped = false;\n else if (ch === '\\\\') escaped = true;\n else if (ch === '\"') inStr = false;\n continue;\n }\n if (ch === '\"') inStr = true;\n else if (ch === open) depth++;\n else if (ch === close) {\n depth--;\n if (depth === 0) return text.slice(start, i + 1);\n }\n }\n return null;\n}\n\n/**\n * Validate `value` against the supported subset of `schema`.\n * Returns a list of human-readable error strings (empty ⇒ valid).\n *\n * Intentionally lenient: unknown keywords and extra object properties are\n * allowed. The goal is to catch structural mistakes worth re-prompting over\n * (wrong type, missing required field), not full JSON Schema conformance.\n */\nexport function validateAgainstSchema(value: unknown, schema: JSONSchema): string[] {\n const errors: string[] = [];\n validateInto(value, schema, 'value', errors);\n return errors;\n}\n\nfunction validateInto(\n value: unknown,\n schema: JSONSchema,\n path: string,\n errors: string[]\n): void {\n if (!schema || typeof schema !== 'object') return;\n\n // enum is authoritative — when present, the value must be one of its members.\n if (Array.isArray(schema.enum)) {\n if (!schema.enum.some((e) => jsonEqual(e, value))) {\n errors.push(`${path} must be one of ${JSON.stringify(schema.enum)}`);\n }\n return;\n }\n\n const types = normalizeTypes(schema);\n if (types && !types.some((t) => matchesType(value, t))) {\n errors.push(`${path} must be of type ${types.join(' | ')}`);\n return; // don't descend into a value of the wrong shape\n }\n\n if (isPlainObject(value)) {\n if (Array.isArray(schema.required)) {\n for (const key of schema.required) {\n if (!(key in value)) errors.push(`${path}.${key} is required`);\n }\n }\n if (schema.properties) {\n for (const [key, sub] of Object.entries(schema.properties)) {\n if (key in value) validateInto(value[key], sub, `${path}.${key}`, errors);\n }\n }\n }\n\n if (Array.isArray(value) && schema.items) {\n value.forEach((item, i) => validateInto(item, schema.items as JSONSchema, `${path}[${i}]`, errors));\n }\n}\n\n/** Resolve the declared type(s), inferring object/array from properties/items. */\nfunction normalizeTypes(schema: JSONSchema): JSONSchemaType[] | null {\n const t = schema.type;\n if (typeof t === 'string') return [t];\n if (Array.isArray(t)) return t;\n if (schema.properties) return ['object'];\n if (schema.items) return ['array'];\n return null;\n}\n\nfunction matchesType(value: unknown, type: JSONSchemaType): boolean {\n switch (type) {\n case 'object':\n return isPlainObject(value);\n case 'array':\n return Array.isArray(value);\n case 'string':\n return typeof value === 'string';\n case 'number':\n return typeof value === 'number' && Number.isFinite(value);\n case 'integer':\n return typeof value === 'number' && Number.isInteger(value);\n case 'boolean':\n return typeof value === 'boolean';\n case 'null':\n return value === null;\n default:\n return true;\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction jsonEqual(a: unknown, b: unknown): boolean {\n return a === b || JSON.stringify(a) === JSON.stringify(b);\n}\n"]}
package/build/types.d.ts CHANGED
@@ -129,6 +129,68 @@ export type LLMStreamEvent = {
129
129
  * Callback function for streaming events.
130
130
  */
131
131
  export type LLMStreamCallback = (event: LLMStreamEvent) => void;
132
+ /**
133
+ * The set of JSON Schema primitive `type` values understood by
134
+ * {@link generateObject}'s local validator.
135
+ */
136
+ export type JSONSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';
137
+ /**
138
+ * A JSON Schema describing the shape you want {@link generateObject} to return.
139
+ *
140
+ * A pragmatic subset is enforced locally — `type`, `properties`, `required`,
141
+ * `items`, and `enum` — which covers most extraction shapes. Any other JSON
142
+ * Schema keywords you include (e.g. `description`, `minLength`) are still sent
143
+ * to the model to guide it, but are not validated on-device. Keep schemas small:
144
+ * on-device models follow flat, shallow shapes far more reliably than deeply
145
+ * nested ones.
146
+ */
147
+ export type JSONSchema = {
148
+ /** Expected JSON type (or a union of types). */
149
+ type?: JSONSchemaType | JSONSchemaType[];
150
+ /** Human-readable hint passed through to the model. */
151
+ description?: string;
152
+ /** For `object` schemas: the schema of each named property. */
153
+ properties?: Record<string, JSONSchema>;
154
+ /** For `object` schemas: property names that must be present. */
155
+ required?: string[];
156
+ /** For `array` schemas: the schema each element must satisfy. */
157
+ items?: JSONSchema;
158
+ /** Restrict the value to this set of literals. */
159
+ enum?: ReadonlyArray<string | number | boolean | null>;
160
+ /** Other JSON Schema keywords are accepted and forwarded to the model. */
161
+ [key: string]: unknown;
162
+ };
163
+ /**
164
+ * Options for {@link generateObject}.
165
+ */
166
+ export type GenerateObjectOptions = {
167
+ /**
168
+ * System prompt used when the messages array has no system message. Defaults
169
+ * to a structured-output-oriented instruction. If a system message is present
170
+ * in the array, this is ignored (the schema instruction is appended to it).
171
+ */
172
+ systemPrompt?: string;
173
+ /**
174
+ * Abort the request. Behaves like {@link LLMSendOptions.signal} — the returned
175
+ * promise rejects with an INFERENCE_CANCELLED {@link ModelError}.
176
+ */
177
+ signal?: AbortSignal;
178
+ /**
179
+ * How many times to re-prompt the model when its output is not valid JSON or
180
+ * does not match the schema. Each repair feeds the error back to the model.
181
+ * Defaults to 2 (i.e. up to 3 generations total).
182
+ */
183
+ maxRepairAttempts?: number;
184
+ };
185
+ /**
186
+ * Result of {@link generateObject}.
187
+ */
188
+ export type GenerateObjectResult<T = unknown> = {
189
+ /** The parsed value, validated against the schema. */
190
+ object: T;
191
+ /** The raw model output that produced `object` (useful for debugging). */
192
+ text: string;
193
+ };
132
194
  /**
133
195
  * A built-in model provided by the OS (e.g. Apple Foundation Models, ML Kit).
134
196
  * These are always available on supported devices -- no download needed.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA+NA;;GAEG;AACH,MAAM,OAAO,UAAW,SAAQ,KAAK;IACnC,IAAI,CAAiB;IACrB,OAAO,CAAS;IAEhB,YAAY,IAAoB,EAAE,OAAe,EAAE,OAAgB;QACjE,KAAK,CAAC,OAAO,IAAI,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF","sourcesContent":["/**\n * Hardware backend for on-device model inference.\n *\n * - 'auto': Try GPU first, fall back to CPU (default)\n * - 'gpu': Force GPU — faster (~40-50 tok/s) but needs more memory\n * - 'cpu': Force CPU — slower (~2-5 tok/s) but works on low-RAM devices\n */\nexport type InferenceBackend = 'auto' | 'gpu' | 'cpu';\n\n/**\n * Sampling / generation parameters applied to a model session.\n *\n * Support is per-backend (on-device runtimes expose different knobs), so these\n * are best-effort — unsupported fields are ignored rather than erroring:\n *\n * | field | Gemma (LiteRT-LM) | Apple Foundation Models | ML Kit |\n * |-------------|:-----------------:|:-----------------------:|:------:|\n * | temperature | ✓ | ✓ | — |\n * | topK | ✓ | — | — |\n * | topP | ✓ | — | — |\n * | seed | ✓ (iOS only) | — | — |\n * | maxTokens | — | ✓ (max output) | — |\n *\n * Notes:\n * - Gemma/LiteRT-LM has no per-generation output-token cap (its `maxNumTokens`\n * is the total KV-cache size, set at load), so `maxTokens` is not honored\n * there. Its sampler (topK/topP/temperature[/seed]) is fixed at conversation\n * creation, which is why generation config lives on setModel() rather than\n * per-call. `seed` is currently wired on iOS only.\n * - The ML Kit built-in (Android default) does not yet apply generation config;\n * it uses its own defaults.\n */\nexport type GenerationConfig = {\n /** Sampling temperature. Lower = more deterministic. Typically 0.0–2.0. */\n temperature?: number;\n /** Nucleus sampling: number of top logits to consider. Must be > 0. */\n topK?: number;\n /** Nucleus sampling: cumulative probability threshold in [0, 1]. */\n topP?: number;\n /** RNG seed for reproducible sampling (Gemma only). */\n seed?: number;\n /** Maximum number of output tokens to generate (Apple FM / ML Kit only). */\n maxTokens?: number;\n};\n\n/**\n * Options for setModel.\n */\nexport type SetModelOptions = {\n /** Hardware backend to use for inference. Defaults to 'auto'. */\n backend?: InferenceBackend;\n /**\n * Default sampling parameters for this model session. Applied when the model\n * is activated and used for all subsequent sendMessage/streamMessage calls\n * until the next setModel(). See {@link GenerationConfig} for per-backend support.\n */\n generation?: GenerationConfig;\n};\n\n/**\n * Role in a conversation message.\n */\nexport type LLMRole = 'system' | 'user' | 'assistant';\n\n/**\n * A single message in a conversation.\n */\nexport type LLMMessage = {\n role: LLMRole;\n content: string;\n};\n\n/**\n * Options for sendMessage.\n */\nexport type LLMSendOptions = {\n /**\n * Default system prompt to use if no system message is provided in the messages array.\n * If a system message exists in the array, this is ignored.\n */\n systemPrompt?: string;\n /**\n * Abort the request. When the signal fires, the returned promise rejects with\n * an INFERENCE_CANCELLED {@link ModelError}.\n *\n * Note: on-device, non-streaming generation cannot always be interrupted\n * mid-decode — abort always unblocks the caller, but the model may keep\n * computing in the background until it finishes, during which a new\n * sendMessage/streamMessage will throw INFERENCE_BUSY. To truly interrupt a\n * long generation, prefer streamMessage().stop().\n */\n signal?: AbortSignal;\n};\n\n/**\n * Response from sendMessage.\n */\nexport type LLMResponse = {\n /** The generated response text */\n text: string;\n};\n\n/**\n * Options for streamMessage.\n */\nexport type LLMStreamOptions = {\n /**\n * Default system prompt to use if no system message is provided in the messages array.\n * If a system message exists in the array, this is ignored.\n */\n systemPrompt?: string;\n};\n\n/**\n * Handle returned by streamMessage.\n */\nexport type LLMStreamHandle = {\n /** Resolves with the final text when streaming completes or is stopped. */\n promise: Promise<LLMResponse>;\n /** Stop streaming. Resolves `promise` with the text accumulated so far. */\n stop: () => void;\n};\n\n/**\n * Event payload for streaming tokens.\n */\nexport type LLMStreamEvent = {\n /** Unique identifier for this streaming session */\n sessionId: string;\n /** The token/chunk of text received */\n token: string;\n /** Accumulated text so far */\n accumulatedText: string;\n /** Whether this is the final chunk */\n isDone: boolean;\n};\n\n/**\n * Callback function for streaming events.\n */\nexport type LLMStreamCallback = (event: LLMStreamEvent) => void;\n\n\n// ============================================================================\n// Model Types\n// ============================================================================\n\n/**\n * A built-in model provided by the OS (e.g. Apple Foundation Models, ML Kit).\n * These are always available on supported devices -- no download needed.\n */\nexport type BuiltInModel = {\n /** Unique model identifier (e.g. 'apple-fm', 'mlkit') */\n id: string;\n /** Human-readable model name */\n name: string;\n /** Whether this model is available on the current device/OS */\n available: boolean;\n /** Platform this model is associated with */\n platform: 'ios' | 'android';\n /** Maximum context window in tokens */\n contextWindow: number;\n};\n\n/**\n * A downloadable model that the user manages (download, load, delete).\n * These require explicit download before use.\n */\nexport type DownloadableModel = {\n /** Unique model identifier (e.g. 'gemma-e2b', 'gemma-e4b') */\n id: string;\n /** Human-readable model name */\n name: string;\n /** Parameter count label (e.g. '2.3B') */\n parameterCount: string;\n /** Download file size in bytes */\n sizeBytes: number;\n /** Maximum context window in tokens */\n contextWindow: number;\n /** Minimum device RAM in bytes required to run */\n minRamBytes: number;\n /** Whether this device meets the model's minimum RAM requirement */\n meetsRequirements: boolean;\n /** Current lifecycle status */\n status: DownloadableModelStatus;\n};\n\n/**\n * Lifecycle status of a downloadable model.\n *\n * - 'not-downloaded': Model file is not on disk\n * - 'downloading': Model file is being downloaded\n * - 'downloaded': Model file is on disk but not loaded into memory. Call\n * setModel() to load it. This survives app restarts, so use it to decide\n * whether a (re-)download is needed.\n * - 'loading': File is on disk, model is being loaded into memory for inference\n * - 'ready': Model is loaded in memory and ready for inference\n */\nexport type DownloadableModelStatus =\n | 'not-downloaded'\n | 'downloading'\n | 'downloaded'\n | 'loading'\n | 'ready';\n\n/**\n * Error codes for model-related operations.\n */\nexport type ModelErrorCode =\n | 'MODEL_NOT_FOUND'\n | 'MODEL_NOT_DOWNLOADED'\n | 'DOWNLOAD_FAILED'\n | 'DOWNLOAD_CORRUPT'\n | 'DOWNLOAD_STORAGE_FULL'\n | 'DOWNLOAD_CANCELLED'\n | 'INFERENCE_OOM'\n | 'INFERENCE_FAILED'\n | 'INFERENCE_BUSY'\n | 'INFERENCE_CANCELLED'\n | 'MODEL_LOAD_FAILED'\n | 'DEVICE_NOT_SUPPORTED'\n | 'UNKNOWN';\n\n/**\n * Structured error for model operations.\n */\nexport class ModelError extends Error {\n code: ModelErrorCode;\n modelId: string;\n\n constructor(code: ModelErrorCode, modelId: string, message?: string) {\n super(message ?? `${code}: ${modelId}`);\n this.name = 'ModelError';\n this.code = code;\n this.modelId = modelId;\n }\n}\n\n/**\n * Event payload for model download progress.\n */\nexport type ModelDownloadProgressEvent = {\n /** Model being downloaded */\n modelId: string;\n /** Download progress from 0 to 1 */\n progress: number;\n};\n\n/**\n * Event payload for model state changes.\n */\nexport type ModelStateChangeEvent = {\n /** Model whose state changed */\n modelId: string;\n /** New status */\n status: DownloadableModelStatus;\n};\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA6SA;;GAEG;AACH,MAAM,OAAO,UAAW,SAAQ,KAAK;IACnC,IAAI,CAAiB;IACrB,OAAO,CAAS;IAEhB,YAAY,IAAoB,EAAE,OAAe,EAAE,OAAgB;QACjE,KAAK,CAAC,OAAO,IAAI,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF","sourcesContent":["/**\n * Hardware backend for on-device model inference.\n *\n * - 'auto': Try GPU first, fall back to CPU (default)\n * - 'gpu': Force GPU — faster (~40-50 tok/s) but needs more memory\n * - 'cpu': Force CPU — slower (~2-5 tok/s) but works on low-RAM devices\n */\nexport type InferenceBackend = 'auto' | 'gpu' | 'cpu';\n\n/**\n * Sampling / generation parameters applied to a model session.\n *\n * Support is per-backend (on-device runtimes expose different knobs), so these\n * are best-effort — unsupported fields are ignored rather than erroring:\n *\n * | field | Gemma (LiteRT-LM) | Apple Foundation Models | ML Kit |\n * |-------------|:-----------------:|:-----------------------:|:------:|\n * | temperature | ✓ | ✓ | — |\n * | topK | ✓ | — | — |\n * | topP | ✓ | — | — |\n * | seed | ✓ (iOS only) | — | — |\n * | maxTokens | — | ✓ (max output) | — |\n *\n * Notes:\n * - Gemma/LiteRT-LM has no per-generation output-token cap (its `maxNumTokens`\n * is the total KV-cache size, set at load), so `maxTokens` is not honored\n * there. Its sampler (topK/topP/temperature[/seed]) is fixed at conversation\n * creation, which is why generation config lives on setModel() rather than\n * per-call. `seed` is currently wired on iOS only.\n * - The ML Kit built-in (Android default) does not yet apply generation config;\n * it uses its own defaults.\n */\nexport type GenerationConfig = {\n /** Sampling temperature. Lower = more deterministic. Typically 0.0–2.0. */\n temperature?: number;\n /** Nucleus sampling: number of top logits to consider. Must be > 0. */\n topK?: number;\n /** Nucleus sampling: cumulative probability threshold in [0, 1]. */\n topP?: number;\n /** RNG seed for reproducible sampling (Gemma only). */\n seed?: number;\n /** Maximum number of output tokens to generate (Apple FM / ML Kit only). */\n maxTokens?: number;\n};\n\n/**\n * Options for setModel.\n */\nexport type SetModelOptions = {\n /** Hardware backend to use for inference. Defaults to 'auto'. */\n backend?: InferenceBackend;\n /**\n * Default sampling parameters for this model session. Applied when the model\n * is activated and used for all subsequent sendMessage/streamMessage calls\n * until the next setModel(). See {@link GenerationConfig} for per-backend support.\n */\n generation?: GenerationConfig;\n};\n\n/**\n * Role in a conversation message.\n */\nexport type LLMRole = 'system' | 'user' | 'assistant';\n\n/**\n * A single message in a conversation.\n */\nexport type LLMMessage = {\n role: LLMRole;\n content: string;\n};\n\n/**\n * Options for sendMessage.\n */\nexport type LLMSendOptions = {\n /**\n * Default system prompt to use if no system message is provided in the messages array.\n * If a system message exists in the array, this is ignored.\n */\n systemPrompt?: string;\n /**\n * Abort the request. When the signal fires, the returned promise rejects with\n * an INFERENCE_CANCELLED {@link ModelError}.\n *\n * Note: on-device, non-streaming generation cannot always be interrupted\n * mid-decode — abort always unblocks the caller, but the model may keep\n * computing in the background until it finishes, during which a new\n * sendMessage/streamMessage will throw INFERENCE_BUSY. To truly interrupt a\n * long generation, prefer streamMessage().stop().\n */\n signal?: AbortSignal;\n};\n\n/**\n * Response from sendMessage.\n */\nexport type LLMResponse = {\n /** The generated response text */\n text: string;\n};\n\n/**\n * Options for streamMessage.\n */\nexport type LLMStreamOptions = {\n /**\n * Default system prompt to use if no system message is provided in the messages array.\n * If a system message exists in the array, this is ignored.\n */\n systemPrompt?: string;\n};\n\n/**\n * Handle returned by streamMessage.\n */\nexport type LLMStreamHandle = {\n /** Resolves with the final text when streaming completes or is stopped. */\n promise: Promise<LLMResponse>;\n /** Stop streaming. Resolves `promise` with the text accumulated so far. */\n stop: () => void;\n};\n\n/**\n * Event payload for streaming tokens.\n */\nexport type LLMStreamEvent = {\n /** Unique identifier for this streaming session */\n sessionId: string;\n /** The token/chunk of text received */\n token: string;\n /** Accumulated text so far */\n accumulatedText: string;\n /** Whether this is the final chunk */\n isDone: boolean;\n};\n\n/**\n * Callback function for streaming events.\n */\nexport type LLMStreamCallback = (event: LLMStreamEvent) => void;\n\n\n// ============================================================================\n// Structured Output (generateObject)\n// ============================================================================\n\n/**\n * The set of JSON Schema primitive `type` values understood by\n * {@link generateObject}'s local validator.\n */\nexport type JSONSchemaType =\n | 'object'\n | 'array'\n | 'string'\n | 'number'\n | 'integer'\n | 'boolean'\n | 'null';\n\n/**\n * A JSON Schema describing the shape you want {@link generateObject} to return.\n *\n * A pragmatic subset is enforced locally — `type`, `properties`, `required`,\n * `items`, and `enum` — which covers most extraction shapes. Any other JSON\n * Schema keywords you include (e.g. `description`, `minLength`) are still sent\n * to the model to guide it, but are not validated on-device. Keep schemas small:\n * on-device models follow flat, shallow shapes far more reliably than deeply\n * nested ones.\n */\nexport type JSONSchema = {\n /** Expected JSON type (or a union of types). */\n type?: JSONSchemaType | JSONSchemaType[];\n /** Human-readable hint passed through to the model. */\n description?: string;\n /** For `object` schemas: the schema of each named property. */\n properties?: Record<string, JSONSchema>;\n /** For `object` schemas: property names that must be present. */\n required?: string[];\n /** For `array` schemas: the schema each element must satisfy. */\n items?: JSONSchema;\n /** Restrict the value to this set of literals. */\n enum?: ReadonlyArray<string | number | boolean | null>;\n /** Other JSON Schema keywords are accepted and forwarded to the model. */\n [key: string]: unknown;\n};\n\n/**\n * Options for {@link generateObject}.\n */\nexport type GenerateObjectOptions = {\n /**\n * System prompt used when the messages array has no system message. Defaults\n * to a structured-output-oriented instruction. If a system message is present\n * in the array, this is ignored (the schema instruction is appended to it).\n */\n systemPrompt?: string;\n /**\n * Abort the request. Behaves like {@link LLMSendOptions.signal} — the returned\n * promise rejects with an INFERENCE_CANCELLED {@link ModelError}.\n */\n signal?: AbortSignal;\n /**\n * How many times to re-prompt the model when its output is not valid JSON or\n * does not match the schema. Each repair feeds the error back to the model.\n * Defaults to 2 (i.e. up to 3 generations total).\n */\n maxRepairAttempts?: number;\n};\n\n/**\n * Result of {@link generateObject}.\n */\nexport type GenerateObjectResult<T = unknown> = {\n /** The parsed value, validated against the schema. */\n object: T;\n /** The raw model output that produced `object` (useful for debugging). */\n text: string;\n};\n\n\n// ============================================================================\n// Model Types\n// ============================================================================\n\n/**\n * A built-in model provided by the OS (e.g. Apple Foundation Models, ML Kit).\n * These are always available on supported devices -- no download needed.\n */\nexport type BuiltInModel = {\n /** Unique model identifier (e.g. 'apple-fm', 'mlkit') */\n id: string;\n /** Human-readable model name */\n name: string;\n /** Whether this model is available on the current device/OS */\n available: boolean;\n /** Platform this model is associated with */\n platform: 'ios' | 'android';\n /** Maximum context window in tokens */\n contextWindow: number;\n};\n\n/**\n * A downloadable model that the user manages (download, load, delete).\n * These require explicit download before use.\n */\nexport type DownloadableModel = {\n /** Unique model identifier (e.g. 'gemma-e2b', 'gemma-e4b') */\n id: string;\n /** Human-readable model name */\n name: string;\n /** Parameter count label (e.g. '2.3B') */\n parameterCount: string;\n /** Download file size in bytes */\n sizeBytes: number;\n /** Maximum context window in tokens */\n contextWindow: number;\n /** Minimum device RAM in bytes required to run */\n minRamBytes: number;\n /** Whether this device meets the model's minimum RAM requirement */\n meetsRequirements: boolean;\n /** Current lifecycle status */\n status: DownloadableModelStatus;\n};\n\n/**\n * Lifecycle status of a downloadable model.\n *\n * - 'not-downloaded': Model file is not on disk\n * - 'downloading': Model file is being downloaded\n * - 'downloaded': Model file is on disk but not loaded into memory. Call\n * setModel() to load it. This survives app restarts, so use it to decide\n * whether a (re-)download is needed.\n * - 'loading': File is on disk, model is being loaded into memory for inference\n * - 'ready': Model is loaded in memory and ready for inference\n */\nexport type DownloadableModelStatus =\n | 'not-downloaded'\n | 'downloading'\n | 'downloaded'\n | 'loading'\n | 'ready';\n\n/**\n * Error codes for model-related operations.\n */\nexport type ModelErrorCode =\n | 'MODEL_NOT_FOUND'\n | 'MODEL_NOT_DOWNLOADED'\n | 'DOWNLOAD_FAILED'\n | 'DOWNLOAD_CORRUPT'\n | 'DOWNLOAD_STORAGE_FULL'\n | 'DOWNLOAD_CANCELLED'\n | 'INFERENCE_OOM'\n | 'INFERENCE_FAILED'\n | 'INFERENCE_BUSY'\n | 'INFERENCE_CANCELLED'\n | 'MODEL_LOAD_FAILED'\n | 'DEVICE_NOT_SUPPORTED'\n | 'UNKNOWN';\n\n/**\n * Structured error for model operations.\n */\nexport class ModelError extends Error {\n code: ModelErrorCode;\n modelId: string;\n\n constructor(code: ModelErrorCode, modelId: string, message?: string) {\n super(message ?? `${code}: ${modelId}`);\n this.name = 'ModelError';\n this.code = code;\n this.modelId = modelId;\n }\n}\n\n/**\n * Event payload for model download progress.\n */\nexport type ModelDownloadProgressEvent = {\n /** Model being downloaded */\n modelId: string;\n /** Download progress from 0 to 1 */\n progress: number;\n};\n\n/**\n * Event payload for model state changes.\n */\nexport type ModelStateChangeEvent = {\n /** Model whose state changed */\n modelId: string;\n /** New status */\n status: DownloadableModelStatus;\n};\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-ai-kit",
3
- "version": "0.5.0",
4
- "description": "On-device AI for Expo apps — run Gemma 4, Apple Foundation Models, and ML Kit locally with zero API keys",
3
+ "version": "0.6.0",
4
+ "description": "On-device AI for Expo — run Gemma 4, Apple Foundation Models & ML Kit locally: streaming, structured output, zero API keys",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
7
7
  "files": [
@@ -13,6 +13,7 @@
13
13
  "android/build.gradle",
14
14
  "scripts/install-litertlm.sh",
15
15
  "src",
16
+ "!src/__tests__",
16
17
  "expo-module.config.json"
17
18
  ],
18
19
  "scripts": {
@@ -43,6 +44,8 @@
43
44
  "apple-foundation-models",
44
45
  "ml-kit",
45
46
  "local-inference",
47
+ "structured-output",
48
+ "json-schema",
46
49
  "expo-ai-kit",
47
50
  "ExpoAiKit"
48
51
  ],
package/src/index.ts CHANGED
@@ -14,7 +14,17 @@ import {
14
14
  ModelError,
15
15
  ModelErrorCode,
16
16
  SetModelOptions,
17
+ JSONSchema,
18
+ GenerateObjectOptions,
19
+ GenerateObjectResult,
17
20
  } from './types';
21
+ import {
22
+ buildSchemaInstruction,
23
+ buildSchemaRepair,
24
+ extractJson,
25
+ validateAgainstSchema,
26
+ REPAIR_INVALID_JSON,
27
+ } from './structured';
18
28
  import { MODEL_REGISTRY, getRegistryEntry } from './models';
19
29
 
20
30
  export * from './types';
@@ -23,6 +33,9 @@ export * from './models';
23
33
  const DEFAULT_SYSTEM_PROMPT =
24
34
  'You are a helpful, friendly assistant. Answer the user directly and concisely.';
25
35
 
36
+ const DEFAULT_OBJECT_SYSTEM_PROMPT =
37
+ 'You output structured data as JSON. Follow the provided JSON Schema exactly.';
38
+
26
39
  let streamIdCounter = 0;
27
40
  function generateSessionId(): string {
28
41
  return `gen_${Date.now()}_${++streamIdCounter}`;
@@ -382,6 +395,121 @@ export function streamMessage(
382
395
  return { promise, stop };
383
396
  }
384
397
 
398
+ /**
399
+ * Generate a typed object instead of free text.
400
+ *
401
+ * You describe the shape you want with a JSON Schema. expo-ai-kit appends a
402
+ * strict instruction to the system prompt, runs the on-device model, extracts
403
+ * the JSON from its output (tolerating prose and ```json fences), validates it
404
+ * against the schema, and — on a parse error or schema mismatch — feeds the
405
+ * error back and re-prompts up to `maxRepairAttempts` times.
406
+ *
407
+ * Works on every backend (Apple Foundation Models, ML Kit, Gemma) because it is
408
+ * orchestrated over {@link sendMessage}: it honors the same single-flight guard,
409
+ * `AbortSignal`, and `systemPrompt` semantics. Keep schemas small and shallow —
410
+ * on-device models follow flat shapes far more reliably than deeply nested ones.
411
+ *
412
+ * @param messages - The conversation, same shape as {@link sendMessage}.
413
+ * @param schema - A JSON Schema describing the desired result.
414
+ * @param options - Optional settings (systemPrompt, signal, maxRepairAttempts).
415
+ * @returns `{ object, text }` — the validated value and the raw output.
416
+ * @throws {ModelError} INFERENCE_FAILED if no schema-valid JSON is produced
417
+ * after the repair attempts. Also propagates INFERENCE_BUSY / INFERENCE_CANCELLED
418
+ * from the underlying generation.
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * type Recipe = { title: string; minutes: number; ingredients: string[] };
423
+ *
424
+ * const { object } = await generateObject<Recipe>(
425
+ * [{ role: 'user', content: 'A quick weeknight pasta.' }],
426
+ * {
427
+ * type: 'object',
428
+ * properties: {
429
+ * title: { type: 'string' },
430
+ * minutes: { type: 'integer' },
431
+ * ingredients: { type: 'array', items: { type: 'string' } },
432
+ * },
433
+ * required: ['title', 'minutes', 'ingredients'],
434
+ * },
435
+ * );
436
+ * object.title; // typed Recipe
437
+ * ```
438
+ */
439
+ export async function generateObject<T = unknown>(
440
+ messages: LLMMessage[],
441
+ schema: JSONSchema,
442
+ options?: GenerateObjectOptions
443
+ ): Promise<GenerateObjectResult<T>> {
444
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
445
+ throw new ModelError(
446
+ 'DEVICE_NOT_SUPPORTED',
447
+ '',
448
+ 'generateObject is only available on iOS and Android'
449
+ );
450
+ }
451
+ if (!messages || messages.length === 0) {
452
+ throw new Error('messages array cannot be empty');
453
+ }
454
+ if (!schema || typeof schema !== 'object') {
455
+ throw new Error('schema must be a JSON Schema object');
456
+ }
457
+
458
+ const maxRepairAttempts = Math.max(0, options?.maxRepairAttempts ?? 2);
459
+ const instruction = buildSchemaInstruction(schema);
460
+
461
+ // Inject the schema instruction. If the caller supplied a system message we
462
+ // append to it (sendMessage reads system from the array); otherwise we carry
463
+ // the instruction via the systemPrompt option, which sendMessage applies when
464
+ // the array has no system message — including on the repair turns we append.
465
+ const sysIdx = messages.findIndex((m) => m.role === 'system');
466
+ let working: LLMMessage[];
467
+ let systemPrompt: string | undefined;
468
+ if (sysIdx >= 0) {
469
+ working = messages.map((m, i) =>
470
+ i === sysIdx ? { role: m.role, content: `${m.content}\n\n${instruction}` } : m
471
+ );
472
+ systemPrompt = undefined; // the array carries the system message
473
+ } else {
474
+ working = [...messages];
475
+ systemPrompt = `${options?.systemPrompt ?? DEFAULT_OBJECT_SYSTEM_PROMPT}\n\n${instruction}`;
476
+ }
477
+
478
+ let lastText = '';
479
+ for (let attempt = 0; attempt <= maxRepairAttempts; attempt++) {
480
+ const { text } = await sendMessage(working, { systemPrompt, signal: options?.signal });
481
+ lastText = text;
482
+
483
+ const parsed = extractJson(text);
484
+ if (parsed.ok) {
485
+ const errors = validateAgainstSchema(parsed.value, schema);
486
+ if (errors.length === 0) {
487
+ return { object: parsed.value as T, text };
488
+ }
489
+ if (attempt < maxRepairAttempts) {
490
+ working = [
491
+ ...working,
492
+ { role: 'assistant', content: text },
493
+ { role: 'user', content: buildSchemaRepair(errors) },
494
+ ];
495
+ }
496
+ } else if (attempt < maxRepairAttempts) {
497
+ working = [
498
+ ...working,
499
+ { role: 'assistant', content: text },
500
+ { role: 'user', content: REPAIR_INVALID_JSON },
501
+ ];
502
+ }
503
+ }
504
+
505
+ throw new ModelError(
506
+ 'INFERENCE_FAILED',
507
+ getActiveModel(),
508
+ `generateObject: model did not return schema-valid JSON after ${maxRepairAttempts + 1} attempt(s). ` +
509
+ `Last output: ${lastText.slice(0, 200)}`
510
+ );
511
+ }
512
+
385
513
  // ============================================================================
386
514
  // Model Management API
387
515
  // ============================================================================