From 756b0b1b6d38d5c8327ecf28f37c4b0684ecfd98 Mon Sep 17 00:00:00 2001 From: Kartik Date: Mon, 20 Jul 2026 15:53:35 +0530 Subject: [PATCH] fix(ts-sdk): lazy-load optional provider SDKs in mem0ai/oss (#6389) --- docs/open-source/configuration.mdx | 2 +- mem0-ts/package.json | 36 +++++++ mem0-ts/src/oss/src/embeddings/fastembed.ts | 14 ++- mem0-ts/src/oss/src/embeddings/google.ts | 22 ++++- mem0-ts/src/oss/src/embeddings/langchain.ts | 2 +- mem0-ts/src/oss/src/embeddings/ollama.ts | 22 ++++- mem0-ts/src/oss/src/embeddings/vertexai.ts | 15 ++- mem0-ts/src/oss/src/llms/anthropic.ts | 19 +++- mem0-ts/src/oss/src/llms/google.ts | 20 +++- mem0-ts/src/oss/src/llms/groq.ts | 20 +++- mem0-ts/src/oss/src/llms/langchain.ts | 19 ++-- mem0-ts/src/oss/src/llms/mistral.ts | 22 ++++- mem0-ts/src/oss/src/llms/ollama.ts | 19 +++- mem0-ts/src/oss/src/rerankers/cohere.ts | 14 ++- mem0-ts/src/oss/src/rerankers/zeroentropy.ts | 14 ++- .../oss/src/storage/SupabaseHistoryManager.ts | 26 ++++- mem0-ts/src/oss/src/utils/load_peer.ts | 13 +++ .../oss/src/vector_stores/azure_ai_search.ts | 60 +++++++++--- .../src/oss/src/vector_stores/azure_mysql.ts | 14 ++- mem0-ts/src/oss/src/vector_stores/baidu.ts | 14 ++- .../src/oss/src/vector_stores/cassandra.ts | 14 ++- mem0-ts/src/oss/src/vector_stores/chroma.ts | 14 ++- .../oss/src/vector_stores/elasticsearch.ts | 14 ++- .../src/oss/src/vector_stores/langchain.ts | 4 +- mem0-ts/src/oss/src/vector_stores/mongodb.ts | 14 ++- .../src/oss/src/vector_stores/opensearch.ts | 14 ++- mem0-ts/src/oss/src/vector_stores/pinecone.ts | 14 ++- mem0-ts/src/oss/src/vector_stores/qdrant.ts | 98 ++++++++++++------- mem0-ts/src/oss/src/vector_stores/redis.ts | 48 ++++++--- mem0-ts/src/oss/src/vector_stores/supabase.ts | 30 +++++- .../src/oss/src/vector_stores/turbopuffer.ts | 14 ++- .../oss/src/vector_stores/upstash_vector.ts | 14 ++- mem0-ts/src/oss/src/vector_stores/valkey.ts | 11 +-- .../src/oss/src/vector_stores/vectorize.ts | 26 ++++- mem0-ts/src/oss/src/vector_stores/weaviate.ts | 14 ++- mem0-ts/src/oss/tests/anthropic-llm.test.ts | 17 +++- mem0-ts/src/oss/tests/optional-peers.test.ts | 11 ++- mem0-ts/src/oss/tests/qdrant-url-port.test.ts | 52 +++++----- 38 files changed, 536 insertions(+), 274 deletions(-) create mode 100644 mem0-ts/src/oss/src/utils/load_peer.ts diff --git a/docs/open-source/configuration.mdx b/docs/open-source/configuration.mdx index e60d72faf..fdc8abbaa 100644 --- a/docs/open-source/configuration.mdx +++ b/docs/open-source/configuration.mdx @@ -148,7 +148,7 @@ See the full catalog in Components - Qdrant connection errors: confirm port `6333` is exposed and the API key (if set) matches. - Empty search results: verify the embedder model name. A mismatch causes dimension errors. - `Unknown reranker` (Python): upgrade the SDK with `pip install --upgrade mem0ai` to load the latest provider registry. -- `Cannot find module` (Node): import from the OSS entry point, `import { Memory } from "mem0ai/oss"`, not `"mem0ai"`. +- `Cannot find module` (Node): two common causes. First, import from the OSS entry point, `import { Memory } from "mem0ai/oss"`, not `"mem0ai"`. Second, provider SDKs are optional peer dependencies loaded on demand, so install the one for the provider you configured (for example `npm install @qdrant/js-client-rest` for Qdrant). Installing `mem0ai` alone only pulls in the providers used by default; you do not need SDKs for providers you never select. =3.0.0 <3.968.0" }, "peerDependenciesMeta": { + "@qdrant/js-client-rest": { + "optional": true + }, + "redis": { + "optional": true + }, + "@supabase/supabase-js": { + "optional": true + }, + "cloudflare": { + "optional": true + }, + "@azure/search-documents": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@langchain/core": { + "optional": true + }, + "@anthropic-ai/sdk": { + "optional": true + }, + "@google/genai": { + "optional": true + }, + "groq-sdk": { + "optional": true + }, + "@mistralai/mistralai": { + "optional": true + }, + "ollama": { + "optional": true + }, "mysql2": { "optional": true }, diff --git a/mem0-ts/src/oss/src/embeddings/fastembed.ts b/mem0-ts/src/oss/src/embeddings/fastembed.ts index 8c3116604..c0983ab9f 100644 --- a/mem0-ts/src/oss/src/embeddings/fastembed.ts +++ b/mem0-ts/src/oss/src/embeddings/fastembed.ts @@ -1,6 +1,7 @@ import type { FlagEmbedding } from "fastembed"; import { Embedder } from "./base"; import { EmbeddingConfig } from "../types"; +import { loadPeer } from "../utils/load_peer"; // FastEmbed only ships a fixed set of ONNX models (fastembed's `EmbeddingModel` // enum, minus CUSTOM). Mirrored here as literals so an invalid model name can @@ -54,14 +55,11 @@ export class FastEmbedEmbedder implements Embedder { * consumers that never touch FastEmbed don't need it installed. */ private async initEmbeddingModel(): Promise { - let sdk: any; - try { - sdk = await import("fastembed"); - } catch { - throw new Error( - "The 'fastembed' package is required to use the FastEmbed embedder. Install it with: npm install fastembed", - ); - } + const sdk = await loadPeer( + "fastembed", + "FastEmbed embedder", + () => import("fastembed"), + ); return sdk.FlagEmbedding.init({ model: this.modelName }); } diff --git a/mem0-ts/src/oss/src/embeddings/google.ts b/mem0-ts/src/oss/src/embeddings/google.ts index 1e342b070..db23ffdee 100644 --- a/mem0-ts/src/oss/src/embeddings/google.ts +++ b/mem0-ts/src/oss/src/embeddings/google.ts @@ -1,21 +1,32 @@ -import { GoogleGenAI } from "@google/genai"; +import type { GoogleGenAI } from "@google/genai"; import { Embedder } from "./base"; import { EmbeddingConfig } from "../types"; +import { loadPeer } from "../utils/load_peer"; export class GoogleEmbedder implements Embedder { - private google: GoogleGenAI; + private google!: GoogleGenAI; private model: string; private embeddingDims: number | undefined; + private readonly apiKey: string | undefined; constructor(config: EmbeddingConfig) { - this.google = new GoogleGenAI({ - apiKey: config.apiKey || process.env.GOOGLE_API_KEY, - }); + this.apiKey = config.apiKey || process.env.GOOGLE_API_KEY; this.model = config.model || "gemini-embedding-001"; this.embeddingDims = config.embeddingDims; } + private async ensureClient(): Promise { + if (this.google) return; + const sdk = await loadPeer( + "@google/genai", + "Google embedder", + () => import("@google/genai"), + ); + this.google = new sdk.GoogleGenAI({ apiKey: this.apiKey }); + } + async embed(text: string): Promise { + await this.ensureClient(); const response = await this.google.models.embedContent({ model: this.model, contents: text, @@ -27,6 +38,7 @@ export class GoogleEmbedder implements Embedder { } async embedBatch(texts: string[]): Promise { + await this.ensureClient(); const response = await this.google.models.embedContent({ model: this.model, contents: texts, diff --git a/mem0-ts/src/oss/src/embeddings/langchain.ts b/mem0-ts/src/oss/src/embeddings/langchain.ts index 7ffb85065..615cd0f8f 100644 --- a/mem0-ts/src/oss/src/embeddings/langchain.ts +++ b/mem0-ts/src/oss/src/embeddings/langchain.ts @@ -1,4 +1,4 @@ -import { Embeddings } from "@langchain/core/embeddings"; +import type { Embeddings } from "@langchain/core/embeddings"; import { Embedder } from "./base"; import { EmbeddingConfig } from "../types"; diff --git a/mem0-ts/src/oss/src/embeddings/ollama.ts b/mem0-ts/src/oss/src/embeddings/ollama.ts index 51d339ce2..be5835366 100644 --- a/mem0-ts/src/oss/src/embeddings/ollama.ts +++ b/mem0-ts/src/oss/src/embeddings/ollama.ts @@ -1,19 +1,19 @@ -import { Ollama } from "ollama"; +import type { Ollama } from "ollama"; import { Embedder } from "./base"; import { EmbeddingConfig } from "../types"; import { logger } from "../utils/logger"; +import { loadPeer } from "../utils/load_peer"; export class OllamaEmbedder implements Embedder { - private ollama: Ollama; + private ollama!: Ollama; private model: string; private embeddingDims?: number; + private readonly host: string; // Using this variable to avoid calling the Ollama server multiple times private initialized: boolean = false; constructor(config: EmbeddingConfig) { - this.ollama = new Ollama({ - host: config.url || config.baseURL || "http://localhost:11434", - }); + this.host = config.url || config.baseURL || "http://localhost:11434"; this.model = config.model || "nomic-embed-text:latest"; this.embeddingDims = config.embeddingDims || 768; this.ensureModelExists().catch((err) => { @@ -21,7 +21,18 @@ export class OllamaEmbedder implements Embedder { }); } + private async ensureClient(): Promise { + if (this.ollama) return; + const sdk = await loadPeer( + "ollama", + "Ollama embedder", + () => import("ollama"), + ); + this.ollama = new sdk.Ollama({ host: this.host }); + } + async embed(text: string): Promise { + await this.ensureClient(); try { await this.ensureModelExists(); } catch (err) { @@ -54,6 +65,7 @@ export class OllamaEmbedder implements Embedder { if (this.initialized) { return true; } + await this.ensureClient(); const local_models = await this.ollama.list(); const target = OllamaEmbedder.normalizeModelName(this.model); if ( diff --git a/mem0-ts/src/oss/src/embeddings/vertexai.ts b/mem0-ts/src/oss/src/embeddings/vertexai.ts index 052d237d6..5af0493ab 100644 --- a/mem0-ts/src/oss/src/embeddings/vertexai.ts +++ b/mem0-ts/src/oss/src/embeddings/vertexai.ts @@ -1,6 +1,7 @@ import type { PredictionServiceClient } from "@google-cloud/aiplatform"; import { Embedder } from "./base"; import { VertexAIConfig } from "../types"; +import { loadPeer } from "../utils/load_peer"; type AIPlatform = typeof import("@google-cloud/aiplatform"); type ClientOptions = NonNullable< @@ -105,15 +106,11 @@ export class VertexAIEmbedder implements Embedder { } private async createClient(): Promise { - let aiplatform: AIPlatform; - try { - aiplatform = await import("@google-cloud/aiplatform"); - } catch (err) { - throw new Error( - "Failed to import '@google-cloud/aiplatform'. Please install it to use the Vertex AI embedding provider: " + - (err as Error).message, - ); - } + const aiplatform: AIPlatform = await loadPeer( + "@google-cloud/aiplatform", + "Vertex AI embedding provider", + () => import("@google-cloud/aiplatform"), + ); const client = new aiplatform.PredictionServiceClient(this.clientOptions); diff --git a/mem0-ts/src/oss/src/llms/anthropic.ts b/mem0-ts/src/oss/src/llms/anthropic.ts index ea91f8ca4..a557dac90 100644 --- a/mem0-ts/src/oss/src/llms/anthropic.ts +++ b/mem0-ts/src/oss/src/llms/anthropic.ts @@ -1,9 +1,11 @@ -import Anthropic from "@anthropic-ai/sdk"; +import type Anthropic from "@anthropic-ai/sdk"; import { LLM, LLMResponse } from "./base"; import { LLMConfig, Message } from "../types"; +import { loadPeer } from "../utils/load_peer"; export class AnthropicLLM implements LLM { - private client: Anthropic; + private client!: Anthropic; + private readonly clientArgs: { apiKey: string; baseURL?: string }; private model: string; private maxTokens: number; private temperature?: number; @@ -20,7 +22,7 @@ export class AnthropicLLM implements LLM { if (config.baseURL) { clientArgs.baseURL = config.baseURL; } - this.client = new Anthropic(clientArgs); + this.clientArgs = clientArgs; this.model = config.model || "claude-sonnet-4-6"; // Defaults mirror the Python provider's AnthropicConfig // (max_tokens=2000, temperature=0.1, top_p omitted). @@ -29,11 +31,22 @@ export class AnthropicLLM implements LLM { this.topP = config.topP; } + private async ensureClient(): Promise { + if (this.client) return; + const sdk = await loadPeer( + "@anthropic-ai/sdk", + "Anthropic LLM", + () => import("@anthropic-ai/sdk"), + ); + this.client = new sdk.default(this.clientArgs); + } + async generateResponse( messages: Message[], responseFormat?: { type: string }, tools?: any[], ): Promise { + await this.ensureClient(); // Extract system message if present const systemMessage = messages.find((msg) => msg.role === "system"); const otherMessages = messages.filter((msg) => msg.role !== "system"); diff --git a/mem0-ts/src/oss/src/llms/google.ts b/mem0-ts/src/oss/src/llms/google.ts index d9a461b98..6c8eb3e64 100644 --- a/mem0-ts/src/oss/src/llms/google.ts +++ b/mem0-ts/src/oss/src/llms/google.ts @@ -1,16 +1,28 @@ -import { GoogleGenAI } from "@google/genai"; +import type { GoogleGenAI } from "@google/genai"; import { LLM, LLMResponse } from "./base"; import { LLMConfig, Message } from "../types"; +import { loadPeer } from "../utils/load_peer"; export class GoogleLLM implements LLM { - private google: GoogleGenAI; + private google!: GoogleGenAI; private model: string; + private readonly apiKey: string | undefined; constructor(config: LLMConfig) { - this.google = new GoogleGenAI({ apiKey: config.apiKey }); + this.apiKey = config.apiKey; this.model = config.model || "gemini-2.0-flash"; } + private async ensureClient(): Promise { + if (this.google) return; + const sdk = await loadPeer( + "@google/genai", + "Google LLM", + () => import("@google/genai"), + ); + this.google = new sdk.GoogleGenAI({ apiKey: this.apiKey }); + } + private formatContents(messages: Message[]) { return messages.map((msg) => ({ parts: [ @@ -30,6 +42,7 @@ export class GoogleLLM implements LLM { responseFormat?: { type: string }, tools?: any[], ): Promise { + await this.ensureClient(); const contents = this.formatContents(messages); // Build config with tools if provided @@ -72,6 +85,7 @@ export class GoogleLLM implements LLM { } async generateChat(messages: Message[]): Promise { + await this.ensureClient(); const completion = await this.google.models.generateContent({ contents: this.formatContents(messages), model: this.model, diff --git a/mem0-ts/src/oss/src/llms/groq.ts b/mem0-ts/src/oss/src/llms/groq.ts index 616262101..94d20cd46 100644 --- a/mem0-ts/src/oss/src/llms/groq.ts +++ b/mem0-ts/src/oss/src/llms/groq.ts @@ -1,24 +1,37 @@ -import { Groq } from "groq-sdk"; +import type { Groq } from "groq-sdk"; import { LLM, LLMResponse } from "./base"; import { LLMConfig, Message } from "../types"; +import { loadPeer } from "../utils/load_peer"; export class GroqLLM implements LLM { - private client: Groq; + private client!: Groq; private model: string; + private readonly apiKey: string; constructor(config: LLMConfig) { const apiKey = config.apiKey || process.env.GROQ_API_KEY; if (!apiKey) { throw new Error("Groq API key is required"); } - this.client = new Groq({ apiKey }); + this.apiKey = apiKey; this.model = config.model || "llama3-70b-8192"; } + private async ensureClient(): Promise { + if (this.client) return; + const sdk = await loadPeer( + "groq-sdk", + "Groq LLM", + () => import("groq-sdk"), + ); + this.client = new sdk.Groq({ apiKey: this.apiKey }); + } + async generateResponse( messages: Message[], responseFormat?: { type: string }, ): Promise { + await this.ensureClient(); const response = await this.client.chat.completions.create({ model: this.model, messages: messages.map((msg) => ({ @@ -35,6 +48,7 @@ export class GroqLLM implements LLM { } async generateChat(messages: Message[]): Promise { + await this.ensureClient(); const response = await this.client.chat.completions.create({ model: this.model, messages: messages.map((msg) => ({ diff --git a/mem0-ts/src/oss/src/llms/langchain.ts b/mem0-ts/src/oss/src/llms/langchain.ts index 522ebd83f..3c21d6f26 100644 --- a/mem0-ts/src/oss/src/llms/langchain.ts +++ b/mem0-ts/src/oss/src/llms/langchain.ts @@ -1,17 +1,16 @@ -import { BaseLanguageModel } from "@langchain/core/language_models/base"; -import { - AIMessage, - HumanMessage, - SystemMessage, - BaseMessage, -} from "@langchain/core/messages"; +import type { BaseLanguageModel } from "@langchain/core/language_models/base"; +import type { BaseMessage } from "@langchain/core/messages"; import { z } from "zod"; import { LLM, LLMResponse } from "./base"; import { LLMConfig, Message } from "../types/index"; // Import the schemas directly into LangchainLLM import { FactRetrievalSchema, MemoryUpdateSchema } from "../prompts"; -const convertToLangchainMessages = (messages: Message[]): BaseMessage[] => { +const convertToLangchainMessages = async ( + messages: Message[], +): Promise => { + const { AIMessage, HumanMessage, SystemMessage } = + await import("@langchain/core/messages"); return messages.map((msg) => { const content = typeof msg.content === "string" @@ -62,7 +61,7 @@ export class LangchainLLM implements LLM { response_format?: { type: string }, tools?: any[], ): Promise { - const langchainMessages = convertToLangchainMessages(messages); + const langchainMessages = await convertToLangchainMessages(messages); let runnable: any = this.llmInstance; const invokeOptions: Record = {}; let isStructuredOutput = false; @@ -170,7 +169,7 @@ export class LangchainLLM implements LLM { } async generateChat(messages: Message[]): Promise { - const langchainMessages = convertToLangchainMessages(messages); + const langchainMessages = await convertToLangchainMessages(messages); try { const response = await this.llmInstance.invoke(langchainMessages); if (response && typeof response.content === "string") { diff --git a/mem0-ts/src/oss/src/llms/mistral.ts b/mem0-ts/src/oss/src/llms/mistral.ts index a80972b6d..ebbf3cbf3 100644 --- a/mem0-ts/src/oss/src/llms/mistral.ts +++ b/mem0-ts/src/oss/src/llms/mistral.ts @@ -1,21 +1,31 @@ -import { Mistral } from "@mistralai/mistralai"; +import type { Mistral } from "@mistralai/mistralai"; import { LLM, LLMResponse } from "./base"; import { LLMConfig, Message } from "../types"; +import { loadPeer } from "../utils/load_peer"; export class MistralLLM implements LLM { - private client: Mistral; + private client!: Mistral; private model: string; + private readonly apiKey: string; constructor(config: LLMConfig) { if (!config.apiKey) { throw new Error("Mistral API key is required"); } - this.client = new Mistral({ - apiKey: config.apiKey, - }); + this.apiKey = config.apiKey; this.model = config.model || "mistral-tiny-latest"; } + private async ensureClient(): Promise { + if (this.client) return; + const sdk = await loadPeer( + "@mistralai/mistralai", + "Mistral LLM", + () => import("@mistralai/mistralai"), + ); + this.client = new sdk.Mistral({ apiKey: this.apiKey }); + } + // Helper function to convert content to string private contentToString(content: any): string { if (typeof content === "string") { @@ -41,6 +51,7 @@ export class MistralLLM implements LLM { responseFormat?: { type: string }, tools?: any[], ): Promise { + await this.ensureClient(); const response = await this.client.chat.complete({ model: this.model, messages: messages.map((msg) => ({ @@ -82,6 +93,7 @@ export class MistralLLM implements LLM { } async generateChat(messages: Message[]): Promise { + await this.ensureClient(); const formattedMessages = messages.map((msg) => ({ role: msg.role as "system" | "user" | "assistant", content: diff --git a/mem0-ts/src/oss/src/llms/ollama.ts b/mem0-ts/src/oss/src/llms/ollama.ts index 987262731..2a57e0526 100644 --- a/mem0-ts/src/oss/src/llms/ollama.ts +++ b/mem0-ts/src/oss/src/llms/ollama.ts @@ -1,29 +1,36 @@ -import { Ollama } from "ollama"; +import type { Ollama } from "ollama"; import { LLM, LLMResponse } from "./base"; import { LLMConfig, Message } from "../types"; import { logger } from "../utils/logger"; +import { loadPeer } from "../utils/load_peer"; export class OllamaLLM implements LLM { - private ollama: Ollama; + private ollama!: Ollama; private model: string; + private readonly host: string; // Using this variable to avoid calling the Ollama server multiple times private initialized: boolean = false; constructor(config: LLMConfig) { - this.ollama = new Ollama({ - host: config.url || config.baseURL || "http://localhost:11434", - }); + this.host = config.url || config.baseURL || "http://localhost:11434"; this.model = config.model || "llama3.1:8b"; this.ensureModelExists().catch((err) => { logger.error(`Error ensuring model exists: ${err}`); }); } + private async ensureClient(): Promise { + if (this.ollama) return; + const sdk = await loadPeer("ollama", "Ollama LLM", () => import("ollama")); + this.ollama = new sdk.Ollama({ host: this.host }); + } + async generateResponse( messages: Message[], responseFormat?: { type: string }, tools?: any[], ): Promise { + await this.ensureClient(); try { await this.ensureModelExists(); } catch (err) { @@ -63,6 +70,7 @@ export class OllamaLLM implements LLM { } async generateChat(messages: Message[]): Promise { + await this.ensureClient(); try { await this.ensureModelExists(); } catch (err) { @@ -93,6 +101,7 @@ export class OllamaLLM implements LLM { if (this.initialized) { return true; } + await this.ensureClient(); const local_models = await this.ollama.list(); if (!local_models.models.find((m: any) => m.name === this.model)) { logger.info(`Pulling model ${this.model}...`); diff --git a/mem0-ts/src/oss/src/rerankers/cohere.ts b/mem0-ts/src/oss/src/rerankers/cohere.ts index 4d886909b..e58a4cae0 100644 --- a/mem0-ts/src/oss/src/rerankers/cohere.ts +++ b/mem0-ts/src/oss/src/rerankers/cohere.ts @@ -1,5 +1,6 @@ import { RerankerConfig } from "../types"; import { Reranker, RerankResult } from "./base"; +import { loadPeer } from "../utils/load_peer"; const DEFAULT_MODEL = "rerank-v3.5"; @@ -41,14 +42,11 @@ export class CohereReranker implements Reranker { } private async createClient(): Promise { - let sdk: any; - try { - sdk = await import("cohere-ai"); - } catch { - throw new Error( - "The 'cohere-ai' package is required to use the Cohere reranker. Install it with: npm install cohere-ai", - ); - } + const sdk = await loadPeer( + "cohere-ai", + "Cohere reranker", + () => import("cohere-ai"), + ); return new sdk.CohereClient({ token: this.apiKey }); } diff --git a/mem0-ts/src/oss/src/rerankers/zeroentropy.ts b/mem0-ts/src/oss/src/rerankers/zeroentropy.ts index db8987475..8f6ccee59 100644 --- a/mem0-ts/src/oss/src/rerankers/zeroentropy.ts +++ b/mem0-ts/src/oss/src/rerankers/zeroentropy.ts @@ -1,5 +1,6 @@ import { RerankerConfig } from "../types"; import { Reranker, RerankResult } from "./base"; +import { loadPeer } from "../utils/load_peer"; const DEFAULT_MODEL = "zerank-1"; @@ -37,14 +38,11 @@ export class ZeroEntropyReranker implements Reranker { } private async createClient(): Promise { - let sdk: any; - try { - sdk = await import("zeroentropy"); - } catch { - throw new Error( - "The 'zeroentropy' package is required to use the ZeroEntropy reranker. Install it with: npm install zeroentropy", - ); - } + const sdk = await loadPeer( + "zeroentropy", + "ZeroEntropy reranker", + () => import("zeroentropy"), + ); return new sdk.ZeroEntropy({ apiKey: this.apiKey }); } diff --git a/mem0-ts/src/oss/src/storage/SupabaseHistoryManager.ts b/mem0-ts/src/oss/src/storage/SupabaseHistoryManager.ts index d8cf0e4c8..2b09a1e5f 100644 --- a/mem0-ts/src/oss/src/storage/SupabaseHistoryManager.ts +++ b/mem0-ts/src/oss/src/storage/SupabaseHistoryManager.ts @@ -1,6 +1,7 @@ -import { createClient, SupabaseClient } from "@supabase/supabase-js"; +import type { SupabaseClient } from "@supabase/supabase-js"; import { v4 as uuidv4 } from "uuid"; import { HistoryManager } from "./base"; +import { loadPeer } from "../utils/load_peer"; interface HistoryEntry { id: string; @@ -20,16 +21,32 @@ interface SupabaseHistoryConfig { } export class SupabaseHistoryManager implements HistoryManager { - private supabase: SupabaseClient; + // ponytail: benign double-construct race — two concurrent first-calls may each + // build a client; createClient opens no connection, so last-write-wins is fine. + private supabase!: SupabaseClient; + private readonly supabaseUrl: string; + private readonly supabaseKey: string; private readonly tableName: string; constructor(config: SupabaseHistoryConfig) { this.tableName = config.tableName || "memory_history"; - this.supabase = createClient(config.supabaseUrl, config.supabaseKey); + this.supabaseUrl = config.supabaseUrl; + this.supabaseKey = config.supabaseKey; this.initializeSupabase().catch(console.error); } + private async ensureClient(): Promise { + if (this.supabase) return; + const sdk = await loadPeer( + "@supabase/supabase-js", + "Supabase history manager", + () => import("@supabase/supabase-js"), + ); + this.supabase = sdk.createClient(this.supabaseUrl, this.supabaseKey); + } + private async initializeSupabase(): Promise { + await this.ensureClient(); // Check if table exists const { error } = await this.supabase .from(this.tableName) @@ -65,6 +82,7 @@ create table ${this.tableName} ( updatedAt?: string, isDeleted: number = 0, ): Promise { + await this.ensureClient(); const historyEntry: HistoryEntry = { id: uuidv4(), memory_id: memoryId, @@ -87,6 +105,7 @@ create table ${this.tableName} ( } async getHistory(memoryId: string): Promise { + await this.ensureClient(); const { data, error } = await this.supabase .from(this.tableName) .select("*") @@ -103,6 +122,7 @@ create table ${this.tableName} ( } async reset(): Promise { + await this.ensureClient(); const { error } = await this.supabase .from(this.tableName) .delete() diff --git a/mem0-ts/src/oss/src/utils/load_peer.ts b/mem0-ts/src/oss/src/utils/load_peer.ts new file mode 100644 index 000000000..e6ba92af9 --- /dev/null +++ b/mem0-ts/src/oss/src/utils/load_peer.ts @@ -0,0 +1,13 @@ +export async function loadPeer( + pkg: string, + label: string, + load: () => Promise, +): Promise { + try { + return await load(); + } catch { + throw new Error( + `The '${pkg}' package is required to use the ${label}. Install it with: npm install ${pkg}`, + ); + } +} diff --git a/mem0-ts/src/oss/src/vector_stores/azure_ai_search.ts b/mem0-ts/src/oss/src/vector_stores/azure_ai_search.ts index a693626d3..52576aca0 100644 --- a/mem0-ts/src/oss/src/vector_stores/azure_ai_search.ts +++ b/mem0-ts/src/oss/src/vector_stores/azure_ai_search.ts @@ -1,7 +1,6 @@ -import { +import type { SearchClient, SearchIndexClient, - AzureKeyCredential, SearchIndex, SearchField, SearchFieldDataType, @@ -13,9 +12,9 @@ import { BinaryQuantizationCompression, VectorizedQuery, } from "@azure/search-documents"; -import { DefaultAzureCredential } from "@azure/identity"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; /** * Configuration interface for Azure AI Search vector store @@ -71,8 +70,8 @@ interface AzureAISearchConfig extends VectorStoreConfig { * Supports vector search with hybrid search, compression, and filtering */ export class AzureAISearch implements VectorStore { - private searchClient: SearchClient; - private indexClient: SearchIndexClient; + private searchClient!: SearchClient; + private indexClient!: SearchIndexClient; private readonly serviceName: string; private readonly indexName: string; private readonly embeddingModelDims: number; @@ -93,25 +92,42 @@ export class AzureAISearch implements VectorStore { this.vectorFilterMode = config.vectorFilterMode || "preFilter"; this.apiKey = config.apiKey; + // Initialize the index + this.initialize().catch(console.error); + } + + private async ensureClient(): Promise { + if (this.searchClient) return; + const searchSdk = await loadPeer( + "@azure/search-documents", + "Azure AI Search vector store", + () => import("@azure/search-documents"), + ); + const serviceEndpoint = `https://${this.serviceName}.search.windows.net`; // Determine authentication: API key or DefaultAzureCredential - const credential = - this.apiKey && this.apiKey !== "" && this.apiKey !== "your-api-key" - ? new AzureKeyCredential(this.apiKey) - : new DefaultAzureCredential(); + let credential: any; + if (this.apiKey && this.apiKey !== "" && this.apiKey !== "your-api-key") { + credential = new searchSdk.AzureKeyCredential(this.apiKey); + } else { + const identitySdk = await loadPeer( + "@azure/identity", + "Azure AI Search without an apiKey", + () => import("@azure/identity"), + ); + credential = new identitySdk.DefaultAzureCredential(); + } - // Initialize clients - this.searchClient = new SearchClient( + this.searchClient = new searchSdk.SearchClient( serviceEndpoint, this.indexName, credential, ); - - this.indexClient = new SearchIndexClient(serviceEndpoint, credential); - - // Initialize the index - this.initialize().catch(console.error); + this.indexClient = new searchSdk.SearchIndexClient( + serviceEndpoint, + credential, + ); } /** @@ -125,6 +141,7 @@ export class AzureAISearch implements VectorStore { } private async _doInitialize(): Promise { + await this.ensureClient(); try { const collections = await this.listCols(); if (!collections.includes(this.indexName)) { @@ -262,6 +279,7 @@ export class AzureAISearch implements VectorStore { ids: string[], payloads: Record[], ): Promise { + await this.initialize(); console.log( `Inserting ${vectors.length} vectors into index ${this.indexName}`, ); @@ -334,6 +352,7 @@ export class AzureAISearch implements VectorStore { topK: number = 5, filters?: SearchFilters, ): Promise { + await this.initialize(); try { const filterExpression = filters ? this.buildFilterExpression(filters) @@ -373,6 +392,7 @@ export class AzureAISearch implements VectorStore { topK: number = 5, filters?: SearchFilters, ): Promise { + await this.initialize(); const filterExpression = filters ? this.buildFilterExpression(filters) : undefined; @@ -429,6 +449,7 @@ export class AzureAISearch implements VectorStore { * Delete a vector by ID */ async delete(vectorId: string): Promise { + await this.initialize(); const response = await this.searchClient.deleteDocuments([ { id: vectorId }, ]); @@ -454,6 +475,7 @@ export class AzureAISearch implements VectorStore { vector: number[], payload: Record, ): Promise { + await this.initialize(); const document: Record = { id: vectorId }; if (vector) { @@ -486,6 +508,7 @@ export class AzureAISearch implements VectorStore { * Retrieve a vector by ID */ async get(vectorId: string): Promise { + await this.initialize(); try { const result = await this.searchClient.getDocument(vectorId); const payloadStr = result.payload as string; @@ -521,6 +544,7 @@ export class AzureAISearch implements VectorStore { * Delete the index */ async deleteCol(): Promise { + await this.initialize(); await this.indexClient.deleteIndex(this.indexName); } @@ -542,6 +566,7 @@ export class AzureAISearch implements VectorStore { filters?: SearchFilters, topK: number = 100, ): Promise<[VectorStoreResult[], number]> { + await this.initialize(); const filterExpression = filters ? this.buildFilterExpression(filters) : undefined; @@ -586,6 +611,7 @@ export class AzureAISearch implements VectorStore { * Required by VectorStore interface */ async getUserId(): Promise { + await this.initialize(); try { // Check if memory_migrations index exists const collections = await this.listCols(); @@ -648,6 +674,7 @@ export class AzureAISearch implements VectorStore { * Required by VectorStore interface */ async setUserId(userId: string): Promise { + await this.initialize(); try { // Get existing point ID or generate new one const searchResults = await this.searchClient.search("*", { @@ -677,6 +704,7 @@ export class AzureAISearch implements VectorStore { * Reset the index by deleting and recreating it */ async reset(): Promise { + await this.initialize(); console.log(`Resetting index ${this.indexName}...`); try { diff --git a/mem0-ts/src/oss/src/vector_stores/azure_mysql.ts b/mem0-ts/src/oss/src/vector_stores/azure_mysql.ts index b76e6437a..123767484 100644 --- a/mem0-ts/src/oss/src/vector_stores/azure_mysql.ts +++ b/mem0-ts/src/oss/src/vector_stores/azure_mysql.ts @@ -1,6 +1,7 @@ import type { Pool, RowDataPacket } from "mysql2/promise"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,127}$/; @@ -94,14 +95,11 @@ export class AzureMySQLDB implements VectorStore { // Loaded dynamically: mysql2 is an optional peer dependency, so a static value import // would break `import { Memory } from "mem0ai/oss"` for everyone else. - let createPool: typeof import("mysql2/promise").createPool; - try { - ({ createPool } = await import("mysql2/promise")); - } catch { - throw new Error( - "The Azure MySQL vector store requires the 'mysql2' package. Install it with: npm install mysql2", - ); - } + const { createPool }: typeof import("mysql2/promise") = await loadPeer( + "mysql2", + "Azure MySQL vector store", + () => import("mysql2/promise"), + ); this.pool = createPool({ host: this.config.host, diff --git a/mem0-ts/src/oss/src/vector_stores/baidu.ts b/mem0-ts/src/oss/src/vector_stores/baidu.ts index 32f9bda5f..645910daa 100644 --- a/mem0-ts/src/oss/src/vector_stores/baidu.ts +++ b/mem0-ts/src/oss/src/vector_stores/baidu.ts @@ -12,6 +12,7 @@ import type { } from "@mochow/mochow-sdk-node"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; type MochowSdk = typeof import("@mochow/mochow-sdk-node"); @@ -156,14 +157,11 @@ export class BaiduDB implements VectorStore { // value import would break `import { Memory } from "mem0ai/oss"` for everyone else. private async loadSdk(): Promise { if (!this.sdk) { - let module: MochowSdk & { default?: MochowSdk }; - try { - module = await import("@mochow/mochow-sdk-node"); - } catch { - throw new Error( - "The Baidu vector store requires the '@mochow/mochow-sdk-node' package. Install it with: npm install @mochow/mochow-sdk-node", - ); - } + const module: MochowSdk & { default?: MochowSdk } = await loadPeer( + "@mochow/mochow-sdk-node", + "Baidu vector store", + () => import("@mochow/mochow-sdk-node"), + ); this.sdk = module.default ?? module; } return this.sdk; diff --git a/mem0-ts/src/oss/src/vector_stores/cassandra.ts b/mem0-ts/src/oss/src/vector_stores/cassandra.ts index e77e1972c..ccd69c52c 100644 --- a/mem0-ts/src/oss/src/vector_stores/cassandra.ts +++ b/mem0-ts/src/oss/src/vector_stores/cassandra.ts @@ -1,5 +1,6 @@ import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; const MIGRATION_ROW_ID = "mem0-user"; const SAFE_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/; @@ -377,14 +378,11 @@ export class CassandraDB implements VectorStore { // Loaded dynamically: cassandra-driver is an optional peer dependency, so a static // value import would break `import { Memory } from "mem0ai/oss"` for everyone else. private async loadDriver(): Promise { - let sdk: any; - try { - sdk = await import("cassandra-driver"); - } catch { - throw new Error( - "The 'cassandra-driver' package is required to use the Cassandra vector store. Install it with: npm install cassandra-driver", - ); - } + const sdk = await loadPeer( + "cassandra-driver", + "Cassandra vector store", + () => import("cassandra-driver"), + ); return sdk.default ?? sdk; } diff --git a/mem0-ts/src/oss/src/vector_stores/chroma.ts b/mem0-ts/src/oss/src/vector_stores/chroma.ts index d4acf7067..9e5d55512 100644 --- a/mem0-ts/src/oss/src/vector_stores/chroma.ts +++ b/mem0-ts/src/oss/src/vector_stores/chroma.ts @@ -1,6 +1,7 @@ import type { ChromaClient, CloudClient } from "chromadb"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; interface ChromaConfig extends VectorStoreConfig { /** Pre-configured ChromaDB client instance. */ @@ -65,14 +66,11 @@ export class ChromaDB implements VectorStore { return config.client; } - let sdk: any; - try { - sdk = await import("chromadb"); - } catch { - throw new Error( - "The 'chromadb' package is required to use the Chroma vector store. Install it with: npm install chromadb", - ); - } + const sdk = await loadPeer( + "chromadb", + "Chroma vector store", + () => import("chromadb"), + ); if (config.apiKey && config.tenant) { return new sdk.CloudClient({ diff --git a/mem0-ts/src/oss/src/vector_stores/elasticsearch.ts b/mem0-ts/src/oss/src/vector_stores/elasticsearch.ts index de5339d24..27ec5dde6 100644 --- a/mem0-ts/src/oss/src/vector_stores/elasticsearch.ts +++ b/mem0-ts/src/oss/src/vector_stores/elasticsearch.ts @@ -1,6 +1,7 @@ import type { Client } from "@elastic/elasticsearch"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; interface ElasticsearchConfig extends VectorStoreConfig { /** Pre-configured Elasticsearch client instance (typed as `any` to keep the @@ -100,14 +101,11 @@ export class ElasticsearchDB implements VectorStore { params.headers = config.headers; } - let sdk: any; - try { - sdk = await import("@elastic/elasticsearch"); - } catch { - throw new Error( - "The '@elastic/elasticsearch' package is required to use the Elasticsearch vector store. Install it with: npm install @elastic/elasticsearch", - ); - } + const sdk = await loadPeer( + "@elastic/elasticsearch", + "Elasticsearch vector store", + () => import("@elastic/elasticsearch"), + ); this.client = new sdk.Client(params); } diff --git a/mem0-ts/src/oss/src/vector_stores/langchain.ts b/mem0-ts/src/oss/src/vector_stores/langchain.ts index cdc8e0a74..8ea0f75c2 100644 --- a/mem0-ts/src/oss/src/vector_stores/langchain.ts +++ b/mem0-ts/src/oss/src/vector_stores/langchain.ts @@ -1,5 +1,4 @@ -import { VectorStore as LangchainVectorStoreInterface } from "@langchain/core/vectorstores"; -import { Document } from "@langchain/core/documents"; +import type { VectorStore as LangchainVectorStoreInterface } from "@langchain/core/vectorstores"; import { VectorStore } from "./base"; // mem0's VectorStore interface import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; @@ -77,6 +76,7 @@ export class LangchainVectorStore implements VectorStore { } // Convert payloads to Langchain Document metadata format + const { Document } = await import("@langchain/core/documents"); const documents = payloads.map((payload, i) => { // Provide empty pageContent, store mem0 id and other data in metadata return new Document({ diff --git a/mem0-ts/src/oss/src/vector_stores/mongodb.ts b/mem0-ts/src/oss/src/vector_stores/mongodb.ts index c383aa97f..f2cdfd309 100644 --- a/mem0-ts/src/oss/src/vector_stores/mongodb.ts +++ b/mem0-ts/src/oss/src/vector_stores/mongodb.ts @@ -1,6 +1,7 @@ import type { MongoClient, Collection, Db } from "mongodb"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; export interface MongoDBConfig extends VectorStoreConfig { url?: string; @@ -42,14 +43,11 @@ export class MongoDB implements VectorStore { if (config.client) { this.client = config.client; } else { - let sdk: any; - try { - sdk = await import("mongodb"); - } catch { - throw new Error( - "The 'mongodb' package is required to use the MongoDB vector store. Install it with: npm install mongodb", - ); - } + const sdk = await loadPeer( + "mongodb", + "MongoDB vector store", + () => import("mongodb"), + ); const url = config.url || "mongodb://localhost:27017"; this.client = new sdk.MongoClient(url, { appName: "Mem0" }); } diff --git a/mem0-ts/src/oss/src/vector_stores/opensearch.ts b/mem0-ts/src/oss/src/vector_stores/opensearch.ts index 4ecc283e1..12964d9f8 100644 --- a/mem0-ts/src/oss/src/vector_stores/opensearch.ts +++ b/mem0-ts/src/oss/src/vector_stores/opensearch.ts @@ -1,6 +1,7 @@ import type { Client } from "@opensearch-project/opensearch"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; type OpenSearchAuth = | { @@ -98,14 +99,11 @@ export class OpenSearchDB implements VectorStore { ? { username: config.user, password: config.password } : undefined); - let sdk: any; - try { - sdk = await import("@opensearch-project/opensearch"); - } catch { - throw new Error( - "The '@opensearch-project/opensearch' package is required to use the OpenSearch vector store. Install it with: npm install @opensearch-project/opensearch", - ); - } + const sdk = await loadPeer( + "@opensearch-project/opensearch", + "OpenSearch vector store", + () => import("@opensearch-project/opensearch"), + ); this.client = new sdk.Client({ node: `${useSSL ? "https" : "http"}://${host}:${port}`, diff --git a/mem0-ts/src/oss/src/vector_stores/pinecone.ts b/mem0-ts/src/oss/src/vector_stores/pinecone.ts index d0d2c9091..303b73ea4 100644 --- a/mem0-ts/src/oss/src/vector_stores/pinecone.ts +++ b/mem0-ts/src/oss/src/vector_stores/pinecone.ts @@ -1,6 +1,7 @@ import type { Pinecone, Index } from "@pinecone-database/pinecone"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; const MIGRATIONS_NAMESPACE = "__mem0_migrations__"; const MIGRATIONS_RECORD_ID = "mem0-user-id"; @@ -82,14 +83,11 @@ export class PineconeDB implements VectorStore { this.client = config.client; } else { const apiKey = config.apiKey || process.env.PINECONE_API_KEY; - let sdk: any; - try { - sdk = await import("@pinecone-database/pinecone"); - } catch { - throw new Error( - "The '@pinecone-database/pinecone' package is required to use the Pinecone vector store. Install it with: npm install @pinecone-database/pinecone", - ); - } + const sdk = await loadPeer( + "@pinecone-database/pinecone", + "Pinecone vector store", + () => import("@pinecone-database/pinecone"), + ); this.client = new sdk.Pinecone({ apiKey }); } } diff --git a/mem0-ts/src/oss/src/vector_stores/qdrant.ts b/mem0-ts/src/oss/src/vector_stores/qdrant.ts index 43a93031c..ddc5101b2 100644 --- a/mem0-ts/src/oss/src/vector_stores/qdrant.ts +++ b/mem0-ts/src/oss/src/vector_stores/qdrant.ts @@ -1,6 +1,7 @@ -import { QdrantClient } from "@qdrant/js-client-rest"; +import type { QdrantClient } from "@qdrant/js-client-rest"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; import * as fs from "fs"; interface QdrantConfig extends VectorStoreConfig { @@ -55,53 +56,64 @@ const KEY_MAP: Record = { }; export class Qdrant implements VectorStore { - private client: QdrantClient; + private client!: QdrantClient; + private readonly config: QdrantConfig; private readonly collectionName: string; private dimension: number; private _initPromise?: Promise; constructor(config: QdrantConfig) { - if (config.client) { - this.client = config.client; - } else { - const params: Record = {}; - if (config.apiKey) { - params.apiKey = config.apiKey; - } - if (config.url) { - params.url = config.url; - // Workaround for qdrant/qdrant-js#59: explicitly pass port to avoid "Illegal host" error - try { - const parsedUrl = new URL(config.url); - params.port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 6333; - } catch (_) { - params.port = 6333; - } - } - if (config.host && config.port) { - params.host = config.host; - params.port = config.port; - } - if (!Object.keys(params).length) { - params.path = config.path; - if (!config.onDisk && config.path) { - if ( - fs.existsSync(config.path) && - fs.statSync(config.path).isDirectory() - ) { - fs.rmSync(config.path, { recursive: true }); - } - } - } - - this.client = new QdrantClient(params); - } - + this.config = config; this.collectionName = config.collectionName; this.dimension = config.dimension || 1536; // Default OpenAI dimension this.initialize().catch(console.error); } + private async ensureClient(): Promise { + if (this.client) return; + const config = this.config; + if (config.client) { + this.client = config.client; + return; + } + const params: Record = {}; + if (config.apiKey) { + params.apiKey = config.apiKey; + } + if (config.url) { + params.url = config.url; + // Workaround for qdrant/qdrant-js#59: explicitly pass port to avoid "Illegal host" error + try { + const parsedUrl = new URL(config.url); + params.port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 6333; + } catch (_) { + params.port = 6333; + } + } + if (config.host && config.port) { + params.host = config.host; + params.port = config.port; + } + if (!Object.keys(params).length) { + params.path = config.path; + if (!config.onDisk && config.path) { + if ( + fs.existsSync(config.path) && + fs.statSync(config.path).isDirectory() + ) { + fs.rmSync(config.path, { recursive: true }); + } + } + } + + const sdk = await loadPeer( + "@qdrant/js-client-rest", + "Qdrant vector store", + () => import("@qdrant/js-client-rest"), + ); + this.client = new sdk.QdrantClient(params); + } + /** * Build a single field condition from a key-value filter pair. * Supports enhanced filter syntax with comparison operators. @@ -270,6 +282,7 @@ export class Qdrant implements VectorStore { ids: string[], payloads: Record[], ): Promise { + await this.initialize(); const points = vectors.map((vector, idx) => ({ id: ids[idx], vector: vector, @@ -290,6 +303,7 @@ export class Qdrant implements VectorStore { topK: number = 5, filters?: SearchFilters, ): Promise { + await this.initialize(); const queryFilter = this.createFilter(filters); const results = await this.client.search(this.collectionName, { vector: query, @@ -305,6 +319,7 @@ export class Qdrant implements VectorStore { } async get(vectorId: string): Promise { + await this.initialize(); const results = await this.client.retrieve(this.collectionName, { ids: [vectorId], with_payload: true, @@ -323,6 +338,7 @@ export class Qdrant implements VectorStore { vector: number[], payload: Record, ): Promise { + await this.initialize(); const point = { id: vectorId, vector: vector, @@ -335,12 +351,14 @@ export class Qdrant implements VectorStore { } async delete(vectorId: string): Promise { + await this.initialize(); await this.client.delete(this.collectionName, { points: [vectorId], }); } async deleteCol(): Promise { + await this.initialize(); await this.client.deleteCollection(this.collectionName); } @@ -348,6 +366,7 @@ export class Qdrant implements VectorStore { filters?: SearchFilters, topK: number = 100, ): Promise<[VectorStoreResult[], number]> { + await this.initialize(); const scrollRequest = { limit: topK, filter: this.createFilter(filters), @@ -380,6 +399,7 @@ export class Qdrant implements VectorStore { } async getUserId(): Promise { + await this.initialize(); try { // Ensure collection exists (idempotent — handles race conditions) await this.ensureCollection("memory_migrations", 1); @@ -417,6 +437,7 @@ export class Qdrant implements VectorStore { } async setUserId(userId: string): Promise { + await this.initialize(); try { // Get existing point ID const result = await this.client.scroll("memory_migrations", { @@ -496,6 +517,7 @@ export class Qdrant implements VectorStore { private async _doInitialize(): Promise { try { + await this.ensureClient(); await this.ensureCollection(this.collectionName, this.dimension); await this.ensureCollection("memory_migrations", 1); } catch (error) { diff --git a/mem0-ts/src/oss/src/vector_stores/redis.ts b/mem0-ts/src/oss/src/vector_stores/redis.ts index 5b72e53b5..c5d8394f7 100644 --- a/mem0-ts/src/oss/src/vector_stores/redis.ts +++ b/mem0-ts/src/oss/src/vector_stores/redis.ts @@ -1,4 +1,3 @@ -import { createClient } from "redis"; import type { RedisClientType, RedisDefaultModules, @@ -8,6 +7,7 @@ import type { } from "redis"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; /** * Escape RediSearch TAG filter special characters. Any punctuation in the @@ -146,15 +146,21 @@ function toCamelCase(obj: Record): Record { } export class RedisDB implements VectorStore { - private client: RedisClientType< + private client!: RedisClientType< RedisDefaultModules & RedisModules & RedisFunctions & RedisScripts >; + private readonly redisUrl: string; + private readonly username?: string; + private readonly password?: string; private readonly indexName: string; private readonly indexPrefix: string; private readonly schema: RedisSchema; private _initPromise?: Promise; constructor(config: RedisConfig) { + this.redisUrl = config.redisUrl; + this.username = config.username; + this.password = config.password; this.indexName = config.collectionName; this.indexPrefix = `mem0:${config.collectionName}`; @@ -177,12 +183,24 @@ export class RedisDB implements VectorStore { }), }; - this.client = createClient({ - url: config.redisUrl, - username: config.username, - password: config.password, + this.initialize().catch((err) => { + console.error("Failed to initialize Redis:", err); + }); + } + + private async ensureClient(): Promise { + if (this.client) return; + const sdk = await loadPeer( + "redis", + "Redis vector store", + () => import("redis"), + ); + this.client = sdk.createClient({ + url: this.redisUrl, + username: this.username, + password: this.password, socket: { - reconnectStrategy: (retries) => { + reconnectStrategy: (retries: number) => { if (retries > 10) { console.error("Max reconnection attempts reached"); return new Error("Max reconnection attempts reached"); @@ -194,10 +212,6 @@ export class RedisDB implements VectorStore { this.client.on("error", (err) => console.error("Redis Client Error:", err)); this.client.on("connect", () => console.log("Redis Client Connected")); - - this.initialize().catch((err) => { - console.error("Failed to initialize Redis:", err); - }); } private async createIndex(): Promise { @@ -260,6 +274,7 @@ export class RedisDB implements VectorStore { } private async _doInitialize(): Promise { + await this.ensureClient(); try { await this.client.connect(); console.log("Connected to Redis"); @@ -331,6 +346,7 @@ export class RedisDB implements VectorStore { ids: string[], payloads: Record[], ): Promise { + await this.initialize(); const data = vectors.map((vector, idx) => { const payload = toSnakeCase(payloads[idx]); const id = ids[idx]; @@ -389,6 +405,7 @@ export class RedisDB implements VectorStore { topK: number = 5, filters?: SearchFilters, ): Promise { + await this.initialize(); const snakeFilters = filters ? toSnakeCase(filters) : undefined; const filterExpr = snakeFilters ? Object.entries(snakeFilters) @@ -456,6 +473,7 @@ export class RedisDB implements VectorStore { } async get(vectorId: string): Promise { + await this.initialize(); try { // Check if the memory exists first const exists = await this.client.exists( @@ -562,6 +580,7 @@ export class RedisDB implements VectorStore { vector: number[], payload: Record, ): Promise { + await this.initialize(); const snakePayload = toSnakeCase(payload); const createdAt = snakePayload.created_at ? new Date(snakePayload.created_at).getTime() @@ -601,6 +620,7 @@ export class RedisDB implements VectorStore { } async delete(vectorId: string): Promise { + await this.initialize(); try { // Check if memory exists first const key = `${this.indexPrefix}:${vectorId}`; @@ -626,6 +646,7 @@ export class RedisDB implements VectorStore { } async deleteCol(): Promise { + await this.initialize(); await this.client.ft.dropIndex(this.indexName); } @@ -633,6 +654,7 @@ export class RedisDB implements VectorStore { filters?: SearchFilters, topK: number = 100, ): Promise<[VectorStoreResult[], number]> { + await this.initialize(); const snakeFilters = filters ? toSnakeCase(filters) : undefined; const filterExpr = snakeFilters ? Object.entries(snakeFilters) @@ -676,10 +698,11 @@ export class RedisDB implements VectorStore { } async close(): Promise { - await this.client.quit(); + if (this.client) await this.client.quit(); } async getUserId(): Promise { + await this.initialize(); try { // Check if the user ID exists in Redis const userId = await this.client.get("memory_migrations:1"); @@ -702,6 +725,7 @@ export class RedisDB implements VectorStore { } async setUserId(userId: string): Promise { + await this.initialize(); try { await this.client.set("memory_migrations:1", userId); } catch (error) { diff --git a/mem0-ts/src/oss/src/vector_stores/supabase.ts b/mem0-ts/src/oss/src/vector_stores/supabase.ts index 748fe8283..783568559 100644 --- a/mem0-ts/src/oss/src/vector_stores/supabase.ts +++ b/mem0-ts/src/oss/src/vector_stores/supabase.ts @@ -1,6 +1,7 @@ -import { createClient, SupabaseClient } from "@supabase/supabase-js"; +import type { SupabaseClient } from "@supabase/supabase-js"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; interface VectorData { id: string; @@ -82,14 +83,17 @@ $$; */ export class SupabaseDB implements VectorStore { - private client: SupabaseClient; + private client!: SupabaseClient; + private readonly supabaseUrl: string; + private readonly supabaseKey: string; private readonly tableName: string; private readonly embeddingColumnName: string; private readonly metadataColumnName: string; private _initPromise?: Promise; constructor(config: SupabaseConfig) { - this.client = createClient(config.supabaseUrl, config.supabaseKey); + this.supabaseUrl = config.supabaseUrl; + this.supabaseKey = config.supabaseKey; this.tableName = config.tableName; this.embeddingColumnName = config.embeddingColumnName || "embedding"; this.metadataColumnName = config.metadataColumnName || "metadata"; @@ -99,6 +103,16 @@ export class SupabaseDB implements VectorStore { }); } + private async ensureClient(): Promise { + if (this.client) return; + const sdk = await loadPeer( + "@supabase/supabase-js", + "Supabase vector store", + () => import("@supabase/supabase-js"), + ); + this.client = sdk.createClient(this.supabaseUrl, this.supabaseKey); + } + async initialize(): Promise { if (!this._initPromise) { this._initPromise = this._doInitialize(); @@ -107,6 +121,7 @@ export class SupabaseDB implements VectorStore { } private async _doInitialize(): Promise { + await this.ensureClient(); try { // Verify table exists and vector operations work by attempting a test insert const testVector = Array(1536).fill(0); @@ -209,6 +224,7 @@ See the SQL migration instructions in the code comments.`, ids: string[], payloads: Record[], ): Promise { + await this.initialize(); try { const data = vectors.map((vector, idx) => ({ id: ids[idx], @@ -237,6 +253,7 @@ See the SQL migration instructions in the code comments.`, topK: number = 5, filters?: SearchFilters, ): Promise { + await this.initialize(); try { const rpcQuery: VectorQueryParams = { query_embedding: query, @@ -265,6 +282,7 @@ See the SQL migration instructions in the code comments.`, } async get(vectorId: string): Promise { + await this.initialize(); try { const { data, error } = await this.client .from(this.tableName) @@ -290,6 +308,7 @@ See the SQL migration instructions in the code comments.`, vector: number[], payload: Record, ): Promise { + await this.initialize(); try { const { error } = await this.client .from(this.tableName) @@ -310,6 +329,7 @@ See the SQL migration instructions in the code comments.`, } async delete(vectorId: string): Promise { + await this.initialize(); try { const { error } = await this.client .from(this.tableName) @@ -324,6 +344,7 @@ See the SQL migration instructions in the code comments.`, } async deleteCol(): Promise { + await this.initialize(); try { const { error } = await this.client .from(this.tableName) @@ -341,6 +362,7 @@ See the SQL migration instructions in the code comments.`, filters?: SearchFilters, topK: number = 100, ): Promise<[VectorStoreResult[], number]> { + await this.initialize(); try { let query = this.client .from(this.tableName) @@ -370,6 +392,7 @@ See the SQL migration instructions in the code comments.`, } async getUserId(): Promise { + await this.initialize(); try { // First check if the table exists const { data: tableExists } = await this.client @@ -421,6 +444,7 @@ See the SQL migration instructions in the code comments.`, } async setUserId(userId: string): Promise { + await this.initialize(); try { const { error: deleteError } = await this.client .from("memory_migrations") diff --git a/mem0-ts/src/oss/src/vector_stores/turbopuffer.ts b/mem0-ts/src/oss/src/vector_stores/turbopuffer.ts index 9ec8773fa..5134aa80a 100644 --- a/mem0-ts/src/oss/src/vector_stores/turbopuffer.ts +++ b/mem0-ts/src/oss/src/vector_stores/turbopuffer.ts @@ -1,5 +1,6 @@ import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; interface TurbopufferConfig extends VectorStoreConfig { apiKey?: string; @@ -48,14 +49,11 @@ export class TurbopufferDB implements VectorStore { } private async createClient(): Promise { - let sdk: any; - try { - sdk = await import("@turbopuffer/turbopuffer"); - } catch { - throw new Error( - "The '@turbopuffer/turbopuffer' package is required to use the Turbopuffer vector store. Install it with: npm install @turbopuffer/turbopuffer", - ); - } + const sdk = await loadPeer( + "@turbopuffer/turbopuffer", + "Turbopuffer vector store", + () => import("@turbopuffer/turbopuffer"), + ); // @turbopuffer/turbopuffer ships `Turbopuffer` as both the default export // and a named export pointing at the same class. Use `.default` since diff --git a/mem0-ts/src/oss/src/vector_stores/upstash_vector.ts b/mem0-ts/src/oss/src/vector_stores/upstash_vector.ts index 8ea6aae9e..2fa539dd6 100644 --- a/mem0-ts/src/oss/src/vector_stores/upstash_vector.ts +++ b/mem0-ts/src/oss/src/vector_stores/upstash_vector.ts @@ -1,6 +1,7 @@ import type { Index, QueryResult, Vector } from "@upstash/vector"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; interface UpstashVectorConfig extends VectorStoreConfig { collectionName: string; @@ -42,14 +43,11 @@ export class UpstashVector implements VectorStore { if (config.client) { this.client = config.client; } else { - let sdk: any; - try { - sdk = await import("@upstash/vector"); - } catch { - throw new Error( - "The '@upstash/vector' package is required to use the Upstash Vector store. Install it with: npm install @upstash/vector", - ); - } + const sdk = await loadPeer( + "@upstash/vector", + "Upstash Vector store", + () => import("@upstash/vector"), + ); this.client = new sdk.Index({ url: config.url, token: config.token, diff --git a/mem0-ts/src/oss/src/vector_stores/valkey.ts b/mem0-ts/src/oss/src/vector_stores/valkey.ts index adf4f6ed9..dc549e99f 100644 --- a/mem0-ts/src/oss/src/vector_stores/valkey.ts +++ b/mem0-ts/src/oss/src/vector_stores/valkey.ts @@ -1,6 +1,7 @@ import { VectorStore } from "./base"; import { SearchFilters, VectorStoreResult } from "../types"; import { ValkeyConfig } from "../types/valkey"; +import { loadPeer } from "../utils/load_peer"; interface ValkeyClient { call: (...args: (string | number | Buffer)[]) => Promise; @@ -142,14 +143,8 @@ function formatTimestamp(timestamp: number, timezone: string = "UTC"): string { return `${yyyy}-${MM}-${dd}T${HH}:${mm}:${ss}${sign}${offHH}:${offMM}`; } -async function loadIovalkey(): Promise { - try { - return await import("iovalkey"); - } catch { - throw new Error( - "iovalkey is required for the Valkey vector store. Install it with: npm install iovalkey", - ); - } +function loadIovalkey(): Promise { + return loadPeer("iovalkey", "Valkey vector store", () => import("iovalkey")); } export class ValkeyDB implements VectorStore { diff --git a/mem0-ts/src/oss/src/vector_stores/vectorize.ts b/mem0-ts/src/oss/src/vector_stores/vectorize.ts index 8e900c142..c58966663 100644 --- a/mem0-ts/src/oss/src/vector_stores/vectorize.ts +++ b/mem0-ts/src/oss/src/vector_stores/vectorize.ts @@ -1,7 +1,8 @@ -import Cloudflare from "cloudflare"; +import type Cloudflare from "cloudflare"; import type { Vectorize, VectorizeVector } from "@cloudflare/workers-types"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; interface VectorizeConfig extends VectorStoreConfig { apiKey?: string; @@ -17,24 +18,36 @@ interface CloudflareVector { export class VectorizeDB implements VectorStore { private client: Cloudflare | null = null; + private apiKey?: string; private dimensions: number; private indexName: string; private accountId: string; private _initPromise?: Promise; constructor(config: VectorizeConfig) { - this.client = new Cloudflare({ apiToken: config.apiKey }); + this.apiKey = config.apiKey; this.dimensions = config.dimension || 1536; this.indexName = config.indexName; this.accountId = config.accountId; this.initialize().catch(console.error); } + private async ensureClient(): Promise { + if (this.client) return; + const sdk = await loadPeer( + "cloudflare", + "Vectorize vector store", + () => import("cloudflare"), + ); + this.client = new sdk.default({ apiToken: this.apiKey }); + } + async insert( vectors: number[][], ids: string[], payloads: Record[], ): Promise { + await this.initialize(); try { const vectorObjects: CloudflareVector[] = vectors.map( (vector, index) => ({ @@ -83,6 +96,7 @@ export class VectorizeDB implements VectorStore { topK: number = 5, filters?: SearchFilters, ): Promise { + await this.initialize(); try { const result = await this.client?.vectorize.indexes.query( this.indexName, @@ -111,6 +125,7 @@ export class VectorizeDB implements VectorStore { } async get(vectorId: string): Promise { + await this.initialize(); try { const result = (await this.client?.vectorize.indexes.getByIds( this.indexName, @@ -139,6 +154,7 @@ export class VectorizeDB implements VectorStore { vector: number[], payload: Record, ): Promise { + await this.initialize(); try { const data: VectorizeVector = { id: vectorId, @@ -173,6 +189,7 @@ export class VectorizeDB implements VectorStore { } async delete(vectorId: string): Promise { + await this.initialize(); try { await this.client?.vectorize.indexes.deleteByIds(this.indexName, { account_id: this.accountId, @@ -187,6 +204,7 @@ export class VectorizeDB implements VectorStore { } async deleteCol(): Promise { + await this.initialize(); try { await this.client?.vectorize.indexes.delete(this.indexName, { account_id: this.accountId, @@ -203,6 +221,7 @@ export class VectorizeDB implements VectorStore { filters?: SearchFilters, topK: number = 20, ): Promise<[VectorStoreResult[], number]> { + await this.initialize(); try { const result = await this.client?.vectorize.indexes.query( this.indexName, @@ -243,6 +262,7 @@ export class VectorizeDB implements VectorStore { } async getUserId(): Promise { + await this.initialize(); try { let found = false; for await (const index of this.client!.vectorize.indexes.list({ @@ -309,6 +329,7 @@ export class VectorizeDB implements VectorStore { } async setUserId(userId: string): Promise { + await this.initialize(); try { // Get existing point ID const result: any = await this.client?.vectorize.indexes.query( @@ -355,6 +376,7 @@ export class VectorizeDB implements VectorStore { } private async _doInitialize(): Promise { + await this.ensureClient(); try { // Check if the index already exists let indexFound = false; diff --git a/mem0-ts/src/oss/src/vector_stores/weaviate.ts b/mem0-ts/src/oss/src/vector_stores/weaviate.ts index 817413edb..3c7d51246 100644 --- a/mem0-ts/src/oss/src/vector_stores/weaviate.ts +++ b/mem0-ts/src/oss/src/vector_stores/weaviate.ts @@ -2,6 +2,7 @@ import type { WeaviateClient } from "weaviate-client"; import { v4 as uuidv4 } from "uuid"; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; interface WeaviateConfig extends VectorStoreConfig { /** Pre-configured Weaviate client instance (typed as `any` to keep the @@ -50,14 +51,11 @@ export class WeaviateDB implements VectorStore { private async ensureClient(): Promise { if (this._client) return; - let sdk: any; - try { - sdk = await import("weaviate-client"); - } catch { - throw new Error( - "The 'weaviate-client' package is required to use the Weaviate vector store. Install it with: npm install weaviate-client", - ); - } + const sdk = await loadPeer( + "weaviate-client", + "Weaviate vector store", + () => import("weaviate-client"), + ); this._sdk = sdk; const { client, clusterUrl, apiKey, additionalHeaders } = this._config; diff --git a/mem0-ts/src/oss/tests/anthropic-llm.test.ts b/mem0-ts/src/oss/tests/anthropic-llm.test.ts index a8d5d3276..b7b038fb5 100644 --- a/mem0-ts/src/oss/tests/anthropic-llm.test.ts +++ b/mem0-ts/src/oss/tests/anthropic-llm.test.ts @@ -23,11 +23,16 @@ describe("AnthropicLLM (unit)", () => { // Regression #5665: a configured baseURL must reach the Anthropic client so // proxy/gateway users are not silently bypassed (TS parity with #5626). - it("forwards baseURL to the Anthropic client when set", () => { - new AnthropicLLM({ + // The client is constructed lazily on first use, so drive generateResponse. + it("forwards baseURL to the Anthropic client when set", async () => { + mockCreate.mockResolvedValueOnce({ + content: [{ type: "text", text: "ok" }], + }); + const llm = new AnthropicLLM({ apiKey: "test-key", baseURL: "https://proxy.example/v1", }); + await llm.generateResponse([{ role: "user", content: "Hi" }]); expect(mockConstructor).toHaveBeenCalledTimes(1); const ctorArgs = mockConstructor.mock.calls[0][0]; @@ -37,8 +42,12 @@ describe("AnthropicLLM (unit)", () => { // When no baseURL is configured the client must not receive a baseURL key // (so the SDK default endpoint is used). - it("does NOT set baseURL when none is configured", () => { - new AnthropicLLM({ apiKey: "test-key" }); + it("does NOT set baseURL when none is configured", async () => { + mockCreate.mockResolvedValueOnce({ + content: [{ type: "text", text: "ok" }], + }); + const llm = new AnthropicLLM({ apiKey: "test-key" }); + await llm.generateResponse([{ role: "user", content: "Hi" }]); expect(mockConstructor).toHaveBeenCalledTimes(1); const ctorArgs = mockConstructor.mock.calls[0][0]; diff --git a/mem0-ts/src/oss/tests/optional-peers.test.ts b/mem0-ts/src/oss/tests/optional-peers.test.ts index a3ec460a1..e017d33fb 100644 --- a/mem0-ts/src/oss/tests/optional-peers.test.ts +++ b/mem0-ts/src/oss/tests/optional-peers.test.ts @@ -22,7 +22,16 @@ function sourceFiles(dir: string, acc: string[] = []): string[] { for (const entry of readdirSync(dir)) { const full = join(dir, entry); if (statSync(full).isDirectory()) { - if (entry !== "tests" && entry !== "__tests__") sourceFiles(full, acc); + // examples/ are dev scripts and community/ is the separate @mem0/community + // package — neither ships in files:["dist"] nor is reachable via the mem0ai/oss + // barrel, so a static import there cannot crash a Memory() consumer. + if ( + entry !== "tests" && + entry !== "__tests__" && + entry !== "examples" && + entry !== "community" + ) + sourceFiles(full, acc); } else if (entry.endsWith(".ts") && !entry.endsWith(".test.ts")) { acc.push(full); } diff --git a/mem0-ts/src/oss/tests/qdrant-url-port.test.ts b/mem0-ts/src/oss/tests/qdrant-url-port.test.ts index ae86ea471..bcb857a55 100644 --- a/mem0-ts/src/oss/tests/qdrant-url-port.test.ts +++ b/mem0-ts/src/oss/tests/qdrant-url-port.test.ts @@ -40,14 +40,15 @@ beforeEach(() => { }); describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => { - it("extracts port from HTTPS URL with explicit port", () => { - new Qdrant({ + it("extracts port from HTTPS URL with explicit port", async () => { + const store = new Qdrant({ url: "https://my-cluster.us-west-1-0.aws.cloud.qdrant.io:6333", apiKey: "test-key", collectionName: "test", embeddingModelDims: 768, dimension: 768, }); + await store.initialize(); expect(capturedParams).toBeDefined(); expect(capturedParams!.url).toBe( @@ -57,47 +58,50 @@ describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => { expect(capturedParams!.apiKey).toBe("test-key"); }); - it("extracts port from HTTP URL with explicit port", () => { - new Qdrant({ + it("extracts port from HTTP URL with explicit port", async () => { + const store = new Qdrant({ url: "http://localhost:6333", collectionName: "test", embeddingModelDims: 768, dimension: 768, }); + await store.initialize(); expect(capturedParams).toBeDefined(); expect(capturedParams!.url).toBe("http://localhost:6333"); expect(capturedParams!.port).toBe(6333); }); - it("defaults to port 6333 when HTTPS URL has no explicit port", () => { - new Qdrant({ + it("defaults to port 6333 when HTTPS URL has no explicit port", async () => { + const store = new Qdrant({ url: "https://my-cluster.cloud.qdrant.io", apiKey: "test-key", collectionName: "test", embeddingModelDims: 768, dimension: 768, }); + await store.initialize(); expect(capturedParams).toBeDefined(); expect(capturedParams!.url).toBe("https://my-cluster.cloud.qdrant.io"); expect(capturedParams!.port).toBe(6333); }); - it("defaults to port 6333 when HTTP URL has no explicit port", () => { - new Qdrant({ + it("defaults to port 6333 when HTTP URL has no explicit port", async () => { + const store = new Qdrant({ url: "http://localhost", collectionName: "test", embeddingModelDims: 768, dimension: 768, }); + await store.initialize(); expect(capturedParams).toBeDefined(); expect(capturedParams!.port).toBe(6333); }); - it("host+port config overrides URL-extracted port", () => { - new Qdrant({ + it("host+port config overrides URL-extracted port", async () => { + const store = new Qdrant({ url: "https://my-cluster.cloud.qdrant.io:6333", host: "custom-host", port: 9999, @@ -106,28 +110,28 @@ describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => { embeddingModelDims: 768, dimension: 768, }); + await store.initialize(); expect(capturedParams).toBeDefined(); expect(capturedParams!.host).toBe("custom-host"); expect(capturedParams!.port).toBe(9999); }); - it("handles invalid URL gracefully without crashing", () => { - expect(() => { - new Qdrant({ - url: "not-a-valid-url", - collectionName: "test", - embeddingModelDims: 768, - dimension: 768, - }); - }).not.toThrow(); + it("handles invalid URL gracefully without crashing", async () => { + const store = new Qdrant({ + url: "not-a-valid-url", + collectionName: "test", + embeddingModelDims: 768, + dimension: 768, + }); + await expect(store.initialize()).resolves.not.toThrow(); expect(capturedParams).toBeDefined(); expect(capturedParams!.url).toBe("not-a-valid-url"); expect(capturedParams!.port).toBe(6333); }); - it("does not pass port when using pre-configured client", () => { + it("does not pass port when using pre-configured client", async () => { const mockClient: any = { createCollection: jest.fn().mockResolvedValue(undefined), getCollection: jest.fn().mockResolvedValue({ @@ -141,12 +145,13 @@ describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => { deleteCollection: jest.fn().mockResolvedValue(undefined), }; - new Qdrant({ + const store = new Qdrant({ client: mockClient, collectionName: "test", embeddingModelDims: 768, dimension: 768, }); + await store.initialize(); // QdrantClient constructor should NOT have been called expect( @@ -154,14 +159,15 @@ describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => { ).not.toHaveBeenCalled(); }); - it("defaults to 6333 when HTTPS URL uses default port 443", () => { - new Qdrant({ + it("defaults to 6333 when HTTPS URL uses default port 443", async () => { + const store = new Qdrant({ url: "https://my-cluster.cloud.qdrant.io:443", apiKey: "test-key", collectionName: "test", embeddingModelDims: 768, dimension: 768, }); + await store.initialize(); // 443 is default for HTTPS, so URL.port returns empty string — we default to 6333 expect(capturedParams).toBeDefined();