From c73c76ff791e2ea5691a79b9afdce40faeef1ca8 Mon Sep 17 00:00:00 2001 From: OpenClaw Contributor Date: Thu, 19 Mar 2026 21:29:46 +0800 Subject: [PATCH 1/5] fix: add omitDimensions support for local embedding requests --- index.ts | 6 ++++++ openclaw.plugin.json | 9 +++++++++ src/embedder.ts | 12 +++++++++--- src/retriever.ts | 8 ++++++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/index.ts b/index.ts index d1c20c31..16d38515 100644 --- a/index.ts +++ b/index.ts @@ -69,6 +69,7 @@ interface PluginConfig { model?: string; baseURL?: string; dimensions?: number; + omitDimensions?: boolean; taskQuery?: string; taskPassage?: string; normalized?: boolean; @@ -1628,6 +1629,7 @@ const memoryLanceDBProPlugin = { model: config.embedding.model || "text-embedding-3-small", baseURL: config.embedding.baseURL, dimensions: config.embedding.dimensions, + omitDimensions: config.embedding.omitDimensions, taskQuery: config.embedding.taskQuery, taskPassage: config.embedding.taskPassage, normalized: config.embedding.normalized, @@ -3274,6 +3276,10 @@ export function parsePluginConfig(value: unknown): PluginConfig { // Accept number, numeric string, or env-var string (e.g. "${EMBED_DIM}"). // Also accept legacy top-level `dimensions` for convenience. dimensions: parsePositiveInt(embedding.dimensions ?? cfg.dimensions), + omitDimensions: + typeof embedding.omitDimensions === "boolean" + ? embedding.omitDimensions + : undefined, taskQuery: typeof embedding.taskQuery === "string" ? embedding.taskQuery diff --git a/openclaw.plugin.json b/openclaw.plugin.json index cf98785a..fe52e34f 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -43,6 +43,10 @@ "type": "integer", "minimum": 1 }, + "omitDimensions": { + "type": "boolean", + "description": "When true, omit the dimensions parameter from embedding requests even if dimensions is configured" + }, "taskQuery": { "type": "string", "description": "Embedding task for queries (provider-specific, e.g. Jina: retrieval.query)" @@ -525,6 +529,11 @@ "help": "Override vector dimensions for custom models not in the built-in lookup table", "advanced": true }, + "embedding.omitDimensions": { + "label": "Omit Request Dimensions", + "help": "Do not send the dimensions parameter to the embedding API even if embedding.dimensions is configured. Useful for local models like Qwen3-Embedding that reject the field.", + "advanced": true + }, "embedding.taskQuery": { "label": "Query Task", "placeholder": "retrieval.query", diff --git a/src/embedder.ts b/src/embedder.ts index 5009425e..e9bb9ee9 100644 --- a/src/embedder.ts +++ b/src/embedder.ts @@ -99,6 +99,9 @@ export interface EmbeddingConfig { normalized?: boolean; /** Enable automatic chunking for documents exceeding context limits (default: true) */ chunking?: boolean; + /** When true, omit the dimensions parameter from embedding requests even if dimensions is set. + * Use this for local models that reject the dimensions parameter with "matryoshka representation" errors. */ + omitDimensions?: boolean; } // Known embedding model dimensions @@ -284,6 +287,8 @@ export class Embedder { /** Optional requested dimensions to pass through to the embedding provider (OpenAI-compatible). */ private readonly _requestDimensions?: number; + /** When true, omit the dimensions parameter even if _requestDimensions is set. */ + private readonly _omitDimensions: boolean; /** Enable automatic chunking for long documents (default: true) */ private readonly _autoChunk: boolean; @@ -298,6 +303,7 @@ export class Embedder { this._taskPassage = config.taskPassage; this._normalized = config.normalized; this._requestDimensions = config.dimensions; + this._omitDimensions = config.omitDimensions === true; // Enable auto-chunking by default for better handling of long documents this._autoChunk = config.chunking !== false; @@ -457,9 +463,9 @@ export class Embedder { if (this._normalized !== undefined) payload.normalized = this._normalized; // Some OpenAI-compatible providers support requesting a specific vector size. - // We only pass it through when explicitly configured to avoid breaking providers - // that reject unknown fields. - if (this._requestDimensions && this._requestDimensions > 0) { + // We only pass it through when explicitly configured, and allow explicitly + // omitting it for local providers/models that reject the dimensions field. + if (!this._omitDimensions && this._requestDimensions && this._requestDimensions > 0) { payload.dimensions = this._requestDimensions; } diff --git a/src/retriever.ts b/src/retriever.ts index 5d50cf11..e2f0b4ee 100644 --- a/src/retriever.ts +++ b/src/retriever.ts @@ -56,6 +56,9 @@ export interface RetrievalConfig { | "pinecone" | "dashscope" | "tei"; + /** Rerank request timeout in milliseconds. Default: 5000 (5 seconds). + * Increase this for slow local rerank models that take longer to process requests. */ + rerankTimeoutMs?: number; /** * Length normalization: penalize long entries that dominate via sheer keyword * density. Formula: score *= 1 / (1 + log2(charLen / anchor)). @@ -121,6 +124,7 @@ export const DEFAULT_RETRIEVAL_CONFIG: RetrievalConfig = { filterNoise: true, rerankModel: "jina-reranker-v3", rerankEndpoint: "https://api.jina.ai/v1/rerank", + rerankTimeoutMs: 5000, lengthNormAnchor: 500, hardMinScore: 0.35, timeDecayHalfLifeDays: 60, @@ -673,9 +677,9 @@ export class MemoryRetriever { results.length, ); - // Timeout: 5 seconds to prevent stalling retrieval pipeline + // Timeout: configurable via rerankTimeoutMs, default 5 seconds to prevent stalling retrieval pipeline const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); + const timeout = setTimeout(() => controller.abort(), this.config.rerankTimeoutMs ?? 5000); const response = await fetch(endpoint, { method: "POST", From 2979df2730eaf7d664065cdc07d6983112410e05 Mon Sep 17 00:00:00 2001 From: OpenClaw Contributor Date: Thu, 19 Mar 2026 21:29:46 +0800 Subject: [PATCH 2/5] feat: improve local embedding error hints --- src/embedder.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/embedder.ts b/src/embedder.ts index e9bb9ee9..18e0b8dc 100644 --- a/src/embedder.ts +++ b/src/embedder.ts @@ -168,6 +168,7 @@ function getProviderLabel(baseURL: string | undefined, model: string): string { if (/api\.jina\.ai/i.test(base)) return "Jina"; if (/localhost:11434|127\.0\.0\.1:11434|\/ollama\b/i.test(base)) return "Ollama"; if (/api\.openai\.com/i.test(base)) return "OpenAI"; + if (/api\.voyageai\.com/i.test(base)) return "Voyage"; try { return new URL(base).host; @@ -248,6 +249,13 @@ export function formatEmbeddingProviderError( return `Embedding provider unreachable (${detailText}). ${hint}`; } + // Detect matryoshka representation error from local models that reject dimensions parameter + if (/matryoshka|dimensions.*not\s*support|unknown.*param.*dimensions/i.test(raw)) { + return `Embedding provider rejected dimensions parameter (${detailText}). ` + + `This model does not support matryoshka representation. ` + + `Set "embedding.omitDimensions": true in your config to stop sending the dimensions parameter.`; + } + return `${genericPrefix}${detailText}`; } From c42a69c8d9738f4c0e2873c097de967dde474959 Mon Sep 17 00:00:00 2001 From: OpenClaw Contributor Date: Wed, 18 Mar 2026 21:03:24 +0800 Subject: [PATCH 3/5] test: add matryoshka error hint test cases - Test matryoshka representation error detection - Test unknown parameter: dimensions variant - Test dimensions not supported phrase variant - All verify omitDimensions hint is present --- package-lock.json | 42 ++++++----------------------- test/embedder-error-hints.test.mjs | 43 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c47f32a..de850adf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@lancedb/lancedb": "^0.26.2", "@sinclair/typebox": "0.34.48", + "apache-arrow": "18.1.0", "openai": "^6.21.0" }, "devDependencies": { @@ -175,7 +176,6 @@ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -184,22 +184,19 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/command-line-usage": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/node": { "version": "20.19.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -209,7 +206,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -225,7 +221,6 @@ "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-18.1.0.tgz", "integrity": "sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/helpers": "^0.5.11", "@types/command-line-args": "^5.2.3", @@ -246,7 +241,6 @@ "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -256,7 +250,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -273,7 +266,6 @@ "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.2" }, @@ -289,7 +281,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -301,15 +292,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/command-line-args": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", "license": "MIT", - "peer": true, "dependencies": { "array-back": "^3.1.0", "find-replace": "^3.0.0", @@ -325,7 +314,6 @@ "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", "license": "MIT", - "peer": true, "dependencies": { "array-back": "^6.2.2", "chalk-template": "^0.4.0", @@ -341,7 +329,6 @@ "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.17" } @@ -351,7 +338,6 @@ "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.17" } @@ -371,7 +357,6 @@ "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", "license": "MIT", - "peer": true, "dependencies": { "array-back": "^3.0.1" }, @@ -383,15 +368,13 @@ "version": "24.12.23", "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.12.23.tgz", "integrity": "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -410,7 +393,6 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz", "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==", - "peer": true, "engines": { "node": ">=0.8" } @@ -419,8 +401,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/openai": { "version": "6.22.0", @@ -454,7 +435,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -467,7 +447,6 @@ "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", "license": "MIT", - "peer": true, "dependencies": { "array-back": "^6.2.2", "wordwrapjs": "^5.1.0" @@ -481,7 +460,6 @@ "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.17" } @@ -490,8 +468,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/typescript": { "version": "5.9.3", @@ -512,7 +489,6 @@ "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -521,15 +497,13 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/wordwrapjs": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz", "integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.17" } diff --git a/test/embedder-error-hints.test.mjs b/test/embedder-error-hints.test.mjs index 0e7d153a..55f43e69 100644 --- a/test/embedder-error-hints.test.mjs +++ b/test/embedder-error-hints.test.mjs @@ -100,6 +100,49 @@ async function run() { ); assert.match(formattedBatch, /^Failed to generate batch embeddings from /, formattedBatch); + const formattedVoyage = formatEmbeddingProviderError( + new Error("unsupported request field"), + { + baseURL: "https://api.voyageai.com/v1", + model: "voyage-3-lite", + }, + ); + assert.match(formattedVoyage, /^Failed to generate embedding from Voyage:/, formattedVoyage); + + // Matryoshka error: local model that rejects dimensions parameter + const matryoshkaError1 = formatEmbeddingProviderError( + new Error("400 Model does not support matryoshka representation"), + { + baseURL: "http://127.0.0.1:8000/v1", + model: "Qwen3-Embedding-0.6B", + }, + ); + assert.match(matryoshkaError1, /rejected dimensions parameter/i, matryoshkaError1); + assert.match(matryoshkaError1, /matryoshka representation/i, matryoshkaError1); + assert.match(matryoshkaError1, /omitDimensions.*true/i, matryoshkaError1); + + // Matryoshka error: "dimensions not supported" variant + const matryoshkaError2 = formatEmbeddingProviderError( + new Error("unknown parameter: dimensions"), + { + baseURL: "http://localhost:11434/v1", + model: "nomic-embed-text", + }, + ); + assert.match(matryoshkaError2, /rejected dimensions parameter/i, matryoshkaError2); + assert.match(matryoshkaError2, /omitDimensions.*true/i, matryoshkaError2); + + // Matryoshka error: "dimensions not supported" phrase variant + const matryoshkaError3 = formatEmbeddingProviderError( + new Error("the dimensions parameter is not supported by this model"), + { + baseURL: "http://127.0.0.1:8000/v1", + model: "bge-m3", + }, + ); + assert.match(matryoshkaError3, /rejected dimensions parameter/i, matryoshkaError3); + assert.match(matryoshkaError3, /omitDimensions.*true/i, matryoshkaError3); + console.log("OK: embedder auth/network error hints verified"); } From c5d96dc81fd10ad2f8e28564a1fd34031153fedc Mon Sep 17 00:00:00 2001 From: OpenClaw Contributor Date: Thu, 19 Mar 2026 21:29:46 +0800 Subject: [PATCH 4/5] docs: document omitDimensions embedding config --- README.md | 6 +++++- README_CN.md | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c5c3c4e7..89934a6b 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ Requirements: If you already have your own OpenAI-compatible services, just replace the relevant block: -- `embedding`: change `apiKey` / `model` / `baseURL` / `dimensions` +- `embedding`: change `apiKey` / `model` / `baseURL` / `dimensions` / `omitDimensions` - `retrieval`: change `rerankProvider` / `rerankEndpoint` / `rerankModel` / `rerankApiKey` - `llm`: change `apiKey` / `model` / `baseURL` / `timeoutMs` @@ -357,6 +357,7 @@ Query → BM25 FTS ─────┘ "model": "jina-embeddings-v5-text-small", "baseURL": "https://api.jina.ai/v1", "dimensions": 1024, + "omitDimensions": false, "taskQuery": "retrieval.query", "taskPassage": "retrieval.passage", "normalized": true @@ -633,6 +634,7 @@ npm install "model": "jina-embeddings-v5-text-small", "baseURL": "https://api.jina.ai/v1", "dimensions": 1024, + "omitDimensions": false, "taskQuery": "retrieval.query", "taskPassage": "retrieval.passage", "normalized": true @@ -645,6 +647,8 @@ npm install } ``` +> If your embedding endpoint rejects the `dimensions` request field (common with some local/OpenAI-compatible models such as Qwen3-Embedding), keep `embedding.dimensions` set for schema/registration purposes and set `"omitDimensions": true` so the plugin does not send the parameter upstream. + 3. Restart and verify: ```bash diff --git a/README_CN.md b/README_CN.md index c1b4a3f8..b4492903 100644 --- a/README_CN.md +++ b/README_CN.md @@ -197,7 +197,7 @@ openclaw logs --follow --plain | rg "memory-lancedb-pro" 如果你已经有自己的 OpenAI-compatible 服务,只需替换对应区块: -- `embedding`:改 `apiKey` / `model` / `baseURL` / `dimensions` +- `embedding`:改 `apiKey` / `model` / `baseURL` / `dimensions` / `omitDimensions` - `retrieval`:改 `rerankProvider` / `rerankEndpoint` / `rerankModel` / `rerankApiKey` - `llm`:改 `apiKey` / `model` / `baseURL` / `timeoutMs` @@ -357,6 +357,7 @@ Query → BM25 FTS ─────┘ "model": "jina-embeddings-v5-text-small", "baseURL": "https://api.jina.ai/v1", "dimensions": 1024, + "omitDimensions": false, "taskQuery": "retrieval.query", "taskPassage": "retrieval.passage", "normalized": true @@ -633,6 +634,7 @@ npm install "model": "jina-embeddings-v5-text-small", "baseURL": "https://api.jina.ai/v1", "dimensions": 1024, + "omitDimensions": false, "taskQuery": "retrieval.query", "taskPassage": "retrieval.passage", "normalized": true @@ -645,6 +647,8 @@ npm install } ``` +> 如果你的嵌入接口不接受 `dimensions` 请求字段(常见于部分本地/OpenAI-compatible 模型,比如 Qwen3-Embedding),建议继续保留 `embedding.dimensions` 以满足 schema/注册校验,同时设置 `"omitDimensions": true`,这样插件就不会把该参数继续发给上游接口。 + 3. 重启并验证: ```bash From b60b58d6eb0cd923ecc09f7541c7d01d614379c6 Mon Sep 17 00:00:00 2001 From: yjjheizhu <86939240+yjjheizhu@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:40:54 +0800 Subject: [PATCH 5/5] chore: drop rerank timeout from PR 264 --- package-lock.json | 48 +++++++++++++++++++++++- src/retriever.ts | 93 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 133 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index de850adf..fcbf1b04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "@lancedb/lancedb": "^0.26.2", "@sinclair/typebox": "0.34.48", "apache-arrow": "18.1.0", - "openai": "^6.21.0" + "json5": "^2.2.3", + "openai": "^6.21.0", + "proper-lockfile": "^4.1.2" }, "devDependencies": { "commander": "^14.0.0", @@ -370,6 +372,12 @@ "integrity": "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==", "license": "Apache-2.0" }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -397,6 +405,18 @@ "node": ">=0.8" } }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -424,12 +444,38 @@ } } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "license": "Apache-2.0" }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/src/retriever.ts b/src/retriever.ts index e2f0b4ee..cc17bf10 100644 --- a/src/retriever.ts +++ b/src/retriever.ts @@ -56,9 +56,6 @@ export interface RetrievalConfig { | "pinecone" | "dashscope" | "tei"; - /** Rerank request timeout in milliseconds. Default: 5000 (5 seconds). - * Increase this for slow local rerank models that take longer to process requests. */ - rerankTimeoutMs?: number; /** * Length normalization: penalize long entries that dominate via sheer keyword * density. Formula: score *= 1 / (1 + log2(charLen / anchor)). @@ -88,6 +85,10 @@ export interface RetrievalConfig { /** Maximum half-life multiplier from access reinforcement. * Prevents frequently accessed memories from becoming immortal. (default: 3) */ maxHalfLifeMultiplier: number; + /** Tag prefixes for exact-match queries (default: ["proj", "env", "team", "scope"]). + * Queries containing these prefixes (e.g. "proj:AIF") will use BM25-only + mustContain + * to avoid semantic false positives from vector search. */ + tagPrefixes: string[]; } export interface RetrievalContext { @@ -124,12 +125,12 @@ export const DEFAULT_RETRIEVAL_CONFIG: RetrievalConfig = { filterNoise: true, rerankModel: "jina-reranker-v3", rerankEndpoint: "https://api.jina.ai/v1/rerank", - rerankTimeoutMs: 5000, lengthNormAnchor: 500, hardMinScore: 0.35, timeDecayHalfLifeDays: 60, reinforcementFactor: 0.5, maxHalfLifeMultiplier: 3, + tagPrefixes: ["proj", "env", "team", "scope"], }; // ============================================================================ @@ -380,8 +381,19 @@ export class MemoryRetriever { const { query, limit, scopeFilter, category, source } = context; const safeLimit = clampInt(limit, 1, 20); + // Check if query contains tag prefixes -> use BM25-only + mustContain + const tagTokens = this.extractTagTokens(query); let results: RetrievalResult[]; - if (this.config.mode === "vector" || !this.store.hasFtsSupport) { + + if (tagTokens.length > 0) { + results = await this.bm25OnlyRetrieval( + query, + tagTokens, + safeLimit, + scopeFilter, + category, + ); + } else if (this.config.mode === "vector" || !this.store.hasFtsSupport) { results = await this.vectorOnlyRetrieval( query, safeLimit, @@ -405,6 +417,15 @@ export class MemoryRetriever { return results; } + private extractTagTokens(query: string): string[] { + if (!this.config.tagPrefixes?.length) return []; + + const pattern = this.config.tagPrefixes.join("|"); + const regex = new RegExp(`(?:${pattern}):[\\w-]+`, "gi"); + const matches = query.match(regex); + return matches || []; + } + private async vectorOnlyRetrieval( query: string, limit: number, @@ -451,6 +472,64 @@ export class MemoryRetriever { return deduplicated.slice(0, limit); } + private async bm25OnlyRetrieval( + query: string, + tagTokens: string[], + limit: number, + scopeFilter?: string[], + category?: string, + ): Promise { + const candidatePoolSize = Math.max(this.config.candidatePoolSize, limit * 2); + + // Run BM25 search + const bm25Results = await this.store.bm25Search( + query, + candidatePoolSize, + scopeFilter, + { excludeInactive: true }, + ); + + // Filter by category if specified + const categoryFiltered = category + ? bm25Results.filter((r) => r.entry.category === category) + : bm25Results; + + // mustContain: only keep entries that literally contain all tag tokens (case-insensitive) + const mustContainFiltered = categoryFiltered.filter((r) => { + const textLower = r.entry.text.toLowerCase(); + return tagTokens.every((t) => textLower.includes(t.toLowerCase())); + }); + + const mapped = mustContainFiltered.map( + (result, index) => + ({ + ...result, + sources: { + bm25: { score: result.score, rank: index + 1 }, + }, + }) as RetrievalResult, + ); + + // Apply same post-processing as hybrid retrieval to avoid behavior regression + const temporallyRanked = this.decayEngine + ? mapped + : this.applyImportanceWeight(this.applyRecencyBoost(mapped)); + + const lengthNormalized = this.applyLengthNormalization(temporallyRanked); + const hardFiltered = lengthNormalized.filter(r => r.score >= this.config.hardMinScore); + + const lifecycleRanked = this.decayEngine + ? this.applyDecayBoost(hardFiltered) + : this.applyTimeDecay(hardFiltered); + + const denoised = this.config.filterNoise + ? filterNoise(lifecycleRanked, r => r.entry.text) + : lifecycleRanked; + + const deduplicated = this.applyMMRDiversity(denoised); + return deduplicated.slice(0, limit); + } + private async hybridRetrieval( query: string, limit: number, @@ -677,9 +756,9 @@ export class MemoryRetriever { results.length, ); - // Timeout: configurable via rerankTimeoutMs, default 5 seconds to prevent stalling retrieval pipeline + // Timeout: 5 seconds to prevent stalling retrieval pipeline const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), this.config.rerankTimeoutMs ?? 5000); + const timeout = setTimeout(() => controller.abort(), 5000); const response = await fetch(endpoint, { method: "POST",