From c2ceb9093ed1123e4cce025971c13671283d3a0f Mon Sep 17 00:00:00 2001 From: Thomas Tupper Date: Sat, 14 Mar 2026 13:16:04 -0500 Subject: [PATCH 1/4] feat: per-purpose inference config for main LLM, embedder, and graph LLM Each purpose can now target a different endpoint/provider via: - MEM0_LLM_BASE_URL / MEM0_LLM_API_KEY (main LLM) - MEM0_EMBED_BASE_URL / MEM0_EMBED_API_KEY (embedder) - MEM0_GRAPH_LLM_BASE_URL / MEM0_GRAPH_LLM_API_KEY (graph LLM) - MEM0_GRAPH_EXTRACTION_STRATEGY (tool_calling or json_prompting) All fall back to shared OPENAI_BASE_URL / OPENAI_API_KEY when not set, so existing deployments are unaffected. Co-Authored-By: Claude Opus 4.6 --- .env.example | 16 ++++++++++++++-- src/index.ts | 35 ++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 510e136..6091aa9 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,22 @@ # ── Inference provider (OpenAI-compatible) ──────────────────────────────────── -# Point at hosted OpenAI, a local Ollama proxy, or foxmemory-infer. +# Shared defaults — used as fallback for any purpose-specific config below. OPENAI_BASE_URL=https://api.openai.com/v1 OPENAI_API_KEY=sk-... -# LLM used for memory extraction (fact extraction + ADD/UPDATE/DELETE decisions) +# ── Per-purpose inference config ────────────────────────────────────────────── +# Each purpose (main LLM, embedder, graph LLM) can target a different endpoint. +# Falls back to the shared OPENAI_BASE_URL / OPENAI_API_KEY when not set. +# This allows e.g. local inference for the main LLM with cloud for embeddings. + +# Main LLM (fact extraction + ADD/UPDATE/DELETE decisions) MEM0_LLM_MODEL=gpt-4.1-nano +# MEM0_LLM_BASE_URL= # falls back to OPENAI_BASE_URL +# MEM0_LLM_API_KEY= # falls back to OPENAI_API_KEY # Embedding model — CANNOT be changed after the collection is created MEM0_EMBED_MODEL=text-embedding-3-small +# MEM0_EMBED_BASE_URL= # falls back to OPENAI_BASE_URL +# MEM0_EMBED_API_KEY= # falls back to OPENAI_API_KEY # ── Vector store (Qdrant) ───────────────────────────────────────────────────── # When using the embedded image, Qdrant runs inside the container at 127.0.0.1. @@ -27,6 +36,9 @@ FOXMEMORY_ANALYTICS_DB_PATH=/data/foxmemory-analytics.db # NEO4J_USERNAME=neo4j # NEO4J_PASSWORD=changeme # MEM0_GRAPH_LLM_MODEL=gpt-4.1-mini +# MEM0_GRAPH_LLM_BASE_URL= # falls back to OPENAI_BASE_URL +# MEM0_GRAPH_LLM_API_KEY= # falls back to OPENAI_API_KEY +# MEM0_GRAPH_EXTRACTION_STRATEGY=tool_calling # or "json_prompting" for models without tool support # MEM0_GRAPH_CUSTOM_PROMPT= # MEM0_GRAPH_SEARCH_THRESHOLD=0.7 # MEM0_GRAPH_NODE_DEDUP_THRESHOLD=0.9 diff --git a/src/index.ts b/src/index.ts index a4a6fb1..550ee9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,19 +23,39 @@ const BUILD_COMMIT = process.env.GIT_SHA || process.env.BUILD_COMMIT || "unknown const BUILD_IMAGE_DIGEST = process.env.IMAGE_DIGEST || "unknown"; const BUILD_TIME = process.env.BUILD_TIME || "unknown"; +// --- Shared defaults (used as fallback for per-purpose config) --- const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL; // e.g. http://foxmemory-infer:8081/v1 const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "local-infer-no-key"; const HAS_OPENAI_API_KEY = Boolean(process.env.OPENAI_API_KEY); + +// --- Per-purpose inference config --- +// Each purpose (main LLM, embedder, graph LLM) can have its own provider, base URL, +// API key, and model. Falls back to the shared OPENAI_* values when not set. +// This allows e.g. local inference for the main LLM with cloud for embeddings. + +// Main LLM (fact extraction, memory decisions) +const LLM_BASE_URL = process.env.MEM0_LLM_BASE_URL || OPENAI_BASE_URL; +const LLM_API_KEY = process.env.MEM0_LLM_API_KEY || OPENAI_API_KEY; const LLM_MODEL = process.env.MEM0_LLM_MODEL || "gpt-4.1-nano"; + +// Embedder +const EMBED_BASE_URL = process.env.MEM0_EMBED_BASE_URL || OPENAI_BASE_URL; +const EMBED_API_KEY = process.env.MEM0_EMBED_API_KEY || OPENAI_API_KEY; const EMBED_MODEL = process.env.MEM0_EMBED_MODEL || "text-embedding-3-small"; // Graph memory (Neo4j). Enabled when NEO4J_URL + NEO4J_PASSWORD are set. -// MEM0_GRAPH_LLM_MODEL lets you use a separate, more capable model for entity/relation extraction. +// MEM0_GRAPH_LLM_* lets you use a separate, more capable model/endpoint for entity/relation extraction. // Recommended hosted: gpt-4o-mini. Recommended local: Qwen2.5-14B-Instruct or Mistral-Small-3.1-24B. const NEO4J_URL = process.env.NEO4J_URL || null; const NEO4J_USERNAME = process.env.NEO4J_USERNAME || "neo4j"; const NEO4J_PASSWORD = process.env.NEO4J_PASSWORD || null; +const GRAPH_LLM_BASE_URL = process.env.MEM0_GRAPH_LLM_BASE_URL || OPENAI_BASE_URL; +const GRAPH_LLM_API_KEY = process.env.MEM0_GRAPH_LLM_API_KEY || OPENAI_API_KEY; const GRAPH_LLM_MODEL = process.env.MEM0_GRAPH_LLM_MODEL || LLM_MODEL; +const GRAPH_EXTRACTION_STRATEGY = process.env.MEM0_GRAPH_EXTRACTION_STRATEGY as + | "tool_calling" + | "json_prompting" + | undefined; // Mutable effective models — start from env vars, overridden by DB on startup. // Use these everywhere instead of the const env vars so hot-reload works. @@ -217,17 +237,17 @@ function createMemory(customPrompt?: string | null, customUpdatePrompt?: string llm: { provider: "openai", config: { - apiKey: OPENAI_API_KEY, + apiKey: LLM_API_KEY, model: effectiveLlmModel, - ...(OPENAI_BASE_URL ? { baseURL: OPENAI_BASE_URL } : {}) + ...(LLM_BASE_URL ? { baseURL: LLM_BASE_URL } : {}) } }, embedder: { provider: "openai", config: { - apiKey: OPENAI_API_KEY, + apiKey: EMBED_API_KEY, model: EMBED_MODEL, - ...(OPENAI_BASE_URL ? { baseURL: OPENAI_BASE_URL } : {}) + ...(EMBED_BASE_URL ? { baseURL: EMBED_BASE_URL } : {}) } }, ...(process.env.QDRANT_HOST @@ -265,11 +285,12 @@ function createMemory(customPrompt?: string | null, customUpdatePrompt?: string llm: { provider: "openai", config: { - apiKey: OPENAI_API_KEY, + apiKey: GRAPH_LLM_API_KEY, model: effectiveGraphLlmModel, - ...(OPENAI_BASE_URL ? { baseURL: OPENAI_BASE_URL } : {}), + ...(GRAPH_LLM_BASE_URL ? { baseURL: GRAPH_LLM_BASE_URL } : {}), }, }, + ...(GRAPH_EXTRACTION_STRATEGY ? { extractionStrategy: GRAPH_EXTRACTION_STRATEGY } : {}), }, } : {}), From c6b3832a394f7fa22abc4e688d97f47d6f49d1bd Mon Sep 17 00:00:00 2001 From: Thomas Tupper Date: Sat, 14 Mar 2026 13:28:40 -0500 Subject: [PATCH 2/4] fix: derive auth diagnostics from per-purpose API keys authMode and health diagnostics now reflect per-purpose keys (MEM0_LLM_API_KEY, MEM0_EMBED_API_KEY, MEM0_GRAPH_LLM_API_KEY) instead of only the shared OPENAI_API_KEY. Base URLs are also reported per-purpose. Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index 550ee9c..46909b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,6 @@ const BUILD_TIME = process.env.BUILD_TIME || "unknown"; // --- Shared defaults (used as fallback for per-purpose config) --- const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL; // e.g. http://foxmemory-infer:8081/v1 const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "local-infer-no-key"; -const HAS_OPENAI_API_KEY = Boolean(process.env.OPENAI_API_KEY); // --- Per-purpose inference config --- // Each purpose (main LLM, embedder, graph LLM) can have its own provider, base URL, @@ -81,8 +80,10 @@ function sanitizeBaseUrl(url?: string) { } } -const AUTH_MODE = HAS_OPENAI_API_KEY ? "api_key" : "local-default"; -const OPENAI_BASE_URL_SANITIZED = sanitizeBaseUrl(OPENAI_BASE_URL); +const HAS_LLM_API_KEY = Boolean(process.env.MEM0_LLM_API_KEY || process.env.OPENAI_API_KEY); +const HAS_EMBED_API_KEY = Boolean(process.env.MEM0_EMBED_API_KEY || process.env.OPENAI_API_KEY); +const HAS_GRAPH_LLM_API_KEY = Boolean(process.env.MEM0_GRAPH_LLM_API_KEY || process.env.OPENAI_API_KEY); +const AUTH_MODE = (HAS_LLM_API_KEY || HAS_EMBED_API_KEY || HAS_GRAPH_LLM_API_KEY) ? "api_key" : "local-default"; // mem0 default prompts (copied from @foxlight-foundation/mem0ai prompts/index.ts). // Used to show the effective prompt even when no custom prompt is set. @@ -518,8 +519,8 @@ app.get("/health", (_req, res) => { embedModel: EMBED_MODEL, diagnostics: { authMode: AUTH_MODE, - openaiApiKeyConfigured: HAS_OPENAI_API_KEY, - openaiBaseUrl: OPENAI_BASE_URL_SANITIZED, + apiKeyConfigured: { llm: HAS_LLM_API_KEY, embed: HAS_EMBED_API_KEY, graphLlm: HAS_GRAPH_LLM_API_KEY }, + baseUrl: { llm: sanitizeBaseUrl(LLM_BASE_URL), embed: sanitizeBaseUrl(EMBED_BASE_URL), graphLlm: sanitizeBaseUrl(GRAPH_LLM_BASE_URL) }, graphEnabled: GRAPH_ENABLED, neo4jUrl: NEO4J_URL, graphLlmModel: GRAPH_ENABLED ? effectiveGraphLlmModel : null, @@ -1557,8 +1558,8 @@ app.get("/v2/health", async (_req, res) => { embedModel: EMBED_MODEL, diagnostics: { authMode: AUTH_MODE, - openaiApiKeyConfigured: HAS_OPENAI_API_KEY, - openaiBaseUrl: OPENAI_BASE_URL_SANITIZED, + apiKeyConfigured: { llm: HAS_LLM_API_KEY, embed: HAS_EMBED_API_KEY, graphLlm: HAS_GRAPH_LLM_API_KEY }, + baseUrl: { llm: sanitizeBaseUrl(LLM_BASE_URL), embed: sanitizeBaseUrl(EMBED_BASE_URL), graphLlm: sanitizeBaseUrl(GRAPH_LLM_BASE_URL) }, graphEnabled: GRAPH_ENABLED, neo4jUrl: NEO4J_URL, graphLlmModel: GRAPH_ENABLED ? effectiveGraphLlmModel : null, @@ -2918,8 +2919,8 @@ const V2_OPENAPI_SPEC = { type: "object", properties: { authMode: { type: "string" }, - openaiApiKeyConfigured: { type: "boolean" }, - openaiBaseUrl: { type: "string", nullable: true }, + apiKeyConfigured: { type: "object", properties: { llm: { type: "boolean" }, embed: { type: "boolean" }, graphLlm: { type: "boolean" } } }, + baseUrl: { type: "object", properties: { llm: { type: "string", nullable: true }, embed: { type: "string", nullable: true }, graphLlm: { type: "string", nullable: true } } }, graphEnabled: { type: "boolean", description: "True when NEO4J_URL + NEO4J_PASSWORD are set." }, neo4jUrl: { type: "string", nullable: true }, graphLlmModel: { type: "string", nullable: true }, @@ -3384,8 +3385,8 @@ app.listen(PORT, () => { JSON.stringify( { authMode: AUTH_MODE, - openaiApiKeyConfigured: HAS_OPENAI_API_KEY, - openaiBaseUrl: OPENAI_BASE_URL_SANITIZED, + apiKeyConfigured: { llm: HAS_LLM_API_KEY, embed: HAS_EMBED_API_KEY, graphLlm: HAS_GRAPH_LLM_API_KEY }, + baseUrl: { llm: sanitizeBaseUrl(LLM_BASE_URL), embed: sanitizeBaseUrl(EMBED_BASE_URL), graphLlm: sanitizeBaseUrl(GRAPH_LLM_BASE_URL) }, llmModel: effectiveLlmModel, embedModel: EMBED_MODEL, qdrantHost: process.env.QDRANT_HOST || null, From 987d3c9f164084f4e179b0432b85f56b4c62ab6b Mon Sep 17 00:00:00 2001 From: Thomas Tupper Date: Sat, 14 Mar 2026 14:03:35 -0500 Subject: [PATCH 3/4] fix: preserve legacy health diagnostics fields for backward compat Restores openaiApiKeyConfigured and openaiBaseUrl alongside the new per-purpose apiKeyConfigured/baseUrl objects so existing monitors and integrations that read the old keys continue working. Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/index.ts b/src/index.ts index 46909b4..aca5f45 100644 --- a/src/index.ts +++ b/src/index.ts @@ -519,6 +519,10 @@ app.get("/health", (_req, res) => { embedModel: EMBED_MODEL, diagnostics: { authMode: AUTH_MODE, + // Legacy fields (kept for backward compat with existing monitors) + openaiApiKeyConfigured: HAS_LLM_API_KEY, + openaiBaseUrl: sanitizeBaseUrl(LLM_BASE_URL), + // Per-purpose fields apiKeyConfigured: { llm: HAS_LLM_API_KEY, embed: HAS_EMBED_API_KEY, graphLlm: HAS_GRAPH_LLM_API_KEY }, baseUrl: { llm: sanitizeBaseUrl(LLM_BASE_URL), embed: sanitizeBaseUrl(EMBED_BASE_URL), graphLlm: sanitizeBaseUrl(GRAPH_LLM_BASE_URL) }, graphEnabled: GRAPH_ENABLED, @@ -1558,6 +1562,10 @@ app.get("/v2/health", async (_req, res) => { embedModel: EMBED_MODEL, diagnostics: { authMode: AUTH_MODE, + // Legacy fields (kept for backward compat with existing monitors) + openaiApiKeyConfigured: HAS_LLM_API_KEY, + openaiBaseUrl: sanitizeBaseUrl(LLM_BASE_URL), + // Per-purpose fields apiKeyConfigured: { llm: HAS_LLM_API_KEY, embed: HAS_EMBED_API_KEY, graphLlm: HAS_GRAPH_LLM_API_KEY }, baseUrl: { llm: sanitizeBaseUrl(LLM_BASE_URL), embed: sanitizeBaseUrl(EMBED_BASE_URL), graphLlm: sanitizeBaseUrl(GRAPH_LLM_BASE_URL) }, graphEnabled: GRAPH_ENABLED, @@ -2919,6 +2927,8 @@ const V2_OPENAPI_SPEC = { type: "object", properties: { authMode: { type: "string" }, + openaiApiKeyConfigured: { type: "boolean", description: "Legacy alias — reflects main LLM API key status." }, + openaiBaseUrl: { type: "string", nullable: true, description: "Legacy alias — reflects main LLM base URL." }, apiKeyConfigured: { type: "object", properties: { llm: { type: "boolean" }, embed: { type: "boolean" }, graphLlm: { type: "boolean" } } }, baseUrl: { type: "object", properties: { llm: { type: "string", nullable: true }, embed: { type: "string", nullable: true }, graphLlm: { type: "string", nullable: true } } }, graphEnabled: { type: "boolean", description: "True when NEO4J_URL + NEO4J_PASSWORD are set." }, From bd45c22ca847f0be555caffe6a2bf1a73cd78ffb Mon Sep 17 00:00:00 2001 From: Thomas Tupper Date: Sat, 14 Mar 2026 14:18:47 -0500 Subject: [PATCH 4/4] fix: validate extraction strategy env var, gate graph auth on GRAPH_ENABLED - MEM0_GRAPH_EXTRACTION_STRATEGY is now validated at startup; typos log a warning and fall back to "tool_calling" instead of silently passing through - AUTH_MODE only considers graph LLM API key when graph memory is actually enabled (NEO4J_URL + NEO4J_PASSWORD set) Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index aca5f45..acc0ece 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,10 +51,13 @@ const NEO4J_PASSWORD = process.env.NEO4J_PASSWORD || null; const GRAPH_LLM_BASE_URL = process.env.MEM0_GRAPH_LLM_BASE_URL || OPENAI_BASE_URL; const GRAPH_LLM_API_KEY = process.env.MEM0_GRAPH_LLM_API_KEY || OPENAI_API_KEY; const GRAPH_LLM_MODEL = process.env.MEM0_GRAPH_LLM_MODEL || LLM_MODEL; -const GRAPH_EXTRACTION_STRATEGY = process.env.MEM0_GRAPH_EXTRACTION_STRATEGY as - | "tool_calling" - | "json_prompting" - | undefined; +const GRAPH_EXTRACTION_STRATEGY = (() => { + const raw = process.env.MEM0_GRAPH_EXTRACTION_STRATEGY; + if (!raw) return undefined; + if (raw === "tool_calling" || raw === "json_prompting") return raw; + console.warn(`[config] invalid MEM0_GRAPH_EXTRACTION_STRATEGY "${raw}" — must be "tool_calling" or "json_prompting", falling back to "tool_calling"`); + return "tool_calling" as const; +})(); // Mutable effective models — start from env vars, overridden by DB on startup. // Use these everywhere instead of the const env vars so hot-reload works. @@ -83,7 +86,7 @@ function sanitizeBaseUrl(url?: string) { const HAS_LLM_API_KEY = Boolean(process.env.MEM0_LLM_API_KEY || process.env.OPENAI_API_KEY); const HAS_EMBED_API_KEY = Boolean(process.env.MEM0_EMBED_API_KEY || process.env.OPENAI_API_KEY); const HAS_GRAPH_LLM_API_KEY = Boolean(process.env.MEM0_GRAPH_LLM_API_KEY || process.env.OPENAI_API_KEY); -const AUTH_MODE = (HAS_LLM_API_KEY || HAS_EMBED_API_KEY || HAS_GRAPH_LLM_API_KEY) ? "api_key" : "local-default"; +const AUTH_MODE = (HAS_LLM_API_KEY || HAS_EMBED_API_KEY || (GRAPH_ENABLED && HAS_GRAPH_LLM_API_KEY)) ? "api_key" : "local-default"; // mem0 default prompts (copied from @foxlight-foundation/mem0ai prompts/index.ts). // Used to show the effective prompt even when no custom prompt is set.