@oh-my-pi/pi-mnemosyne 15.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +107 -0
  2. package/dist/types/cli.d.ts +35 -0
  3. package/dist/types/config.d.ts +77 -0
  4. package/dist/types/core/aaak.d.ts +55 -0
  5. package/dist/types/core/annotations.d.ts +75 -0
  6. package/dist/types/core/banks.d.ts +33 -0
  7. package/dist/types/core/beam/consolidate.d.ts +32 -0
  8. package/dist/types/core/beam/helpers.d.ts +59 -0
  9. package/dist/types/core/beam/index.d.ts +59 -0
  10. package/dist/types/core/beam/recall.d.ts +32 -0
  11. package/dist/types/core/beam/schema.d.ts +2 -0
  12. package/dist/types/core/beam/store.d.ts +35 -0
  13. package/dist/types/core/beam/types.d.ts +233 -0
  14. package/dist/types/core/binary-vectors.d.ts +54 -0
  15. package/dist/types/core/chat-normalize.d.ts +13 -0
  16. package/dist/types/core/content-sanitizer.d.ts +18 -0
  17. package/dist/types/core/cost-log.d.ts +13 -0
  18. package/dist/types/core/embeddings.d.ts +35 -0
  19. package/dist/types/core/entities.d.ts +7 -0
  20. package/dist/types/core/episodic-graph.d.ts +89 -0
  21. package/dist/types/core/extraction/client.d.ts +31 -0
  22. package/dist/types/core/extraction/diagnostics.d.ts +51 -0
  23. package/dist/types/core/extraction/prompts.d.ts +2 -0
  24. package/dist/types/core/extraction.d.ts +6 -0
  25. package/dist/types/core/index.d.ts +4 -0
  26. package/dist/types/core/llm-backends.d.ts +21 -0
  27. package/dist/types/core/local-llm.d.ts +15 -0
  28. package/dist/types/core/memory.d.ts +160 -0
  29. package/dist/types/core/migrations/e6-triplestore-split.d.ts +17 -0
  30. package/dist/types/core/migrations/index.d.ts +1 -0
  31. package/dist/types/core/mmr.d.ts +8 -0
  32. package/dist/types/core/orchestrator.d.ts +20 -0
  33. package/dist/types/core/patterns.d.ts +61 -0
  34. package/dist/types/core/plugins.d.ts +109 -0
  35. package/dist/types/core/polyphonic-recall.d.ts +66 -0
  36. package/dist/types/core/query-cache.d.ts +47 -0
  37. package/dist/types/core/query-intent.d.ts +20 -0
  38. package/dist/types/core/recall-diagnostics.d.ts +48 -0
  39. package/dist/types/core/runtime-options.d.ts +61 -0
  40. package/dist/types/core/shmr.d.ts +56 -0
  41. package/dist/types/core/streaming.d.ts +136 -0
  42. package/dist/types/core/synonyms.d.ts +46 -0
  43. package/dist/types/core/temporal-parser.d.ts +16 -0
  44. package/dist/types/core/token-counter.d.ts +8 -0
  45. package/dist/types/core/triples.d.ts +63 -0
  46. package/dist/types/core/typed-memory.d.ts +39 -0
  47. package/dist/types/core/veracity-consolidation.d.ts +60 -0
  48. package/dist/types/core/weibull.d.ts +96 -0
  49. package/dist/types/db.d.ts +16 -0
  50. package/dist/types/diagnose.d.ts +24 -0
  51. package/dist/types/dr/index.d.ts +1 -0
  52. package/dist/types/dr/recovery.d.ts +68 -0
  53. package/dist/types/index.d.ts +5 -0
  54. package/dist/types/mcp-server.d.ts +40 -0
  55. package/dist/types/mcp-tools.d.ts +484 -0
  56. package/dist/types/migrations/e6-triplestore-split.d.ts +1 -0
  57. package/dist/types/migrations/index.d.ts +1 -0
  58. package/dist/types/types.d.ts +145 -0
  59. package/dist/types/util/datetime.d.ts +8 -0
  60. package/dist/types/util/env.d.ts +10 -0
  61. package/dist/types/util/ids.d.ts +3 -0
  62. package/dist/types/util/lru.d.ts +12 -0
  63. package/dist/types/util/regex.d.ts +10 -0
  64. package/package.json +82 -0
  65. package/src/cli.ts +390 -0
  66. package/src/config.ts +326 -0
  67. package/src/core/aaak.ts +142 -0
  68. package/src/core/annotations.ts +457 -0
  69. package/src/core/banks.ts +133 -0
  70. package/src/core/beam/consolidate.ts +963 -0
  71. package/src/core/beam/helpers.ts +920 -0
  72. package/src/core/beam/index.ts +353 -0
  73. package/src/core/beam/recall.ts +1091 -0
  74. package/src/core/beam/schema.ts +423 -0
  75. package/src/core/beam/store.ts +818 -0
  76. package/src/core/beam/types.ts +268 -0
  77. package/src/core/binary-vectors.ts +336 -0
  78. package/src/core/chat-normalize.ts +160 -0
  79. package/src/core/content-sanitizer.ts +136 -0
  80. package/src/core/cost-log.ts +103 -0
  81. package/src/core/embeddings.ts +490 -0
  82. package/src/core/entities.ts +259 -0
  83. package/src/core/episodic-graph.ts +708 -0
  84. package/src/core/extraction/client.ts +162 -0
  85. package/src/core/extraction/diagnostics.ts +193 -0
  86. package/src/core/extraction/prompts.ts +31 -0
  87. package/src/core/extraction.ts +335 -0
  88. package/src/core/index.ts +30 -0
  89. package/src/core/llm-backends.ts +51 -0
  90. package/src/core/local-llm.ts +436 -0
  91. package/src/core/memory.ts +617 -0
  92. package/src/core/migrations/e6-triplestore-split.ts +211 -0
  93. package/src/core/migrations/index.ts +1 -0
  94. package/src/core/mmr.ts +71 -0
  95. package/src/core/orchestrator.ts +53 -0
  96. package/src/core/patterns.ts +484 -0
  97. package/src/core/plugins.ts +375 -0
  98. package/src/core/polyphonic-recall.ts +563 -0
  99. package/src/core/query-cache.ts +370 -0
  100. package/src/core/query-intent.ts +139 -0
  101. package/src/core/recall-diagnostics.ts +157 -0
  102. package/src/core/runtime-options.ts +108 -0
  103. package/src/core/shmr.ts +471 -0
  104. package/src/core/streaming.ts +419 -0
  105. package/src/core/synonyms.ts +197 -0
  106. package/src/core/temporal-parser.ts +363 -0
  107. package/src/core/token-counter.ts +30 -0
  108. package/src/core/triples.ts +452 -0
  109. package/src/core/typed-memory.ts +407 -0
  110. package/src/core/veracity-consolidation.ts +477 -0
  111. package/src/core/weibull.ts +124 -0
  112. package/src/db.ts +128 -0
  113. package/src/diagnose.ts +174 -0
  114. package/src/dr/index.ts +1 -0
  115. package/src/dr/recovery.ts +405 -0
  116. package/src/index.ts +32 -0
  117. package/src/mcp-server.ts +155 -0
  118. package/src/mcp-tools.ts +961 -0
  119. package/src/migrations/e6-triplestore-split.ts +1 -0
  120. package/src/migrations/index.ts +1 -0
  121. package/src/types.ts +157 -0
  122. package/src/util/datetime.ts +69 -0
  123. package/src/util/env.ts +65 -0
  124. package/src/util/ids.ts +19 -0
  125. package/src/util/lru.ts +48 -0
  126. package/src/util/regex.ts +165 -0
@@ -0,0 +1,490 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { EmbeddingModel, FlagEmbedding } from "fastembed";
3
+ import { getMnemosyneRuntimeOptions, resolveEmbeddingProvider } from "./runtime-options";
4
+
5
+ export type Vector = number[];
6
+ export type EmbeddingMatrix = Vector[];
7
+
8
+ export interface EmbeddingProvider {
9
+ embed(texts: readonly string[]): unknown | Promise<unknown>;
10
+ available?(): boolean | Promise<boolean>;
11
+ }
12
+
13
+ type StandardEmbeddingModel = Exclude<EmbeddingModel, EmbeddingModel.CUSTOM>;
14
+
15
+ interface LocalEmbeddingModel {
16
+ embed(texts: string[], batchSize?: number): unknown;
17
+ queryEmbed?(query: string): Promise<number[]>;
18
+ }
19
+
20
+ type LocalModelInitOptions = {
21
+ model: StandardEmbeddingModel;
22
+ cacheDir?: string;
23
+ showDownloadProgress?: boolean;
24
+ };
25
+ type LocalModelInitializer = (options: LocalModelInitOptions) => Promise<LocalEmbeddingModel>;
26
+
27
+ const FASTEMBED_CACHE_DIR = `${process.env.HOME ?? ""}/.hermes/cache/fastembed`;
28
+ const QUERY_CACHE_MAX = 512;
29
+
30
+ let providerOverride: EmbeddingProvider | null = null;
31
+ let localModelPromise: Promise<LocalEmbeddingModel> | null = null;
32
+ let localModelInitializer: LocalModelInitializer = defaultLocalModelInitializer;
33
+ let apiCallCount = 0;
34
+ const queryCache = new Map<string, Vector>();
35
+
36
+ function defaultLocalModelInitializer(options: LocalModelInitOptions): Promise<LocalEmbeddingModel> {
37
+ return FlagEmbedding.init(options) as Promise<LocalEmbeddingModel>;
38
+ }
39
+
40
+ function activeEmbeddingOptions() {
41
+ return getMnemosyneRuntimeOptions()?.embeddings;
42
+ }
43
+
44
+ function env(name: string): string {
45
+ return process.env[name] ?? "";
46
+ }
47
+
48
+ function truthy(value: string): boolean {
49
+ switch (value.trim().toLowerCase()) {
50
+ case "1":
51
+ case "true":
52
+ case "yes":
53
+ case "on":
54
+ return true;
55
+ default:
56
+ return false;
57
+ }
58
+ }
59
+
60
+ function inTestRuntime(): boolean {
61
+ return env("NODE_ENV") === "test" || env("BUN_ENV") === "test";
62
+ }
63
+
64
+ function embeddingsDisabled(): boolean {
65
+ const active = activeEmbeddingOptions();
66
+ if (active?.disabled !== undefined) {
67
+ return active.disabled;
68
+ }
69
+ return truthy(env("MNEMOSYNE_NO_EMBEDDINGS"));
70
+ }
71
+
72
+ function embeddingApiKey(): string {
73
+ const active = activeEmbeddingOptions();
74
+ if (active?.apiKey !== undefined) {
75
+ return active.apiKey;
76
+ }
77
+ return env("MNEMOSYNE_EMBEDDING_API_KEY") || env("OPENROUTER_API_KEY") || env("OPENAI_API_KEY");
78
+ }
79
+
80
+ function embeddingBaseUrl(): string {
81
+ const active = activeEmbeddingOptions();
82
+ if (active?.apiUrl !== undefined) {
83
+ return active.apiUrl;
84
+ }
85
+ return env("MNEMOSYNE_EMBEDDING_API_URL") || env("OPENROUTER_BASE_URL") || "https://openrouter.ai/api/v1";
86
+ }
87
+
88
+ function defaultModel(): string {
89
+ const active = activeEmbeddingOptions();
90
+ if (active?.model !== undefined) {
91
+ return active.model;
92
+ }
93
+ return env("MNEMOSYNE_EMBEDDING_MODEL") || "BAAI/bge-small-en-v1.5";
94
+ }
95
+
96
+ export function isApiModel(modelName: string): boolean {
97
+ if (
98
+ modelName.startsWith("openai/") ||
99
+ modelName.includes("text-embedding") ||
100
+ modelName.startsWith("text-embedding")
101
+ ) {
102
+ return true;
103
+ }
104
+ const active = activeEmbeddingOptions();
105
+ const baseUrl = active?.apiUrl ?? (env("MNEMOSYNE_EMBEDDING_API_URL") || env("OPENROUTER_BASE_URL"));
106
+ if (baseUrl !== undefined && baseUrl !== "" && !baseUrl.includes("openrouter.ai")) {
107
+ return true;
108
+ }
109
+ return truthy(env("MNEMOSYNE_EMBEDDINGS_VIA_API"));
110
+ }
111
+
112
+ export function embeddingDimFor(modelName: string): number {
113
+ const override = Number.parseInt(env("MNEMOSYNE_EMBEDDING_DIM"), 10);
114
+ if (Number.isFinite(override)) {
115
+ return override;
116
+ }
117
+
118
+ const dims: Record<string, number> = {
119
+ "BAAI/bge-small-en-v1.5": 384,
120
+ "BAAI/bge-base-en-v1.5": 768,
121
+ "BAAI/bge-large-en-v1.5": 1024,
122
+ "BAAI/bge-small-zh-v1.5": 512,
123
+ "BAAI/bge-base-zh-v1.5": 768,
124
+ "BAAI/bge-large-zh-v1.5": 1024,
125
+ "intfloat/multilingual-e5-small": 384,
126
+ "intfloat/multilingual-e5-base": 768,
127
+ "intfloat/multilingual-e5-large": 1024,
128
+ "BAAI/bge-m3": 1024,
129
+ "BAAI/bge-multilingual-gemma2": 3584,
130
+ "openai/text-embedding-3-small": 1536,
131
+ "openai/text-embedding-3-large": 3072,
132
+ "text-embedding-3-small": 1536,
133
+ "text-embedding-3-large": 3072,
134
+ "jina-embeddings-v5-omni-nano": 768,
135
+ "jina-embeddings-v5-omni-small": 1024,
136
+ };
137
+ return dims[modelName] ?? 384;
138
+ }
139
+
140
+ function normalizeVector(input: unknown): Vector | null {
141
+ // Accept Array or TypedArray (ArrayLike with length and numeric indexed access)
142
+ if (input == null || typeof input !== "object") {
143
+ return null;
144
+ }
145
+ const arr = input as unknown as ArrayLike<unknown>;
146
+ if (typeof arr.length !== "number" || !Number.isFinite(arr.length)) {
147
+ return null;
148
+ }
149
+ // Must be an Array or TypedArray (ArrayBuffer.isView), reject plain objects
150
+ if (!Array.isArray(input) && !ArrayBuffer.isView(input)) {
151
+ return null;
152
+ }
153
+ const vector = new Array<number>(arr.length);
154
+ for (let i = 0; i < arr.length; i += 1) {
155
+ const value = Number(arr[i]);
156
+ if (!Number.isFinite(value)) {
157
+ return null;
158
+ }
159
+ vector[i] = value;
160
+ }
161
+ return vector;
162
+ }
163
+ function isVectorLike(value: unknown): boolean {
164
+ if (value == null || typeof value !== "object") {
165
+ return false;
166
+ }
167
+ // Accept Array or TypedArray (ArrayBuffer.isView), but reject DataView
168
+ if (Array.isArray(value)) {
169
+ return true;
170
+ }
171
+ if (ArrayBuffer.isView(value)) {
172
+ // Reject DataView as it's not numeric-indexed
173
+ return !(value instanceof DataView);
174
+ }
175
+ return false;
176
+ }
177
+
178
+ function appendNormalized(rows: Vector[], input: unknown): boolean {
179
+ if (Array.isArray(input) && input.length > 0 && isVectorLike(input[0])) {
180
+ for (const item of input) {
181
+ const row = normalizeVector(item);
182
+ if (row === null) {
183
+ return false;
184
+ }
185
+ rows.push(row);
186
+ }
187
+ return true;
188
+ }
189
+
190
+ const vector = normalizeVector(input);
191
+ if (vector !== null) {
192
+ rows.push(vector);
193
+ return true;
194
+ }
195
+ return false;
196
+ }
197
+
198
+ async function normalizeEmbeddingResult(result: unknown): Promise<EmbeddingMatrix | null> {
199
+ const rows: Vector[] = [];
200
+ if (Array.isArray(result)) {
201
+ return appendNormalized(rows, result) ? rows : null;
202
+ }
203
+ if (result !== null && typeof result === "object" && Symbol.asyncIterator in result) {
204
+ for await (const item of result as AsyncIterable<unknown>) {
205
+ if (!appendNormalized(rows, item)) {
206
+ return null;
207
+ }
208
+ }
209
+ return rows;
210
+ }
211
+ if (result !== null && typeof result === "object" && Symbol.iterator in result) {
212
+ for (const item of result as Iterable<unknown>) {
213
+ if (!appendNormalized(rows, item)) {
214
+ return null;
215
+ }
216
+ }
217
+ return rows;
218
+ }
219
+ return null;
220
+ }
221
+
222
+ function cacheGet(key: string): Vector | null {
223
+ const value = queryCache.get(key);
224
+ if (value === undefined) {
225
+ return null;
226
+ }
227
+ queryCache.delete(key);
228
+ queryCache.set(key, value);
229
+ return value;
230
+ }
231
+
232
+ function cacheSet(key: string, value: Vector): void {
233
+ if (queryCache.has(key)) {
234
+ queryCache.delete(key);
235
+ }
236
+ queryCache.set(key, value);
237
+ if (queryCache.size > QUERY_CACHE_MAX) {
238
+ const oldest = queryCache.keys().next().value as string | undefined;
239
+ if (oldest !== undefined) {
240
+ queryCache.delete(oldest);
241
+ }
242
+ }
243
+ }
244
+
245
+ function fastembedModelName(modelName: string): StandardEmbeddingModel | null {
246
+ const known: Record<string, StandardEmbeddingModel> = {
247
+ "BAAI/bge-small-en-v1.5": EmbeddingModel.BGESmallENV15,
248
+ "BAAI/bge-base-en-v1.5": EmbeddingModel.BGEBaseENV15,
249
+ "BAAI/bge-small-en": EmbeddingModel.BGESmallEN,
250
+ "BAAI/bge-base-en": EmbeddingModel.BGEBaseEN,
251
+ "BAAI/bge-small-zh-v1.5": EmbeddingModel.BGESmallZH,
252
+ "intfloat/multilingual-e5-large": EmbeddingModel.MLE5Large,
253
+ "sentence-transformers/all-MiniLM-L6-v2": EmbeddingModel.AllMiniLML6V2,
254
+ };
255
+ return known[modelName] ?? null;
256
+ }
257
+
258
+ async function getLocalModel(): Promise<LocalEmbeddingModel | null> {
259
+ if (isApiModel(defaultModel()) || embeddingsDisabled() || inTestRuntime()) {
260
+ return null;
261
+ }
262
+ if (localModelPromise !== null) {
263
+ return localModelPromise;
264
+ }
265
+
266
+ const modelName = fastembedModelName(defaultModel());
267
+ if (modelName === null) {
268
+ return null;
269
+ }
270
+ mkdirSync(FASTEMBED_CACHE_DIR, { recursive: true });
271
+ const loading = localModelInitializer({
272
+ model: modelName,
273
+ cacheDir: FASTEMBED_CACHE_DIR,
274
+ showDownloadProgress: false,
275
+ });
276
+ localModelPromise = loading;
277
+ try {
278
+ return await loading;
279
+ } catch {
280
+ if (localModelPromise === loading) localModelPromise = null;
281
+ return null;
282
+ }
283
+ }
284
+
285
+ async function embedApi(texts: readonly string[]): Promise<EmbeddingMatrix | null> {
286
+ const baseUrl = embeddingBaseUrl();
287
+ const isCustom = !baseUrl.includes("openrouter.ai");
288
+ const apiKey = embeddingApiKey();
289
+ if (!isCustom && apiKey === "") {
290
+ return null;
291
+ }
292
+
293
+ const headers: Record<string, string> = {
294
+ "Content-Type": "application/json",
295
+ "HTTP-Referer": "https://mnemosyne.site",
296
+ "X-Title": "Mnemosyne Embedding",
297
+ };
298
+ if (apiKey !== "") {
299
+ headers.Authorization = `Bearer ${apiKey}`;
300
+ }
301
+
302
+ for (let attempt = 0; attempt < 3; attempt += 1) {
303
+ try {
304
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/embeddings`, {
305
+ method: "POST",
306
+ headers,
307
+ body: JSON.stringify({ model: defaultModel(), input: texts }),
308
+ signal: AbortSignal.timeout(30000),
309
+ });
310
+ if ((response.status === 429 || response.status === 503) && attempt < 2) {
311
+ await Bun.sleep(2 ** attempt * 1000);
312
+ continue;
313
+ }
314
+ if (!response.ok) {
315
+ return null;
316
+ }
317
+ const data = (await response.json()) as { data?: Array<{ embedding?: unknown }> };
318
+ const rows = data.data;
319
+ if (rows === undefined) {
320
+ return null;
321
+ }
322
+ const vectors: Vector[] = [];
323
+ for (const row of rows) {
324
+ const vector = normalizeVector(row.embedding);
325
+ if (vector === null) {
326
+ return null;
327
+ }
328
+ vectors.push(vector);
329
+ }
330
+ apiCallCount += 1;
331
+ return vectors;
332
+ } catch {
333
+ return null;
334
+ }
335
+ }
336
+ return null;
337
+ }
338
+
339
+ async function providerAvailable(provider: EmbeddingProvider): Promise<boolean> {
340
+ if (provider.available === undefined) {
341
+ return true;
342
+ }
343
+ try {
344
+ return await provider.available();
345
+ } catch {
346
+ return false;
347
+ }
348
+ }
349
+
350
+ export function setEmbeddingProviderForTests(provider: EmbeddingProvider | null | undefined): void {
351
+ providerOverride = provider ?? null;
352
+ queryCache.clear();
353
+ }
354
+
355
+ export const setEmbeddingProvider = setEmbeddingProviderForTests;
356
+
357
+ export function setLocalModelInitializerForTests(initializer: LocalModelInitializer | null | undefined): void {
358
+ localModelInitializer = initializer ?? defaultLocalModelInitializer;
359
+ localModelPromise = null;
360
+ queryCache.clear();
361
+ }
362
+
363
+ export function resetEmbeddingProviderForTests(): void {
364
+ providerOverride = null;
365
+ localModelPromise = null;
366
+ localModelInitializer = defaultLocalModelInitializer;
367
+ apiCallCount = 0;
368
+ queryCache.clear();
369
+ }
370
+
371
+ export const resetEmbeddingStateForTests = resetEmbeddingProviderForTests;
372
+
373
+ export async function available(): Promise<boolean> {
374
+ if (embeddingsDisabled()) {
375
+ return false;
376
+ }
377
+ const active = activeEmbeddingOptions();
378
+ const activeProvider = resolveEmbeddingProvider(active?.provider);
379
+ if (activeProvider !== undefined) {
380
+ return providerAvailable(activeProvider);
381
+ }
382
+ if (providerOverride !== null) {
383
+ return providerAvailable(providerOverride);
384
+ }
385
+ if (isApiModel(defaultModel())) {
386
+ const baseUrl = active?.apiUrl ?? (env("MNEMOSYNE_EMBEDDING_API_URL") || env("OPENROUTER_BASE_URL"));
387
+ if (baseUrl !== undefined && baseUrl !== "" && !baseUrl.includes("openrouter.ai")) {
388
+ return true;
389
+ }
390
+ return embeddingApiKey() !== "";
391
+ }
392
+ if (inTestRuntime()) {
393
+ return false;
394
+ }
395
+ return fastembedModelName(defaultModel()) !== null;
396
+ }
397
+
398
+ export function availableApi(): boolean {
399
+ return embeddingApiKey() !== "";
400
+ }
401
+
402
+ export async function embedQuery(text: string): Promise<Vector | null> {
403
+ if (text === "" || embeddingsDisabled()) {
404
+ return null;
405
+ }
406
+ const cached = cacheGet(text);
407
+ if (cached !== null) {
408
+ return cached;
409
+ }
410
+ const vectors = await embed([text]);
411
+ const vector = vectors?.[0] ?? null;
412
+ if (vector !== null) {
413
+ cacheSet(text, vector);
414
+ }
415
+ return vector;
416
+ }
417
+
418
+ export async function embed(texts: readonly string[]): Promise<EmbeddingMatrix | null> {
419
+ if (texts.length === 0 || embeddingsDisabled()) {
420
+ return null;
421
+ }
422
+ const activeProvider = resolveEmbeddingProvider(activeEmbeddingOptions()?.provider);
423
+ if (activeProvider !== undefined) {
424
+ try {
425
+ return await normalizeEmbeddingResult(await activeProvider.embed(texts));
426
+ } catch {
427
+ return null;
428
+ }
429
+ }
430
+ if (providerOverride !== null) {
431
+ try {
432
+ return await normalizeEmbeddingResult(await providerOverride.embed(texts));
433
+ } catch {
434
+ return null;
435
+ }
436
+ }
437
+ if (isApiModel(defaultModel())) {
438
+ return embedApi(texts);
439
+ }
440
+ if (texts.length === 1) {
441
+ const cached = cacheGet(texts[0] ?? "");
442
+ if (cached !== null) {
443
+ return [cached];
444
+ }
445
+ }
446
+ const model = await getLocalModel();
447
+ if (model === null) {
448
+ return null;
449
+ }
450
+ try {
451
+ const vectors = await normalizeEmbeddingResult(await model.embed([...texts]));
452
+ if (vectors !== null && vectors.length === 1) {
453
+ cacheSet(texts[0] ?? "", vectors[0] ?? []);
454
+ }
455
+ return vectors;
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+
461
+ export function serialize(vec: readonly number[]): string {
462
+ return JSON.stringify(vec);
463
+ }
464
+
465
+ export function cosineSimilarity(a: readonly number[], b: readonly number[]): number {
466
+ const length = Math.min(a.length, b.length);
467
+ if (length === 0) {
468
+ return 0;
469
+ }
470
+ let dot = 0;
471
+ let normA = 0;
472
+ let normB = 0;
473
+ for (let i = 0; i < length; i += 1) {
474
+ const av = a[i] ?? 0;
475
+ const bv = b[i] ?? 0;
476
+ dot += av * bv;
477
+ normA += av * av;
478
+ normB += bv * bv;
479
+ }
480
+ if (normA === 0 || normB === 0) {
481
+ return 0;
482
+ }
483
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
484
+ }
485
+ export function getEmbeddingApiCallCountForTests(): number {
486
+ return apiCallCount;
487
+ }
488
+
489
+ export const DEFAULT_MODEL = defaultModel();
490
+ export const EMBEDDING_DIM = embeddingDimFor(DEFAULT_MODEL);