@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/retry.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//@ts-check
|
|
2
1
|
/**
|
|
3
2
|
* The transport policy every client in this package shares: which
|
|
4
3
|
* failures are transient, how long to wait before trying again, what a
|
|
@@ -12,58 +11,26 @@
|
|
|
12
11
|
* Everything here is internal to the package; the public surface is
|
|
13
12
|
* the `retry` option each client documents.
|
|
14
13
|
*/
|
|
15
|
-
|
|
16
|
-
import { AiError } from './errors.js';
|
|
14
|
+
import { AiError } from "./errors.js";
|
|
17
15
|
import { backoffDelay, parseRetryAfter, sleep as defaultSleep, abortError } from '@jarenjs/core/retry';
|
|
18
16
|
export { abortError };
|
|
19
|
-
|
|
17
|
+
/** The `retry` option of every client. */
|
|
18
|
+
/** The option with its defaults filled in. */
|
|
20
19
|
/**
|
|
21
|
-
* The `retry` option of every client.
|
|
22
|
-
* @typedef {Object} RetryOptions
|
|
23
|
-
* @property {number} [attempts] - the TOTAL number of tries (default 3;
|
|
24
|
-
* 1 disables retrying)
|
|
25
|
-
* @property {number} [baseMs] - the first backoff (default 500); each
|
|
26
|
-
* later one doubles, with full jitter
|
|
27
|
-
* @property {number} [maxMs] - the ceiling on any single wait (default
|
|
28
|
-
* 8 000) — a provider `Retry-After` included: a provider asking for a
|
|
29
|
-
* minute gets the cap, and the value it asked for rides the final
|
|
30
|
-
* error as `retryAfterMs` for the caller to honour
|
|
31
|
-
* @property {() => number} [random] - the jitter source, for
|
|
32
|
-
* deterministic tests
|
|
33
|
-
* @property {(ms: number, signal?: AbortSignal) => Promise<void>} [sleep]
|
|
34
|
-
* - the wait itself, for deterministic tests; the default is a timer
|
|
35
|
-
* that rejects with the abort reason the moment `signal` aborts
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The option with its defaults filled in.
|
|
40
|
-
* @typedef {Object} RetryPolicy
|
|
41
|
-
* @property {number} attempts
|
|
42
|
-
* @property {number} baseMs
|
|
43
|
-
* @property {number} maxMs
|
|
44
|
-
* @property {() => number} random
|
|
45
|
-
* @property {(ms: number, signal?: AbortSignal) => Promise<void>} sleep
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param {RetryOptions | undefined} retry
|
|
50
|
-
* @returns {RetryPolicy}
|
|
51
20
|
*/
|
|
52
21
|
export function normalizeRetry(retry) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
22
|
+
return {
|
|
23
|
+
attempts: Math.max(1, retry?.attempts ?? 3),
|
|
24
|
+
baseMs: retry?.baseMs ?? 500,
|
|
25
|
+
maxMs: retry?.maxMs ?? 8000,
|
|
26
|
+
random: retry?.random ?? Math.random,
|
|
27
|
+
sleep: retry?.sleep ?? defaultSleep,
|
|
28
|
+
};
|
|
60
29
|
}
|
|
61
|
-
|
|
62
30
|
/** Statuses worth a retry: timeout, rate limit, server-side failure. */
|
|
63
31
|
function isRetryableStatus(status) {
|
|
64
|
-
|
|
32
|
+
return status === 0 || status === 408 || status === 429 || status >= 500;
|
|
65
33
|
}
|
|
66
|
-
|
|
67
34
|
/**
|
|
68
35
|
* Whether a failure is the transient kind. A transport error with a
|
|
69
36
|
* retryable status is (a network failure before any response counts
|
|
@@ -72,28 +39,24 @@ function isRetryableStatus(status) {
|
|
|
72
39
|
* tiers, and safe to retry precisely because nothing was delivered.
|
|
73
40
|
* Anything else — a caller error, a 401, a 404 — is final on the first
|
|
74
41
|
* try.
|
|
75
|
-
* @param {unknown} err
|
|
76
|
-
* @returns {boolean}
|
|
77
42
|
*/
|
|
78
43
|
export function isTransientFailure(err) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
44
|
+
if (!(err instanceof AiError))
|
|
45
|
+
return false;
|
|
46
|
+
return err.code === 'AI0003'
|
|
47
|
+
|| (err.code === 'AI0002' && isRetryableStatus(err.status ?? -1));
|
|
82
48
|
}
|
|
83
|
-
|
|
84
49
|
/**
|
|
85
50
|
* The wait before the next try: exponential backoff with full jitter,
|
|
86
51
|
* capped at `maxMs` — unless the provider named a `Retry-After`, which
|
|
87
52
|
* wins up to the same cap.
|
|
88
|
-
* @param
|
|
89
|
-
* @param
|
|
90
|
-
* @
|
|
91
|
-
* @returns {number} milliseconds
|
|
53
|
+
* @param attempt - the try that just failed, counted from 1
|
|
54
|
+
* @param retryAfter - the provider's ask, in ms
|
|
55
|
+
* @returns milliseconds
|
|
92
56
|
*/
|
|
93
57
|
export function retryDelay(policy, attempt, retryAfter) {
|
|
94
|
-
|
|
58
|
+
return backoffDelay({ ...policy, policy: 'ai-compat' }, attempt, retryAfter);
|
|
95
59
|
}
|
|
96
|
-
|
|
97
60
|
/**
|
|
98
61
|
* Run `once` until it settles. A failure `retryable` accepts backs off
|
|
99
62
|
* and tries again while tries remain; anything else is thrown as it
|
|
@@ -102,81 +65,63 @@ export function retryDelay(policy, attempt, retryAfter) {
|
|
|
102
65
|
* during backoff rejects with the abort reason, exactly like an abort
|
|
103
66
|
* during the request — nothing is ever retried past an abort.
|
|
104
67
|
* @template T
|
|
105
|
-
* @param
|
|
106
|
-
* @param {() => Promise<T>} once - one request/response cycle
|
|
107
|
-
* @param {{ signal?: AbortSignal, retryable: (failure: AiError) => boolean }} options
|
|
68
|
+
* @param once - one request/response cycle
|
|
108
69
|
* - `retryable` is the wire's own judgment over a coded failure (the
|
|
109
70
|
* chat client, for one, stops retrying once a streamed delta has
|
|
110
71
|
* reached the caller); it is never asked about an uncoded error
|
|
111
|
-
* @returns {Promise<T>}
|
|
112
72
|
*/
|
|
113
73
|
export async function withRetry(policy, once, options) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
74
|
+
for (let attempt = 1;; attempt++) {
|
|
75
|
+
try {
|
|
76
|
+
return await once();
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (options.signal?.aborted)
|
|
80
|
+
throw abortError(options.signal);
|
|
81
|
+
const failure = err;
|
|
82
|
+
const coded = failure instanceof AiError;
|
|
83
|
+
if (!(coded && options.retryable(failure) && attempt < policy.attempts)) {
|
|
84
|
+
if (coded && (failure.code === 'AI0002' || failure.code === 'AI0003'))
|
|
85
|
+
failure.attempts = attempt;
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
await policy.sleep(retryDelay(policy, attempt, failure.retryAfterMs), options.signal);
|
|
89
|
+
}
|
|
117
90
|
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
if (options.signal?.aborted) throw abortError(options.signal);
|
|
120
|
-
const failure = /** @type {any} */ (err);
|
|
121
|
-
const coded = failure instanceof AiError;
|
|
122
|
-
if (!(coded && options.retryable(failure) && attempt < policy.attempts)) {
|
|
123
|
-
if (coded && (failure.code === 'AI0002' || failure.code === 'AI0003'))
|
|
124
|
-
failure.attempts = attempt;
|
|
125
|
-
throw err;
|
|
126
|
-
}
|
|
127
|
-
await policy.sleep(retryDelay(policy, attempt, failure.retryAfterMs), options.signal);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
91
|
}
|
|
131
|
-
|
|
132
92
|
/**
|
|
133
93
|
* The `AI0002` for a response that is not ok: the status, a short
|
|
134
94
|
* excerpt of the body, and the provider's `Retry-After` in ms.
|
|
135
|
-
* @param {any} response
|
|
136
|
-
* @param {string} url
|
|
137
|
-
* @returns {Promise<AiError>}
|
|
138
95
|
*/
|
|
139
96
|
export async function httpFailure(response, url) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`HTTP ${response.status} from ${url}${excerpt === '' ? '' : `: ${excerpt}`}`,
|
|
143
|
-
{ status: response.status, retryAfterMs: retryAfterMs(response) });
|
|
97
|
+
const excerpt = await readErrorExcerpt(response);
|
|
98
|
+
return new AiError('AI0002', `HTTP ${response.status} from ${url}${excerpt === '' ? '' : `: ${excerpt}`}`, { status: response.status, retryAfterMs: retryAfterMs(response) });
|
|
144
99
|
}
|
|
145
|
-
|
|
146
100
|
/**
|
|
147
101
|
* What to throw when `fetch` itself threw: an abort exactly as it came
|
|
148
102
|
* (the caller's own signal, never retried, never rewrapped); anything
|
|
149
103
|
* else the `AI0002` of a failure before any response, status 0.
|
|
150
|
-
* @param {any} err
|
|
151
|
-
* @param {string} url
|
|
152
|
-
* @returns {any}
|
|
153
104
|
*/
|
|
154
105
|
export function transportFailure(err, url) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
`network error calling ${url}: ${err?.message ?? err}`,
|
|
158
|
-
{ status: 0, cause: err });
|
|
106
|
+
if (err?.name === 'AbortError')
|
|
107
|
+
return err;
|
|
108
|
+
return new AiError('AI0002', `network error calling ${url}: ${err?.message ?? err}`, { status: 0, cause: err });
|
|
159
109
|
}
|
|
160
|
-
|
|
161
110
|
/**
|
|
162
111
|
* Parse a `Retry-After` header (delta-seconds or HTTP-date) into ms.
|
|
163
|
-
* @param {any} response
|
|
164
|
-
* @returns {number | undefined}
|
|
165
112
|
*/
|
|
166
113
|
export function retryAfterMs(response) {
|
|
167
|
-
|
|
114
|
+
return parseRetryAfter(response?.headers?.get?.('retry-after'));
|
|
168
115
|
}
|
|
169
|
-
|
|
170
116
|
/**
|
|
171
|
-
* @
|
|
172
|
-
* @returns {Promise<string>} a short excerpt of the error body
|
|
117
|
+
* @returns a short excerpt of the error body
|
|
173
118
|
*/
|
|
174
119
|
async function readErrorExcerpt(response) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
120
|
+
try {
|
|
121
|
+
const text = await response.text();
|
|
122
|
+
return text.length > 300 ? `${text.slice(0, 300)}…` : text;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
182
127
|
}
|
package/src/routing.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/** Optional host routing for model-only calls. No tools execute inside this boundary. */
|
|
2
|
+
export declare const MODEL_PURPOSES: readonly string[];
|
|
3
3
|
/** A selector returns a route or ordered fallback routes: `{client, identity}`.
|
|
4
4
|
* A route identity is host data; it never controls provider policy. Embedding callers
|
|
5
5
|
* use their embedding API directly: this chat wrapper refuses that purpose so a
|
|
6
6
|
* fallback cannot silently change an index's embedding identity.
|
|
7
|
-
* @param
|
|
8
|
-
|
|
9
|
-
* @param {ModelContext} context */
|
|
10
|
-
export function createRoutedClient(options: {
|
|
7
|
+
* @param context */
|
|
8
|
+
export declare function createRoutedClient(options: {
|
|
11
9
|
client: any;
|
|
12
10
|
selectModel?: (context: ModelContext) => any;
|
|
13
11
|
limits?: ModelLimits;
|
|
@@ -17,15 +15,13 @@ export function createRoutedClient(options: {
|
|
|
17
15
|
endpoint: any;
|
|
18
16
|
complete(request: any): Promise<any>;
|
|
19
17
|
};
|
|
20
|
-
/** Optional host routing for model-only calls. No tools execute inside this boundary. */
|
|
21
|
-
export const MODEL_PURPOSES: readonly string[];
|
|
22
18
|
export type ModelLimits = {
|
|
23
19
|
deadlineMs?: number;
|
|
24
20
|
outputTokens?: number;
|
|
25
21
|
reasoningTokens?: number;
|
|
26
22
|
};
|
|
27
23
|
export type ModelContext = {
|
|
28
|
-
purpose:
|
|
24
|
+
purpose: 'author' | 'subcall' | 'embedding' | 'stylesheet';
|
|
29
25
|
grammar?: string;
|
|
30
26
|
depth?: number;
|
|
31
27
|
limits?: ModelLimits;
|
package/src/routing.js
CHANGED
|
@@ -1,87 +1,99 @@
|
|
|
1
|
-
//@ts-check
|
|
2
1
|
/** Optional host routing for model-only calls. No tools execute inside this boundary. */
|
|
3
2
|
export const MODEL_PURPOSES = Object.freeze(['author', 'subcall', 'embedding', 'stylesheet']);
|
|
4
|
-
|
|
5
|
-
/** @typedef {{ deadlineMs?: number, outputTokens?: number, reasoningTokens?: number }} ModelLimits */
|
|
6
|
-
/** @typedef {{ purpose: 'author'|'subcall'|'embedding'|'stylesheet', grammar?: string, depth?: number, limits?: ModelLimits }} ModelContext */
|
|
7
|
-
|
|
8
3
|
/** A selector returns a route or ordered fallback routes: `{client, identity}`.
|
|
9
4
|
* A route identity is host data; it never controls provider policy. Embedding callers
|
|
10
5
|
* use their embedding API directly: this chat wrapper refuses that purpose so a
|
|
11
6
|
* fallback cannot silently change an index's embedding identity.
|
|
12
|
-
* @param
|
|
13
|
-
* limits?: ModelLimits, onRoute?: (event: any) => void, account?: any }} options
|
|
14
|
-
* @param {ModelContext} context */
|
|
7
|
+
* @param context */
|
|
15
8
|
export function createRoutedClient(options, context) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
9
|
+
if (!MODEL_PURPOSES.includes(context.purpose) || context.purpose === 'embedding')
|
|
10
|
+
throw new TypeError('chat routing requires author, stylesheet or subcall purpose');
|
|
11
|
+
const limits = { ...options.limits, ...context.limits };
|
|
12
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
13
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
14
|
+
throw new TypeError(`${name} must be a positive integer`);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
endpoint: options.client.endpoint,
|
|
18
|
+
async complete(request) {
|
|
19
|
+
if (request.tools?.length || request.toolChoice !== undefined)
|
|
20
|
+
throw new TypeError('model routing does not retry tool-bearing requests');
|
|
21
|
+
const choice = options.selectModel ? await options.selectModel({ ...context, limits }) : null;
|
|
22
|
+
const routes = choice === null ? [{ client: options.client, identity: 'default' }]
|
|
23
|
+
: Array.isArray(choice) ? choice : [choice];
|
|
24
|
+
if (routes.length === 0)
|
|
25
|
+
throw new TypeError('selectModel returned no routes');
|
|
26
|
+
let last;
|
|
27
|
+
for (const [index, route] of routes.entries()) {
|
|
28
|
+
if (!route?.client?.complete || typeof route.identity !== 'string' || !route.identity)
|
|
29
|
+
throw new TypeError('a model route needs client and identity');
|
|
30
|
+
request.signal?.throwIfAborted();
|
|
31
|
+
const stopped = options.account?.stop();
|
|
32
|
+
if (stopped)
|
|
33
|
+
throw new Error(stopped);
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const remaining = options.account?.remaining?.().ms;
|
|
36
|
+
const deadline = Math.min(limits.deadlineMs ?? Infinity, remaining ?? Infinity);
|
|
37
|
+
const signal = request.signal
|
|
38
|
+
? Number.isFinite(deadline) ? AbortSignal.any([request.signal, controller.signal]) : request.signal
|
|
39
|
+
: controller.signal;
|
|
40
|
+
let timer;
|
|
41
|
+
const started = performance.now();
|
|
42
|
+
const event = {
|
|
43
|
+
...context, identity: route.identity, fallback: index, limits, outcome: 'provider', ms: 0, usage: null,
|
|
44
|
+
schemaBytes: request.responseFormat?.schema ? new TextEncoder().encode(JSON.stringify(request.responseFormat.schema)).length : 0,
|
|
45
|
+
finishReason: null, errorCode: null
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
options.account?.reserve();
|
|
49
|
+
const bounded = {
|
|
50
|
+
...request, signal,
|
|
51
|
+
...(limits.outputTokens ? { maxTokens: Math.min(request.maxTokens ?? Infinity, limits.outputTokens) } : {}),
|
|
52
|
+
...(limits.reasoningTokens ? {
|
|
53
|
+
reasoning: {
|
|
54
|
+
...request.reasoning,
|
|
55
|
+
max_tokens: Math.min(request.reasoning?.max_tokens ?? Infinity, limits.reasoningTokens)
|
|
56
|
+
}
|
|
57
|
+
} : {}),
|
|
58
|
+
};
|
|
59
|
+
const pending = Promise.resolve().then(() => route.client.complete(bounded));
|
|
60
|
+
const reply = await Promise.race([pending, new Promise((_, reject) => {
|
|
61
|
+
const abort = () => reject(signal.reason ?? new Error('aborted'));
|
|
62
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
63
|
+
// Remove the listener when even an abort-ignoring client eventually settles.
|
|
64
|
+
pending.then(() => signal.removeEventListener('abort', abort), () => signal.removeEventListener('abort', abort));
|
|
65
|
+
if (Number.isFinite(deadline))
|
|
66
|
+
timer = setTimeout(() => controller.abort(new Error('model deadline')), deadline);
|
|
67
|
+
})]);
|
|
68
|
+
event.usage = reply.usage ?? null;
|
|
69
|
+
event.finishReason = reply.finishReason ?? null;
|
|
70
|
+
options.account?.settle(reply.usage, JSON.stringify(request.messages) + String(reply.message?.content ?? ''));
|
|
71
|
+
const usage = reply.usage;
|
|
72
|
+
if ((limits.outputTokens && usage?.completion_tokens > limits.outputTokens)
|
|
73
|
+
|| (limits.reasoningTokens && usage?.completion_tokens_details?.reasoning_tokens > limits.reasoningTokens)) {
|
|
74
|
+
event.outcome = 'token-limit';
|
|
75
|
+
throw new Error('provider exceeded model token ceiling');
|
|
76
|
+
}
|
|
77
|
+
event.outcome = 'complete';
|
|
78
|
+
return reply;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
last = error;
|
|
82
|
+
event.errorCode = error?.code ?? error?.name ?? 'Error';
|
|
83
|
+
if (controller.signal.aborted)
|
|
84
|
+
event.outcome = 'timeout';
|
|
85
|
+
if (request.signal?.aborted) {
|
|
86
|
+
event.outcome = 'aborted';
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
event.ms = performance.now() - started;
|
|
93
|
+
options.onRoute?.(event);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw last;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
87
99
|
}
|
package/src/sse.d.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file The `@tangleai/models/sse` subpath: the data-only Server-Sent-
|
|
3
|
+
* Events decoder the streaming client consumes — the one SSE codec of
|
|
4
|
+
* the suite, which lives in `@jarenjs/core/text/sse` and is re-exported
|
|
5
|
+
* here so this package's public surface is unchanged. Feed it network
|
|
6
|
+
* chunks in any split (mid-line, mid-event, CR/LF/CRLF); it yields the
|
|
7
|
+
* complete `data:` payloads in order; non-data fields and comments are
|
|
8
|
+
* ignored, multi-line data joins with a newline per the specification.
|
|
9
|
+
*/
|
|
10
|
+
export { createSseDecoder } from '@jarenjs/core/text/sse';
|
package/src/sse.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//@ts-check
|
|
2
1
|
/**
|
|
3
2
|
* @file The `@tangleai/models/sse` subpath: the data-only Server-Sent-
|
|
4
3
|
* Events decoder the streaming client consumes — the one SSE codec of
|
|
@@ -8,5 +7,4 @@
|
|
|
8
7
|
* complete `data:` payloads in order; non-data fields and comments are
|
|
9
8
|
* ignored, multi-line data joins with a newline per the specification.
|
|
10
9
|
*/
|
|
11
|
-
|
|
12
10
|
export { createSseDecoder } from '@jarenjs/core/text/sse';
|
package/src/structured.d.ts
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured output: schema in, validated JSON value out — on every
|
|
3
|
+
* provider tier.
|
|
4
|
+
*
|
|
5
|
+
* The helper sends one (non-streaming) chat request constrained by a
|
|
6
|
+
* JSON Schema, using the strongest mechanism the provider speaks
|
|
7
|
+
* (`response_format: json_schema` → JSON mode → schema embedded in a
|
|
8
|
+
* system instruction), parses the reply, and validates it locally with
|
|
9
|
+
* `@jarenjs/validate` — the same validator that guards the toolbox.
|
|
10
|
+
* Local validation is never optional: provider structured-output
|
|
11
|
+
* implementations enforce varying schema subsets, so the server is an
|
|
12
|
+
* accelerator, not an authority. On failure the instancePath'd errors
|
|
13
|
+
* go back to the model for a bounded number of repair rounds.
|
|
14
|
+
*/
|
|
1
15
|
/**
|
|
2
16
|
* Strip an accidental markdown fence from a reply ("```json … ```") —
|
|
3
17
|
* the classic prompt-embedded-schema failure mode.
|
|
4
18
|
*
|
|
5
19
|
* Exported because every place this package parses a model's JSON has to
|
|
6
20
|
* make the same allowance, and two copies of "how forgiving are we about
|
|
7
|
-
* fences" is two answers to one question: `program.
|
|
21
|
+
* fences" is two answers to one question: `program.ts`'s sub-calls parse
|
|
8
22
|
* replies the same way structured generation does.
|
|
9
|
-
* @param {string} text
|
|
10
23
|
*/
|
|
11
|
-
export function unfence(text: string): string;
|
|
24
|
+
export declare function unfence(text: string): string;
|
|
12
25
|
/**
|
|
13
26
|
* Create a structured-output generator over a chat client.
|
|
14
27
|
*
|
|
15
|
-
* @param {{ client: { endpoint: { provider: string }, complete: (request: any) => Promise<any> },
|
|
16
|
-
* schema: any,
|
|
17
|
-
* name?: string,
|
|
18
|
-
* strict?: boolean,
|
|
19
|
-
* validator?: (value: any) => any,
|
|
20
|
-
* refs?: any[],
|
|
21
|
-
* gate?: ((value: any) => any) | Array<(value: any) => any>,
|
|
22
|
-
* stream?: boolean,
|
|
23
|
-
* onAttempt?: (event: { attempt: number, outcome: string, errors: any[] }) => void,
|
|
24
|
-
* maxRepairs?: number }} options
|
|
25
28
|
* - `validator` overrides the internally compiled check (any
|
|
26
29
|
* function returning a boolean or `{ valid, errors }`).
|
|
27
30
|
* - `refs` are other JSON Schemas the `schema` references by `$id`,
|
|
@@ -50,11 +53,8 @@ export function unfence(text: string): string;
|
|
|
50
53
|
* callers who never asked for one.
|
|
51
54
|
* - `maxRepairs` is how many failed rounds may go back to the model
|
|
52
55
|
* with the validation errors (default 1).
|
|
53
|
-
* @returns {{ generate: (messages: any[], hooks?: { signal?: AbortSignal }) => Promise<
|
|
54
|
-
* { value: any, raw: string, attempts: number } |
|
|
55
|
-
* { errors: any[], raw: string, attempts: number }> }}
|
|
56
56
|
*/
|
|
57
|
-
export function createStructuredOutput(options: {
|
|
57
|
+
export declare function createStructuredOutput(options: {
|
|
58
58
|
client: {
|
|
59
59
|
endpoint: {
|
|
60
60
|
provider: string;
|
|
@@ -79,10 +79,12 @@ export function createStructuredOutput(options: {
|
|
|
79
79
|
signal?: AbortSignal;
|
|
80
80
|
}) => Promise<{
|
|
81
81
|
value: any;
|
|
82
|
+
errors?: undefined;
|
|
82
83
|
raw: string;
|
|
83
84
|
attempts: number;
|
|
84
85
|
} | {
|
|
85
86
|
errors: any[];
|
|
87
|
+
value?: undefined;
|
|
86
88
|
raw: string;
|
|
87
89
|
attempts: number;
|
|
88
90
|
}>;
|