@mindees/ai 0.1.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.
Files changed (53) hide show
  1. package/LICENSE +31 -0
  2. package/README.md +57 -0
  3. package/dist/contract.d.ts +113 -0
  4. package/dist/contract.d.ts.map +1 -0
  5. package/dist/contract.js +18 -0
  6. package/dist/contract.js.map +1 -0
  7. package/dist/devtools.d.ts +43 -0
  8. package/dist/devtools.d.ts.map +1 -0
  9. package/dist/devtools.js +77 -0
  10. package/dist/devtools.js.map +1 -0
  11. package/dist/errors.d.ts +21 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +18 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/index.d.ts +26 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +29 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/json.d.ts +76 -0
  20. package/dist/json.d.ts.map +1 -0
  21. package/dist/json.js +256 -0
  22. package/dist/json.js.map +1 -0
  23. package/dist/mappers.d.ts +52 -0
  24. package/dist/mappers.d.ts.map +1 -0
  25. package/dist/mappers.js +312 -0
  26. package/dist/mappers.js.map +1 -0
  27. package/dist/mock.d.ts +26 -0
  28. package/dist/mock.d.ts.map +1 -0
  29. package/dist/mock.js +69 -0
  30. package/dist/mock.js.map +1 -0
  31. package/dist/object.d.ts +78 -0
  32. package/dist/object.d.ts.map +1 -0
  33. package/dist/object.js +140 -0
  34. package/dist/object.js.map +1 -0
  35. package/dist/on-device.d.ts +13 -0
  36. package/dist/on-device.d.ts.map +1 -0
  37. package/dist/on-device.js +33 -0
  38. package/dist/on-device.js.map +1 -0
  39. package/dist/server.d.ts +42 -0
  40. package/dist/server.d.ts.map +1 -0
  41. package/dist/server.js +64 -0
  42. package/dist/server.js.map +1 -0
  43. package/dist/sse.d.ts +24 -0
  44. package/dist/sse.d.ts.map +1 -0
  45. package/dist/sse.js +81 -0
  46. package/dist/sse.js.map +1 -0
  47. package/dist/standard-schema.d.ts +89 -0
  48. package/dist/standard-schema.d.ts.map +1 -0
  49. package/dist/tools.d.ts +61 -0
  50. package/dist/tools.d.ts.map +1 -0
  51. package/dist/tools.js +195 -0
  52. package/dist/tools.js.map +1 -0
  53. package/package.json +40 -0
package/dist/json.js ADDED
@@ -0,0 +1,256 @@
1
+ import { AiError } from "./errors.js";
2
+ //#region src/json.ts
3
+ /**
4
+ * Pure, no-`eval` JSON helpers shared by structured output (`object.ts`) and the
5
+ * tool-calling loop (`tools.ts`): extracting a JSON value from decorated model text,
6
+ * sanitizing untrusted parsed data against prototype-pollution + DoS, validating through a
7
+ * Standard Schema, and formatting validation issues. Model output is untrusted — every
8
+ * function here is fail-closed. See `docs/adr/0019-synapse-structured-output.md`.
9
+ *
10
+ * @module
11
+ */
12
+ /** Keys that must never appear in untrusted parsed objects (prototype-pollution vectors). */
13
+ const FORBIDDEN_KEYS = new Set([
14
+ "__proto__",
15
+ "constructor",
16
+ "prototype"
17
+ ]);
18
+ /**
19
+ * Max characters of untrusted model text handed to `JSON.parse`. A size gate enforced BEFORE
20
+ * parsing (mirrors the SSE backend's `MAX_SSE_BUFFER`) so a hostile payload can't be fully
21
+ * allocated before the post-parse limits in {@link sanitizeJson} would reject it. ~8M chars.
22
+ */
23
+ const DEFAULT_MAX_INPUT_CHARS = 8 * 1024 * 1024;
24
+ /** Generous-but-bounded defaults (structured output can be larger than an SDUI tree). */
25
+ const DEFAULT_SANITIZE_LIMITS = {
26
+ maxDepth: 64,
27
+ maxNodes: 1e5,
28
+ maxStringLength: 1e6,
29
+ maxProps: 1e3
30
+ };
31
+ function isPlainObject(value) {
32
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33
+ }
34
+ function tryParse(text) {
35
+ try {
36
+ return {
37
+ ok: true,
38
+ value: JSON.parse(text)
39
+ };
40
+ } catch {
41
+ return {
42
+ ok: false,
43
+ reason: "not valid JSON"
44
+ };
45
+ }
46
+ }
47
+ /**
48
+ * Find the content of the first fenced code block (```` ```json ```` or bare ```` ``` ````),
49
+ * or `undefined` if there is no closed fence. Locates the fence with `indexOf` only — the
50
+ * inner content is handed to `JSON.parse`, never to a regex or `eval`.
51
+ */
52
+ function fencedBlock(text) {
53
+ const open = text.indexOf("```");
54
+ if (open === -1) return void 0;
55
+ const afterFence = open + 3;
56
+ const newline = text.indexOf("\n", afterFence);
57
+ if (newline === -1) return void 0;
58
+ const close = text.indexOf("```", newline + 1);
59
+ if (close === -1) return void 0;
60
+ return text.slice(newline + 1, close);
61
+ }
62
+ /**
63
+ * Scan for the first balanced `{…}` or `[…]` span, tracking string + escape state so braces
64
+ * inside string literals don't miscount. Returns the substring or `undefined`. Linear, no
65
+ * regex, no `eval`.
66
+ */
67
+ function firstBalancedSpan(text) {
68
+ let start = -1;
69
+ for (let i = 0; i < text.length; i++) {
70
+ const ch = text[i];
71
+ if (ch === "{" || ch === "[") {
72
+ start = i;
73
+ break;
74
+ }
75
+ }
76
+ if (start === -1) return void 0;
77
+ const stack = [];
78
+ let inStr = false;
79
+ let escaped = false;
80
+ for (let i = start; i < text.length; i++) {
81
+ const ch = text[i];
82
+ if (inStr) {
83
+ if (escaped) escaped = false;
84
+ else if (ch === "\\") escaped = true;
85
+ else if (ch === "\"") inStr = false;
86
+ continue;
87
+ }
88
+ if (ch === "\"") inStr = true;
89
+ else if (ch === "{") stack.push("}");
90
+ else if (ch === "[") stack.push("]");
91
+ else if (ch === "}" || ch === "]") {
92
+ stack.pop();
93
+ if (stack.length === 0) return text.slice(start, i + 1);
94
+ }
95
+ }
96
+ }
97
+ /**
98
+ * Extract a JSON value from model text, trying in a fixed, testable order:
99
+ * 1. parse the whole (trimmed) text; 2. parse the first fenced code block; 3. parse the
100
+ * first balanced brace/bracket span. Never uses a capturing regex or `eval`.
101
+ */
102
+ function extractJson(text, maxChars = DEFAULT_MAX_INPUT_CHARS) {
103
+ if (text.length > maxChars) return {
104
+ ok: false,
105
+ reason: `input exceeds ${maxChars} characters`
106
+ };
107
+ const direct = tryParse(text.trim());
108
+ if (direct.ok) return direct;
109
+ const fenced = fencedBlock(text);
110
+ if (fenced !== void 0) {
111
+ const parsed = tryParse(fenced.trim());
112
+ if (parsed.ok) return parsed;
113
+ }
114
+ const span = firstBalancedSpan(text);
115
+ if (span !== void 0) {
116
+ const parsed = tryParse(span);
117
+ if (parsed.ok) return parsed;
118
+ }
119
+ return {
120
+ ok: false,
121
+ reason: "no valid JSON value found in model output"
122
+ };
123
+ }
124
+ /**
125
+ * Best-effort parse of *incomplete* JSON for streaming previews: closes any open string and
126
+ * unbalanced brackets, drops a dangling `,`/`:`, and parses. Returns `undefined` whenever it
127
+ * can't safely produce a value (never throws, never guesses a value token). The result is
128
+ * **unvalidated and unsanitized** — callers must treat it as an untyped preview only.
129
+ */
130
+ function lenientParseJson(text, maxChars = DEFAULT_MAX_INPUT_CHARS) {
131
+ if (text.length > maxChars) return void 0;
132
+ let start = -1;
133
+ for (let i = 0; i < text.length; i++) if (text[i] === "{" || text[i] === "[") {
134
+ start = i;
135
+ break;
136
+ }
137
+ if (start === -1) return void 0;
138
+ const stack = [];
139
+ let inStr = false;
140
+ let escaped = false;
141
+ for (let i = start; i < text.length; i++) {
142
+ const ch = text[i];
143
+ if (inStr) {
144
+ if (escaped) escaped = false;
145
+ else if (ch === "\\") escaped = true;
146
+ else if (ch === "\"") inStr = false;
147
+ continue;
148
+ }
149
+ if (ch === "\"") inStr = true;
150
+ else if (ch === "{") stack.push("}");
151
+ else if (ch === "[") stack.push("]");
152
+ else if (ch === "}" || ch === "]") stack.pop();
153
+ }
154
+ let candidate = text.slice(start);
155
+ if (inStr) candidate += "\"";
156
+ let end = candidate.length;
157
+ while (end > 0) {
158
+ const c = candidate[end - 1];
159
+ if (c === " " || c === "\n" || c === "\r" || c === " ") end--;
160
+ else break;
161
+ }
162
+ if (end > 0 && (candidate[end - 1] === "," || candidate[end - 1] === ":")) end--;
163
+ candidate = candidate.slice(0, end);
164
+ for (let i = stack.length - 1; i >= 0; i--) candidate += stack[i];
165
+ try {
166
+ return JSON.parse(candidate);
167
+ } catch {
168
+ return;
169
+ }
170
+ }
171
+ /**
172
+ * Deep-clone untrusted parsed JSON into a fresh value, throwing `AiError('INVALID_OBJECT')`
173
+ * on any prototype-pollution key (`__proto__`/`constructor`/`prototype`) at any depth or on
174
+ * any limit breach. Run this BEFORE handing a value to a Standard Schema validator — a
175
+ * validator can itself pollute the prototype by touching a poisoned object.
176
+ */
177
+ function sanitizeJson(value, limits = DEFAULT_SANITIZE_LIMITS) {
178
+ let nodes = 0;
179
+ const walk = (v, depth) => {
180
+ if (depth > limits.maxDepth) throw new AiError("INVALID_OBJECT", `value exceeds max depth ${limits.maxDepth}`);
181
+ if (++nodes > limits.maxNodes) throw new AiError("INVALID_OBJECT", `value exceeds max nodes ${limits.maxNodes}`);
182
+ if (v === null) return null;
183
+ const t = typeof v;
184
+ if (t === "string") {
185
+ if (v.length > limits.maxStringLength) throw new AiError("INVALID_OBJECT", `string exceeds max length ${limits.maxStringLength}`);
186
+ return v;
187
+ }
188
+ if (t === "number") {
189
+ if (!Number.isFinite(v)) throw new AiError("INVALID_OBJECT", "numbers must be finite");
190
+ return v;
191
+ }
192
+ if (t === "boolean") return v;
193
+ if (Array.isArray(v)) return v.map((x) => walk(x, depth + 1));
194
+ if (isPlainObject(v)) {
195
+ const keys = Object.keys(v);
196
+ if (keys.length > limits.maxProps) throw new AiError("INVALID_OBJECT", `object exceeds max keys ${limits.maxProps}`);
197
+ const out = {};
198
+ for (const k of keys) {
199
+ if (FORBIDDEN_KEYS.has(k)) throw new AiError("INVALID_OBJECT", `forbidden key "${k}"`);
200
+ out[k] = walk(v[k], depth + 1);
201
+ }
202
+ return out;
203
+ }
204
+ throw new AiError("INVALID_OBJECT", `unsupported value of type ${t}`);
205
+ };
206
+ return walk(value, 0);
207
+ }
208
+ /**
209
+ * Cheap recursive check for any prototype-pollution own key, WITHOUT cloning. Used to skip
210
+ * emitting an unsanitized streaming preview that carries a poison key (so a consumer who
211
+ * naively deep-merges the preview can't be tricked into polluting the prototype). Depth-capped
212
+ * and fail-closed: a value nested past `maxDepth` is treated as forbidden (skip the preview)
213
+ * rather than risking a stack overflow on a deeply-nested untrusted payload.
214
+ */
215
+ function containsForbiddenKey(value, maxDepth = DEFAULT_SANITIZE_LIMITS.maxDepth, depth = 0) {
216
+ if (depth > maxDepth) return true;
217
+ if (Array.isArray(value)) return value.some((v) => containsForbiddenKey(v, maxDepth, depth + 1));
218
+ if (isPlainObject(value)) for (const k of Object.keys(value)) {
219
+ if (FORBIDDEN_KEYS.has(k)) return true;
220
+ if (containsForbiddenKey(value[k], maxDepth, depth + 1)) return true;
221
+ }
222
+ return false;
223
+ }
224
+ /** Render Standard Schema issues into one readable line: `path: message; path2: message2`. */
225
+ function formatIssues(issues) {
226
+ return (Array.isArray(issues) ? issues : []).map((issue) => {
227
+ const path = Array.isArray(issue.path) ? issue.path.map((seg) => String(typeof seg === "object" && seg !== null ? seg.key : seg)).join(".") : void 0;
228
+ return path ? `${path}: ${issue.message}` : issue.message;
229
+ }).join("; ");
230
+ }
231
+ /**
232
+ * Validate a (already-sanitized) value through a Standard Schema, awaiting an async validator.
233
+ * Defensively narrows a malformed validator result (non-array `issues`) into a failure rather
234
+ * than crashing. Discriminates on `issues` truthiness — a valid output may legitimately be
235
+ * `undefined`, so `value` is not a reliable discriminant.
236
+ */
237
+ async function validateStandard(schema, value) {
238
+ const raw = schema["~standard"].validate(value);
239
+ const result = raw instanceof Promise ? await raw : raw;
240
+ if (typeof result !== "object" || result === null) return {
241
+ ok: false,
242
+ issues: []
243
+ };
244
+ if (result.issues) return {
245
+ ok: false,
246
+ issues: Array.isArray(result.issues) ? result.issues : []
247
+ };
248
+ return {
249
+ ok: true,
250
+ value: result.value
251
+ };
252
+ }
253
+ //#endregion
254
+ export { DEFAULT_MAX_INPUT_CHARS, DEFAULT_SANITIZE_LIMITS, containsForbiddenKey, extractJson, formatIssues, lenientParseJson, sanitizeJson, validateStandard };
255
+
256
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.js","names":[],"sources":["../src/json.ts"],"sourcesContent":["/**\n * Pure, no-`eval` JSON helpers shared by structured output (`object.ts`) and the\n * tool-calling loop (`tools.ts`): extracting a JSON value from decorated model text,\n * sanitizing untrusted parsed data against prototype-pollution + DoS, validating through a\n * Standard Schema, and formatting validation issues. Model output is untrusted — every\n * function here is fail-closed. See `docs/adr/0019-synapse-structured-output.md`.\n *\n * @module\n */\n\nimport { AiError } from './errors'\nimport type { StandardSchemaV1 } from './standard-schema'\n\n/** Keys that must never appear in untrusted parsed objects (prototype-pollution vectors). */\nconst FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n/**\n * Max characters of untrusted model text handed to `JSON.parse`. A size gate enforced BEFORE\n * parsing (mirrors the SSE backend's `MAX_SSE_BUFFER`) so a hostile payload can't be fully\n * allocated before the post-parse limits in {@link sanitizeJson} would reject it. ~8M chars.\n */\nexport const DEFAULT_MAX_INPUT_CHARS = 8 * 1024 * 1024\n\n/** Limits that bound a sanitized value so hostile model JSON can't exhaust memory. */\nexport interface SanitizeLimits {\n /** Max nesting depth. */\n readonly maxDepth: number\n /** Max total nodes (objects + arrays + primitives) across the whole value. */\n readonly maxNodes: number\n /** Max length of any single string. */\n readonly maxStringLength: number\n /** Max own keys on any single object. */\n readonly maxProps: number\n}\n\n/** Generous-but-bounded defaults (structured output can be larger than an SDUI tree). */\nexport const DEFAULT_SANITIZE_LIMITS: SanitizeLimits = {\n maxDepth: 64,\n maxNodes: 100_000,\n maxStringLength: 1_000_000,\n maxProps: 1000,\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** The outcome of {@link extractJson}. */\nexport type ExtractResult =\n | { readonly ok: true; readonly value: unknown }\n | {\n readonly ok: false\n readonly reason: string\n }\n\nfunction tryParse(text: string): ExtractResult {\n try {\n return { ok: true, value: JSON.parse(text) }\n } catch {\n return { ok: false, reason: 'not valid JSON' }\n }\n}\n\n/**\n * Find the content of the first fenced code block (```` ```json ```` or bare ```` ``` ````),\n * or `undefined` if there is no closed fence. Locates the fence with `indexOf` only — the\n * inner content is handed to `JSON.parse`, never to a regex or `eval`.\n */\nfunction fencedBlock(text: string): string | undefined {\n const open = text.indexOf('```')\n if (open === -1) return undefined\n // Skip the opening fence and an optional language tag up to the end of that line.\n const afterFence = open + 3\n const newline = text.indexOf('\\n', afterFence)\n if (newline === -1) return undefined\n const close = text.indexOf('```', newline + 1)\n if (close === -1) return undefined\n return text.slice(newline + 1, close)\n}\n\n/**\n * Scan for the first balanced `{…}` or `[…]` span, tracking string + escape state so braces\n * inside string literals don't miscount. Returns the substring or `undefined`. Linear, no\n * regex, no `eval`.\n */\nfunction firstBalancedSpan(text: string): string | undefined {\n let start = -1\n for (let i = 0; i < text.length; i++) {\n const ch = text[i]\n if (ch === '{' || ch === '[') {\n start = i\n break\n }\n }\n if (start === -1) return undefined\n const stack: string[] = []\n let inStr = false\n let escaped = false\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 === '{') stack.push('}')\n else if (ch === '[') stack.push(']')\n else if (ch === '}' || ch === ']') {\n stack.pop()\n if (stack.length === 0) return text.slice(start, i + 1)\n }\n }\n return undefined\n}\n\n/**\n * Extract a JSON value from model text, trying in a fixed, testable order:\n * 1. parse the whole (trimmed) text; 2. parse the first fenced code block; 3. parse the\n * first balanced brace/bracket span. Never uses a capturing regex or `eval`.\n */\nexport function extractJson(\n text: string,\n maxChars: number = DEFAULT_MAX_INPUT_CHARS,\n): ExtractResult {\n // Size gate BEFORE any JSON.parse so a hostile payload isn't fully allocated first.\n if (text.length > maxChars) {\n return { ok: false, reason: `input exceeds ${maxChars} characters` }\n }\n const direct = tryParse(text.trim())\n if (direct.ok) return direct\n\n const fenced = fencedBlock(text)\n if (fenced !== undefined) {\n const parsed = tryParse(fenced.trim())\n if (parsed.ok) return parsed\n }\n\n const span = firstBalancedSpan(text)\n if (span !== undefined) {\n const parsed = tryParse(span)\n if (parsed.ok) return parsed\n }\n\n return { ok: false, reason: 'no valid JSON value found in model output' }\n}\n\n/**\n * Best-effort parse of *incomplete* JSON for streaming previews: closes any open string and\n * unbalanced brackets, drops a dangling `,`/`:`, and parses. Returns `undefined` whenever it\n * can't safely produce a value (never throws, never guesses a value token). The result is\n * **unvalidated and unsanitized** — callers must treat it as an untyped preview only.\n */\nexport function lenientParseJson(\n text: string,\n maxChars: number = DEFAULT_MAX_INPUT_CHARS,\n): unknown {\n if (text.length > maxChars) return undefined\n let start = -1\n for (let i = 0; i < text.length; i++) {\n if (text[i] === '{' || text[i] === '[') {\n start = i\n break\n }\n }\n if (start === -1) return undefined\n\n const stack: string[] = []\n let inStr = false\n let escaped = false\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 === '{') stack.push('}')\n else if (ch === '[') stack.push(']')\n else if (ch === '}' || ch === ']') stack.pop()\n }\n\n let candidate = text.slice(start)\n if (inStr) candidate += '\"'\n // Drop trailing whitespace then a single dangling structural char (e.g. `{\"a\":` or `[1,`).\n let end = candidate.length\n while (end > 0) {\n const c = candidate[end - 1]\n if (c === ' ' || c === '\\n' || c === '\\r' || c === '\\t') end--\n else break\n }\n if (end > 0 && (candidate[end - 1] === ',' || candidate[end - 1] === ':')) end--\n candidate = candidate.slice(0, end)\n for (let i = stack.length - 1; i >= 0; i--) candidate += stack[i]\n\n try {\n return JSON.parse(candidate)\n } catch {\n return undefined\n }\n}\n\n/**\n * Deep-clone untrusted parsed JSON into a fresh value, throwing `AiError('INVALID_OBJECT')`\n * on any prototype-pollution key (`__proto__`/`constructor`/`prototype`) at any depth or on\n * any limit breach. Run this BEFORE handing a value to a Standard Schema validator — a\n * validator can itself pollute the prototype by touching a poisoned object.\n */\nexport function sanitizeJson(\n value: unknown,\n limits: SanitizeLimits = DEFAULT_SANITIZE_LIMITS,\n): unknown {\n let nodes = 0\n const walk = (v: unknown, depth: number): unknown => {\n if (depth > limits.maxDepth) {\n throw new AiError('INVALID_OBJECT', `value exceeds max depth ${limits.maxDepth}`)\n }\n if (++nodes > limits.maxNodes) {\n throw new AiError('INVALID_OBJECT', `value exceeds max nodes ${limits.maxNodes}`)\n }\n if (v === null) return null\n const t = typeof v\n if (t === 'string') {\n if ((v as string).length > limits.maxStringLength) {\n throw new AiError('INVALID_OBJECT', `string exceeds max length ${limits.maxStringLength}`)\n }\n return v\n }\n if (t === 'number') {\n if (!Number.isFinite(v)) throw new AiError('INVALID_OBJECT', 'numbers must be finite')\n return v\n }\n if (t === 'boolean') return v\n if (Array.isArray(v)) return v.map((x) => walk(x, depth + 1))\n if (isPlainObject(v)) {\n const keys = Object.keys(v)\n if (keys.length > limits.maxProps) {\n throw new AiError('INVALID_OBJECT', `object exceeds max keys ${limits.maxProps}`)\n }\n const out: Record<string, unknown> = {}\n for (const k of keys) {\n if (FORBIDDEN_KEYS.has(k)) throw new AiError('INVALID_OBJECT', `forbidden key \"${k}\"`)\n out[k] = walk(v[k], depth + 1)\n }\n return out\n }\n throw new AiError('INVALID_OBJECT', `unsupported value of type ${t}`)\n }\n return walk(value, 0)\n}\n\n/**\n * Cheap recursive check for any prototype-pollution own key, WITHOUT cloning. Used to skip\n * emitting an unsanitized streaming preview that carries a poison key (so a consumer who\n * naively deep-merges the preview can't be tricked into polluting the prototype). Depth-capped\n * and fail-closed: a value nested past `maxDepth` is treated as forbidden (skip the preview)\n * rather than risking a stack overflow on a deeply-nested untrusted payload.\n */\nexport function containsForbiddenKey(\n value: unknown,\n maxDepth: number = DEFAULT_SANITIZE_LIMITS.maxDepth,\n depth = 0,\n): boolean {\n if (depth > maxDepth) return true // fail-closed: too deep to vet → don't emit\n if (Array.isArray(value)) return value.some((v) => containsForbiddenKey(v, maxDepth, depth + 1))\n if (isPlainObject(value)) {\n for (const k of Object.keys(value)) {\n if (FORBIDDEN_KEYS.has(k)) return true\n if (containsForbiddenKey(value[k], maxDepth, depth + 1)) return true\n }\n }\n return false\n}\n\n/** Render Standard Schema issues into one readable line: `path: message; path2: message2`. */\nexport function formatIssues(issues: ReadonlyArray<StandardSchemaV1.Issue>): string {\n const list: ReadonlyArray<StandardSchemaV1.Issue> = Array.isArray(issues) ? issues : []\n const lines = list.map((issue) => {\n // String()-coerce each segment: a path key may be a symbol (PropertyKey), and\n // `Array.join`'s implicit coercion throws on symbols — that must not escape the pipeline.\n // Guard the array shape too — a malformed validator may hand back a non-array `path`.\n const path = Array.isArray(issue.path)\n ? issue.path\n .map((seg) => String(typeof seg === 'object' && seg !== null ? seg.key : seg))\n .join('.')\n : undefined\n return path ? `${path}: ${issue.message}` : issue.message\n })\n return lines.join('; ')\n}\n\n/** A normalized validation outcome (sync — the Promise has already been awaited). */\nexport type ValidationOutcome<T> =\n | { readonly ok: true; readonly value: T }\n | { readonly ok: false; readonly issues: ReadonlyArray<StandardSchemaV1.Issue> }\n\n/**\n * Validate a (already-sanitized) value through a Standard Schema, awaiting an async validator.\n * Defensively narrows a malformed validator result (non-array `issues`) into a failure rather\n * than crashing. Discriminates on `issues` truthiness — a valid output may legitimately be\n * `undefined`, so `value` is not a reliable discriminant.\n */\nexport async function validateStandard<S extends StandardSchemaV1>(\n schema: S,\n value: unknown,\n): Promise<ValidationOutcome<StandardSchemaV1.InferOutput<S>>> {\n const raw = schema['~standard'].validate(value)\n const result = raw instanceof Promise ? await raw : raw\n // A buggy/hostile validator may return a non-object — treat that as a (repairable) failure\n // rather than crashing on a property access.\n if (typeof result !== 'object' || result === null) {\n return { ok: false, issues: [] }\n }\n if (result.issues) {\n return { ok: false, issues: Array.isArray(result.issues) ? result.issues : [] }\n }\n return { ok: true, value: result.value }\n}\n"],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import { AiChunk, AiResult, GenerateRequest } from "./contract.js";
2
+
3
+ //#region src/mappers.d.ts
4
+ /**
5
+ * Parse one SSE `data` JSON into zero or more {@link AiChunk}s. Returns an array (empty
6
+ * to skip a non-content event) because a single event can legitimately carry more than
7
+ * one logical chunk — e.g. an OpenAI-compatible server that puts a content delta AND the
8
+ * terminal `finish_reason`/`usage` in the same event (a 1-chunk-per-event parser would
9
+ * drop the finish/usage).
10
+ */
11
+ type StreamParser = (data: unknown) => readonly AiChunk[];
12
+ /**
13
+ * A provider wire mapper. A custom mapper whose `buildRequest` cannot express
14
+ * `request.tools` MUST throw rather than silently drop them (the built-in openai/anthropic
15
+ * mappers serialize them; the on-device backend throws).
16
+ */
17
+ interface ProviderMapper {
18
+ /**
19
+ * How `apiKey` is sent. `'anthropic'` → `x-api-key` + `anthropic-version`; `'bearer'`
20
+ * (the default when omitted) → `Authorization: Bearer`. Declared on the mapper so auth
21
+ * follows the chosen provider whether it is selected by name or by object — and so a
22
+ * custom Anthropic-compatible mapper can opt into `x-api-key` auth.
23
+ */
24
+ readonly auth?: 'bearer' | 'anthropic';
25
+ /** Build the HTTP path + JSON body for a request. */
26
+ buildRequest(request: GenerateRequest, model: string, stream: boolean): {
27
+ path: string;
28
+ body: unknown;
29
+ };
30
+ /** Parse a non-streaming JSON response into an {@link AiResult}. */
31
+ parseResponse(json: unknown): AiResult;
32
+ /**
33
+ * Create a {@link StreamParser} for one stream. Returned fresh per stream so it may hold
34
+ * per-stream state (e.g. the last `finish_reason`) without leaking across concurrent
35
+ * streams that share this mapper singleton.
36
+ */
37
+ createStreamParser(): StreamParser;
38
+ }
39
+ /** OpenAI-compatible `/chat/completions` mapper (also fits many local/compatible servers). */
40
+ declare const openaiMapper: ProviderMapper;
41
+ /** Anthropic `/v1/messages` mapper. */
42
+ declare const anthropicMapper: ProviderMapper;
43
+ /** The built-in mappers by adapter name. */
44
+ declare const MAPPERS: {
45
+ readonly openai: ProviderMapper;
46
+ readonly anthropic: ProviderMapper;
47
+ };
48
+ /** A built-in provider adapter name. */
49
+ type AdapterName = keyof typeof MAPPERS;
50
+ //#endregion
51
+ export { AdapterName, ProviderMapper, StreamParser, anthropicMapper, openaiMapper };
52
+ //# sourceMappingURL=mappers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mappers.d.ts","names":[],"sources":["../src/mappers.ts"],"mappings":";;;;;;;;;;KA6BY,YAAA,IAAgB,IAAA,uBAA2B,OAAO;;;;;;UAO7C,cAAA;EAaI;;;;;;EAAA,SANV,IAAA;EAcyB;EAZlC,YAAA,CACE,OAAA,EAAS,eAAA,EACT,KAAA,UACA,MAAA;IACG,IAAA;IAAc,IAAA;EAAA;EAgOpB;EA9NC,aAAA,CAAc,IAAA,YAAgB,QAAA;EAuT/B;;;AAAA;AAGD;EApTE,kBAAA,IAAsB,YAAA;AAAA;;cA0IX,YAAA,EAAc,cA8E1B;;cAGY,eAAA,EAAiB,cAsF7B;;cAGY,OAAA;EAAA,iBAAuE,cAAA;EAAA,oBAAA,cAAA;AAAA;;KAGxE,WAAA,gBAA2B,OAAO"}
@@ -0,0 +1,312 @@
1
+ import { messageText } from "./contract.js";
2
+ //#region src/mappers.ts
3
+ /**
4
+ * Per-provider wire mappers (OpenAI-compatible + Anthropic), isolating the HTTP shape so
5
+ * the server backend never imports a vendor SDK and a provider change is a contained
6
+ * edit. All response/stream parsing is defensive — model/server JSON is untrusted. Tool
7
+ * mapping is added in 11C (with tools). See `docs/adr/0018-synapse-server-backend.md`.
8
+ *
9
+ * @module
10
+ */
11
+ function asRecord(value) {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
13
+ }
14
+ function asString(value) {
15
+ return typeof value === "string" ? value : void 0;
16
+ }
17
+ function asNumber(value) {
18
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
19
+ }
20
+ /** Build a {@link Usage} omitting undefined fields (exactOptionalPropertyTypes-safe). */
21
+ function usageOf(inputTokens, outputTokens) {
22
+ const usage = {};
23
+ if (inputTokens !== void 0) usage.inputTokens = inputTokens;
24
+ if (outputTokens !== void 0) usage.outputTokens = outputTokens;
25
+ return usage;
26
+ }
27
+ /** Parse OpenAI's `function.arguments` (a JSON STRING) defensively into a value. */
28
+ function parseJsonArgs(value) {
29
+ if (typeof value !== "string") return value ?? {};
30
+ try {
31
+ return JSON.parse(value);
32
+ } catch {
33
+ return {};
34
+ }
35
+ }
36
+ /** Serialize a tool result to the string the wire APIs expect (never throws, cycle-safe-ish). */
37
+ function toWireString(value) {
38
+ if (typeof value === "string") return value;
39
+ try {
40
+ return JSON.stringify(value) ?? String(value);
41
+ } catch {
42
+ return String(value);
43
+ }
44
+ }
45
+ /** Split a message's parts (string content has only text). */
46
+ function partsOf(content) {
47
+ if (typeof content === "string") return {
48
+ text: content,
49
+ calls: [],
50
+ results: []
51
+ };
52
+ return {
53
+ text: content.filter((p) => p.type === "text").map((p) => p.text).join(""),
54
+ calls: content.filter((p) => p.type === "tool-call"),
55
+ results: content.filter((p) => p.type === "tool-result")
56
+ };
57
+ }
58
+ /**
59
+ * Serialize messages to the OpenAI chat shape, round-tripping tool turns: an assistant turn
60
+ * with tool calls emits `tool_calls`; a `tool` message emits one `{ role:'tool', tool_call_id }`
61
+ * per result. Without this the tool loop's 2nd turn would lose its tool context.
62
+ */
63
+ function openaiMessages(messages) {
64
+ const out = [];
65
+ for (const m of messages) {
66
+ const { text, calls, results } = partsOf(m.content);
67
+ if (results.length > 0) for (const r of results) out.push({
68
+ role: "tool",
69
+ tool_call_id: r.id,
70
+ content: toWireString(r.result)
71
+ });
72
+ else if (calls.length > 0) out.push({
73
+ role: m.role,
74
+ content: text || null,
75
+ tool_calls: calls.map((c) => ({
76
+ id: c.id,
77
+ type: "function",
78
+ function: {
79
+ name: c.name,
80
+ arguments: JSON.stringify(c.args ?? {})
81
+ }
82
+ }))
83
+ });
84
+ else out.push({
85
+ role: m.role,
86
+ content: text
87
+ });
88
+ }
89
+ return out;
90
+ }
91
+ /**
92
+ * Serialize non-system messages to the Anthropic shape, round-tripping tool turns: assistant
93
+ * tool calls become `tool_use` blocks; a `tool` message becomes a USER message of `tool_result`
94
+ * blocks (Anthropic carries tool results in the user turn).
95
+ */
96
+ function anthropicMessages(messages) {
97
+ const out = [];
98
+ for (const m of messages) {
99
+ if (m.role === "system") continue;
100
+ const { text, calls, results } = partsOf(m.content);
101
+ if (results.length > 0) out.push({
102
+ role: "user",
103
+ content: results.map((r) => ({
104
+ type: "tool_result",
105
+ tool_use_id: r.id,
106
+ content: toWireString(r.result)
107
+ }))
108
+ });
109
+ else if (calls.length > 0) {
110
+ const content = [];
111
+ if (text) content.push({
112
+ type: "text",
113
+ text
114
+ });
115
+ for (const c of calls) content.push({
116
+ type: "tool_use",
117
+ id: c.id,
118
+ name: c.name,
119
+ input: c.args ?? {}
120
+ });
121
+ out.push({
122
+ role: "assistant",
123
+ content
124
+ });
125
+ } else out.push({
126
+ role: m.role,
127
+ content: text
128
+ });
129
+ }
130
+ return out;
131
+ }
132
+ /** Add `toolCalls` to an {@link AiResult} only when present (exactOptionalPropertyTypes-safe). */
133
+ function withToolCalls(result, toolCalls) {
134
+ return toolCalls.length > 0 ? {
135
+ ...result,
136
+ toolCalls
137
+ } : result;
138
+ }
139
+ const OPENAI_FINISH = Object.assign(Object.create(null), {
140
+ stop: "stop",
141
+ length: "length",
142
+ tool_calls: "tool-calls"
143
+ });
144
+ const ANTHROPIC_FINISH = Object.assign(Object.create(null), {
145
+ end_turn: "stop",
146
+ max_tokens: "length",
147
+ tool_use: "tool-calls"
148
+ });
149
+ /** OpenAI-compatible `/chat/completions` mapper (also fits many local/compatible servers). */
150
+ const openaiMapper = {
151
+ auth: "bearer",
152
+ buildRequest(request, model, stream) {
153
+ const body = {
154
+ model,
155
+ stream,
156
+ messages: openaiMessages(request.messages)
157
+ };
158
+ if (request.temperature !== void 0) body.temperature = request.temperature;
159
+ if (request.maxOutputTokens !== void 0) body.max_tokens = request.maxOutputTokens;
160
+ if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
161
+ type: "function",
162
+ function: {
163
+ name: t.name,
164
+ ...t.description !== void 0 ? { description: t.description } : {},
165
+ parameters: t.parameters ?? {
166
+ type: "object",
167
+ properties: {}
168
+ }
169
+ }
170
+ }));
171
+ if (stream) body.stream_options = { include_usage: true };
172
+ return {
173
+ path: "/chat/completions",
174
+ body
175
+ };
176
+ },
177
+ parseResponse(json) {
178
+ const root = asRecord(json);
179
+ const choice = asRecord((root?.choices)?.[0]);
180
+ const message = asRecord(choice?.message);
181
+ const usage = asRecord(root?.usage);
182
+ const toolCalls = [];
183
+ for (const raw of message?.tool_calls ?? []) {
184
+ const tc = asRecord(raw);
185
+ const fn = asRecord(tc?.function);
186
+ const name = asString(fn?.name);
187
+ if (!name) continue;
188
+ toolCalls.push({
189
+ type: "tool-call",
190
+ id: asString(tc?.id) ?? name,
191
+ name,
192
+ args: parseJsonArgs(fn?.arguments)
193
+ });
194
+ }
195
+ return withToolCalls({
196
+ text: asString(message?.content) ?? "",
197
+ finishReason: OPENAI_FINISH[asString(choice?.finish_reason) ?? ""] ?? "stop",
198
+ usage: usageOf(asNumber(usage?.prompt_tokens), asNumber(usage?.completion_tokens))
199
+ }, toolCalls);
200
+ },
201
+ createStreamParser() {
202
+ let lastReason = "stop";
203
+ return (data) => {
204
+ const root = asRecord(data);
205
+ const choice = asRecord((root?.choices)?.[0]);
206
+ const out = [];
207
+ const delta = asString(asRecord(choice?.delta)?.content);
208
+ if (delta) out.push({
209
+ type: "text-delta",
210
+ delta
211
+ });
212
+ const finish = asString(choice?.finish_reason);
213
+ if (finish) lastReason = OPENAI_FINISH[finish] ?? "stop";
214
+ const usage = asRecord(root?.usage);
215
+ if (finish || usage) out.push({
216
+ type: "finish",
217
+ finishReason: lastReason,
218
+ usage: usageOf(asNumber(usage?.prompt_tokens), asNumber(usage?.completion_tokens))
219
+ });
220
+ return out;
221
+ };
222
+ }
223
+ };
224
+ /** Anthropic `/v1/messages` mapper. */
225
+ const anthropicMapper = {
226
+ auth: "anthropic",
227
+ buildRequest(request, model, stream) {
228
+ const system = request.messages.filter((m) => m.role === "system").map((m) => messageText(m)).join("\n");
229
+ const body = {
230
+ model,
231
+ stream,
232
+ messages: anthropicMessages(request.messages),
233
+ max_tokens: request.maxOutputTokens ?? 1024
234
+ };
235
+ if (system) body.system = system;
236
+ if (request.temperature !== void 0) body.temperature = request.temperature;
237
+ if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
238
+ name: t.name,
239
+ ...t.description !== void 0 ? { description: t.description } : {},
240
+ input_schema: t.parameters ?? {
241
+ type: "object",
242
+ properties: {}
243
+ }
244
+ }));
245
+ return {
246
+ path: "/v1/messages",
247
+ body
248
+ };
249
+ },
250
+ parseResponse(json) {
251
+ const root = asRecord(json);
252
+ const content = root?.content ?? [];
253
+ let text = "";
254
+ const toolCalls = [];
255
+ for (const part of content) {
256
+ const block = asRecord(part);
257
+ if (asString(block?.type) === "tool_use") {
258
+ const name = asString(block?.name);
259
+ if (!name) continue;
260
+ toolCalls.push({
261
+ type: "tool-call",
262
+ id: asString(block?.id) ?? name,
263
+ name,
264
+ args: block?.input ?? {}
265
+ });
266
+ } else text += asString(block?.text) ?? "";
267
+ }
268
+ const usage = asRecord(root?.usage);
269
+ return withToolCalls({
270
+ text,
271
+ finishReason: ANTHROPIC_FINISH[asString(root?.stop_reason) ?? ""] ?? "stop",
272
+ usage: usageOf(asNumber(usage?.input_tokens), asNumber(usage?.output_tokens))
273
+ }, toolCalls);
274
+ },
275
+ createStreamParser() {
276
+ let inputTokens;
277
+ return (data) => {
278
+ const root = asRecord(data);
279
+ const type = asString(root?.type);
280
+ if (type === "message_start") {
281
+ inputTokens = asNumber(asRecord(asRecord(root?.message)?.usage)?.input_tokens);
282
+ return [];
283
+ }
284
+ if (type === "content_block_delta") {
285
+ const delta = asString(asRecord(root?.delta)?.text);
286
+ return delta ? [{
287
+ type: "text-delta",
288
+ delta
289
+ }] : [];
290
+ }
291
+ if (type === "message_delta") {
292
+ const stop = asString(asRecord(root?.delta)?.stop_reason);
293
+ const usage = asRecord(root?.usage);
294
+ return [{
295
+ type: "finish",
296
+ finishReason: ANTHROPIC_FINISH[stop ?? ""] ?? "stop",
297
+ usage: usageOf(inputTokens, asNumber(usage?.output_tokens))
298
+ }];
299
+ }
300
+ return [];
301
+ };
302
+ }
303
+ };
304
+ /** The built-in mappers by adapter name. */
305
+ const MAPPERS = {
306
+ openai: openaiMapper,
307
+ anthropic: anthropicMapper
308
+ };
309
+ //#endregion
310
+ export { MAPPERS, anthropicMapper, openaiMapper };
311
+
312
+ //# sourceMappingURL=mappers.js.map