diff --git a/mem0-ts/src/oss/src/embeddings/azure.ts b/mem0-ts/src/oss/src/embeddings/azure.ts index 41bdc41d8..4026522b6 100644 --- a/mem0-ts/src/oss/src/embeddings/azure.ts +++ b/mem0-ts/src/oss/src/embeddings/azure.ts @@ -5,7 +5,7 @@ import { EmbeddingConfig } from "../types"; export class AzureOpenAIEmbedder implements Embedder { private client: AzureOpenAI; private model: string; - private embeddingDims?: number; + private embeddingDims: number | undefined; constructor(config: EmbeddingConfig) { if (!config.apiKey || !config.modelProperties?.endpoint) { @@ -20,13 +20,16 @@ export class AzureOpenAIEmbedder implements Embedder { ...rest, }); this.model = config.model || "text-embedding-3-small"; - this.embeddingDims = config.embeddingDims || 1536; + this.embeddingDims = config.embeddingDims; } async embed(text: string): Promise { const response = await this.client.embeddings.create({ model: this.model, input: text, + ...(this.embeddingDims !== undefined && { + dimensions: this.embeddingDims, + }), }); return response.data[0].embedding; } @@ -35,6 +38,9 @@ export class AzureOpenAIEmbedder implements Embedder { const response = await this.client.embeddings.create({ model: this.model, input: texts, + ...(this.embeddingDims !== undefined && { + dimensions: this.embeddingDims, + }), }); return response.data.map((item) => item.embedding); } diff --git a/mem0-ts/src/oss/src/embeddings/google.ts b/mem0-ts/src/oss/src/embeddings/google.ts index 13d2aca59..1e342b070 100644 --- a/mem0-ts/src/oss/src/embeddings/google.ts +++ b/mem0-ts/src/oss/src/embeddings/google.ts @@ -5,21 +5,23 @@ import { EmbeddingConfig } from "../types"; export class GoogleEmbedder implements Embedder { private google: GoogleGenAI; private model: string; - private embeddingDims?: number; + private embeddingDims: number | undefined; constructor(config: EmbeddingConfig) { this.google = new GoogleGenAI({ apiKey: config.apiKey || process.env.GOOGLE_API_KEY, }); this.model = config.model || "gemini-embedding-001"; - this.embeddingDims = config.embeddingDims || 1536; + this.embeddingDims = config.embeddingDims; } async embed(text: string): Promise { const response = await this.google.models.embedContent({ model: this.model, contents: text, - config: { outputDimensionality: this.embeddingDims }, + ...(this.embeddingDims !== undefined && { + config: { outputDimensionality: this.embeddingDims }, + }), }); return response.embeddings![0].values!; } @@ -28,7 +30,9 @@ export class GoogleEmbedder implements Embedder { const response = await this.google.models.embedContent({ model: this.model, contents: texts, - config: { outputDimensionality: this.embeddingDims }, + ...(this.embeddingDims !== undefined && { + config: { outputDimensionality: this.embeddingDims }, + }), }); return response.embeddings!.map((item) => item.values!); } diff --git a/mem0-ts/src/oss/src/embeddings/openai.ts b/mem0-ts/src/oss/src/embeddings/openai.ts index f3de81a7f..fc3b7bd4c 100644 --- a/mem0-ts/src/oss/src/embeddings/openai.ts +++ b/mem0-ts/src/oss/src/embeddings/openai.ts @@ -5,7 +5,7 @@ import { EmbeddingConfig } from "../types"; export class OpenAIEmbedder implements Embedder { private openai: OpenAI; private model: string; - private embeddingDims?: number; + private embeddingDims: number | undefined; constructor(config: EmbeddingConfig) { this.openai = new OpenAI({ @@ -13,13 +13,16 @@ export class OpenAIEmbedder implements Embedder { baseURL: config.baseURL || config.url, }); this.model = config.model || "text-embedding-3-small"; - this.embeddingDims = config.embeddingDims || 1536; + this.embeddingDims = config.embeddingDims; } async embed(text: string): Promise { const response = await this.openai.embeddings.create({ model: this.model, input: text, + ...(this.embeddingDims !== undefined && { + dimensions: this.embeddingDims, + }), }); return response.data[0].embedding; } @@ -28,6 +31,9 @@ export class OpenAIEmbedder implements Embedder { const response = await this.openai.embeddings.create({ model: this.model, input: texts, + ...(this.embeddingDims !== undefined && { + dimensions: this.embeddingDims, + }), }); return response.data.map((item) => item.embedding); } diff --git a/mem0-ts/src/oss/tests/azure-embedder.test.ts b/mem0-ts/src/oss/tests/azure-embedder.test.ts new file mode 100644 index 000000000..5b6f46ac3 --- /dev/null +++ b/mem0-ts/src/oss/tests/azure-embedder.test.ts @@ -0,0 +1,163 @@ +/// +/** + * Azure OpenAI Embedder — unit tests (mocked Azure OpenAI client). + * Verifies that the `dimensions` parameter is only passed to the API + * when the user explicitly configures `embeddingDims`. + */ + +const mockEmbeddingsCreate = jest.fn(); + +jest.mock("openai", () => { + return { + __esModule: true, + AzureOpenAI: jest.fn().mockImplementation(() => ({ + embeddings: { create: mockEmbeddingsCreate }, + })), + }; +}); + +import { AzureOpenAIEmbedder } from "../src/embeddings/azure"; + +const mockEmbedding = [0.1, 0.2, 0.3, 0.4, 0.5]; + +const baseConfig = { + apiKey: "test-key", + modelProperties: { endpoint: "https://test.openai.azure.com" }, +}; + +describe("AzureOpenAIEmbedder (unit)", () => { + beforeEach(() => { + mockEmbeddingsCreate.mockReset(); + mockEmbeddingsCreate.mockResolvedValue({ + data: [{ embedding: mockEmbedding }], + }); + }); + + describe("dimensions parameter", () => { + it("does NOT pass dimensions when embeddingDims is not set", async () => { + const embedder = new AzureOpenAIEmbedder(baseConfig); + + await embedder.embed("hello"); + + expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1); + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs).not.toHaveProperty("dimensions"); + expect(callArgs).toEqual({ + model: "text-embedding-3-small", + input: "hello", + }); + }); + + it("passes dimensions when embeddingDims is explicitly set", async () => { + const embedder = new AzureOpenAIEmbedder({ + ...baseConfig, + embeddingDims: 1024, + }); + + await embedder.embed("hello"); + + expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1); + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs).toEqual({ + model: "text-embedding-3-small", + input: "hello", + dimensions: 1024, + }); + }); + + it("passes dimensions=1536 when embeddingDims is explicitly set to 1536", async () => { + const embedder = new AzureOpenAIEmbedder({ + ...baseConfig, + embeddingDims: 1536, + }); + + await embedder.embed("hello"); + + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs).toHaveProperty("dimensions", 1536); + }); + + it("does NOT pass dimensions in embedBatch when embeddingDims is not set", async () => { + mockEmbeddingsCreate.mockResolvedValue({ + data: [{ embedding: mockEmbedding }, { embedding: mockEmbedding }], + }); + + const embedder = new AzureOpenAIEmbedder(baseConfig); + + await embedder.embedBatch(["hello", "world"]); + + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs).not.toHaveProperty("dimensions"); + }); + + it("passes dimensions in embedBatch when embeddingDims is explicitly set", async () => { + mockEmbeddingsCreate.mockResolvedValue({ + data: [{ embedding: mockEmbedding }, { embedding: mockEmbedding }], + }); + + const embedder = new AzureOpenAIEmbedder({ + ...baseConfig, + embeddingDims: 512, + }); + + await embedder.embedBatch(["hello", "world"]); + + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs).toEqual({ + model: "text-embedding-3-small", + input: ["hello", "world"], + dimensions: 512, + }); + }); + }); + + describe("basic functionality", () => { + it("embed() returns the embedding vector", async () => { + const embedder = new AzureOpenAIEmbedder(baseConfig); + + const result = await embedder.embed("hello"); + expect(result).toEqual(mockEmbedding); + }); + + it("embedBatch() returns vectors for multiple inputs", async () => { + const batch = [ + [0.1, 0.2], + [0.3, 0.4], + ]; + mockEmbeddingsCreate.mockResolvedValue({ + data: batch.map((embedding) => ({ embedding })), + }); + + const embedder = new AzureOpenAIEmbedder(baseConfig); + + const result = await embedder.embedBatch(["text1", "text2"]); + expect(result).toEqual(batch); + }); + + it("uses custom model when provided", async () => { + const embedder = new AzureOpenAIEmbedder({ + ...baseConfig, + model: "text-embedding-3-large", + }); + + await embedder.embed("hello"); + + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs.model).toBe("text-embedding-3-large"); + }); + + it("throws when API key is missing", () => { + expect(() => { + new AzureOpenAIEmbedder({ + modelProperties: { endpoint: "https://test.openai.azure.com" }, + }); + }).toThrow("Azure OpenAI requires both API key and endpoint"); + }); + + it("throws when endpoint is missing", () => { + expect(() => { + new AzureOpenAIEmbedder({ apiKey: "test-key" }); + }).toThrow("Azure OpenAI requires both API key and endpoint"); + }); + }); +}); diff --git a/mem0-ts/src/oss/tests/google-embedder.test.ts b/mem0-ts/src/oss/tests/google-embedder.test.ts new file mode 100644 index 000000000..a53ad0673 --- /dev/null +++ b/mem0-ts/src/oss/tests/google-embedder.test.ts @@ -0,0 +1,153 @@ +/// +/** + * Google Embedder — unit tests (mocked Google GenAI client). + * Verifies that the `outputDimensionality` config is only passed to the API + * when the user explicitly configures `embeddingDims`. + */ + +const mockEmbedContent = jest.fn(); + +jest.mock("@google/genai", () => { + return { + __esModule: true, + GoogleGenAI: jest.fn().mockImplementation(() => ({ + models: { embedContent: mockEmbedContent }, + })), + }; +}); + +import { GoogleEmbedder } from "../src/embeddings/google"; + +const mockEmbedding = [0.1, 0.2, 0.3, 0.4, 0.5]; + +describe("GoogleEmbedder (unit)", () => { + beforeEach(() => { + mockEmbedContent.mockReset(); + mockEmbedContent.mockResolvedValue({ + embeddings: [{ values: mockEmbedding }], + }); + }); + + describe("outputDimensionality parameter", () => { + it("does NOT pass config when embeddingDims is not set", async () => { + const embedder = new GoogleEmbedder({ + apiKey: "test-key", + }); + + await embedder.embed("hello"); + + expect(mockEmbedContent).toHaveBeenCalledTimes(1); + const callArgs = mockEmbedContent.mock.calls[0][0]; + expect(callArgs).not.toHaveProperty("config"); + expect(callArgs).toEqual({ + model: "gemini-embedding-001", + contents: "hello", + }); + }); + + it("passes outputDimensionality when embeddingDims is explicitly set", async () => { + const embedder = new GoogleEmbedder({ + apiKey: "test-key", + embeddingDims: 768, + }); + + await embedder.embed("hello"); + + expect(mockEmbedContent).toHaveBeenCalledTimes(1); + const callArgs = mockEmbedContent.mock.calls[0][0]; + expect(callArgs).toEqual({ + model: "gemini-embedding-001", + contents: "hello", + config: { outputDimensionality: 768 }, + }); + }); + + it("passes outputDimensionality=1536 when embeddingDims is explicitly set to 1536", async () => { + const embedder = new GoogleEmbedder({ + apiKey: "test-key", + embeddingDims: 1536, + }); + + await embedder.embed("hello"); + + const callArgs = mockEmbedContent.mock.calls[0][0]; + expect(callArgs).toHaveProperty("config"); + expect(callArgs.config).toEqual({ outputDimensionality: 1536 }); + }); + + it("does NOT pass config in embedBatch when embeddingDims is not set", async () => { + mockEmbedContent.mockResolvedValue({ + embeddings: [{ values: mockEmbedding }, { values: mockEmbedding }], + }); + + const embedder = new GoogleEmbedder({ + apiKey: "test-key", + }); + + await embedder.embedBatch(["hello", "world"]); + + const callArgs = mockEmbedContent.mock.calls[0][0]; + expect(callArgs).not.toHaveProperty("config"); + }); + + it("passes outputDimensionality in embedBatch when embeddingDims is explicitly set", async () => { + mockEmbedContent.mockResolvedValue({ + embeddings: [{ values: mockEmbedding }, { values: mockEmbedding }], + }); + + const embedder = new GoogleEmbedder({ + apiKey: "test-key", + embeddingDims: 512, + }); + + await embedder.embedBatch(["hello", "world"]); + + const callArgs = mockEmbedContent.mock.calls[0][0]; + expect(callArgs).toEqual({ + model: "gemini-embedding-001", + contents: ["hello", "world"], + config: { outputDimensionality: 512 }, + }); + }); + }); + + describe("basic functionality", () => { + it("embed() returns the embedding vector", async () => { + const embedder = new GoogleEmbedder({ + apiKey: "test-key", + }); + + const result = await embedder.embed("hello"); + expect(result).toEqual(mockEmbedding); + }); + + it("embedBatch() returns vectors for multiple inputs", async () => { + const batch = [ + [0.1, 0.2], + [0.3, 0.4], + ]; + mockEmbedContent.mockResolvedValue({ + embeddings: batch.map((values) => ({ values })), + }); + + const embedder = new GoogleEmbedder({ + apiKey: "test-key", + }); + + const result = await embedder.embedBatch(["text1", "text2"]); + expect(result).toEqual(batch); + }); + + it("uses custom model when provided", async () => { + const embedder = new GoogleEmbedder({ + apiKey: "test-key", + model: "text-embedding-004", + }); + + await embedder.embed("hello"); + + const callArgs = mockEmbedContent.mock.calls[0][0]; + expect(callArgs.model).toBe("text-embedding-004"); + }); + }); +}); diff --git a/mem0-ts/src/oss/tests/openai-embedder.test.ts b/mem0-ts/src/oss/tests/openai-embedder.test.ts new file mode 100644 index 000000000..1dc7ba2a1 --- /dev/null +++ b/mem0-ts/src/oss/tests/openai-embedder.test.ts @@ -0,0 +1,152 @@ +/// +/** + * OpenAI Embedder — unit tests (mocked OpenAI client). + * Verifies that the `dimensions` parameter is only passed to the API + * when the user explicitly configures `embeddingDims`. + */ + +const mockEmbeddingsCreate = jest.fn(); + +jest.mock("openai", () => { + return { + __esModule: true, + default: jest.fn().mockImplementation(() => ({ + embeddings: { create: mockEmbeddingsCreate }, + })), + }; +}); + +import { OpenAIEmbedder } from "../src/embeddings/openai"; + +const mockEmbedding = [0.1, 0.2, 0.3, 0.4, 0.5]; + +describe("OpenAIEmbedder (unit)", () => { + beforeEach(() => { + mockEmbeddingsCreate.mockReset(); + mockEmbeddingsCreate.mockResolvedValue({ + data: [{ embedding: mockEmbedding }], + }); + }); + + describe("dimensions parameter", () => { + it("does NOT pass dimensions when embeddingDims is not set", async () => { + const embedder = new OpenAIEmbedder({ + apiKey: "test-key", + }); + + await embedder.embed("hello"); + + expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1); + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs).not.toHaveProperty("dimensions"); + expect(callArgs).toEqual({ + model: "text-embedding-3-small", + input: "hello", + }); + }); + + it("passes dimensions when embeddingDims is explicitly set", async () => { + const embedder = new OpenAIEmbedder({ + apiKey: "test-key", + embeddingDims: 1024, + }); + + await embedder.embed("hello"); + + expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1); + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs).toEqual({ + model: "text-embedding-3-small", + input: "hello", + dimensions: 1024, + }); + }); + + it("passes dimensions=1536 when embeddingDims is explicitly set to 1536", async () => { + const embedder = new OpenAIEmbedder({ + apiKey: "test-key", + embeddingDims: 1536, + }); + + await embedder.embed("hello"); + + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs).toHaveProperty("dimensions", 1536); + }); + + it("does NOT pass dimensions in embedBatch when embeddingDims is not set", async () => { + mockEmbeddingsCreate.mockResolvedValue({ + data: [{ embedding: mockEmbedding }, { embedding: mockEmbedding }], + }); + + const embedder = new OpenAIEmbedder({ + apiKey: "test-key", + }); + + await embedder.embedBatch(["hello", "world"]); + + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs).not.toHaveProperty("dimensions"); + }); + + it("passes dimensions in embedBatch when embeddingDims is explicitly set", async () => { + mockEmbeddingsCreate.mockResolvedValue({ + data: [{ embedding: mockEmbedding }, { embedding: mockEmbedding }], + }); + + const embedder = new OpenAIEmbedder({ + apiKey: "test-key", + embeddingDims: 512, + }); + + await embedder.embedBatch(["hello", "world"]); + + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs).toEqual({ + model: "text-embedding-3-small", + input: ["hello", "world"], + dimensions: 512, + }); + }); + }); + + describe("basic functionality", () => { + it("embed() returns the embedding vector", async () => { + const embedder = new OpenAIEmbedder({ + apiKey: "test-key", + }); + + const result = await embedder.embed("hello"); + expect(result).toEqual(mockEmbedding); + }); + + it("embedBatch() returns vectors for multiple inputs", async () => { + const batch = [ + [0.1, 0.2], + [0.3, 0.4], + ]; + mockEmbeddingsCreate.mockResolvedValue({ + data: batch.map((embedding) => ({ embedding })), + }); + + const embedder = new OpenAIEmbedder({ + apiKey: "test-key", + }); + + const result = await embedder.embedBatch(["text1", "text2"]); + expect(result).toEqual(batch); + }); + + it("uses custom model when provided", async () => { + const embedder = new OpenAIEmbedder({ + apiKey: "test-key", + model: "text-embedding-3-large", + }); + + await embedder.embed("hello"); + + const callArgs = mockEmbeddingsCreate.mock.calls[0][0]; + expect(callArgs.model).toBe("text-embedding-3-large"); + }); + }); +});