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.
- package/README.md +35 -5
- package/build/index.d.ts +43 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +99 -0
- package/build/index.js.map +1 -1
- package/build/structured.d.ts +36 -0
- package/build/structured.d.ts.map +1 -0
- package/build/structured.js +190 -0
- package/build/structured.js.map +1 -0
- package/build/types.d.ts +62 -0
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/package.json +5 -2
- package/src/index.ts +128 -0
- package/src/structured.ts +202 -0
- package/src/types.ts +78 -0
|
@@ -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.
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,iEAAiE;IACjE,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B;;;;OAIG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAEtD;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,sCAAsC;IACtC,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;AAOhE;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,yDAAyD;IACzD,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,SAAS,EAAE,OAAO,CAAC;IACnB,6CAA6C;IAC7C,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAC;IAC5B,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,cAAc,EAAE,MAAM,CAAC;IACvB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,iBAAiB,EAAE,OAAO,CAAC;IAC3B,+BAA+B;IAC/B,MAAM,EAAE,uBAAuB,CAAC;CACjC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,uBAAuB,GAC/B,gBAAgB,GAChB,aAAa,GACb,YAAY,GACZ,SAAS,GACT,OAAO,CAAC;AAEZ;;GAEG;AACH,MAAM,MAAM,cAAc,GACtB,iBAAiB,GACjB,sBAAsB,GACtB,iBAAiB,GACjB,kBAAkB,GAClB,uBAAuB,GACvB,oBAAoB,GACpB,eAAe,GACf,kBAAkB,GAClB,gBAAgB,GAChB,qBAAqB,GACrB,mBAAmB,GACnB,sBAAsB,GACtB,SAAS,CAAC;AAEd;;GAEG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;gBAEJ,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;CAMpE;AAED;;GAEG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB;IACjB,MAAM,EAAE,uBAAuB,CAAC;CACjC,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,iEAAiE;IACjE,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B;;;;OAIG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAEtD;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,sCAAsC;IACtC,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;AAOhE;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,SAAS,GACT,MAAM,CAAC;AAEX;;;;;;;;;GASG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,gDAAgD;IAChD,IAAI,CAAC,EAAE,cAAc,GAAG,cAAc,EAAE,CAAC;IACzC,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,iEAAiE;IACjE,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,kDAAkD;IAClD,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;IACvD,0EAA0E;IAC1E,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,oBAAoB,CAAC,CAAC,GAAG,OAAO,IAAI;IAC9C,sDAAsD;IACtD,MAAM,EAAE,CAAC,CAAC;IACV,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAOF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,yDAAyD;IACzD,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,SAAS,EAAE,OAAO,CAAC;IACnB,6CAA6C;IAC7C,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAC;IAC5B,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,cAAc,EAAE,MAAM,CAAC;IACvB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,iBAAiB,EAAE,OAAO,CAAC;IAC3B,+BAA+B;IAC/B,MAAM,EAAE,uBAAuB,CAAC;CACjC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,uBAAuB,GAC/B,gBAAgB,GAChB,aAAa,GACb,YAAY,GACZ,SAAS,GACT,OAAO,CAAC;AAEZ;;GAEG;AACH,MAAM,MAAM,cAAc,GACtB,iBAAiB,GACjB,sBAAsB,GACtB,iBAAiB,GACjB,kBAAkB,GAClB,uBAAuB,GACvB,oBAAoB,GACpB,eAAe,GACf,kBAAkB,GAClB,gBAAgB,GAChB,qBAAqB,GACrB,mBAAmB,GACnB,sBAAsB,GACtB,SAAS,CAAC;AAEd;;GAEG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;gBAEJ,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;CAMpE;AAED;;GAEG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB;IACjB,MAAM,EAAE,uBAAuB,CAAC;CACjC,CAAC"}
|
package/build/types.js.map
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "On-device AI for Expo
|
|
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
|
// ============================================================================
|