fix(ts-sdk): re-raise LLM extraction transport failures instead of returning [] (#6102)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
This commit is contained in:
@@ -79,7 +79,18 @@ import { logger } from "../utils/logger";
|
|||||||
import { normalizeExpirationDate, payloadIsExpired } from "../utils/expiration";
|
import { normalizeExpirationDate, payloadIsExpired } from "../utils/expiration";
|
||||||
import { getOrCreateMem0UserId } from "../../../client/config";
|
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 = [
|
const ENTITY_PARAMS = [
|
||||||
"user_id",
|
"user_id",
|
||||||
"agent_id",
|
"agent_id",
|
||||||
@@ -871,7 +882,7 @@ export class Memory {
|
|||||||
)) as string;
|
)) as string;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("LLM extraction failed:", e);
|
console.error("LLM extraction failed:", e);
|
||||||
return [];
|
throw new LLMError(`LLM extraction failed: ${e}`, { cause: e });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse response
|
// Parse response
|
||||||
|
|||||||
@@ -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`).
|
||||||
|
*/
|
||||||
|
/// <reference types="jest" />
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user