From 28e4d819f88729d2d61f7b38cdcb65c6bf62ca93 Mon Sep 17 00:00:00 2001 From: Shaurya Date: Fri, 10 Jul 2026 20:46:08 +0530 Subject: [PATCH] fix(ts-sdk): re-raise LLM extraction transport failures instead of returning [] (#6102) Co-authored-by: Kartik --- mem0-ts/src/oss/src/memory/index.ts | 15 ++- .../src/oss/tests/memory.llm-error.test.ts | 104 ++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 mem0-ts/src/oss/tests/memory.llm-error.test.ts diff --git a/mem0-ts/src/oss/src/memory/index.ts b/mem0-ts/src/oss/src/memory/index.ts index 22cd33f12..23a6708e8 100644 --- a/mem0-ts/src/oss/src/memory/index.ts +++ b/mem0-ts/src/oss/src/memory/index.ts @@ -79,7 +79,18 @@ import { logger } from "../utils/logger"; import { normalizeExpirationDate, payloadIsExpired } from "../utils/expiration"; import { getOrCreateMem0UserId } from "../../../client/config"; -// Entity params that must be passed via filters - check both snake_case and camelCase +export class LLMError extends Error { + readonly cause?: unknown; + + constructor(message: string, options: { cause?: unknown } = {}) { + super(message); + this.name = "LLMError"; + this.cause = options.cause; + Object.setPrototypeOf(this, new.target.prototype); + } +} + +// Entity params that must be passed via filters check both snake_case and camelCase const ENTITY_PARAMS = [ "user_id", "agent_id", @@ -871,7 +882,7 @@ export class Memory { )) as string; } catch (e) { console.error("LLM extraction failed:", e); - return []; + throw new LLMError(`LLM extraction failed: ${e}`, { cause: e }); } // Parse response diff --git a/mem0-ts/src/oss/tests/memory.llm-error.test.ts b/mem0-ts/src/oss/tests/memory.llm-error.test.ts new file mode 100644 index 000000000..326f37802 --- /dev/null +++ b/mem0-ts/src/oss/tests/memory.llm-error.test.ts @@ -0,0 +1,104 @@ +/** + * Regression test for #6101 / #5903: LLM extraction transport failures + * (rate limits, timeouts, connection errors) must propagate as a typed + * `LLMError`, not be silently swallowed into an empty result. Mirrors the + * Python SDK regression test added in #5878 + * (`test_llm_extraction_exception_is_reraised`). + */ +/// +import { Memory, LLMError } from "../src/memory"; +import type { SearchResult } from "../src/types"; + +jest.setTimeout(15000); + +// Mock Google modules to prevent @google/genai crash in CI +jest.mock("../src/embeddings/google", () => ({ + GoogleEmbedder: jest.fn(), +})); +jest.mock("../src/llms/google", () => ({ + GoogleLLM: jest.fn(), +})); + +class _ProviderError extends Error {} + +jest.mock("../src/llms/openai", () => ({ + OpenAILLM: jest.fn().mockImplementation(() => ({ + generateResponse: jest + .fn() + .mockRejectedValue(new _ProviderError("429 rate limit")), + })), +})); + +const mockEmbedding = new Array(1536).fill(0.1); +jest.mock("../src/embeddings/openai", () => ({ + OpenAIEmbedder: jest.fn().mockImplementation(() => ({ + embed: jest.fn().mockResolvedValue(mockEmbedding), + embedBatch: jest + .fn() + .mockImplementation((texts: string[]) => + Promise.resolve(texts.map(() => mockEmbedding)), + ), + embeddingDims: 1536, + })), +})); + +function createMemory(): Memory { + return new Memory({ + version: "v1.1", + embedder: { + provider: "openai", + config: { apiKey: "test-key", model: "text-embedding-3-small" }, + }, + vectorStore: { + provider: "memory", + config: { + collectionName: `test-llm-error-${Date.now()}`, + dimension: 1536, + dbPath: ":memory:", + }, + }, + llm: { + provider: "openai", + config: { apiKey: "test-key", model: "gpt-5-mini" }, + }, + historyDbPath: ":memory:", + }); +} + +describe("Memory - LLM extraction transport failures", () => { + let memory: Memory; + const userId = `llm_error_test_${Date.now()}`; + + beforeAll(async () => { + memory = createMemory(); + }); + + afterAll(async () => { + await memory.reset(); + }); + + test("add() rejects with LLMError instead of returning an empty result", async () => { + await expect( + memory.add("this should trigger a provider failure", { userId }), + ).rejects.toBeInstanceOf(LLMError); + }); + + test("thrown LLMError preserves the original error as its cause", async () => { + let caught: unknown; + try { + const result: SearchResult = await memory.add("trigger failure again", { + userId, + }); + // Should never reach here — fail loudly if the call resolves. + throw new Error( + `Expected add() to reject, but it resolved with: ${JSON.stringify(result)}`, + ); + } catch (e) { + caught = e; + } + + expect(caught).toBeInstanceOf(LLMError); + expect((caught as LLMError).cause).toBeInstanceOf(_ProviderError); + expect((caught as LLMError).message).toContain("429 rate limit"); + }); +});