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:
co-authored by
Claude Opus 4.6
Kartik
parent
db8ac61713
commit
34ed122ef3
@@ -10,6 +10,7 @@ export class OpenAILLM implements LLM {
|
|||||||
this.openai = new OpenAI({
|
this.openai = new OpenAI({
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
baseURL: config.baseURL,
|
baseURL: config.baseURL,
|
||||||
|
...(config.timeout != null && { timeout: config.timeout }),
|
||||||
});
|
});
|
||||||
this.model = config.model || "gpt-5-mini";
|
this.model = config.model || "gpt-5-mini";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ export class OpenAIStructuredLLM implements LLM {
|
|||||||
private model: string;
|
private model: string;
|
||||||
|
|
||||||
constructor(config: LLMConfig) {
|
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";
|
this.model = config.model || "gpt-5-mini";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export interface LLMConfig {
|
|||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
model?: string | any;
|
model?: string | any;
|
||||||
modelProperties?: Record<string, any>;
|
modelProperties?: Record<string, any>;
|
||||||
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemoryConfig {
|
export interface MemoryConfig {
|
||||||
@@ -129,6 +130,7 @@ export const MemoryConfigSchema = z.object({
|
|||||||
modelProperties: z.record(z.string(), z.any()).optional(),
|
modelProperties: z.record(z.string(), z.any()).optional(),
|
||||||
baseURL: z.string().optional(),
|
baseURL: z.string().optional(),
|
||||||
url: z.string().optional(),
|
url: z.string().optional(),
|
||||||
|
timeout: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
historyDbPath: z.string().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