/** * OSS provider wizard — provider definitions, config builders, and validation. * * Used by the init command for both interactive wizard and non-interactive * --oss-* flag paths. */ import { join } from "node:path"; import { homedir } from "node:os"; // ============================================================================ // Provider definitions // ============================================================================ export interface ProviderDef { id: string; label: string; needsApiKey: boolean; needsUrl: boolean; envVar?: string; defaultModel: string; defaultUrl?: string; } export const LLM_PROVIDERS: ProviderDef[] = [ { id: "openai", label: "OpenAI (requires API key)", needsApiKey: true, needsUrl: false, envVar: "OPENAI_API_KEY", defaultModel: "gpt-5-mini" }, { id: "ollama", label: "Ollama (local, no API key)", needsApiKey: false, needsUrl: true, defaultModel: "llama3.1:8b", defaultUrl: "http://localhost:11434" }, { id: "anthropic", label: "Anthropic (requires API key)", needsApiKey: true, needsUrl: false, envVar: "ANTHROPIC_API_KEY", defaultModel: "claude-sonnet-4-5-20250514" }, ]; export interface EmbedderDef extends ProviderDef { defaultDims: number; } export const EMBEDDER_PROVIDERS: EmbedderDef[] = [ { id: "openai", label: "OpenAI (requires API key)", needsApiKey: true, needsUrl: false, envVar: "OPENAI_API_KEY", defaultModel: "text-embedding-3-small", defaultDims: 1536 }, { id: "ollama", label: "Ollama (local, no API key)", needsApiKey: false, needsUrl: true, defaultModel: "nomic-embed-text", defaultUrl: "http://localhost:11434", defaultDims: 768 }, ]; export interface VectorDef { id: string; label: string; needsConnection: boolean; defaultUrl?: string; defaultPort?: number; setupHint?: string; } export const VECTOR_PROVIDERS: VectorDef[] = [ { id: "qdrant", label: "Qdrant (requires server — Docker or cloud)", needsConnection: true, defaultUrl: "http://localhost:6333", defaultPort: 6333, setupHint: "docker run -d -p 6333:6333 qdrant/qdrant" }, { id: "pgvector", label: "PGVector (requires PostgreSQL + pgvector extension)", needsConnection: true, defaultPort: 5432, setupHint: "docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres pgvector/pgvector:pg17" }, ]; export const KNOWN_EMBEDDER_DIMS: Record = { "text-embedding-3-small": 1536, "text-embedding-3-large": 3072, "text-embedding-ada-002": 1536, "nomic-embed-text": 768, "mxbai-embed-large": 1024, "all-minilm": 384, "snowflake-arctic-embed": 1024, }; export function collectionNameForDims(dims: number): string { return `mem0_${dims}d`; } // ============================================================================ // Config builders // ============================================================================ export interface LlmConfigInput { apiKey?: string; model?: string; url?: string; } export function buildOssLlmConfig( providerId: string, input: LlmConfigInput, ): { provider: string; config: Record } { const def = LLM_PROVIDERS.find((p) => p.id === providerId); if (!def) throw new Error(`Unknown LLM provider: ${providerId}`); const config: Record = { model: input.model || def.defaultModel, }; if (input.apiKey) config.apiKey = input.apiKey; if (providerId === "ollama") { config.url = input.url || def.defaultUrl; } return { provider: providerId, config }; } export interface EmbedderConfigInput { apiKey?: string; model?: string; url?: string; } export function buildOssEmbedderConfig( providerId: string, input: EmbedderConfigInput, ): { provider: string; config: Record; dims: number | undefined } { const def = EMBEDDER_PROVIDERS.find((p) => p.id === providerId); if (!def) throw new Error(`Unknown embedder provider: ${providerId}`); const model = input.model || def.defaultModel; const config: Record = { model }; if (input.apiKey) config.apiKey = input.apiKey; if (providerId === "ollama") { config.url = input.url || def.defaultUrl; } const dims = KNOWN_EMBEDDER_DIMS[model] ?? def.defaultDims; if (dims) config.embeddingDims = dims; return { provider: providerId, config, dims }; } export interface VectorConfigInput { url?: string; host?: string; port?: string; user?: string; password?: string; dbname?: string; apiKey?: string; dims?: number; } export function buildOssVectorConfig( providerId: string, input: VectorConfigInput, ): { provider: string; config: Record } { const config: Record = {}; if (providerId === "qdrant") { config.url = input.url || "http://localhost:6333"; config.onDisk = true; if (input.apiKey) config.apiKey = input.apiKey; } else if (providerId === "pgvector") { config.host = input.host || "localhost"; config.port = parseInt(input.port || "5432", 10); if (input.user) config.user = input.user; if (input.password) config.password = input.password; config.dbname = input.dbname || "postgres"; } if (input.dims) { config.dimension = input.dims; config.embeddingModelDims = input.dims; config.collectionName = collectionNameForDims(input.dims); } return { provider: providerId, config }; } export async function checkOllamaConnectivity(url: string): Promise<{ ok: boolean; error?: string }> { try { const resp = await fetch(`${url.replace(/\/+$/, "")}/api/tags`, { signal: AbortSignal.timeout(3000) }); if (resp.ok) return { ok: true }; return { ok: false, error: `Ollama returned HTTP ${resp.status}` }; } catch { return { ok: false, error: `Cannot reach Ollama at ${url}. Install: https://ollama.com/download` }; } } export async function checkPgConnectivity(host: string, port: number): Promise<{ ok: boolean; error?: string }> { return new Promise((resolve) => { import("node:net").then(({ createConnection }) => { const sock = createConnection({ host, port, timeout: 3000 }); sock.once("connect", () => { sock.destroy(); resolve({ ok: true }); }); sock.once("timeout", () => { sock.destroy(); resolve({ ok: false, error: `PostgreSQL not reachable at ${host}:${port}` }); }); sock.once("error", () => { sock.destroy(); resolve({ ok: false, error: `PostgreSQL not reachable at ${host}:${port}. Ensure PostgreSQL with pgvector extension is running.` }); }); }); }); } export async function checkQdrantConnectivity(url: string): Promise<{ ok: boolean; error?: string }> { try { const resp = await fetch(`${url.replace(/\/+$/, "")}/healthz`, { signal: AbortSignal.timeout(3000) }); if (resp.ok) return { ok: true }; return { ok: false, error: `Qdrant returned HTTP ${resp.status}` }; } catch (err) { return { ok: false, error: `Cannot reach Qdrant at ${url}. Start it with: docker run -d -p 6333:6333 qdrant/qdrant` }; } } // ============================================================================ // Non-interactive flag validation // ============================================================================ export interface OssFlags { ossLlm?: string; ossLlmKey?: string; ossLlmModel?: string; ossLlmUrl?: string; ossEmbedder?: string; ossEmbedderKey?: string; ossEmbedderModel?: string; ossEmbedderUrl?: string; ossVector?: string; ossVectorUrl?: string; ossVectorHost?: string; ossVectorPort?: string; ossVectorUser?: string; ossVectorPassword?: string; ossVectorDbname?: string; ossVectorDims?: string; } export function validateOssFlags( flags: OssFlags, ): { error?: string } { const llmId = flags.ossLlm || "openai"; const llmDef = LLM_PROVIDERS.find((p) => p.id === llmId); if (!llmDef) return { error: `Unknown LLM provider: ${llmId}. Valid: ${LLM_PROVIDERS.map((p) => p.id).join(", ")}` }; if (llmDef.needsApiKey && !flags.ossLlmKey) { return { error: `--oss-llm-key required when --oss-llm is ${llmId}` }; } const embId = flags.ossEmbedder || "openai"; const embDef = EMBEDDER_PROVIDERS.find((p) => p.id === embId); if (!embDef) return { error: `Unknown embedder provider: ${embId}. Valid: ${EMBEDDER_PROVIDERS.map((p) => p.id).join(", ")}` }; if (embDef.needsApiKey && !flags.ossEmbedderKey && !flags.ossLlmKey) { return { error: `--oss-embedder-key required when --oss-embedder is ${embId}` }; } const vecId = flags.ossVector || "qdrant"; const vecDef = VECTOR_PROVIDERS.find((p) => p.id === vecId); if (!vecDef) return { error: `Unknown vector store provider: ${vecId}. Valid: ${VECTOR_PROVIDERS.map((p) => p.id).join(", ")}` }; if (vecId === "pgvector" && !flags.ossVectorUser) { return { error: "--oss-vector-user required when --oss-vector is pgvector" }; } return {}; }