fix(ts): forward timeout config to OpenAI client in JS OSS LLM providers (#4770)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
This commit is contained in:
Jean Ibarz
2026-04-23 15:59:27 +02:00
committed by GitHub
parent db8ac61713
commit 34ed122ef3
5 changed files with 208 additions and 1 deletions
+1
View File
@@ -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";
}
@@ -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";
}
+2
View File
@@ -48,6 +48,7 @@ export interface LLMConfig {
apiKey?: string;
model?: string | any;
modelProperties?: Record<string, any>;
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(),
+141
View File
@@ -0,0 +1,141 @@
/// <reference types="jest" />
/**
* 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",
});
});
});
@@ -0,0 +1,59 @@
/// <reference types="jest" />
/**
* 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");
});
});