Skip to content
Draft
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
12 changes: 10 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ const isCliMode = () => process.env.OPENCLAW_CLI === "1";

// Import core components
import { MemoryStore, validateStoragePath } from "./src/store.js";
import { createEmbedder, getVectorDimensions } from "./src/embedder.js";
import {
createEmbedder,
getEffectiveVectorDimensions,
} from "./src/embedder.js";
import { createRetriever, DEFAULT_RETRIEVAL_CONFIG } from "./src/retriever.js";
import { createScopeManager, resolveScopeFilter, isSystemBypassId, parseAgentIdFromSessionKey } from "./src/scopes.js";
import { createMigrator } from "./src/migrate.js";
Expand Down Expand Up @@ -91,6 +94,7 @@ interface PluginConfig {
model?: string;
baseURL?: string;
dimensions?: number;
requestDimensions?: number;
omitDimensions?: boolean;
taskQuery?: string;
taskPassage?: string;
Expand Down Expand Up @@ -1651,9 +1655,10 @@ const memoryLanceDBProPlugin = {
);
}

const vectorDim = getVectorDimensions(
const vectorDim = getEffectiveVectorDimensions(
config.embedding.model || "text-embedding-3-small",
config.embedding.dimensions,
config.embedding.requestDimensions,
);

// Initialize core components
Expand All @@ -1664,6 +1669,7 @@ const memoryLanceDBProPlugin = {
model: config.embedding.model || "text-embedding-3-small",
baseURL: config.embedding.baseURL,
dimensions: config.embedding.dimensions,
requestDimensions: config.embedding.requestDimensions,
omitDimensions: config.embedding.omitDimensions,
taskQuery: config.embedding.taskQuery,
taskPassage: config.embedding.taskPassage,
Expand Down Expand Up @@ -3845,6 +3851,8 @@ 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),
// Intentionally no top-level fallback: requestDimensions is request-only.
requestDimensions: parsePositiveInt(embedding.requestDimensions),
omitDimensions:
typeof embedding.omitDimensions === "boolean"
? embedding.omitDimensions
Expand Down
22 changes: 17 additions & 5 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,17 @@
},
"dimensions": {
"type": "integer",
"minimum": 1
"minimum": 1,
"description": "Internal vector dimensions for LanceDB schema sizing and local embedding validation"
},
"requestDimensions": {
"type": "integer",
"minimum": 1,
"description": "Optional dimensions/output_dimension value to send to embedding providers that support variable output sizes"
},
"omitDimensions": {
"type": "boolean",
"description": "When true, omit the dimensions parameter from embedding requests even if dimensions is configured"
"description": "When true, omit dimensions/output_dimension from embedding requests even if requestDimensions is configured"
},
"taskQuery": {
"type": "string",
Expand Down Expand Up @@ -875,14 +881,20 @@
"advanced": true
},
"embedding.dimensions": {
"label": "Vector Dimensions",
"label": "Schema Dimensions",
"placeholder": "auto-detected from model",
"help": "Override vector dimensions for custom models not in the built-in lookup table",
"help": "Internal vector dimensions used for LanceDB schema sizing and local embedding validation. Override this for custom models not in the built-in lookup table.",
"advanced": true
},
"embedding.requestDimensions": {
"label": "Request Dimensions",
"placeholder": "omit by default",
"help": "Optional dimensions/output_dimension value to send to the embedding API. If unset, no request-side dimensions field is sent.",
"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.",
"help": "Do not send dimensions/output_dimension to the embedding API even if embedding.requestDimensions is configured. Useful for local models like Qwen3-Embedding that reject the field.",
"advanced": true
},
"embedding.taskQuery": {
Expand Down
19 changes: 17 additions & 2 deletions src/embedder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export interface EmbeddingConfig {
apiKey: string | string[];
model: string;
baseURL?: string;
/** Internal vector dimension for schema sizing and local validation. */
dimensions?: number;
/** Optional API request output dimension for providers that support it. */
requestDimensions?: number;

/** Optional task type for query embeddings (e.g. "retrieval.query") */
taskQuery?: string;
Expand Down Expand Up @@ -419,6 +422,14 @@ export function getVectorDimensions(model: string, overrideDims?: number): numbe
return dims;
}

export function getEffectiveVectorDimensions(
model: string,
dimensions?: number,
requestDimensions?: number,
): number {
return getVectorDimensions(model, requestDimensions ?? dimensions);
}

// ============================================================================
// Embedder Class
// ============================================================================
Expand Down Expand Up @@ -456,7 +467,7 @@ export class Embedder {
this._taskQuery = config.taskQuery;
this._taskPassage = config.taskPassage;
this._normalized = config.normalized;
this._requestDimensions = config.dimensions;
this._requestDimensions = config.requestDimensions;
this._omitDimensions = config.omitDimensions === true;
// Enable auto-chunking by default for better handling of long documents
this._autoChunk = config.chunking !== false;
Expand Down Expand Up @@ -500,7 +511,11 @@ export class Embedder {
console.log(`[memory-lancedb-pro] Initialized ${this.clients.length} API keys for round-robin rotation`);
}

this.dimensions = getVectorDimensions(config.model, config.dimensions);
this.dimensions = getEffectiveVectorDimensions(
config.model,
config.dimensions,
config.requestDimensions,
);
this._cache = new EmbeddingCache(256, 30); // 256 entries, 30 min TTL
}

Expand Down
26 changes: 22 additions & 4 deletions test/embedder-error-hints.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ async function run() {
installMockEmbeddingClient(jinaEmbedder, async (payload) => {
assert.equal(payload.task, "retrieval.passage");
assert.equal(payload.normalized, true);
assert.equal(payload.dimensions, 1024);
assert.equal(payload.dimensions, undefined, "jina should not send dimensions unless requestDimensions is set");
return createEmbeddingResponse(1024);
});
await jinaEmbedder.embedPassage("hello");
Expand All @@ -144,7 +144,7 @@ async function run() {
});
installMockEmbeddingClient(genericEmbedder, async (payload) => {
assert.equal(payload.encoding_format, "float");
assert.equal(payload.dimensions, 384);
assert.equal(payload.dimensions, undefined, "generic profile should not send dimensions unless requestDimensions is set");
return createEmbeddingResponse(384);
});
await genericEmbedder.embedPassage("hello");
Expand Down Expand Up @@ -196,7 +196,7 @@ async function run() {
apiKey: "test-key",
model: "voyage-4-lite",
baseURL: "https://api.voyageai.com/v1",
dimensions: 512,
requestDimensions: 512,
});
installMockEmbeddingClient(voyageDimEmbedder, async (payload) => {
assert.equal(payload.output_dimension, 512, "voyage should send output_dimension");
Expand All @@ -211,7 +211,7 @@ async function run() {
await withEmbeddingCaptureServer(
(payload) => {
assert.equal(payload.encoding_format, "float", "generic profile should send encoding_format");
assert.equal(payload.dimensions, 384, "generic profile should send dimensions");
assert.equal(payload.dimensions, undefined, "generic profile should not send dimensions by default");
assert.equal(payload.task, undefined, "generic profile should not send task");
assert.equal(payload.normalized, undefined, "generic profile should not send normalized");
return { body: createEmbeddingResponse(384) };
Expand All @@ -228,6 +228,24 @@ async function run() {
},
);

await withEmbeddingCaptureServer(
(payload) => {
assert.equal(payload.encoding_format, "float", "generic profile should send encoding_format");
assert.equal(payload.dimensions, 384, "generic profile should send dimensions when requestDimensions is set");
return { body: createEmbeddingResponse(384) };
},
async ({ baseURL }) => {
const embedder = new Embedder({
provider: "openai-compatible",
apiKey: "test-key",
model: "text-embedding-3-small",
baseURL,
requestDimensions: 384,
});
await embedder.embedPassage("hello world");
},
);

await withJsonServer(
403,
{ error: { message: "Invalid API key", code: "invalid_api_key" } },
Expand Down
53 changes: 48 additions & 5 deletions test/plugin-manifest-regression.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ assert.equal(
"boolean",
"embedding.omitDimensions should be declared in the plugin schema",
);
assert.equal(
manifest.configSchema.properties.embedding.properties.requestDimensions?.type,
"integer",
"embedding.requestDimensions should be declared in the plugin schema",
);
assert.ok(
Object.prototype.hasOwnProperty.call(manifest.uiHints, "embedding.requestDimensions"),
"uiHints should expose embedding.requestDimensions",
);
assert.equal(
manifest.configSchema.properties.sessionMemory.properties.enabled.default,
false,
Expand Down Expand Up @@ -327,14 +336,48 @@ try {
});
const requestCountBeforeWithDimensions = embeddingRequests.length;
await withDimensionsTool.execute("tool-3", {
text: "dimensions should be sent by default",
text: "dimensions should stay internal by default",
scope: "global",
});
const withDimensionsRequest = embeddingRequests.at(requestCountBeforeWithDimensions);
assert.equal(
withDimensionsRequest?.dimensions,
Object.prototype.hasOwnProperty.call(withDimensionsRequest ?? {}, "dimensions"),
false,
"embedding.dimensions should be used for local schema sizing, not forwarded by default",
);

const withRequestDimensionsApi = createMockApi({
dbPath: path.join(workDir, "db-with-request-dimensions"),
autoCapture: false,
autoRecall: false,
embedding: {
provider: "openai-compatible",
apiKey: "dummy",
model: "text-embedding-3-small",
baseURL: embeddingBaseURL,
requestDimensions: 4,
},
});
plugin.register(withRequestDimensionsApi);
const withRequestDimensionsTool = withRequestDimensionsApi.toolFactories.memory_store({
agentId: "main",
sessionKey: "agent:main:test",
});
const requestCountBeforeRequestDimensions = embeddingRequests.length;
const withRequestDimensionsResult = await withRequestDimensionsTool.execute("tool-3b", {
text: "requestDimensions should drive both request payload and local schema size",
scope: "global",
});
assert.equal(
withRequestDimensionsResult.details.action,
"created",
"requestDimensions-only config should still create memories end-to-end",
);
const withRequestDimensionsRequest = embeddingRequests.at(requestCountBeforeRequestDimensions);
assert.equal(
withRequestDimensionsRequest?.dimensions,
4,
"embedding.dimensions should be forwarded by default",
"embedding.requestDimensions should be forwarded to embedding requests",
);

const omitDimensionsApi = createMockApi({
Expand All @@ -346,7 +389,7 @@ try {
apiKey: "dummy",
model: "text-embedding-3-small",
baseURL: embeddingBaseURL,
dimensions: 4,
requestDimensions: 4,
omitDimensions: true,
},
});
Expand All @@ -364,7 +407,7 @@ try {
assert.equal(
Object.prototype.hasOwnProperty.call(omitDimensionsRequest, "dimensions"),
false,
"embedding.omitDimensions=true should omit dimensions from embedding requests",
"embedding.omitDimensions=true should omit dimensions from embedding requests even when requestDimensions is set",
);
} finally {
await new Promise((resolve) => embeddingServer.close(resolve));
Expand Down
Loading