From 34ed122ef376d9379b89deb10ead0c63285a7748 Mon Sep 17 00:00:00 2001 From: Jean Ibarz Date: Thu, 23 Apr 2026 15:59:27 +0200 Subject: [PATCH] fix(ts): forward timeout config to OpenAI client in JS OSS LLM providers (#4770) Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Kartik --- mem0-ts/src/oss/src/llms/openai.ts | 1 + mem0-ts/src/oss/src/llms/openai_structured.ts | 6 +- mem0-ts/src/oss/src/types/index.ts | 2 + mem0-ts/src/oss/tests/openai-llm.test.ts | 141 ++++++++++++++++++ .../oss/tests/openai-structured-llm.test.ts | 59 ++++++++ 5 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 mem0-ts/src/oss/tests/openai-llm.test.ts create mode 100644 mem0-ts/src/oss/tests/openai-structured-llm.test.ts diff --git a/mem0-ts/src/oss/src/llms/openai.ts b/mem0-ts/src/oss/src/llms/openai.ts index 2ba1843af..41644ca35 100644 --- a/mem0-ts/src/oss/src/llms/openai.ts +++ b/mem0-ts/src/oss/src/llms/openai.ts @@ -10,6 +10,7 @@ export class OpenAILLM implements LLM { this.openai = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseURL, + ...(config.timeout != null && { timeout: config.timeout }), }); this.model = config.model || "gpt-5-mini"; } diff --git a/mem0-ts/src/oss/src/llms/openai_structured.ts b/mem0-ts/src/oss/src/llms/openai_structured.ts index 30f50e6eb..9c6b90634 100644 --- a/mem0-ts/src/oss/src/llms/openai_structured.ts +++ b/mem0-ts/src/oss/src/llms/openai_structured.ts @@ -7,7 +7,11 @@ export class OpenAIStructuredLLM implements LLM { private model: string; constructor(config: LLMConfig) { - this.openai = new OpenAI({ apiKey: config.apiKey }); + this.openai = new OpenAI({ + apiKey: config.apiKey, + baseURL: config.baseURL, + ...(config.timeout != null && { timeout: config.timeout }), + }); this.model = config.model || "gpt-5-mini"; } diff --git a/mem0-ts/src/oss/src/types/index.ts b/mem0-ts/src/oss/src/types/index.ts index d93061d33..fd5ec7188 100644 --- a/mem0-ts/src/oss/src/types/index.ts +++ b/mem0-ts/src/oss/src/types/index.ts @@ -48,6 +48,7 @@ export interface LLMConfig { apiKey?: string; model?: string | any; modelProperties?: Record; + timeout?: number; } export interface MemoryConfig { @@ -129,6 +130,7 @@ export const MemoryConfigSchema = z.object({ modelProperties: z.record(z.string(), z.any()).optional(), baseURL: z.string().optional(), url: z.string().optional(), + timeout: z.number().optional(), }), }), historyDbPath: z.string().optional(), diff --git a/mem0-ts/src/oss/tests/openai-llm.test.ts b/mem0-ts/src/oss/tests/openai-llm.test.ts new file mode 100644 index 000000000..5b584f230 --- /dev/null +++ b/mem0-ts/src/oss/tests/openai-llm.test.ts @@ -0,0 +1,141 @@ +/// +/** + * OpenAI LLM — unit tests (mocked openai). + * + * Regression tests for #4707: timeout config was silently ignored, + * causing add() to hang indefinitely on slow LLM responses. + */ + +let capturedConstructorArgs: any; +const mockCreate = jest.fn(); + +jest.mock("openai", () => { + return jest.fn().mockImplementation((args: any) => { + capturedConstructorArgs = args; + return { + chat: { completions: { create: mockCreate } }, + }; + }); +}); + +import { OpenAILLM } from "../src/llms/openai"; + +describe("OpenAILLM (unit)", () => { + beforeEach(() => { + capturedConstructorArgs = undefined; + mockCreate.mockClear(); + }); + + it("forwards timeout to the OpenAI client constructor", () => { + new OpenAILLM({ + apiKey: "test-key", + baseURL: "http://localhost:8080/v1", + timeout: 5000, + }); + + expect(capturedConstructorArgs).toMatchObject({ + apiKey: "test-key", + baseURL: "http://localhost:8080/v1", + timeout: 5000, + }); + }); + + it("forwards timeout: 0 to the OpenAI client (explicit zero is valid)", () => { + new OpenAILLM({ + apiKey: "test-key", + baseURL: "http://localhost:8080/v1", + timeout: 0, + }); + + expect(capturedConstructorArgs.timeout).toBe(0); + }); + + it("omits timeout from the OpenAI client when not configured", () => { + new OpenAILLM({ + apiKey: "test-key", + baseURL: "http://localhost:8080/v1", + }); + + expect(capturedConstructorArgs).toMatchObject({ + apiKey: "test-key", + baseURL: "http://localhost:8080/v1", + }); + expect(capturedConstructorArgs).not.toHaveProperty("timeout"); + }); + + it("generateResponse() returns text content", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: '{"facts": ["hello"]}', + role: "assistant", + tool_calls: null, + }, + }, + ], + }); + + const llm = new OpenAILLM({ apiKey: "test-key" }); + const result = await llm.generateResponse([ + { role: "user", content: "Hi" }, + ]); + + expect(mockCreate).toHaveBeenCalledTimes(1); + expect(result).toBe('{"facts": ["hello"]}'); + }); + + it("generateResponse() handles tool calls", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { + content: "", + role: "assistant", + tool_calls: [ + { + function: { + name: "get_weather", + arguments: '{"city": "London"}', + }, + }, + ], + }, + }, + ], + }); + + const llm = new OpenAILLM({ apiKey: "test-key" }); + const result = await llm.generateResponse( + [{ role: "user", content: "What is the weather?" }], + undefined, + [{ type: "function", function: { name: "get_weather" } }], + ); + + expect(result).toEqual({ + content: "", + role: "assistant", + toolCalls: [{ name: "get_weather", arguments: '{"city": "London"}' }], + }); + }); + + it("generateChat() returns LLMResponse shape", async () => { + mockCreate.mockResolvedValueOnce({ + choices: [ + { + message: { content: "I can help.", role: "assistant" }, + }, + ], + }); + + const llm = new OpenAILLM({ apiKey: "test-key" }); + const result = await llm.generateChat([ + { role: "user", content: "Help me" }, + ]); + + expect(result).toEqual({ + content: "I can help.", + role: "assistant", + }); + }); +}); diff --git a/mem0-ts/src/oss/tests/openai-structured-llm.test.ts b/mem0-ts/src/oss/tests/openai-structured-llm.test.ts new file mode 100644 index 000000000..f8ddc576f --- /dev/null +++ b/mem0-ts/src/oss/tests/openai-structured-llm.test.ts @@ -0,0 +1,59 @@ +/// +/** + * OpenAI Structured LLM — unit tests (mocked openai). + * + * Sibling fix for #4707: OpenAIStructuredLLM had the same timeout gap. + */ + +let capturedConstructorArgs: any; +const mockCreate = jest.fn(); + +jest.mock("openai", () => { + return jest.fn().mockImplementation((args: any) => { + capturedConstructorArgs = args; + return { + chat: { completions: { create: mockCreate } }, + }; + }); +}); + +import { OpenAIStructuredLLM } from "../src/llms/openai_structured"; + +describe("OpenAIStructuredLLM (unit)", () => { + beforeEach(() => { + capturedConstructorArgs = undefined; + mockCreate.mockClear(); + }); + + it("forwards timeout to the OpenAI client constructor", () => { + new OpenAIStructuredLLM({ + apiKey: "test-key", + timeout: 15000, + }); + + expect(capturedConstructorArgs).toMatchObject({ + apiKey: "test-key", + timeout: 15000, + }); + }); + + it("forwards timeout: 0 to the OpenAI client (explicit zero is valid)", () => { + new OpenAIStructuredLLM({ + apiKey: "test-key", + timeout: 0, + }); + + expect(capturedConstructorArgs.timeout).toBe(0); + }); + + it("omits timeout from the OpenAI client when not configured", () => { + new OpenAIStructuredLLM({ + apiKey: "test-key", + }); + + expect(capturedConstructorArgs).toMatchObject({ + apiKey: "test-key", + }); + expect(capturedConstructorArgs).not.toHaveProperty("timeout"); + }); +});