Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ interface PluginConfig {
model?: string;
baseURL?: string;
dimensions?: number;
omitDimensions?: boolean;
taskQuery?: string;
taskPassage?: string;
normalized?: boolean;
Expand Down Expand Up @@ -1691,6 +1692,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,
Expand Down Expand Up @@ -3593,6 +3595,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
Expand Down
9 changes: 9 additions & 0 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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)"
Expand Down Expand Up @@ -764,6 +768,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",
Expand Down
11 changes: 9 additions & 2 deletions src/embedder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ export interface EmbeddingConfig {
taskPassage?: string;
/** Optional flag to request normalized embeddings (provider-dependent, e.g. Jina v5) */
normalized?: 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;
/** Enable automatic chunking for documents exceeding context limits (default: true) */
chunking?: boolean;
}
Expand Down Expand Up @@ -410,6 +413,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;

Expand All @@ -424,6 +429,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;
const profile = detectEmbeddingProviderProfile(this._baseURL, this._model);
Expand Down Expand Up @@ -641,8 +647,9 @@ export class Embedder {
}

// Output dimension: field name is provider-defined.
// Only sent when explicitly configured to avoid breaking providers that reject unknown fields.
if (this._capabilities.dimensionsField && this._requestDimensions && this._requestDimensions > 0) {
// Only sent when explicitly configured, unless omitDimensions is enabled for
// local or provider-compatible models that reject the dimensions field.
if (!this._omitDimensions && this._capabilities.dimensionsField && this._requestDimensions && this._requestDimensions > 0) {
payload[this._capabilities.dimensionsField] = this._requestDimensions;
}

Expand Down
66 changes: 66 additions & 0 deletions test/plugin-manifest-regression.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ assert.equal(
true,
"embedding.chunking schema default should match runtime default",
);
assert.equal(
manifest.configSchema.properties.embedding.properties.omitDimensions?.type,
"boolean",
"embedding.omitDimensions should be declared in the plugin schema",
);
assert.equal(
manifest.configSchema.properties.sessionMemory.properties.enabled.default,
false,
Expand All @@ -127,6 +132,7 @@ assert.equal(

const workDir = mkdtempSync(path.join(tmpdir(), "memory-plugin-regression-"));
const services = [];
const embeddingRequests = [];

try {
const api = createMockApi(
Expand Down Expand Up @@ -204,6 +210,7 @@ try {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
embeddingRequests.push(payload);
const inputs = Array.isArray(payload.input) ? payload.input : [payload.input];

if (inputs.some((input) => String(input).length > threshold)) {
Expand Down Expand Up @@ -293,6 +300,65 @@ try {
"created",
"embedding.chunking=true should recover from long-document embedding errors",
);

const withDimensionsApi = createMockApi({
dbPath: path.join(workDir, "db-with-dimensions"),
autoCapture: false,
autoRecall: false,
embedding: {
provider: "openai-compatible",
apiKey: "dummy",
model: "text-embedding-3-small",
baseURL: embeddingBaseURL,
dimensions: 4,
},
});
plugin.register(withDimensionsApi);
const withDimensionsTool = withDimensionsApi.toolFactories.memory_store({
agentId: "main",
sessionKey: "agent:main:test",
});
const requestCountBeforeWithDimensions = embeddingRequests.length;
await withDimensionsTool.execute("tool-3", {
text: "dimensions should be sent by default",
scope: "global",
});
const withDimensionsRequest = embeddingRequests.at(requestCountBeforeWithDimensions);
assert.equal(
withDimensionsRequest?.dimensions,
4,
"embedding.dimensions should be forwarded by default",
);

const omitDimensionsApi = createMockApi({
dbPath: path.join(workDir, "db-omit-dimensions"),
autoCapture: false,
autoRecall: false,
embedding: {
provider: "openai-compatible",
apiKey: "dummy",
model: "text-embedding-3-small",
baseURL: embeddingBaseURL,
dimensions: 4,
omitDimensions: true,
},
});
plugin.register(omitDimensionsApi);
const omitDimensionsTool = omitDimensionsApi.toolFactories.memory_store({
agentId: "main",
sessionKey: "agent:main:test",
});
const requestCountBeforeOmitDimensions = embeddingRequests.length;
await omitDimensionsTool.execute("tool-4", {
text: "dimensions should be omitted when configured",
scope: "global",
});
const omitDimensionsRequest = embeddingRequests.at(requestCountBeforeOmitDimensions);
assert.equal(
Object.prototype.hasOwnProperty.call(omitDimensionsRequest, "dimensions"),
false,
"embedding.omitDimensions=true should omit dimensions from embedding requests",
);
} finally {
await new Promise((resolve) => embeddingServer.close(resolve));
}
Expand Down