diff --git a/mem0-ts/src/oss/src/config/defaults.ts b/mem0-ts/src/oss/src/config/defaults.ts index ae2a514be..bbce65cc6 100644 --- a/mem0-ts/src/oss/src/config/defaults.ts +++ b/mem0-ts/src/oss/src/config/defaults.ts @@ -34,12 +34,6 @@ export const DEFAULT_MEMORY_CONFIG: MemoryConfig = { username: process.env.NEO4J_USERNAME || "neo4j", password: process.env.NEO4J_PASSWORD || "password", }, - llm: { - provider: "openai", - config: { - model: "gpt-4-turbo-preview", - }, - }, }, historyStore: { provider: "sqlite", diff --git a/mem0-ts/src/oss/src/memory/graph_memory.ts b/mem0-ts/src/oss/src/memory/graph_memory.ts index 6de73940d..3996cffb0 100644 --- a/mem0-ts/src/oss/src/memory/graph_memory.ts +++ b/mem0-ts/src/oss/src/memory/graph_memory.ts @@ -80,18 +80,18 @@ export class MemoryGraph { ); this.llmProvider = "openai"; + let llmConfig = this.config.llm.config; + if (this.config.llm?.provider) { this.llmProvider = this.config.llm.provider; } if (this.config.graphStore?.llm?.provider) { this.llmProvider = this.config.graphStore.llm.provider; + llmConfig = this.config.graphStore.llm.config ?? llmConfig; } - this.llm = LLMFactory.create(this.llmProvider, this.config.llm.config); - this.structuredLlm = LLMFactory.create( - this.llmProvider, - this.config.llm.config, - ); + this.llm = LLMFactory.create(this.llmProvider, llmConfig); + this.structuredLlm = LLMFactory.create(this.llmProvider, llmConfig); this.threshold = 0.7; } diff --git a/mem0-ts/src/oss/tests/config-manager.test.ts b/mem0-ts/src/oss/tests/config-manager.test.ts index 8444564db..2aab9bc0e 100644 --- a/mem0-ts/src/oss/tests/config-manager.test.ts +++ b/mem0-ts/src/oss/tests/config-manager.test.ts @@ -349,6 +349,78 @@ describe("ConfigManager", () => { }); }); +// ───────────────────────────────────────────────────────────────────────── +// Graph store LLM config propagation (issue #3425) +// ───────────────────────────────────────────────────────────────────────── +describe("mergeConfig - graph store LLM config (issue #3425)", () => { + const baseEmbedder = { + provider: "openai", + config: { apiKey: "test-key" }, + }; + const baseVectorStore = { + provider: "memory", + config: { collectionName: "test" }, + }; + const graphStoreNeo4j = { + provider: "neo4j", + config: { + url: "neo4j://localhost:7687", + username: "neo4j", + password: "password", + }, + }; + + it("should NOT have a default graphStore.llm — root llm should be the fallback", () => { + const config = ConfigManager.mergeConfig({ + embedder: baseEmbedder, + vectorStore: baseVectorStore, + llm: { + provider: "anthropic", + config: { model: "claude-sonnet-4-20250514" }, + }, + graphStore: graphStoreNeo4j, + }); + + // graphStore should NOT have its own llm after merge + expect(config.graphStore?.llm).toBeUndefined(); + // root llm should be anthropic + expect(config.llm.provider).toBe("anthropic"); + expect(config.llm.config.model).toBe("claude-sonnet-4-20250514"); + }); + + it("should preserve explicit graphStore.llm when user provides it", () => { + const config = ConfigManager.mergeConfig({ + embedder: baseEmbedder, + vectorStore: baseVectorStore, + llm: { + provider: "anthropic", + config: { model: "claude-sonnet-4-20250514" }, + }, + graphStore: { + ...graphStoreNeo4j, + llm: { provider: "openai", config: { model: "gpt-4o" } }, + }, + }); + + // graphStore should have its own llm + expect(config.graphStore?.llm?.provider).toBe("openai"); + expect(config.graphStore?.llm?.config).toEqual({ model: "gpt-4o" }); + // root llm should still be anthropic + expect(config.llm.provider).toBe("anthropic"); + }); + + it("should not have graphStore.llm when user does not provide one", () => { + const config = ConfigManager.mergeConfig({ + embedder: baseEmbedder, + vectorStore: baseVectorStore, + llm: { provider: "openai", config: { model: "gpt-4o" } }, + }); + + // Default graphStore should not have llm + expect(config.graphStore?.llm).toBeUndefined(); + }); +}); + // ───────────────────────────────────────────────────────────────────────── // Memory class – LM Studio end-to-end flow (mocked factories) // ───────────────────────────────────────────────────────────────────────── diff --git a/mem0-ts/src/oss/tests/graph-memory-parsing.test.ts b/mem0-ts/src/oss/tests/graph-memory-parsing.test.ts index ff26f355c..0a3689d22 100644 --- a/mem0-ts/src/oss/tests/graph-memory-parsing.test.ts +++ b/mem0-ts/src/oss/tests/graph-memory-parsing.test.ts @@ -511,6 +511,137 @@ describe("Prompt construction — all json_object sites include 'json'", () => { // 5. Edge cases – malformed entity fields in _removeSpacesFromEntities // ═══════════════════════════════════════════════════════════════════════════ +// ═══════════════════════════════════════════════════════════════════════════ +// 5a. LLM config propagation — graph store uses correct provider & config +// Regression test for https://github.com/mem0ai/mem0/issues/3425 +// ═══════════════════════════════════════════════════════════════════════════ + +describe("LLM config propagation to graph store (issue #3425)", () => { + const { LLMFactory } = require("../src/utils/factory"); + + beforeEach(() => { + (LLMFactory.create as jest.Mock).mockClear(); + }); + + it("uses root llm config when no graphStore.llm is provided", () => { + const config = { + graphStore: { + config: { + url: "bolt://localhost:7687", + username: "neo4j", + password: "test", + }, + }, + embedder: { provider: "openai", config: {} }, + llm: { + provider: "anthropic", + config: { model: "claude-sonnet-4-20250514", apiKey: "sk-ant-test" }, + }, + } as any; + + new MemoryGraph(config); + + expect(LLMFactory.create).toHaveBeenCalledWith("anthropic", { + model: "claude-sonnet-4-20250514", + apiKey: "sk-ant-test", + }); + // Both llm and structuredLlm should use the same config + expect(LLMFactory.create).toHaveBeenCalledTimes(2); + expect(LLMFactory.create).toHaveBeenNthCalledWith(1, "anthropic", { + model: "claude-sonnet-4-20250514", + apiKey: "sk-ant-test", + }); + expect(LLMFactory.create).toHaveBeenNthCalledWith(2, "anthropic", { + model: "claude-sonnet-4-20250514", + apiKey: "sk-ant-test", + }); + }); + + it("uses graphStore.llm config when provided, overriding root llm", () => { + const config = { + graphStore: { + config: { + url: "bolt://localhost:7687", + username: "neo4j", + password: "test", + }, + llm: { + provider: "openai", + config: { model: "gpt-4o", apiKey: "sk-openai-test" }, + }, + }, + embedder: { provider: "openai", config: {} }, + llm: { + provider: "anthropic", + config: { model: "claude-sonnet-4-20250514", apiKey: "sk-ant-test" }, + }, + } as any; + + new MemoryGraph(config); + + // Should use graphStore.llm, NOT root llm + expect(LLMFactory.create).toHaveBeenNthCalledWith(1, "openai", { + model: "gpt-4o", + apiKey: "sk-openai-test", + }); + expect(LLMFactory.create).toHaveBeenNthCalledWith(2, "openai", { + model: "gpt-4o", + apiKey: "sk-openai-test", + }); + }); + + it("falls back to root llm config when graphStore.llm.config is undefined", () => { + // Note: in practice, Zod schema requires config when graphStore.llm is + // present. This tests the defensive fallback in MemoryGraph itself. + const config = { + graphStore: { + config: { + url: "bolt://localhost:7687", + username: "neo4j", + password: "test", + }, + llm: { + provider: "openai", + // config explicitly undefined + config: undefined, + }, + }, + embedder: { provider: "openai", config: {} }, + llm: { + provider: "anthropic", + config: { model: "claude-sonnet-4-20250514" }, + }, + } as any; + + new MemoryGraph(config); + + // Provider from graphStore.llm, but config falls back to root llm.config + expect(LLMFactory.create).toHaveBeenNthCalledWith(1, "openai", { + model: "claude-sonnet-4-20250514", + }); + }); + + it("defaults to openai when neither root nor graphStore llm provider is set", () => { + const config = { + graphStore: { + config: { + url: "bolt://localhost:7687", + username: "neo4j", + password: "test", + }, + }, + embedder: { provider: "openai", config: {} }, + llm: { config: { model: "gpt-4" } }, + } as any; + + new MemoryGraph(config); + + expect(LLMFactory.create).toHaveBeenNthCalledWith(1, "openai", { + model: "gpt-4", + }); + }); +}); + describe("_removeSpacesFromEntities (via _establishNodesRelationsFromData)", () => { it("normalises spaces and case in entity source/relationship/destination", async () => { mockGenerateResponse.mockResolvedValueOnce({