@tangleai/models 0.21.1 → 0.25.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/CHANGELOG.md +30 -0
- package/README.md +2 -1
- package/package.json +3 -3
- package/src/check.d.ts +9 -10
- package/src/check.js +14 -18
- package/src/client.d.ts +59 -102
- package/src/client.js +307 -351
- package/src/embed.d.ts +54 -36
- package/src/embed.js +202 -287
- package/src/embedding-vector.d.ts +9 -4
- package/src/embedding-vector.js +13 -17
- package/src/errors.d.ts +26 -9
- package/src/errors.js +22 -24
- package/src/grammar.d.ts +6 -7
- package/src/grammar.js +39 -30
- package/src/index.d.ts +10 -9
- package/src/index.js +9 -10
- package/src/providers.d.ts +41 -31
- package/src/providers.js +79 -99
- package/src/replay.d.ts +52 -37
- package/src/replay.js +31 -59
- package/src/retry.d.ts +45 -86
- package/src/retry.js +50 -105
- package/src/routing.d.ts +5 -9
- package/src/routing.js +91 -79
- package/src/sse.d.ts +10 -1
- package/src/sse.js +0 -2
- package/src/structured.d.ts +19 -17
- package/src/structured.js +95 -124
package/src/structured.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//@ts-check
|
|
2
1
|
/**
|
|
3
2
|
* Structured output: schema in, validated JSON value out — on every
|
|
4
3
|
* provider tier.
|
|
@@ -13,14 +12,11 @@
|
|
|
13
12
|
* accelerator, not an authority. On failure the instancePath'd errors
|
|
14
13
|
* go back to the model for a bounded number of repair rounds.
|
|
15
14
|
*/
|
|
16
|
-
|
|
17
15
|
import { JarenValidator } from '@jarenjs/validate';
|
|
18
16
|
import { checkOutcome, composeChecks } from '@jarenjs/core/check';
|
|
19
|
-
import { PROVIDERS } from
|
|
20
|
-
|
|
17
|
+
import { PROVIDERS } from "./providers.js";
|
|
21
18
|
/** Validation errors reported per failed generation: enough to repair. */
|
|
22
19
|
const MAX_ERRORS = 8;
|
|
23
|
-
|
|
24
20
|
/**
|
|
25
21
|
* Normalize an injected check's errors into the compact records a
|
|
26
22
|
* repair prompt carries. The default `JarenValidator` check reports
|
|
@@ -35,81 +31,64 @@ const MAX_ERRORS = 8;
|
|
|
35
31
|
* `message` so the record never carries the code and the path twice —
|
|
36
32
|
* the composed `message` already contains both, and the fields beside
|
|
37
33
|
* it are the structured copies.
|
|
38
|
-
* @param {any[]} raw
|
|
39
34
|
*/
|
|
40
35
|
function normalizeErrors(raw) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
return raw.slice(0, MAX_ERRORS).map((e) => {
|
|
37
|
+
const out = {
|
|
38
|
+
instancePath: e.docPath ?? e.instancePath ?? '',
|
|
39
|
+
keyword: e.code ?? e.keyword ?? '',
|
|
40
|
+
message: e.reason ?? e.message ?? 'invalid',
|
|
41
|
+
};
|
|
42
|
+
if (e.code !== undefined)
|
|
43
|
+
out.code = e.code;
|
|
44
|
+
if (e.docPath !== undefined)
|
|
45
|
+
out.docPath = e.docPath;
|
|
46
|
+
return out;
|
|
47
|
+
});
|
|
52
48
|
}
|
|
53
|
-
|
|
54
49
|
/**
|
|
55
50
|
* The system instruction for providers that cannot (fully) constrain
|
|
56
51
|
* decoding: the schema travels in the prompt and the reply must be the
|
|
57
52
|
* bare JSON value.
|
|
58
|
-
* @param {any} schema
|
|
59
53
|
*/
|
|
60
54
|
function schemaInstruction(schema) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
return [
|
|
56
|
+
'Reply with a single JSON value that validates against this JSON Schema.',
|
|
57
|
+
'Output ONLY the JSON — no prose, no code fences.',
|
|
58
|
+
'',
|
|
59
|
+
JSON.stringify(schema),
|
|
60
|
+
].join('\n');
|
|
67
61
|
}
|
|
68
|
-
|
|
69
62
|
/**
|
|
70
63
|
* Strip an accidental markdown fence from a reply ("```json … ```") —
|
|
71
64
|
* the classic prompt-embedded-schema failure mode.
|
|
72
65
|
*
|
|
73
66
|
* Exported because every place this package parses a model's JSON has to
|
|
74
67
|
* make the same allowance, and two copies of "how forgiving are we about
|
|
75
|
-
* fences" is two answers to one question: `program.
|
|
68
|
+
* fences" is two answers to one question: `program.ts`'s sub-calls parse
|
|
76
69
|
* replies the same way structured generation does.
|
|
77
|
-
* @param {string} text
|
|
78
70
|
*/
|
|
79
71
|
export function unfence(text) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
72
|
+
const trimmed = text.trim();
|
|
73
|
+
const match = /^```(?:json)?\s*([\s\S]*?)\s*```$/.exec(trimmed);
|
|
74
|
+
return match === null ? trimmed : match[1];
|
|
83
75
|
}
|
|
84
|
-
|
|
85
76
|
/**
|
|
86
77
|
* Compile a schema that may reference others by `$id`, registering the
|
|
87
78
|
* referenced schemas first. Every Jaren engine-document grammar
|
|
88
79
|
* (fsm/dag/app/JSLT) composes the query grammar by `$ref`, so a plain
|
|
89
80
|
* `.compile(schema)` on one of them throws "Can not resolve schema".
|
|
90
|
-
* @param
|
|
91
|
-
* @param {any[]} [refs]
|
|
92
|
-
* @returns {(value: any) => any}
|
|
81
|
+
* @param [refs]
|
|
93
82
|
*/
|
|
94
83
|
function compileWithRefs(schema, refs) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
const v = new JarenValidator({ skipErrors: false, collectErrors: true });
|
|
85
|
+
for (const ref of refs ?? [])
|
|
86
|
+
v.addSchema(ref);
|
|
87
|
+
return v.compile(schema);
|
|
98
88
|
}
|
|
99
|
-
|
|
100
89
|
/**
|
|
101
90
|
* Create a structured-output generator over a chat client.
|
|
102
91
|
*
|
|
103
|
-
* @param {{ client: { endpoint: { provider: string }, complete: (request: any) => Promise<any> },
|
|
104
|
-
* schema: any,
|
|
105
|
-
* name?: string,
|
|
106
|
-
* strict?: boolean,
|
|
107
|
-
* validator?: (value: any) => any,
|
|
108
|
-
* refs?: any[],
|
|
109
|
-
* gate?: ((value: any) => any) | Array<(value: any) => any>,
|
|
110
|
-
* stream?: boolean,
|
|
111
|
-
* onAttempt?: (event: { attempt: number, outcome: string, errors: any[] }) => void,
|
|
112
|
-
* maxRepairs?: number }} options
|
|
113
92
|
* - `validator` overrides the internally compiled check (any
|
|
114
93
|
* function returning a boolean or `{ valid, errors }`).
|
|
115
94
|
* - `refs` are other JSON Schemas the `schema` references by `$id`,
|
|
@@ -138,82 +117,74 @@ function compileWithRefs(schema, refs) {
|
|
|
138
117
|
* callers who never asked for one.
|
|
139
118
|
* - `maxRepairs` is how many failed rounds may go back to the model
|
|
140
119
|
* with the validation errors (default 1).
|
|
141
|
-
* @returns {{ generate: (messages: any[], hooks?: { signal?: AbortSignal }) => Promise<
|
|
142
|
-
* { value: any, raw: string, attempts: number } |
|
|
143
|
-
* { errors: any[], raw: string, attempts: number }> }}
|
|
144
120
|
*/
|
|
145
121
|
export function createStructuredOutput(options) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
122
|
+
const { client, schema } = options;
|
|
123
|
+
if (schema === null || typeof schema !== 'object')
|
|
124
|
+
throw new TypeError('createStructuredOutput needs a JSON Schema object');
|
|
125
|
+
const name = options.name ?? 'result';
|
|
126
|
+
const maxRepairs = options.maxRepairs ?? 1;
|
|
127
|
+
// Non-streaming stays the DEFAULT, and the reason is a behaviour change
|
|
128
|
+
// rather than a preference: a caller reading `raw` or counting attempts
|
|
129
|
+
// gets the same answer either way, but a caller that passed a client
|
|
130
|
+
// with `onDelta` wired sees deltas start arriving where none did
|
|
131
|
+
// before. The measurement that made this an option: the campaign's
|
|
132
|
+
// authoring timeouts were on STREAMED calls, so streaming is not the
|
|
133
|
+
// cure for a slow authoring turn — but it is how a long turn stays
|
|
134
|
+
// observable, and a caller who wants that should not have to wrap the
|
|
135
|
+
// client to get it.
|
|
136
|
+
const stream = options.stream ?? false;
|
|
137
|
+
const base = options.validator ?? compileWithRefs(schema, options.refs);
|
|
138
|
+
const gates = options.gate === undefined ? [] : [].concat(options.gate);
|
|
139
|
+
const check = composeChecks(...gates);
|
|
140
|
+
const tier = PROVIDERS[client.endpoint.provider]?.structured ?? null;
|
|
141
|
+
/**
|
|
142
|
+
* @param messages - wire-shape conversation to answer
|
|
143
|
+
* @param [hooks]
|
|
144
|
+
*/
|
|
145
|
+
async function generate(messages, hooks = {}) {
|
|
146
|
+
const request = { stream, signal: hooks.signal };
|
|
147
|
+
let turn = [...messages];
|
|
148
|
+
if (tier === 'json_schema') {
|
|
149
|
+
request.responseFormat = { name, schema, strict: options.strict ?? true };
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// JSON mode (or nothing): the schema travels in the prompt;
|
|
153
|
+
// local validation makes the weaker tiers safe
|
|
154
|
+
if (tier === 'json')
|
|
155
|
+
request.responseFormat = { type: 'json' };
|
|
156
|
+
turn = [{ role: 'system', content: schemaInstruction(schema) }, ...turn];
|
|
157
|
+
}
|
|
158
|
+
let raw = '';
|
|
159
|
+
let errors = [];
|
|
160
|
+
for (let attempt = 1; attempt <= 1 + maxRepairs; attempt++) {
|
|
161
|
+
const result = await client.complete({ ...request, messages: turn });
|
|
162
|
+
raw = result.message.content;
|
|
163
|
+
let value;
|
|
164
|
+
try {
|
|
165
|
+
value = JSON.parse(unfence(raw));
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
errors = [{ instancePath: '', keyword: 'parse', message: `the reply is not JSON: ${err.message}` }];
|
|
169
|
+
options.onAttempt?.({ attempt, outcome: 'schema', errors });
|
|
170
|
+
turn = [...turn,
|
|
171
|
+
{ role: 'assistant', content: raw },
|
|
172
|
+
{ role: 'user', content: 'That reply was not parseable JSON. Reply again with ONLY the JSON value.' }];
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const shape = checkOutcome(base(value));
|
|
176
|
+
const outcome = shape.valid ? checkOutcome(check(value)) : shape;
|
|
177
|
+
if (outcome.valid) {
|
|
178
|
+
options.onAttempt?.({ attempt, outcome: 'valid', errors: [] });
|
|
179
|
+
return { value, raw, attempts: attempt };
|
|
180
|
+
}
|
|
181
|
+
errors = normalizeErrors(outcome.errors);
|
|
182
|
+
options.onAttempt?.({ attempt, outcome: shape.valid ? 'gate' : 'schema', errors });
|
|
183
|
+
turn = [...turn,
|
|
184
|
+
{ role: 'assistant', content: raw },
|
|
185
|
+
{ role: 'user', content: `That JSON does not validate against the schema. Fix exactly these and reply with ONLY the corrected JSON value:\n${JSON.stringify(errors)}` }];
|
|
186
|
+
}
|
|
187
|
+
return { errors, raw, attempts: 1 + maxRepairs };
|
|
176
188
|
}
|
|
177
|
-
|
|
178
|
-
// JSON mode (or nothing): the schema travels in the prompt;
|
|
179
|
-
// local validation makes the weaker tiers safe
|
|
180
|
-
if (tier === 'json') request.responseFormat = { type: 'json' };
|
|
181
|
-
turn = [{ role: 'system', content: schemaInstruction(schema) }, ...turn];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
let raw = '';
|
|
185
|
-
/** @type {any[]} */
|
|
186
|
-
let errors = [];
|
|
187
|
-
for (let attempt = 1; attempt <= 1 + maxRepairs; attempt++) {
|
|
188
|
-
const result = await client.complete({ ...request, messages: turn });
|
|
189
|
-
raw = result.message.content;
|
|
190
|
-
/** @type {any} */
|
|
191
|
-
let value;
|
|
192
|
-
try {
|
|
193
|
-
value = JSON.parse(unfence(raw));
|
|
194
|
-
}
|
|
195
|
-
catch (err) {
|
|
196
|
-
errors = [{ instancePath: '', keyword: 'parse', message: `the reply is not JSON: ${/** @type {Error} */ (err).message}` }];
|
|
197
|
-
options.onAttempt?.({ attempt, outcome: 'schema', errors });
|
|
198
|
-
turn = [...turn,
|
|
199
|
-
{ role: 'assistant', content: raw },
|
|
200
|
-
{ role: 'user', content: 'That reply was not parseable JSON. Reply again with ONLY the JSON value.' }];
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
const shape = checkOutcome(base(value));
|
|
204
|
-
const outcome = shape.valid ? checkOutcome(check(value)) : shape;
|
|
205
|
-
if (outcome.valid) {
|
|
206
|
-
options.onAttempt?.({ attempt, outcome: 'valid', errors: [] });
|
|
207
|
-
return { value, raw, attempts: attempt };
|
|
208
|
-
}
|
|
209
|
-
errors = normalizeErrors(outcome.errors);
|
|
210
|
-
options.onAttempt?.({ attempt, outcome: shape.valid ? 'gate' : 'schema', errors });
|
|
211
|
-
turn = [...turn,
|
|
212
|
-
{ role: 'assistant', content: raw },
|
|
213
|
-
{ role: 'user', content: `That JSON does not validate against the schema. Fix exactly these and reply with ONLY the corrected JSON value:\n${JSON.stringify(errors)}` }];
|
|
214
|
-
}
|
|
215
|
-
return { errors, raw, attempts: 1 + maxRepairs };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return { generate };
|
|
189
|
+
return { generate };
|
|
219
190
|
}
|