feat(memory-lancedb): support custom embedding endpoints
Add support for self-hosted OpenAI-compatible embedding servers:
- Add `embedding.baseUrl` config option for custom endpoint URL
- Add `embedding.dimensions` config option to override vector dimensions
- Remove model enum restriction to allow any model name
- Update Embeddings class to pass baseURL to OpenAI client
This enables users to run local embedding models (e.g., via llama.cpp,
text-embeddings-inference, or other OpenAI-compatible servers) instead
of requiring the OpenAI API.
Example config:
```json
{
"embedding": {
"apiKey": "not-needed",
"baseUrl": "http://localhost:8080/v1",
"model": "my-local-model",
"dimensions": 4096
}
}
```
This commit is contained in:
parent
01e0d3a320
commit
d2b1dde73b
@ -6,12 +6,24 @@
|
|||||||
"label": "OpenAI API Key",
|
"label": "OpenAI API Key",
|
||||||
"sensitive": true,
|
"sensitive": true,
|
||||||
"placeholder": "sk-proj-...",
|
"placeholder": "sk-proj-...",
|
||||||
"help": "API key for OpenAI embeddings (or use ${OPENAI_API_KEY})"
|
"help": "API key for embeddings (use 'not-needed' for local servers)"
|
||||||
},
|
},
|
||||||
"embedding.model": {
|
"embedding.model": {
|
||||||
"label": "Embedding Model",
|
"label": "Embedding Model",
|
||||||
"placeholder": "text-embedding-3-small",
|
"placeholder": "text-embedding-3-small",
|
||||||
"help": "OpenAI embedding model to use"
|
"help": "Embedding model name"
|
||||||
|
},
|
||||||
|
"embedding.baseUrl": {
|
||||||
|
"label": "Custom Endpoint URL",
|
||||||
|
"placeholder": "http://localhost:8080/v1",
|
||||||
|
"help": "Custom OpenAI-compatible embedding endpoint (for local/self-hosted servers)",
|
||||||
|
"advanced": true
|
||||||
|
},
|
||||||
|
"embedding.dimensions": {
|
||||||
|
"label": "Vector Dimensions",
|
||||||
|
"placeholder": "1536",
|
||||||
|
"help": "Override vector dimensions (required for custom models not in built-in list)",
|
||||||
|
"advanced": true
|
||||||
},
|
},
|
||||||
"dbPath": {
|
"dbPath": {
|
||||||
"label": "Database Path",
|
"label": "Database Path",
|
||||||
@ -39,11 +51,15 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"model": {
|
"model": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"baseUrl": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"description": "Custom OpenAI-compatible endpoint URL"
|
||||||
"text-embedding-3-small",
|
},
|
||||||
"text-embedding-3-large"
|
"dimensions": {
|
||||||
]
|
"type": "number",
|
||||||
|
"description": "Override vector dimensions for custom models"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@ -7,6 +7,8 @@ export type MemoryConfig = {
|
|||||||
provider: "openai";
|
provider: "openai";
|
||||||
model?: string;
|
model?: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
|
baseUrl?: string; // Custom endpoint URL (for local/self-hosted embeddings)
|
||||||
|
dimensions?: number; // Override vector dimensions
|
||||||
};
|
};
|
||||||
dbPath?: string;
|
dbPath?: string;
|
||||||
autoCapture?: boolean;
|
autoCapture?: boolean;
|
||||||
@ -34,10 +36,14 @@ function assertAllowedKeys(
|
|||||||
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
|
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function vectorDimsForModel(model: string): number {
|
export function vectorDimsForModel(model: string, customDims?: number): number {
|
||||||
|
// Custom dimensions override built-in model lookup
|
||||||
|
if (customDims) return customDims;
|
||||||
const dims = EMBEDDING_DIMENSIONS[model];
|
const dims = EMBEDDING_DIMENSIONS[model];
|
||||||
if (!dims) {
|
if (!dims) {
|
||||||
throw new Error(`Unsupported embedding model: ${model}`);
|
throw new Error(
|
||||||
|
`Unsupported embedding model: ${model}. Specify dimensions manually via embedding.dimensions.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return dims;
|
return dims;
|
||||||
}
|
}
|
||||||
@ -54,7 +60,10 @@ function resolveEnvVars(value: string): string {
|
|||||||
|
|
||||||
function resolveEmbeddingModel(embedding: Record<string, unknown>): string {
|
function resolveEmbeddingModel(embedding: Record<string, unknown>): string {
|
||||||
const model = typeof embedding.model === "string" ? embedding.model : DEFAULT_MODEL;
|
const model = typeof embedding.model === "string" ? embedding.model : DEFAULT_MODEL;
|
||||||
vectorDimsForModel(model);
|
// Skip dimension validation if custom dimensions provided
|
||||||
|
if (typeof embedding.dimensions !== "number") {
|
||||||
|
vectorDimsForModel(model);
|
||||||
|
}
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,15 +79,19 @@ export const memoryConfigSchema = {
|
|||||||
if (!embedding || typeof embedding.apiKey !== "string") {
|
if (!embedding || typeof embedding.apiKey !== "string") {
|
||||||
throw new Error("embedding.apiKey is required");
|
throw new Error("embedding.apiKey is required");
|
||||||
}
|
}
|
||||||
assertAllowedKeys(embedding, ["apiKey", "model"], "embedding config");
|
assertAllowedKeys(embedding, ["apiKey", "model", "baseUrl", "dimensions"], "embedding config");
|
||||||
|
|
||||||
const model = resolveEmbeddingModel(embedding);
|
const model = resolveEmbeddingModel(embedding);
|
||||||
|
const baseUrl = typeof embedding.baseUrl === "string" ? embedding.baseUrl : undefined;
|
||||||
|
const dimensions = typeof embedding.dimensions === "number" ? embedding.dimensions : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
embedding: {
|
embedding: {
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
model,
|
model,
|
||||||
apiKey: resolveEnvVars(embedding.apiKey),
|
apiKey: resolveEnvVars(embedding.apiKey),
|
||||||
|
baseUrl,
|
||||||
|
dimensions,
|
||||||
},
|
},
|
||||||
dbPath: typeof cfg.dbPath === "string" ? cfg.dbPath : DEFAULT_DB_PATH,
|
dbPath: typeof cfg.dbPath === "string" ? cfg.dbPath : DEFAULT_DB_PATH,
|
||||||
autoCapture: cfg.autoCapture !== false,
|
autoCapture: cfg.autoCapture !== false,
|
||||||
@ -90,12 +103,24 @@ export const memoryConfigSchema = {
|
|||||||
label: "OpenAI API Key",
|
label: "OpenAI API Key",
|
||||||
sensitive: true,
|
sensitive: true,
|
||||||
placeholder: "sk-proj-...",
|
placeholder: "sk-proj-...",
|
||||||
help: "API key for OpenAI embeddings (or use ${OPENAI_API_KEY})",
|
help: "API key for embeddings (use 'not-needed' for local servers)",
|
||||||
},
|
},
|
||||||
"embedding.model": {
|
"embedding.model": {
|
||||||
label: "Embedding Model",
|
label: "Embedding Model",
|
||||||
placeholder: DEFAULT_MODEL,
|
placeholder: DEFAULT_MODEL,
|
||||||
help: "OpenAI embedding model to use",
|
help: "Embedding model name",
|
||||||
|
},
|
||||||
|
"embedding.baseUrl": {
|
||||||
|
label: "Custom Endpoint URL",
|
||||||
|
placeholder: "http://localhost:8080/v1",
|
||||||
|
help: "Custom OpenAI-compatible embedding endpoint (for local/self-hosted servers)",
|
||||||
|
advanced: true,
|
||||||
|
},
|
||||||
|
"embedding.dimensions": {
|
||||||
|
label: "Vector Dimensions",
|
||||||
|
placeholder: "1536",
|
||||||
|
help: "Override vector dimensions (required for custom models not in built-in list)",
|
||||||
|
advanced: true,
|
||||||
},
|
},
|
||||||
dbPath: {
|
dbPath: {
|
||||||
label: "Database Path",
|
label: "Database Path",
|
||||||
|
|||||||
@ -156,8 +156,9 @@ class Embeddings {
|
|||||||
constructor(
|
constructor(
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
private model: string,
|
private model: string,
|
||||||
|
baseUrl?: string,
|
||||||
) {
|
) {
|
||||||
this.client = new OpenAI({ apiKey });
|
this.client = new OpenAI({ apiKey, baseURL: baseUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
async embed(text: string): Promise<number[]> {
|
async embed(text: string): Promise<number[]> {
|
||||||
@ -223,9 +224,16 @@ const memoryPlugin = {
|
|||||||
register(api: MoltbotPluginApi) {
|
register(api: MoltbotPluginApi) {
|
||||||
const cfg = memoryConfigSchema.parse(api.pluginConfig);
|
const cfg = memoryConfigSchema.parse(api.pluginConfig);
|
||||||
const resolvedDbPath = api.resolvePath(cfg.dbPath!);
|
const resolvedDbPath = api.resolvePath(cfg.dbPath!);
|
||||||
const vectorDim = vectorDimsForModel(cfg.embedding.model ?? "text-embedding-3-small");
|
const vectorDim = vectorDimsForModel(
|
||||||
|
cfg.embedding.model ?? "text-embedding-3-small",
|
||||||
|
cfg.embedding.dimensions,
|
||||||
|
);
|
||||||
const db = new MemoryDB(resolvedDbPath, vectorDim);
|
const db = new MemoryDB(resolvedDbPath, vectorDim);
|
||||||
const embeddings = new Embeddings(cfg.embedding.apiKey, cfg.embedding.model!);
|
const embeddings = new Embeddings(
|
||||||
|
cfg.embedding.apiKey,
|
||||||
|
cfg.embedding.model!,
|
||||||
|
cfg.embedding.baseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`memory-lancedb: plugin registered (db: ${resolvedDbPath}, lazy init)`,
|
`memory-lancedb: plugin registered (db: ${resolvedDbPath}, lazy init)`,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user