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..acc0ece 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,19 +23,41 @@ 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 = (() => { + 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. @@ -61,8 +83,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 || (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. @@ -217,17 +241,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 +289,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 } : {}), }, } : {}), @@ -497,8 +522,12 @@ app.get("/health", (_req, res) => { embedModel: EMBED_MODEL, diagnostics: { authMode: AUTH_MODE, - openaiApiKeyConfigured: HAS_OPENAI_API_KEY, - openaiBaseUrl: OPENAI_BASE_URL_SANITIZED, + // 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, neo4jUrl: NEO4J_URL, graphLlmModel: GRAPH_ENABLED ? effectiveGraphLlmModel : null, @@ -1536,8 +1565,12 @@ app.get("/v2/health", async (_req, res) => { embedModel: EMBED_MODEL, diagnostics: { authMode: AUTH_MODE, - openaiApiKeyConfigured: HAS_OPENAI_API_KEY, - openaiBaseUrl: OPENAI_BASE_URL_SANITIZED, + // 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, neo4jUrl: NEO4J_URL, graphLlmModel: GRAPH_ENABLED ? effectiveGraphLlmModel : null, @@ -2897,8 +2930,10 @@ const V2_OPENAPI_SPEC = { type: "object", properties: { authMode: { type: "string" }, - openaiApiKeyConfigured: { type: "boolean" }, - openaiBaseUrl: { type: "string", nullable: true }, + 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." }, neo4jUrl: { type: "string", nullable: true }, graphLlmModel: { type: "string", nullable: true }, @@ -3363,8 +3398,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,