@oh-my-pi/pi-mnemopi 18.16.0 → 18.18.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 +14 -0
- package/README.md +1 -0
- package/dist/types/core/beam/store.d.ts +12 -9
- package/dist/types/core/runtime-options.d.ts +3 -0
- package/package.json +7 -7
- package/src/core/beam/store.ts +88 -36
- package/src/core/embeddings.ts +60 -3
- package/src/core/memory.ts +4 -1
- package/src/core/runtime-options.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.18.0] - 2026-11-07
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed embedding rebuilds failing on hosted APIs that cap the inputs per request: `embed()` now splits large lists into requests of at most `MNEMOPI_EMBEDDING_BATCH_SIZE` texts (default 64; set `10` for DashScope) or the `embeddings.batchSize` runtime option, and returns `null` when the API returns a different number of vectors than texts ([#18278](https://github.com/can1357/oh-my-pi/pull/18278) by [@aleister1102](https://github.com/aleister1102))
|
|
10
|
+
- Fixed an embedding model change deleting every stored vector when the new provider had no key, did not answer, or rejected a batch partway through: the store now embeds the whole corpus with the new model before replacing anything, keeps the old vectors if any request fails, and tries again on the next open ([#18278](https://github.com/can1357/oh-my-pi/pull/18278) by [@aleister1102](https://github.com/aleister1102))
|
|
11
|
+
- Fixed rejected embedding API requests leaving no trace: the status and the first 300 characters of the response body now go to the log at `debug`, or at `warn` when runtime debug is on ([#18278](https://github.com/can1357/oh-my-pi/pull/18278) by [@aleister1102](https://github.com/aleister1102))
|
|
12
|
+
|
|
13
|
+
## [18.17.0] - 2026-11-06
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Development installs reuse fastembed’s ONNX Runtime instead of installing a duplicate native runtime.
|
|
18
|
+
|
|
5
19
|
## [18.14.2] - 2026-10-31
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -89,6 +89,7 @@ Common environment fallbacks:
|
|
|
89
89
|
- `MNEMOPI_NO_EMBEDDINGS=1`: force FTS-only recall.
|
|
90
90
|
- `MNEMOPI_EMBEDDING_MODEL`: defaults to `BAAI/bge-small-en-v1.5`.
|
|
91
91
|
- `MNEMOPI_EMBEDDING_API_URL` and `MNEMOPI_EMBEDDING_API_KEY`: OpenAI-compatible embedding endpoint.
|
|
92
|
+
- `MNEMOPI_EMBEDDING_BATCH_SIZE`: texts per embedding API request, default `64`. Larger inputs are split into several requests. Set it lower for a provider with a smaller per-request limit, for example `10` for DashScope.
|
|
92
93
|
- `MNEMOPI_LLM_ENABLED=1`, `MNEMOPI_LLM_BASE_URL`, `MNEMOPI_LLM_API_KEY`, `MNEMOPI_LLM_MODEL`: OpenAI-compatible LLM endpoint.
|
|
93
94
|
|
|
94
95
|
Local embeddings use the `fastembed` npm package. Its default `BGESmallENV15` model is 384-dimensional and uses the package's CLS pooling plus vector normalization path. Local GGUF LLMs are not available in this package.
|
|
@@ -26,18 +26,21 @@ type StoreRememberBatchOptions = RememberBatchOptions & {
|
|
|
26
26
|
* Every `memory_embeddings` row is stamped with the model that produced it (see
|
|
27
27
|
* `runEmbedding` in `helpers.ts`). When the configured embedding model changes,
|
|
28
28
|
* its vector dimension changes too, so the previously-stored vectors are no
|
|
29
|
-
* longer comparable. On a mismatch we
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* longer comparable. On a mismatch we embed one probe text with the new model. Once
|
|
30
|
+
* it answers we embed the whole corpus under the new model in memory. Only when every
|
|
31
|
+
* batch comes back do we replace the stored vectors (the `memory_embeddings` table, the
|
|
32
|
+
* `episodic_memory.binary_vector` column, and the sqlite-vec `vec_episodes` index) in one
|
|
33
|
+
* transaction, so a rejected batch leaves the old vectors in place. The work runs as a
|
|
34
|
+
* tracked background task on `beam.pendingExtractions`, so `flushExtractions()` drains it.
|
|
33
35
|
*
|
|
34
36
|
* Runs once per store open; a fresh store (no embeddings) or an already-current
|
|
35
37
|
* store is a no-op. The destructive wipe is skipped whenever it could not be
|
|
36
|
-
* rebuilt
|
|
37
|
-
*
|
|
38
|
-
* stale-but-valid corpus is never
|
|
39
|
-
* inside the active runtime-options scope
|
|
40
|
-
* `embeddingsDisabled()` reflect the per-instance
|
|
38
|
+
* rebuilt: embeddings disabled via the runtime option OR the `MNEMOPI_NO_EMBEDDINGS`
|
|
39
|
+
* env, an unresolved (empty) active model, or a provider that does not answer the
|
|
40
|
+
* probe (missing key, unreachable endpoint). A stale-but-valid corpus is never
|
|
41
|
+
* destroyed without a replacement. MUST run inside the active runtime-options scope
|
|
42
|
+
* so `currentEmbeddingModel()` / `embeddingsDisabled()` reflect the per-instance
|
|
43
|
+
* configuration.
|
|
41
44
|
*/
|
|
42
45
|
export declare function reconcileEmbeddingModel(beam: BeamMemoryState): void;
|
|
43
46
|
export declare function remember(beam: BeamMemoryState, content: string, options?: StoreRememberOptions): string;
|
|
@@ -31,6 +31,8 @@ export interface MnemopiEmbeddingRuntimeOptions {
|
|
|
31
31
|
provider?: MnemopiEmbeddingProvider | ((texts: readonly string[]) => EmbeddingOutput | Promise<EmbeddingOutput>);
|
|
32
32
|
/** Override `MNEMOPI_EMBEDDING_MAX_INPUT_CHARS`. `0` disables the cap. See `config.embeddingMaxInputChars`. */
|
|
33
33
|
maxInputChars?: number;
|
|
34
|
+
/** Override `MNEMOPI_EMBEDDING_BATCH_SIZE`: max texts per embedding API request. */
|
|
35
|
+
batchSize?: number;
|
|
34
36
|
}
|
|
35
37
|
export interface MnemopiLlmRuntimeOptions {
|
|
36
38
|
enabled?: boolean;
|
|
@@ -57,6 +59,7 @@ export interface ResolvedMnemopiEmbeddingRuntimeOptions {
|
|
|
57
59
|
apiKey?: ApiKey;
|
|
58
60
|
provider?: MnemopiEmbeddingProvider;
|
|
59
61
|
maxInputChars?: number;
|
|
62
|
+
batchSize?: number;
|
|
60
63
|
}
|
|
61
64
|
export interface ResolvedMnemopiLlmRuntimeOptions {
|
|
62
65
|
enabled?: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-mnemopi",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.18.0",
|
|
5
5
|
"description": "Local SQLite memory engine for omp agents",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -35,17 +35,17 @@
|
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"check": "oxlint . && oxfmt --check --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts' && bun run check:types",
|
|
38
|
-
"check:types": "
|
|
38
|
+
"check:types": "bun check --no-script -p tsconfig.json",
|
|
39
39
|
"lint": "oxlint .",
|
|
40
40
|
"test": "bun test --parallel",
|
|
41
41
|
"fix": "oxlint --fix --fix-suggestions . && bun run fmt",
|
|
42
42
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@oh-my-pi/pi-ai": "18.
|
|
46
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
47
|
-
"@oh-my-pi/pi-natives": "18.
|
|
48
|
-
"@oh-my-pi/pi-utils": "18.
|
|
45
|
+
"@oh-my-pi/pi-ai": "18.18.0",
|
|
46
|
+
"@oh-my-pi/pi-catalog": "18.18.0",
|
|
47
|
+
"@oh-my-pi/pi-natives": "18.18.0",
|
|
48
|
+
"@oh-my-pi/pi-utils": "18.18.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"fastembed": "3.0.0",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.3.14",
|
|
64
64
|
"fastembed": "3.0.0",
|
|
65
|
-
"onnxruntime-node": "1.
|
|
65
|
+
"onnxruntime-node": "1.21.0"
|
|
66
66
|
},
|
|
67
67
|
"engines": {
|
|
68
68
|
"bun": ">=1.4.4"
|
package/src/core/beam/store.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { logger, withLoopPhase } from "@oh-my-pi/pi-utils";
|
|
|
3
3
|
import { transaction } from "../../db";
|
|
4
4
|
import { toUtcIso } from "../../util/datetime";
|
|
5
5
|
import { generateId } from "../../util/ids";
|
|
6
|
-
import { currentEmbeddingModel, embeddingsDisabled } from "../embeddings";
|
|
6
|
+
import { currentEmbeddingModel, embed, embeddingsDisabled } from "../embeddings";
|
|
7
7
|
import { EpisodicGraph } from "../episodic-graph";
|
|
8
8
|
import { countExtractedFactCategories, extractFactCategoriesSafe } from "../extraction";
|
|
9
9
|
import { getMnemopiRuntimeOptions, withMnemopiRuntimeOptions } from "../runtime-options";
|
|
@@ -351,18 +351,21 @@ const EMBED_REBUILD_BATCH = 128;
|
|
|
351
351
|
* Every `memory_embeddings` row is stamped with the model that produced it (see
|
|
352
352
|
* `runEmbedding` in `helpers.ts`). When the configured embedding model changes,
|
|
353
353
|
* its vector dimension changes too, so the previously-stored vectors are no
|
|
354
|
-
* longer comparable. On a mismatch we
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
354
|
+
* longer comparable. On a mismatch we embed one probe text with the new model. Once
|
|
355
|
+
* it answers we embed the whole corpus under the new model in memory. Only when every
|
|
356
|
+
* batch comes back do we replace the stored vectors (the `memory_embeddings` table, the
|
|
357
|
+
* `episodic_memory.binary_vector` column, and the sqlite-vec `vec_episodes` index) in one
|
|
358
|
+
* transaction, so a rejected batch leaves the old vectors in place. The work runs as a
|
|
359
|
+
* tracked background task on `beam.pendingExtractions`, so `flushExtractions()` drains it.
|
|
358
360
|
*
|
|
359
361
|
* Runs once per store open; a fresh store (no embeddings) or an already-current
|
|
360
362
|
* store is a no-op. The destructive wipe is skipped whenever it could not be
|
|
361
|
-
* rebuilt
|
|
362
|
-
*
|
|
363
|
-
* stale-but-valid corpus is never
|
|
364
|
-
* inside the active runtime-options scope
|
|
365
|
-
* `embeddingsDisabled()` reflect the per-instance
|
|
363
|
+
* rebuilt: embeddings disabled via the runtime option OR the `MNEMOPI_NO_EMBEDDINGS`
|
|
364
|
+
* env, an unresolved (empty) active model, or a provider that does not answer the
|
|
365
|
+
* probe (missing key, unreachable endpoint). A stale-but-valid corpus is never
|
|
366
|
+
* destroyed without a replacement. MUST run inside the active runtime-options scope
|
|
367
|
+
* so `currentEmbeddingModel()` / `embeddingsDisabled()` reflect the per-instance
|
|
368
|
+
* configuration.
|
|
366
369
|
*/
|
|
367
370
|
export function reconcileEmbeddingModel(beam: BeamMemoryState): void {
|
|
368
371
|
if (embeddingsDisabled()) return;
|
|
@@ -381,35 +384,84 @@ export function reconcileEmbeddingModel(beam: BeamMemoryState): void {
|
|
|
381
384
|
// (NULL/unstamped counts as a mismatch via `IS NOT`).
|
|
382
385
|
const mismatch = beam.db.query("SELECT 1 FROM memory_embeddings WHERE model IS NOT ? LIMIT 1").get(active);
|
|
383
386
|
if (mismatch) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
387
|
+
// Replace stored vectors only once the whole corpus embeds under the new model. A missing key,
|
|
388
|
+
// an unreachable endpoint, or a rejected batch keeps the stale vectors; the next open retries.
|
|
389
|
+
const migrate = async (): Promise<void> => {
|
|
390
|
+
try {
|
|
391
|
+
const probe = await embed(["mnemopi embedding probe"]);
|
|
392
|
+
if (probe === null || probe.length !== 1) {
|
|
393
|
+
logger.warn("mnemopi: embedding provider did not answer; keeping stored embeddings", { to: active });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const staleModels = beam.db
|
|
397
|
+
.query("SELECT DISTINCT model FROM memory_embeddings WHERE model IS NOT ?")
|
|
398
|
+
.all(active) as { model: string | null }[];
|
|
399
|
+
const live = beam.db
|
|
400
|
+
.query(`
|
|
401
|
+
SELECT id AS memoryId, COALESCE(embed_text, content) AS content FROM working_memory WHERE superseded_by IS NULL
|
|
402
|
+
UNION ALL
|
|
403
|
+
SELECT id AS memoryId, content FROM episodic_memory WHERE superseded_by IS NULL
|
|
404
|
+
`)
|
|
405
|
+
.all() as EmbedItem[];
|
|
394
406
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
407
|
+
// Embed the whole corpus before touching anything stored. embed() handles the provider's
|
|
408
|
+
// per-request cap; a rejected chunk abandons the rebuild and leaves the stored vectors alone.
|
|
409
|
+
const toEmbed = live.filter(item => item.content.trim() !== "");
|
|
410
|
+
const staged: { memoryId: string; json: string }[] = [];
|
|
411
|
+
for (let offset = 0; offset < toEmbed.length; offset += EMBED_REBUILD_BATCH) {
|
|
412
|
+
const chunk = toEmbed.slice(offset, offset + EMBED_REBUILD_BATCH);
|
|
413
|
+
const vectors = await embed(chunk.map(item => item.content));
|
|
414
|
+
if (vectors === null || vectors.length !== chunk.length) {
|
|
415
|
+
logger.warn("mnemopi: embedding rebuild incomplete; keeping stored embeddings", {
|
|
416
|
+
to: active,
|
|
417
|
+
embedded: staged.length,
|
|
418
|
+
total: toEmbed.length,
|
|
419
|
+
});
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
chunk.forEach((item, index) => {
|
|
423
|
+
const vector = vectors[index];
|
|
424
|
+
if (vector !== undefined) {
|
|
425
|
+
staged.push({ memoryId: item.memoryId, json: JSON.stringify(Array.from(vector)) });
|
|
426
|
+
}
|
|
427
|
+
});
|
|
403
428
|
}
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
429
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
430
|
+
transaction(beam.db, () => {
|
|
431
|
+
// Only stale-model rows go. A vector that a concurrent remember() stamped under the active
|
|
432
|
+
// model survives, and the INSERT below overwrites any row this rebuild covers.
|
|
433
|
+
beam.db.query("DELETE FROM memory_embeddings WHERE model IS NOT ?").run(active);
|
|
434
|
+
beam.db.run("UPDATE episodic_memory SET binary_vector = NULL");
|
|
435
|
+
if (vecAvailable(beam.db)) {
|
|
436
|
+
try {
|
|
437
|
+
beam.db.run("DELETE FROM vec_episodes");
|
|
438
|
+
} catch {
|
|
439
|
+
// sqlite-vec cleanup is best-effort; rebuild correctness takes precedence.
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
using insertEmbedding = beam.db.prepare(
|
|
443
|
+
"INSERT OR REPLACE INTO memory_embeddings(memory_id, embedding_json, model) VALUES (?, ?, ?)",
|
|
444
|
+
);
|
|
445
|
+
for (const row of staged) insertEmbedding.run(row.memoryId, row.json, active);
|
|
446
|
+
});
|
|
447
|
+
beam.caches.queryCache?.invalidate();
|
|
448
|
+
|
|
449
|
+
logger.info("mnemopi: embedding model changed, rebuilt", {
|
|
450
|
+
from: staleModels.map(row => row.model ?? "(unstamped)"),
|
|
451
|
+
to: active,
|
|
452
|
+
count: staged.length,
|
|
453
|
+
});
|
|
454
|
+
} catch (error) {
|
|
455
|
+
// A store closed during shutdown lands here; the next open resumes from the stamps.
|
|
456
|
+
logger.debug("mnemopi: embedding migration aborted", { error: String(error) });
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const task = withMnemopiRuntimeOptions(getMnemopiRuntimeOptions(), migrate);
|
|
460
|
+
const pending = beam.pendingExtractions;
|
|
461
|
+
if (pending !== undefined) {
|
|
462
|
+
pending.add(task);
|
|
463
|
+
void task.finally(() => pending.delete(task));
|
|
464
|
+
}
|
|
413
465
|
return;
|
|
414
466
|
}
|
|
415
467
|
|
package/src/core/embeddings.ts
CHANGED
|
@@ -361,7 +361,53 @@ async function getLocalModel(): Promise<LocalEmbeddingModel | null> {
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
const DEFAULT_EMBEDDING_BATCH_SIZE = 64;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Max texts per `/embeddings` request. Some hosted providers cap the inputs per
|
|
368
|
+
* request (DashScope: 10), and a rejected request fails the whole call. Reads the
|
|
369
|
+
* runtime scope's `embeddings.batchSize`, then `MNEMOPI_EMBEDDING_BATCH_SIZE`, then 64.
|
|
370
|
+
*/
|
|
371
|
+
function embeddingBatchSize(): number {
|
|
372
|
+
return (
|
|
373
|
+
positiveBatchSize(activeEmbeddingOptions()?.batchSize) ??
|
|
374
|
+
positiveBatchSize(Number.parseInt($env.MNEMOPI_EMBEDDING_BATCH_SIZE ?? "", 10)) ??
|
|
375
|
+
DEFAULT_EMBEDDING_BATCH_SIZE
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Floors a configured size to a whole request cap; undefined unless it is at least 1. */
|
|
380
|
+
function positiveBatchSize(value: number | undefined): number | undefined {
|
|
381
|
+
if (value === undefined || !Number.isFinite(value)) return undefined;
|
|
382
|
+
const size = Math.floor(value);
|
|
383
|
+
return size >= 1 ? size : undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Same level policy as the local-model failures: debug, or warn when runtime debug is on.
|
|
387
|
+
function logEmbeddingRejection(status: number, body: string): void {
|
|
388
|
+
logger[mnemopiDebugEnabled() ? "warn" : "debug"]("mnemopi embedding request rejected", {
|
|
389
|
+
status,
|
|
390
|
+
body: body.slice(0, 300),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
364
394
|
async function embedApi(texts: readonly string[]): Promise<EmbeddingMatrix | null> {
|
|
395
|
+
const size = embeddingBatchSize();
|
|
396
|
+
if (texts.length <= size) {
|
|
397
|
+
return embedApiBatch(texts);
|
|
398
|
+
}
|
|
399
|
+
const matrix: EmbeddingMatrix = [];
|
|
400
|
+
for (let offset = 0; offset < texts.length; offset += size) {
|
|
401
|
+
const part = await embedApiBatch(texts.slice(offset, offset + size));
|
|
402
|
+
if (part === null) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
matrix.push(...part);
|
|
406
|
+
}
|
|
407
|
+
return matrix;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function embedApiBatch(texts: readonly string[]): Promise<EmbeddingMatrix | null> {
|
|
365
411
|
const baseUrl = embeddingBaseUrl();
|
|
366
412
|
const isCustom = !hostMatchesUrl(baseUrl, "openrouter");
|
|
367
413
|
const apiKey = embeddingApiKey();
|
|
@@ -392,24 +438,35 @@ async function embedApi(texts: readonly string[]): Promise<EmbeddingMatrix | nul
|
|
|
392
438
|
defaultDelayMs: attempt => 2 ** attempt * 1000,
|
|
393
439
|
});
|
|
394
440
|
if (res.status === 401) {
|
|
395
|
-
|
|
441
|
+
// Keep the provider's denial text: the log can only show what the error carries.
|
|
442
|
+
const detail = (await res.text().catch(() => "")).trim();
|
|
443
|
+
throw new ProviderHttpError(detail || "mnemopi embedding request unauthorized (401)", 401, {
|
|
444
|
+
headers: res.headers,
|
|
445
|
+
});
|
|
396
446
|
}
|
|
397
447
|
return res;
|
|
398
448
|
});
|
|
399
449
|
if (!response.ok) {
|
|
450
|
+
logEmbeddingRejection(response.status, await response.text().catch(() => ""));
|
|
400
451
|
return null;
|
|
401
452
|
}
|
|
402
453
|
const data = (await response.json()) as { data?: Array<{ embedding: number[] }> };
|
|
403
454
|
return withLoopPhase("mnemopi.embed", () => {
|
|
404
455
|
const { data: rows } = data;
|
|
405
|
-
|
|
456
|
+
// A short or long matrix would stamp vectors onto the wrong memories.
|
|
457
|
+
if (rows === undefined || rows.length !== texts.length) {
|
|
406
458
|
return null;
|
|
407
459
|
}
|
|
408
460
|
apiCallCount += 1;
|
|
409
461
|
return rows.map(row => new Float32Array(row.embedding));
|
|
410
462
|
});
|
|
411
463
|
} catch (error) {
|
|
412
|
-
|
|
464
|
+
const status = extractHttpStatusFromError(error);
|
|
465
|
+
if (status === undefined) {
|
|
466
|
+
logger.debug("mnemopi embedding request failed", { error: String(error) });
|
|
467
|
+
} else {
|
|
468
|
+
logEmbeddingRejection(status, error instanceof Error ? error.message : "");
|
|
469
|
+
}
|
|
413
470
|
return null;
|
|
414
471
|
}
|
|
415
472
|
}
|
package/src/core/memory.ts
CHANGED
|
@@ -194,6 +194,7 @@ function resolveRuntimeOptions(options: MnemopiOptions): ResolvedMnemopiRuntimeO
|
|
|
194
194
|
const embeddingApiKey = options.embeddingApiKey ?? nestedEmbeddings?.apiKey;
|
|
195
195
|
const embeddingProvider = resolveEmbeddingProvider(nestedEmbeddings?.provider);
|
|
196
196
|
const embeddingMaxInputChars = nestedEmbeddings?.maxInputChars;
|
|
197
|
+
const embeddingBatchSize = nestedEmbeddings?.batchSize;
|
|
197
198
|
|
|
198
199
|
const embeddings =
|
|
199
200
|
embeddingDisabled !== undefined ||
|
|
@@ -201,7 +202,8 @@ function resolveRuntimeOptions(options: MnemopiOptions): ResolvedMnemopiRuntimeO
|
|
|
201
202
|
embeddingApiUrl !== undefined ||
|
|
202
203
|
embeddingApiKey !== undefined ||
|
|
203
204
|
embeddingProvider !== undefined ||
|
|
204
|
-
embeddingMaxInputChars !== undefined
|
|
205
|
+
embeddingMaxInputChars !== undefined ||
|
|
206
|
+
embeddingBatchSize !== undefined
|
|
205
207
|
? {
|
|
206
208
|
disabled: embeddingDisabled,
|
|
207
209
|
model: embeddingModel,
|
|
@@ -209,6 +211,7 @@ function resolveRuntimeOptions(options: MnemopiOptions): ResolvedMnemopiRuntimeO
|
|
|
209
211
|
apiKey: embeddingApiKey,
|
|
210
212
|
provider: embeddingProvider,
|
|
211
213
|
maxInputChars: embeddingMaxInputChars,
|
|
214
|
+
batchSize: embeddingBatchSize,
|
|
212
215
|
}
|
|
213
216
|
: undefined;
|
|
214
217
|
|
|
@@ -41,6 +41,8 @@ export interface MnemopiEmbeddingRuntimeOptions {
|
|
|
41
41
|
provider?: MnemopiEmbeddingProvider | ((texts: readonly string[]) => EmbeddingOutput | Promise<EmbeddingOutput>);
|
|
42
42
|
/** Override `MNEMOPI_EMBEDDING_MAX_INPUT_CHARS`. `0` disables the cap. See `config.embeddingMaxInputChars`. */
|
|
43
43
|
maxInputChars?: number;
|
|
44
|
+
/** Override `MNEMOPI_EMBEDDING_BATCH_SIZE`: max texts per embedding API request. */
|
|
45
|
+
batchSize?: number;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export interface MnemopiLlmRuntimeOptions {
|
|
@@ -70,6 +72,7 @@ export interface ResolvedMnemopiEmbeddingRuntimeOptions {
|
|
|
70
72
|
apiKey?: ApiKey;
|
|
71
73
|
provider?: MnemopiEmbeddingProvider;
|
|
72
74
|
maxInputChars?: number;
|
|
75
|
+
batchSize?: number;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
export interface ResolvedMnemopiLlmRuntimeOptions {
|