@tangleai/models 0.21.1 → 0.24.1

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/src/embed.js CHANGED
@@ -1,4 +1,3 @@
1
- //@ts-check
2
1
  /**
3
2
  * Embeddings: the `/embeddings` wire of the same OpenAI-compatible
4
3
  * provider family the chat client speaks, and the seam every consumer
@@ -28,203 +27,133 @@
28
27
  * client that trusts reply order gets exactly that from a provider
29
28
  * that answers a batch out of order.
30
29
  */
31
-
32
30
  import { fnv1a } from '@jarenjs/core/string';
33
31
  import { l2Normalize } from '@jarenjs/core/vector';
34
-
35
- import { AiError } from './errors.js';
36
- import { verifyEmbeddingComponents } from './embedding-vector.js';
37
- import { resolveEndpoint } from './providers.js';
38
- import { normalizeRetry, withRetry, isTransientFailure, httpFailure, transportFailure, abortError } from './retry.js';
39
- import { normalizeCache, replayKey, verifyEmbeddingEntry, now } from './replay.js';
40
-
32
+ import { AiError } from "./errors.js";
33
+ import { verifyEmbeddingComponents } from "./embedding-vector.js";
34
+ import { resolveEndpoint } from "./providers.js";
35
+ import { normalizeRetry, withRetry, isTransientFailure, httpFailure, transportFailure, abortError } from "./retry.js";
36
+ import { normalizeCache, replayKey, verifyEmbeddingEntry, now } from "./replay.js";
37
+ /** The embedder seam: what every consumer of embeddings in this package
38
+ * takes, and what a host implements to bring its own. */
41
39
  /**
42
- * The embedder seam: what every consumer of embeddings in this package
43
- * takes, and what a host implements to bring its own.
44
- * @typedef {Object} Embedder
45
- * @property {(texts: string[], options?: { signal?: AbortSignal }) => Promise<Float32Array[]>} embed
46
- * - one vector per input, in input order; rejects `AI0001` for
47
- * anything but a non-empty array of strings
48
- * @property {string} model - the name half of a vector's identity
49
- * @property {number | undefined} dims - the width half; the wire client
50
- * leaves it undefined until its first reply settles it, unless the
51
- * caller configured it
52
- */
53
-
54
- /**
55
- * @param {unknown} texts
56
- * @returns {asserts texts is string[]}
57
40
  */
58
41
  function assertTexts(texts) {
59
- if (!Array.isArray(texts) || texts.length === 0 || !texts.every((text) => typeof text === 'string'))
60
- throw new AiError('AI0001', 'embed() needs a non-empty array of strings');
42
+ if (!Array.isArray(texts) || texts.length === 0 || !texts.every((text) => typeof text === 'string'))
43
+ throw new AiError('AI0001', 'embed() needs a non-empty array of strings');
61
44
  }
62
-
63
- /**
64
- * The OpenAI-compatible `/embeddings` client over the existing provider
65
- * set. The endpoint is `${base}/embeddings` from the same resolved base
66
- * the chat client uses, with the same auth and headers; the request is
67
- * one non-streaming POST of `{ model, input }`.
68
- *
69
- * Retries follow the chat client exactly: transient transport failures
70
- * (network, 408, 429, 5xx) and a malformed 200 back off with full
71
- * jitter and try again, `Retry-After` wins up to `maxMs`, an abort ends
72
- * everything at once. A structurally wrong reply — the wrong width for
73
- * a fixed model — is therefore reported after `attempts` tries; a probe
74
- * that wants a fast answer sets `retry: { attempts: 1 }`.
75
- *
76
- * @param {{ provider?: string, baseUrl?: string, apiKey?: string,
77
- * model?: string, dims?: number, headers?: Record<string, string>,
78
- * fetch?: typeof fetch, timeoutMs?: number,
79
- * retry?: import('./retry.js').RetryOptions,
80
- * cache?: import('./replay.js').ReplayCache }} [options]
81
- * - `model` is required: it is half of every vector's identity.
82
- * - `cache` is the replay seam (`createChatClient` documents the
83
- * contract). Here it is PER TEXT: each input is keyed by the
84
- * credential-free endpoint, the model and the text; the texts the
85
- * adapter remembers come back from it, only the rest travel — in one
86
- * wire call, in input order — and every bought vector is remembered
87
- * as `{ vector: number[], ms }` (the wall time of the batch). A
88
- * call whose every text is remembered makes no wire call at all, and
89
- * its first replay settles `dims` exactly as a first reply would.
90
- * - `dims` pins the other half up front; left out, the width of the
91
- * first reply becomes the client's, and every later reply must
92
- * match it. Give it when the identity must be known before the
93
- * first call.
94
- * - `timeoutMs` bounds each attempt, and a timed-out attempt retries
95
- * like a network failure. Unset means no timeout, as `complete()`
96
- * has none — a batch of long texts on a local runtime legitimately
97
- * takes a while; `probeEmbeddings` sets 5 000 ms, as `probeProvider`
98
- * does.
99
- * - `retry` is the chat client's option, unchanged (see
100
- * `createChatClient`).
101
- * @returns {Embedder & { provider: string }} the seam, plus the
102
- * resolved provider name; `dims` is the settled width
103
- */
104
45
  export function createEmbeddingClient(options = {}) {
105
- const endpoint = resolveEndpoint(options);
106
- const model = endpoint.model;
107
- if (typeof model !== 'string' || model === '')
108
- throw new AiError('AI0001', 'createEmbeddingClient needs a model — it is half of every vector\'s identity');
109
- if (options.dims !== undefined && !(Number.isInteger(options.dims) && options.dims > 0))
110
- throw new AiError('AI0001', `dims must be a positive integer, got ${String(options.dims)}`);
111
- const timeoutMs = options.timeoutMs;
112
- if (timeoutMs !== undefined && !(Number.isFinite(timeoutMs) && timeoutMs > 0))
113
- throw new AiError('AI0001', `timeoutMs must be a positive number, got ${String(timeoutMs)}`);
114
- const url = `${endpoint.base}/embeddings`;
115
- const fetchFn = options.fetch ?? ((u, init) => globalThis.fetch(u, init));
116
- const retry = normalizeRetry(options.retry);
117
- const cache = normalizeCache(options.cache);
118
- /** @type {number | undefined} */
119
- let dims = options.dims;
120
-
121
- /**
122
- * One request/response cycle.
123
- * @param {string[]} texts
124
- * @param {AbortSignal | undefined} signal
125
- * @returns {Promise<Float32Array[]>}
126
- */
127
- async function attemptOnce(texts, signal) {
128
- // the attempt's own deadline rides beside the caller's signal; which
129
- // of the two fired decides whether the failure is a retryable
130
- // timeout or the caller's abort
131
- /** @type {AbortController | null} */
132
- let deadline = null;
133
- /** @type {any} */
134
- let timer;
135
- let requestSignal = signal;
136
- if (timeoutMs !== undefined) {
137
- deadline = new AbortController();
138
- timer = setTimeout(() => /** @type {AbortController} */ (deadline).abort(), timeoutMs);
139
- requestSignal = signal === undefined ? deadline.signal : AbortSignal.any([signal, deadline.signal]);
140
- }
141
- /** @type {string} */
142
- let text;
143
- try {
144
- const response = await fetchFn(url, {
145
- method: 'POST',
146
- headers: endpoint.headers,
147
- body: JSON.stringify({ model, input: texts }),
148
- signal: requestSignal,
149
- });
150
- if (response.ok !== true) throw await httpFailure(response, url);
151
- text = await response.text();
46
+ const endpoint = resolveEndpoint(options);
47
+ const model = endpoint.model;
48
+ if (typeof model !== 'string' || model === '')
49
+ throw new AiError('AI0001', 'createEmbeddingClient needs a model — it is half of every vector\'s identity');
50
+ if (options.dims !== undefined && !(Number.isInteger(options.dims) && options.dims > 0))
51
+ throw new AiError('AI0001', `dims must be a positive integer, got ${String(options.dims)}`);
52
+ const timeoutMs = options.timeoutMs;
53
+ if (timeoutMs !== undefined && !(Number.isFinite(timeoutMs) && timeoutMs > 0))
54
+ throw new AiError('AI0001', `timeoutMs must be a positive number, got ${String(timeoutMs)}`);
55
+ const url = `${endpoint.base}/embeddings`;
56
+ const fetchFn = options.fetch ?? ((u, init) => globalThis.fetch(u, init));
57
+ const retry = normalizeRetry(options.retry);
58
+ const cache = normalizeCache(options.cache);
59
+ let dims = options.dims;
60
+ /**
61
+ * One request/response cycle.
62
+ */
63
+ async function attemptOnce(texts, signal) {
64
+ // the attempt's own deadline rides beside the caller's signal; which
65
+ // of the two fired decides whether the failure is a retryable
66
+ // timeout or the caller's abort
67
+ let deadline = null;
68
+ let timer;
69
+ let requestSignal = signal;
70
+ if (timeoutMs !== undefined) {
71
+ deadline = new AbortController();
72
+ timer = setTimeout(() => deadline.abort(), timeoutMs);
73
+ requestSignal = signal === undefined ? deadline.signal : AbortSignal.any([signal, deadline.signal]);
74
+ }
75
+ let text;
76
+ try {
77
+ const response = await fetchFn(url, {
78
+ method: 'POST',
79
+ headers: endpoint.headers,
80
+ body: JSON.stringify({ model, input: texts }),
81
+ signal: requestSignal,
82
+ });
83
+ if (response.ok !== true)
84
+ throw await httpFailure(response, url);
85
+ text = await response.text();
86
+ }
87
+ catch (err) {
88
+ if (err instanceof AiError)
89
+ throw err;
90
+ if (deadline !== null && deadline.signal.aborted && signal?.aborted !== true)
91
+ throw new AiError('AI0002', `no answer from ${url} within ${timeoutMs} ms`, { status: 0, cause: err });
92
+ throw transportFailure(err, url);
93
+ }
94
+ finally {
95
+ clearTimeout(timer);
96
+ }
97
+ let payload;
98
+ try {
99
+ payload = JSON.parse(text);
100
+ }
101
+ catch {
102
+ throw new AiError('AI0003', `malformed embeddings reply: ${text.slice(0, 120)}`);
103
+ }
104
+ const vectors = reassemble(payload, texts.length, dims);
105
+ // the first reply settles the width; from here every reply must match
106
+ if (dims === undefined)
107
+ dims = vectors[0].length;
108
+ return vectors;
152
109
  }
153
- catch (err) {
154
- if (err instanceof AiError) throw err;
155
- if (deadline !== null && deadline.signal.aborted && signal?.aborted !== true)
156
- throw new AiError('AI0002', `no answer from ${url} within ${timeoutMs} ms`, { status: 0, cause: err });
157
- throw transportFailure(err, url);
110
+ /**
111
+ * @param [options]
112
+ */
113
+ async function embed(texts, options = {}) {
114
+ assertTexts(texts);
115
+ const { signal } = options;
116
+ /** @param batch */
117
+ const buy = (batch) => withRetry(retry, () => attemptOnce(batch, signal), { signal, retryable: isTransientFailure });
118
+ if (cache === null)
119
+ return buy(texts);
120
+ // per text: what the adapter remembers is placed, what it does not
121
+ // is bought in one call and remembered; a remembered vector's width
122
+ // is held to the settled one, and settles it when nothing has
123
+ const keys = texts.map((text) => replayKey('embeddings', endpoint, { model, input: text }));
124
+ const out = new Array(texts.length);
125
+ const missing = [];
126
+ for (let i = 0; i < texts.length; i++) {
127
+ const hit = await cache.get(keys[i]);
128
+ if (hit === undefined) {
129
+ missing.push(i);
130
+ continue;
131
+ }
132
+ const vector = verifyEmbeddingEntry(hit, dims);
133
+ if (dims === undefined)
134
+ dims = vector.length;
135
+ out[i] = vector;
136
+ }
137
+ if (missing.length > 0) {
138
+ const started = now();
139
+ const vectors = await buy(missing.map((i) => texts[i]));
140
+ const ms = now() - started;
141
+ for (let j = 0; j < missing.length; j++) {
142
+ out[missing[j]] = vectors[j];
143
+ await cache.set(keys[missing[j]], { vector: Array.from(vectors[j]), ms });
144
+ }
145
+ }
146
+ return out;
158
147
  }
159
- finally {
160
- clearTimeout(timer);
161
- }
162
- /** @type {any} */
163
- let payload;
164
- try {
165
- payload = JSON.parse(text);
166
- }
167
- catch {
168
- throw new AiError('AI0003', `malformed embeddings reply: ${text.slice(0, 120)}`);
169
- }
170
- const vectors = reassemble(payload, texts.length, dims);
171
- // the first reply settles the width; from here every reply must match
172
- if (dims === undefined) dims = vectors[0].length;
173
- return vectors;
174
- }
175
-
176
- /**
177
- * @param {string[]} texts
178
- * @param {{ signal?: AbortSignal }} [options]
179
- * @returns {Promise<Float32Array[]>}
180
- */
181
- async function embed(texts, options = {}) {
182
- assertTexts(texts);
183
- const { signal } = options;
184
- /** @param {string[]} batch */
185
- const buy = (batch) => withRetry(retry, () => attemptOnce(batch, signal), { signal, retryable: isTransientFailure });
186
- if (cache === null) return buy(texts);
187
-
188
- // per text: what the adapter remembers is placed, what it does not
189
- // is bought in one call and remembered; a remembered vector's width
190
- // is held to the settled one, and settles it when nothing has
191
- const keys = texts.map((text) => replayKey('embeddings', endpoint, { model, input: text }));
192
- /** @type {Float32Array[]} */
193
- const out = new Array(texts.length);
194
- /** @type {number[]} */
195
- const missing = [];
196
- for (let i = 0; i < texts.length; i++) {
197
- const hit = await cache.get(keys[i]);
198
- if (hit === undefined) {
199
- missing.push(i);
200
- continue;
201
- }
202
- const vector = verifyEmbeddingEntry(hit, dims);
203
- if (dims === undefined) dims = vector.length;
204
- out[i] = vector;
205
- }
206
- if (missing.length > 0) {
207
- const started = now();
208
- const vectors = await buy(missing.map((i) => texts[i]));
209
- const ms = now() - started;
210
- for (let j = 0; j < missing.length; j++) {
211
- out[missing[j]] = vectors[j];
212
- await cache.set(keys[missing[j]], { vector: Array.from(vectors[j]), ms });
213
- }
214
- }
215
- return out;
216
- }
217
-
218
- return {
219
- embed,
220
- model,
221
- provider: endpoint.provider,
222
- get dims() {
223
- return dims;
224
- },
225
- };
148
+ return {
149
+ embed,
150
+ model,
151
+ provider: endpoint.provider,
152
+ get dims() {
153
+ return dims;
154
+ },
155
+ };
226
156
  }
227
-
228
157
  /**
229
158
  * The verification: `data[]` reassembled by each item's `index`, every
230
159
  * input filled exactly once, every vector a non-empty array of finite
@@ -232,44 +161,40 @@ export function createEmbeddingClient(options = {}) {
232
161
  * that failed. Nothing is guessed: an item without a usable index is
233
162
  * refused rather than placed by position, and a vector of the wrong
234
163
  * width is refused rather than cut or padded.
235
- * @param {any} payload
236
- * @param {number} count - the number of inputs sent
237
- * @param {number | undefined} dims - the width every vector must have,
164
+ * @param count - the number of inputs sent
165
+ * @param dims - the width every vector must have,
238
166
  * or undefined to let input 0's vector settle it for this reply
239
- * @returns {Float32Array[]}
240
167
  */
241
168
  function reassemble(payload, count, dims) {
242
- const data = payload?.data;
243
- if (!Array.isArray(data))
244
- throw new AiError('AI0003', 'malformed embeddings reply: no data[] in the response');
245
- if (data.length !== count)
246
- throw new AiError('AI0003', `embeddings reply carried ${data.length} items for ${count} inputs`);
247
- /** @type {any[]} */
248
- const slots = new Array(count);
249
- for (let i = 0; i < count; i++) {
250
- const item = data[i];
251
- const index = item?.index;
252
- if (!Number.isInteger(index) || index < 0 || index >= count)
253
- throw new AiError('AI0003',
254
- `embeddings reply item ${i} carries no usable index for ${count} inputs (got ${JSON.stringify(index)})`);
255
- if (slots[index] !== undefined)
256
- throw new AiError('AI0003', `input ${index} received two embeddings`);
257
- slots[index] = item.embedding;
258
- }
259
- // `count` items over `count` distinct indices: every slot is filled
260
- const out = new Array(count);
261
- for (let i = 0; i < count; i++) {
262
- const embedding = slots[i];
263
- if (!Array.isArray(embedding) || embedding.length === 0)
264
- throw new AiError('AI0003', `input ${i} received an empty embedding`);
265
- if (dims === undefined) dims = embedding.length;
266
- if (embedding.length !== dims)
267
- throw new AiError('AI0003', `input ${i} received ${embedding.length} dimensions, expected ${dims}`);
268
- out[i] = verifyEmbeddingComponents(embedding, `input ${i} received`);
269
- }
270
- return out;
169
+ const data = payload?.data;
170
+ if (!Array.isArray(data))
171
+ throw new AiError('AI0003', 'malformed embeddings reply: no data[] in the response');
172
+ if (data.length !== count)
173
+ throw new AiError('AI0003', `embeddings reply carried ${data.length} items for ${count} inputs`);
174
+ const slots = new Array(count);
175
+ for (let i = 0; i < count; i++) {
176
+ const item = data[i];
177
+ const index = item?.index;
178
+ if (!Number.isInteger(index) || index < 0 || index >= count)
179
+ throw new AiError('AI0003', `embeddings reply item ${i} carries no usable index for ${count} inputs (got ${JSON.stringify(index)})`);
180
+ if (slots[index] !== undefined)
181
+ throw new AiError('AI0003', `input ${index} received two embeddings`);
182
+ slots[index] = item.embedding;
183
+ }
184
+ // `count` items over `count` distinct indices: every slot is filled
185
+ const out = new Array(count);
186
+ for (let i = 0; i < count; i++) {
187
+ const embedding = slots[i];
188
+ if (!Array.isArray(embedding) || embedding.length === 0)
189
+ throw new AiError('AI0003', `input ${i} received an empty embedding`);
190
+ if (dims === undefined)
191
+ dims = embedding.length;
192
+ if (embedding.length !== dims)
193
+ throw new AiError('AI0003', `input ${i} received ${embedding.length} dimensions, expected ${dims}`);
194
+ out[i] = verifyEmbeddingComponents(embedding, `input ${i} received`);
195
+ }
196
+ return out;
271
197
  }
272
-
273
198
  /**
274
199
  * Probe the embeddings wire before relying on it: can this key/URL/model
275
200
  * embed at all, and at what width? Embeds one word with exactly the
@@ -277,43 +202,37 @@ function reassemble(payload, count, dims) {
277
202
  * (default 5 000, as `probeProvider`). Never throws — the result object
278
203
  * is the settings-UI contract, and the live proof that a provider
279
204
  * really serves `/embeddings` beside `/chat/completions`.
280
- * @param {{ provider?: string, baseUrl?: string, apiKey?: string,
281
- * model?: string, dims?: number, headers?: Record<string, string>,
282
- * fetch?: typeof fetch, timeoutMs?: number }} [options]
283
- * @returns {Promise<{ ok: true, model: string, dims: number } |
284
- * { ok: false, status?: number, error: string }>}
205
+ * @param [options]
285
206
  */
286
207
  export async function probeEmbeddings(options = {}) {
287
- /** @type {ReturnType<typeof createEmbeddingClient>} */
288
- let client;
289
- try {
290
- // never through a cache: a probe's job is to prove the wire answers today
291
- client = createEmbeddingClient({
292
- ...options,
293
- cache: undefined,
294
- retry: { attempts: 1 },
295
- timeoutMs: options.timeoutMs ?? 5000,
296
- });
297
- }
298
- catch (err) {
299
- return { ok: false, error: /** @type {Error} */ (err).message };
300
- }
301
- try {
302
- const [vector] = await client.embed(['probe']);
303
- return { ok: true, model: client.model, dims: vector.length };
304
- }
305
- catch (err) {
306
- const status = err instanceof AiError && typeof err.status === 'number' && err.status > 0
307
- ? err.status
308
- : undefined;
309
- return {
310
- ok: false,
311
- ...(status === undefined ? {} : { status }),
312
- error: /** @type {any} */ (err)?.message ?? String(err),
313
- };
314
- }
208
+ let client;
209
+ try {
210
+ // never through a cache: a probe's job is to prove the wire answers today
211
+ client = createEmbeddingClient({
212
+ ...options,
213
+ cache: undefined,
214
+ retry: { attempts: 1 },
215
+ timeoutMs: options.timeoutMs ?? 5000,
216
+ });
217
+ }
218
+ catch (err) {
219
+ return { ok: false, error: err.message };
220
+ }
221
+ try {
222
+ const [vector] = await client.embed(['probe']);
223
+ return { ok: true, model: client.model, dims: vector.length };
224
+ }
225
+ catch (err) {
226
+ const status = err instanceof AiError && typeof err.status === 'number' && err.status > 0
227
+ ? err.status
228
+ : undefined;
229
+ return {
230
+ ok: false,
231
+ ...(status === undefined ? {} : { status }),
232
+ error: err?.message ?? String(err),
233
+ };
234
+ }
315
235
  }
316
-
317
236
  /**
318
237
  * The deterministic reference embedder — demo-grade, for tests and
319
238
  * offline demos. Each text becomes the bag of its case-folded character
@@ -325,38 +244,34 @@ export async function probeEmbeddings(options = {}) {
325
244
  * thing — so it exercises retrieval mechanics (does the right memory
326
245
  * reach the prompt?) without saying anything about embedding quality,
327
246
  * which belongs to a real model behind the same seam.
328
- * @param {{ dims?: number }} [options] - the width (default 64); the
247
+ * @param [options] - the width (default 64); the
329
248
  * identity is `hash-trigram-<dims>`, so two widths never mix
330
- * @returns {Embedder & { dims: number }}
331
249
  */
332
250
  export function createHashEmbedder(options = {}) {
333
- const dims = options.dims ?? 64;
334
- if (!(Number.isInteger(dims) && dims > 0))
335
- throw new AiError('AI0001', `createHashEmbedder needs a positive integer dims, got ${String(dims)}`);
336
- const model = `hash-trigram-${dims}`;
337
-
338
- /**
339
- * @param {string} text
340
- * @returns {Float32Array}
341
- */
342
- function trigramVector(text) {
343
- const counts = new Float32Array(dims);
344
- const padded = ` ${text.toLowerCase()} `;
345
- for (let i = 0; i + 3 <= padded.length; i++)
346
- counts[fnv1a(padded.slice(i, i + 3)) % dims] += 1;
347
- // counts is finite and non-empty, so this is never null; a text
348
- // without a single trigram stays the zero vector, which scores 0
349
- // against everything rather than NaN
350
- return /** @type {Float32Array} */ (l2Normalize(counts));
351
- }
352
-
353
- return {
354
- model,
355
- dims,
356
- async embed(texts, options = {}) {
357
- assertTexts(texts);
358
- if (options.signal?.aborted) throw abortError(options.signal);
359
- return texts.map(trigramVector);
360
- },
361
- };
251
+ const dims = options.dims ?? 64;
252
+ if (!(Number.isInteger(dims) && dims > 0))
253
+ throw new AiError('AI0001', `createHashEmbedder needs a positive integer dims, got ${String(dims)}`);
254
+ const model = `hash-trigram-${dims}`;
255
+ /**
256
+ */
257
+ function trigramVector(text) {
258
+ const counts = new Float32Array(dims);
259
+ const padded = ` ${text.toLowerCase()} `;
260
+ for (let i = 0; i + 3 <= padded.length; i++)
261
+ counts[fnv1a(padded.slice(i, i + 3)) % dims] += 1;
262
+ // counts is finite and non-empty, so this is never null; a text
263
+ // without a single trigram stays the zero vector, which scores 0
264
+ // against everything rather than NaN
265
+ return l2Normalize(counts);
266
+ }
267
+ return {
268
+ model,
269
+ dims,
270
+ async embed(texts, options = {}) {
271
+ assertTexts(texts);
272
+ if (options.signal?.aborted)
273
+ throw abortError(options.signal);
274
+ return texts.map(trigramVector);
275
+ },
276
+ };
362
277
  }
@@ -1,9 +1,14 @@
1
+ /**
2
+ * The embeddings wire and replay share one AI0003 conversion policy:
3
+ * each component must remain finite in the Float32Array the client
4
+ * returns. Shape and width are checked by the caller, which also
5
+ * supplies the subject naming the input or stored entry that failed.
6
+ */
1
7
  /**
2
8
  * Verify embedding components into a fresh Float32Array, refusing
3
9
  * non-numbers, non-finite values and finite numbers that overflow
4
10
  * Float32. The source array is never changed.
5
- * @param {any[]} components - an array whose shape and width were checked
6
- * @param {string} subject - the subject and verb of a refusal
7
- * @returns {Float32Array}
11
+ * @param components - an array whose shape and width were checked
12
+ * @param subject - the subject and verb of a refusal
8
13
  */
9
- export function verifyEmbeddingComponents(components: any[], subject: string): Float32Array;
14
+ export declare function verifyEmbeddingComponents(components: any[], subject: string): Float32Array;
@@ -1,30 +1,26 @@
1
- //@ts-check
2
1
  /**
3
2
  * The embeddings wire and replay share one AI0003 conversion policy:
4
3
  * each component must remain finite in the Float32Array the client
5
4
  * returns. Shape and width are checked by the caller, which also
6
5
  * supplies the subject naming the input or stored entry that failed.
7
6
  */
8
-
9
- import { AiError } from './errors.js';
10
-
7
+ import { AiError } from "./errors.js";
11
8
  /**
12
9
  * Verify embedding components into a fresh Float32Array, refusing
13
10
  * non-numbers, non-finite values and finite numbers that overflow
14
11
  * Float32. The source array is never changed.
15
- * @param {any[]} components - an array whose shape and width were checked
16
- * @param {string} subject - the subject and verb of a refusal
17
- * @returns {Float32Array}
12
+ * @param components - an array whose shape and width were checked
13
+ * @param subject - the subject and verb of a refusal
18
14
  */
19
15
  export function verifyEmbeddingComponents(components, subject) {
20
- const vector = new Float32Array(components.length);
21
- for (let i = 0; i < components.length; i++) {
22
- const x = components[i];
23
- if (typeof x !== 'number' || !Number.isFinite(x))
24
- throw new AiError('AI0003', `${subject} a component that is not a finite number at ${i}`);
25
- vector[i] = x;
26
- if (!Number.isFinite(vector[i]))
27
- throw new AiError('AI0003', `${subject} a component outside the finite Float32 range at ${i}`);
28
- }
29
- return vector;
16
+ const vector = new Float32Array(components.length);
17
+ for (let i = 0; i < components.length; i++) {
18
+ const x = components[i];
19
+ if (typeof x !== 'number' || !Number.isFinite(x))
20
+ throw new AiError('AI0003', `${subject} a component that is not a finite number at ${i}`);
21
+ vector[i] = x;
22
+ if (!Number.isFinite(vector[i]))
23
+ throw new AiError('AI0003', `${subject} a component outside the finite Float32 range at ${i}`);
24
+ }
25
+ return vector;
30
26
  }
package/src/errors.d.ts CHANGED
@@ -1,11 +1,32 @@
1
- export class AiError extends CodedError {
1
+ /**
2
+ * The model transport error type. Every failure the package itself raises
3
+ * carries a stable code, like the rest of the suite:
4
+ *
5
+ * AI0001 — invalid configuration or request (caller error)
6
+ * AI0002 — the provider answered with an HTTP error status
7
+ * AI0003 — the provider answered with a malformed payload
8
+ *
9
+ * Tool execution and the agent loop never throw for content-level
10
+ * problems (an unknown tool, invalid tool input, a tool that throws) —
11
+ * those come back as `{ error }` results the model can read and
12
+ * recover from. AiError is reserved for the transport and for misuse.
13
+ *
14
+ * Refinement (`refine.ts`) rejects with its own `AI01xx` codes, enumerated
15
+ * there. They are the same idea one step further out: never thrown, they
16
+ * travel as records with a pointer, because their reader is a model
17
+ * repairing its own proposal.
18
+ */
19
+ import { CodedError } from '@jarenjs/core/errors';
20
+ export declare class AiError extends CodedError {
21
+ status?: number;
22
+ attempts?: number;
23
+ retryAfterMs?: number;
2
24
  /**
3
- * @param {string} code - stable error code ('AI0001' | 'AI0002' | 'AI0003')
4
- * @param {string} reason - The bare reason; `message` is composed as
25
+ * @param code - stable error code ('AI0001' | 'AI0002' | 'AI0003')
26
+ * @param reason - The bare reason; `message` is composed as
5
27
  * `${code}: ${reason}` per the coded contract (transport errors
6
28
  * have no document, so there is never a location).
7
- * @param {{ status?: number, attempts?: number, retryAfterMs?: number,
8
- * cause?: unknown }} [meta] - transport metadata: the HTTP status
29
+ * @param [meta] - transport metadata: the HTTP status
9
30
  * (`0` for a network failure before any response), how many tries
10
31
  * the client made, and the provider's `Retry-After` in ms
11
32
  */
@@ -15,8 +36,4 @@ export class AiError extends CodedError {
15
36
  retryAfterMs?: number;
16
37
  cause?: unknown;
17
38
  });
18
- status: number;
19
- attempts: number;
20
- retryAfterMs: number;
21
39
  }
22
- import { CodedError } from '@jarenjs/core/errors';