feat(ts-sdk): add FastEmbed embedding provider (#5862)

Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
Rod Boev
2026-07-06 21:56:11 +05:30
committed by GitHub
co-authored by kartik-mem0
parent 03b41ab00f
commit fec7cdf118
12 changed files with 680 additions and 7 deletions
@@ -298,6 +298,38 @@ describe("ConfigManager", () => {
});
});
describe("mergeConfig - FastEmbed defaults", () => {
const baseLlm = { provider: "openai", config: { apiKey: "k" } };
it("does not inject the OpenAI default embedder model into fastembed", () => {
const cfg = ConfigManager.mergeConfig({
embedder: {
provider: "fastembed",
config: {},
},
vectorStore: { provider: "memory", config: {} },
llm: baseLlm,
});
expect(cfg.embedder.provider).toBe("fastembed");
expect(cfg.embedder.config.model).toBeUndefined();
});
it("treats FastEmbed provider casing the same way as the factory", () => {
const cfg = ConfigManager.mergeConfig({
embedder: {
provider: "FastEmbed",
config: {},
},
vectorStore: { provider: "memory", config: {} },
llm: baseLlm,
});
expect(cfg.embedder.provider).toBe("FastEmbed");
expect(cfg.embedder.config.model).toBeUndefined();
});
});
describe("mergeConfig - LM Studio LLM config", () => {
const baseEmbedder = { provider: "openai", config: { apiKey: "k" } };
@@ -603,6 +635,30 @@ describe("Memory – LM Studio end-to-end flow", () => {
expect(result.results[0].memory).toBe("User likes hiking");
});
it("preserves the FastEmbed provider default model through the Memory config path", async () => {
const mem = new MemoryClass({
embedder: {
provider: "fastembed",
config: {},
},
vectorStore: { provider: "memory", config: { collectionName: "test" } },
llm: {
provider: "openai",
config: { apiKey: "test-key" },
},
disableHistory: true,
});
await mem.getAll({ filters: { user_id: "u1" } });
expect(mockEmbedderFactory.create).toHaveBeenCalledWith(
"fastembed",
expect.objectContaining({
model: undefined,
}),
);
});
it("add flow works with lmstudio LLM for fact extraction", async () => {
mockLlm.generateResponse.mockResolvedValueOnce(
'{"facts":["User loves sushi"]}',
@@ -20,6 +20,11 @@ jest.mock("../src/embeddings/google", () => ({
.fn()
.mockImplementation((config) => ({ type: "google-embedder", config })),
}));
jest.mock("../src/embeddings/fastembed", () => ({
FastEmbedEmbedder: jest
.fn()
.mockImplementation((config) => ({ type: "fastembed-embedder", config })),
}));
jest.mock("../src/embeddings/azure", () => ({
AzureOpenAIEmbedder: jest
.fn()
@@ -193,6 +198,7 @@ describe("EmbedderFactory", () => {
["google"],
["gemini"],
["azure_openai"],
["fastembed"],
["langchain"],
["lmstudio"],
["together"],
@@ -0,0 +1,123 @@
/// <reference types="jest" />
/**
* FastEmbed Embedder — unit tests (mocked FastEmbed client).
* Verifies async generator handling for embed and embedBatch.
*/
const mockInit = jest.fn();
const mockEmbed = jest.fn();
jest.mock("fastembed", () => {
return {
__esModule: true,
EmbeddingModel: {
BGESmallENV15: "fast-bge-small-en-v1.5",
},
FlagEmbedding: {
init: mockInit,
},
};
});
import { FastEmbedEmbedder } from "../src/embeddings/fastembed";
const mockVectorBatch = [
[0.1, 0.2],
[0.3, 0.4],
];
const mockEmbedding = [0.1, 0.2, 0.3];
const createEmbeddingGenerator = async function* (batches: number[][][]) {
for (const batch of batches) {
yield batch;
}
};
describe("FastEmbedEmbedder (unit)", () => {
beforeEach(() => {
mockInit.mockReset();
mockEmbed.mockReset();
mockInit.mockResolvedValue({
embed: mockEmbed,
});
});
it("embed() initializes lazily, normalizes newlines, and returns the first vector", async () => {
mockEmbed.mockReturnValue(
createEmbeddingGenerator([[mockEmbedding], mockVectorBatch]),
);
const embedder = new FastEmbedEmbedder({
model: "fast-bge-small-en-v1.5",
});
expect(mockInit).not.toHaveBeenCalled();
const result = await embedder.embed("Example\ntext");
expect(mockInit).toHaveBeenCalledTimes(1);
expect(mockInit).toHaveBeenCalledWith({
model: "fast-bge-small-en-v1.5",
});
expect(mockEmbed).toHaveBeenCalledTimes(1);
expect(mockEmbed.mock.calls[0][0]).toEqual(["Example text"]);
expect(result).toEqual(mockEmbedding);
});
it("embedBatch() consumes all async embedding batches and reuses the initialized model", async () => {
mockEmbed.mockReturnValueOnce(createEmbeddingGenerator([[mockEmbedding]]));
mockEmbed.mockReturnValueOnce(
createEmbeddingGenerator([[mockEmbedding], mockVectorBatch]),
);
const embedder = new FastEmbedEmbedder({});
await embedder.embed("warmup");
const result = await embedder.embedBatch([
"first text",
"second text",
"third text",
]);
expect(mockEmbed).toHaveBeenCalledWith([
"first text",
"second text",
"third text",
]);
expect(mockInit).toHaveBeenCalledTimes(1);
expect(result).toEqual([mockEmbedding, ...mockVectorBatch]);
});
it("embed() throws when FastEmbed yields no embeddings", async () => {
mockEmbed.mockReturnValue(createEmbeddingGenerator([]));
const embedder = new FastEmbedEmbedder({});
await expect(embedder.embed("missing")).rejects.toThrow(
"FastEmbed embed() returned no embeddings",
);
});
it("embed() retries initialization after a transient init failure", async () => {
mockInit.mockRejectedValueOnce(new Error("transient init failure"));
mockInit.mockResolvedValueOnce({
embed: mockEmbed,
});
mockEmbed.mockReturnValue(createEmbeddingGenerator([[mockEmbedding]]));
const embedder = new FastEmbedEmbedder({});
await expect(embedder.embed("first attempt")).rejects.toThrow(
"transient init failure",
);
const result = await embedder.embed("second attempt");
expect(mockInit).toHaveBeenCalledTimes(2);
expect(result).toEqual(mockEmbedding);
});
it("throws a clear error for an unsupported model instead of failing at init", () => {
expect(
() => new FastEmbedEmbedder({ model: "text-embedding-3-small" }),
).toThrow(/Unsupported FastEmbed model "text-embedding-3-small"/);
expect(mockInit).not.toHaveBeenCalled();
});
});