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:
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user