first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,623 @@
import axios from 'axios';
import { tool } from 'langchain';
import { Readable } from 'node:stream';
import z from 'zod';
export const weatherTool = tool(
({ city }) => {
return `It's always sunny in ${city}!`;
},
{
name: 'get_weather',
description: 'Get weather for a given city.',
schema: z.object({
city: z.string(),
}),
},
);
export const mockToolCallResponse = {
id: 'resp_02a127c1e73b5fe4016989e989cb188195a27d4911084e4223',
object: 'response',
created_at: 1770645897,
status: 'completed',
background: false,
billing: { payer: 'developer' },
completed_at: 1770645898,
error: null,
frequency_penalty: 0,
incomplete_details: null,
instructions: null,
max_output_tokens: null,
max_tool_calls: null,
model: 'gpt-4o-2024-08-06',
output: [
{
id: 'fc_02a127c1e73b5fe4016989e98a70b881959fb6cf1d58b5db8b',
type: 'function_call',
status: 'completed',
arguments: '{"city":"Tokyo"}',
call_id: 'call_YONsRdkCKu8Sh8WGkUiXqlYW',
name: 'get_weather',
},
],
parallel_tool_calls: true,
presence_penalty: 0,
previous_response_id: null,
prompt_cache_key: null,
prompt_cache_retention: null,
reasoning: { effort: null, summary: null },
safety_identifier: null,
service_tier: 'default',
store: false,
temperature: 1,
text: { format: { type: 'text' }, verbosity: 'medium' },
tool_choice: 'auto',
tools: [
{
type: 'function',
description: 'Get weather for a given city.',
name: 'get_weather',
parameters: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
additionalProperties: false,
},
strict: true,
},
],
top_logprobs: 0,
top_p: 1,
truncation: 'disabled',
usage: {
input_tokens: 46,
input_tokens_details: { cached_tokens: 0 },
output_tokens: 15,
output_tokens_details: { reasoning_tokens: 0 },
total_tokens: 61,
},
user: null,
metadata: {},
};
export const mockFinalResponse = {
id: 'resp_00a8729c01103919016989e98b13888190bca486b9676ce0cd',
object: 'response',
created_at: 1770645899,
status: 'completed',
background: false,
billing: { payer: 'developer' },
completed_at: 1770645899,
error: null,
frequency_penalty: 0,
incomplete_details: null,
instructions: null,
max_output_tokens: null,
max_tool_calls: null,
model: 'gpt-4o-2024-08-06',
output: [
{
id: 'msg_00a8729c01103919016989e98bb58c81909a9eb194728f1db0',
type: 'message',
status: 'completed',
content: [
{
type: 'output_text',
annotations: [],
logprobs: [],
text: "It's always sunny in Tokyo!",
},
],
role: 'assistant',
},
],
parallel_tool_calls: true,
presence_penalty: 0,
previous_response_id: null,
prompt_cache_key: null,
prompt_cache_retention: null,
reasoning: { effort: null, summary: null },
safety_identifier: null,
service_tier: 'default',
store: false,
temperature: 1,
text: { format: { type: 'text' }, verbosity: 'medium' },
tool_choice: 'auto',
tools: [
{
type: 'function',
description: 'Get weather for a given city.',
name: 'get_weather',
parameters: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
additionalProperties: false,
},
strict: true,
},
],
top_logprobs: 0,
top_p: 1,
truncation: 'disabled',
usage: {
input_tokens: 76,
input_tokens_details: { cached_tokens: 0 },
output_tokens: 8,
output_tokens_details: { reasoning_tokens: 0 },
total_tokens: 84,
},
user: null,
metadata: {},
};
export const mockStreamToolCallEvents = [
{
type: 'event',
data: {
type: 'response.created',
response: {
id: 'resp_stream_001',
object: 'response',
created_at: 1770647361,
status: 'in_progress',
model: 'gpt-4o-2024-08-06',
},
sequence_number: 0,
},
},
{
type: 'event',
data: {
type: 'response.in_progress',
response: {
id: 'resp_stream_001',
status: 'in_progress',
},
sequence_number: 1,
},
},
{
type: 'event',
data: {
type: 'response.output_item.added',
item: {
id: 'fc_stream_001',
type: 'function_call',
status: 'in_progress',
arguments: '',
call_id: 'call_StreamTest123',
name: 'get_weather',
},
output_index: 0,
sequence_number: 2,
},
},
{
type: 'event',
data: {
type: 'response.function_call_arguments.delta',
delta: '{"',
item_id: 'fc_stream_001',
output_index: 0,
sequence_number: 3,
},
},
{
type: 'event',
data: {
type: 'response.function_call_arguments.delta',
delta: 'city',
item_id: 'fc_stream_001',
output_index: 0,
sequence_number: 4,
},
},
{
type: 'event',
data: {
type: 'response.function_call_arguments.delta',
delta: '":"',
item_id: 'fc_stream_001',
output_index: 0,
sequence_number: 5,
},
},
{
type: 'event',
data: {
type: 'response.function_call_arguments.delta',
delta: 'Tokyo',
item_id: 'fc_stream_001',
output_index: 0,
sequence_number: 6,
},
},
{
type: 'event',
data: {
type: 'response.function_call_arguments.delta',
delta: '"}',
item_id: 'fc_stream_001',
output_index: 0,
sequence_number: 7,
},
},
{
type: 'event',
data: {
type: 'response.function_call_arguments.done',
arguments: '{"city":"Tokyo"}',
item_id: 'fc_stream_001',
output_index: 0,
sequence_number: 8,
},
},
{
type: 'event',
data: {
type: 'response.output_item.done',
item: {
id: 'fc_stream_001',
type: 'function_call',
status: 'completed',
arguments: '{"city":"Tokyo"}',
call_id: 'call_StreamTest123',
name: 'get_weather',
},
output_index: 0,
sequence_number: 9,
},
},
{
type: 'event',
data: {
type: 'response.completed',
response: {
id: 'resp_stream_001',
object: 'response',
created_at: 1770647361,
status: 'completed',
completed_at: 1770647362,
model: 'gpt-4o-2024-08-06',
output: [
{
id: 'fc_stream_001',
type: 'function_call',
status: 'completed',
arguments: '{"city":"Tokyo"}',
call_id: 'call_StreamTest123',
name: 'get_weather',
},
],
usage: {
input_tokens: 46,
input_tokens_details: {
cached_tokens: 0,
},
output_tokens: 15,
output_tokens_details: {
reasoning_tokens: 0,
},
total_tokens: 61,
},
},
sequence_number: 10,
},
},
{
type: 'done',
data: null,
},
];
export const mockStreamFinalResponseEvents = [
{
type: 'event',
data: {
type: 'response.created',
response: {
id: 'resp_stream_002',
object: 'response',
created_at: 1770647362,
status: 'in_progress',
model: 'gpt-4o-2024-08-06',
},
sequence_number: 0,
},
},
{
type: 'event',
data: {
type: 'response.in_progress',
response: {
id: 'resp_stream_002',
status: 'in_progress',
},
sequence_number: 1,
},
},
{
type: 'event',
data: {
type: 'response.output_item.added',
item: {
id: 'msg_stream_002',
type: 'message',
status: 'in_progress',
content: [],
role: 'assistant',
},
output_index: 0,
sequence_number: 2,
},
},
{
type: 'event',
data: {
type: 'response.content_part.added',
content_index: 0,
item_id: 'msg_stream_002',
output_index: 0,
part: {
type: 'output_text',
annotations: [],
logprobs: [],
text: '',
},
sequence_number: 3,
},
},
{
type: 'event',
data: {
type: 'response.output_text.delta',
content_index: 0,
delta: "It's",
item_id: 'msg_stream_002',
logprobs: [],
output_index: 0,
sequence_number: 4,
},
},
{
type: 'event',
data: {
type: 'response.output_text.delta',
content_index: 0,
delta: ' always',
item_id: 'msg_stream_002',
logprobs: [],
output_index: 0,
sequence_number: 5,
},
},
{
type: 'event',
data: {
type: 'response.output_text.delta',
content_index: 0,
delta: ' sunny',
item_id: 'msg_stream_002',
logprobs: [],
output_index: 0,
sequence_number: 6,
},
},
{
type: 'event',
data: {
type: 'response.output_text.delta',
content_index: 0,
delta: ' in',
item_id: 'msg_stream_002',
logprobs: [],
output_index: 0,
sequence_number: 7,
},
},
{
type: 'event',
data: {
type: 'response.output_text.delta',
content_index: 0,
delta: ' Tokyo',
item_id: 'msg_stream_002',
logprobs: [],
output_index: 0,
sequence_number: 8,
},
},
{
type: 'event',
data: {
type: 'response.output_text.delta',
content_index: 0,
delta: '!',
item_id: 'msg_stream_002',
logprobs: [],
output_index: 0,
sequence_number: 9,
},
},
{
type: 'event',
data: {
type: 'response.output_text.done',
content_index: 0,
item_id: 'msg_stream_002',
logprobs: [],
output_index: 0,
sequence_number: 10,
text: "It's always sunny in Tokyo!",
},
},
{
type: 'event',
data: {
type: 'response.content_part.done',
content_index: 0,
item_id: 'msg_stream_002',
output_index: 0,
part: {
type: 'output_text',
annotations: [],
logprobs: [],
text: "It's always sunny in Tokyo!",
},
sequence_number: 11,
},
},
{
type: 'event',
data: {
type: 'response.output_item.done',
item: {
id: 'msg_stream_002',
type: 'message',
status: 'completed',
content: [
{
type: 'output_text',
annotations: [],
logprobs: [],
text: "It's always sunny in Tokyo!",
},
],
role: 'assistant',
},
output_index: 0,
sequence_number: 12,
},
},
{
type: 'event',
data: {
type: 'response.completed',
response: {
id: 'resp_stream_002',
object: 'response',
created_at: 1770647362,
status: 'completed',
completed_at: 1770647363,
model: 'gpt-4o-2024-08-06',
output: [
{
id: 'msg_stream_002',
type: 'message',
status: 'completed',
content: [
{
type: 'output_text',
annotations: [],
logprobs: [],
text: "It's always sunny in Tokyo!",
},
],
role: 'assistant',
},
],
usage: {
input_tokens: 76,
input_tokens_details: {
cached_tokens: 0,
},
output_tokens: 8,
output_tokens_details: {
reasoning_tokens: 0,
},
total_tokens: 84,
},
},
sequence_number: 13,
},
},
{
type: 'done',
data: null,
},
];
export function createSSEStream(events: Array<{ type: string; data: unknown }>) {
const stream = new Readable({
read() {},
});
let eventIndex = 0;
function sendData() {
setTimeout(() => {
if (eventIndex < events.length) {
const event = events[eventIndex];
if (event.type === 'done') {
stream.push('data: [DONE]\n\n');
stream.push(null);
} else {
stream.push(`data: ${JSON.stringify(event.data)}\n\n`);
}
eventIndex++;
sendData();
}
}, 50);
}
sendData();
return stream;
}
export function createMockHttpRequests() {
return {
httpRequest: async (
method: string,
url: string,
body?: object,
headers?: Record<string, string>,
) => {
const response = await axios({
method,
url,
data: body,
headers: {
...headers,
'Content-Type': 'application/json',
Authorization: 'Bearer test-api-key',
},
validateStatus: () => true, // Don't throw on any status
});
return {
ok: response.status >= 200 && response.status < 300,
status: response.status,
statusText: response.statusText,
body: response.data,
};
},
openStream: async (
method: string,
url: string,
body?: object,
headers?: Record<string, string>,
) => {
const response = await axios({
method,
url,
data: body,
headers: {
...headers,
'Content-Type': 'application/json',
Authorization: 'Bearer test-api-key',
},
responseType: 'stream',
validateStatus: () => true, // Don't throw on any status
});
return {
ok: response.status >= 200 && response.status < 300,
status: response.status,
statusText: response.statusText,
body: response.data,
};
},
};
}
@@ -0,0 +1,280 @@
import { createAgent, HumanMessage } from 'langchain';
import nock from 'nock';
import { LangchainChatModelAdapter } from 'src';
import { OpenAIChatModel } from './openai';
import {
createMockHttpRequests,
createSSEStream,
mockFinalResponse,
mockStreamFinalResponseEvents,
mockStreamToolCallEvents,
mockToolCallResponse,
weatherTool,
} from './openai.fixtures';
describe('OpenAI Integration with Langchain Agent', () => {
const baseURL = 'https://api.openai.com/v1';
beforeEach(() => {
nock.cleanAll();
});
afterEach(() => {
nock.cleanAll();
});
it('should execute agent with tool calling through langchain adapter', async () => {
nock(baseURL)
.post('/responses', (body) => {
expect(body).toMatchObject({
model: 'gpt-4o',
input: 'What is the weather in tokyo?',
tools: [
{
type: 'function',
name: 'get_weather',
description: 'Get weather for a given city.',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
},
},
required: ['city'],
additionalProperties: false,
},
},
],
parallel_tool_calls: true,
store: false,
stream: false,
});
return true;
})
.reply(200, mockToolCallResponse);
nock(baseURL)
.post('/responses', (body) => {
expect(body).toMatchObject({
model: 'gpt-4o',
input: expect.arrayContaining([
{ role: 'user', content: 'What is the weather in tokyo?' },
{
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: '' }],
},
{
type: 'function_call',
call_id: 'call_YONsRdkCKu8Sh8WGkUiXqlYW',
name: 'get_weather',
arguments: '{"city":"Tokyo"}',
},
{
type: 'function_call_output',
call_id: 'call_YONsRdkCKu8Sh8WGkUiXqlYW',
output: "It's always sunny in Tokyo!",
},
]),
parallel_tool_calls: true,
store: false,
stream: false,
});
return true;
})
.reply(200, mockFinalResponse);
const openaiChatModel = new OpenAIChatModel('gpt-4o', createMockHttpRequests(), { baseURL });
const chatModel = new LangchainChatModelAdapter(openaiChatModel);
const agent = createAgent({
model: chatModel,
tools: [weatherTool],
});
const result = await agent.invoke({
messages: [new HumanMessage('What is the weather in tokyo?')],
});
expect(result).toBeDefined();
expect(result.messages).toHaveLength(4);
expect(result.messages[0]).toMatchObject({
content: 'What is the weather in tokyo?',
});
expect(result.messages[1]).toMatchObject({
id: 'resp_02a127c1e73b5fe4016989e989cb188195a27d4911084e4223',
tool_calls: [
{
type: 'tool_call',
id: 'call_YONsRdkCKu8Sh8WGkUiXqlYW',
name: 'get_weather',
args: {
city: 'Tokyo',
},
},
],
});
expect(result.messages[2]).toMatchObject({
content: "It's always sunny in Tokyo!",
name: 'get_weather',
tool_call_id: 'call_YONsRdkCKu8Sh8WGkUiXqlYW',
});
expect(result.messages[3]).toMatchObject({
id: 'resp_00a8729c01103919016989e98b13888190bca486b9676ce0cd',
content: [
{
type: 'text',
text: "It's always sunny in Tokyo!",
},
],
});
expect(nock.isDone()).toBe(true);
});
it('should execute agent with streaming through langchain adapter', async () => {
nock(baseURL)
.post('/responses', (body) => {
expect(body).toMatchObject({
model: 'gpt-4o',
input: 'What is the weather in tokyo?',
stream: true,
});
return true;
})
.reply(() => {
const stream = createSSEStream(mockStreamToolCallEvents);
return [
200,
stream,
{
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
];
});
nock(baseURL)
.post('/responses', (body) => {
expect(body).toMatchObject({
model: 'gpt-4o',
stream: true,
});
return true;
})
.reply(() => {
const stream = createSSEStream(mockStreamFinalResponseEvents);
return [
200,
stream,
{
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
];
});
const openaiChatModel = new OpenAIChatModel('gpt-4o', createMockHttpRequests(), { baseURL });
const chatModel = new LangchainChatModelAdapter(openaiChatModel);
const agent = createAgent({
model: chatModel,
tools: [weatherTool],
});
const chunks: unknown[] = [];
const stream = await agent.stream(
{ messages: [{ role: 'user', content: 'What is the weather in tokyo?' }] },
{ streamMode: 'messages' },
);
for await (const chunk of stream) {
chunks.push(chunk);
}
expect(chunks).toHaveLength(10);
const getChunkData = (chunk: unknown) => {
const chunkArray = chunk as unknown[];
return {
message: chunkArray[0] as Record<string, unknown>,
metadata: chunkArray[1] as Record<string, unknown>,
};
};
const { message: message1, metadata: metadata1 } = getChunkData(chunks[0]);
const toolCalls1 = message1.tool_calls as Array<Record<string, unknown>>;
expect(toolCalls1).toHaveLength(1);
expect(toolCalls1[0]).toMatchObject({
name: 'get_weather',
args: {
city: 'Tokyo',
},
id: 'call_StreamTest123',
type: 'tool_call',
});
expect(metadata1.langgraph_step).toBeDefined();
const { message: message2 } = getChunkData(chunks[1]);
expect(message2.usage_metadata).toEqual({
input_tokens: 46,
output_tokens: 15,
total_tokens: 61,
});
const responseMetadata2 = message2.response_metadata as Record<string, unknown>;
expect(responseMetadata2.finish_reason).toBe('stop');
const { message: message3 } = getChunkData(chunks[2]);
expect(message3.content).toBe("It's always sunny in Tokyo!");
expect(message3.tool_call_id).toBe('call_StreamTest123');
expect(message3.name).toBe('get_weather');
const { message: message4 } = getChunkData(chunks[3]);
const content4 = message4.content as Array<{ type: string; text: string }>;
expect(content4[0].text).toBe("It's");
const { message: message5 } = getChunkData(chunks[4]);
const content5 = message5.content as Array<{ type: string; text: string }>;
expect(content5[0].text).toBe(' always');
const { message: message6 } = getChunkData(chunks[5]);
const content6 = message6.content as Array<{ type: string; text: string }>;
expect(content6[0].text).toBe(' sunny');
const { message: message7 } = getChunkData(chunks[6]);
const content7 = message7.content as Array<{ type: string; text: string }>;
expect(content7[0].text).toBe(' in');
const { message: message8 } = getChunkData(chunks[7]);
const content8 = message8.content as Array<{ type: string; text: string }>;
expect(content8[0].text).toBe(' Tokyo');
const { message: message9 } = getChunkData(chunks[8]);
const content9 = message9.content as Array<{ type: string; text: string }>;
expect(content9[0].text).toBe('!');
const { message: message10 } = getChunkData(chunks[9]);
expect(message10.usage_metadata).toEqual({
input_tokens: 76,
output_tokens: 8,
total_tokens: 84,
});
const responseMetadata10 = message10.response_metadata as Record<string, unknown>;
expect(responseMetadata10.finish_reason).toBe('stop');
for (const chunk of chunks) {
const { metadata } = getChunkData(chunk);
expect(metadata).toBeDefined();
expect(metadata.langgraph_step).toBeDefined();
}
expect(nock.isDone()).toBe(true);
});
});
@@ -0,0 +1,536 @@
import type { JSONSchema7 } from 'json-schema';
import type { IHttpRequestMethods } from 'n8n-workflow';
import {
BaseChatModel,
getParametersJsonSchema,
parseSSEStream,
type TokenUsage,
type Tool,
type ToolCall,
type ChatModelConfig,
type GenerateResult,
type Message,
type MessageContent,
type ProviderTool,
type StreamChunk,
} from 'src';
// Types
type OpenAITool =
| {
type: 'function';
name: string;
description?: string;
parameters: JSONSchema7;
strict?: boolean;
}
| {
type: 'web_search';
};
type OpenAIToolChoice = 'auto' | 'required' | 'none' | { type: 'function'; name: string };
type ResponsesInputItem =
| { role: 'user'; content: string }
| { role: 'user'; content: Array<{ type: 'input_text'; text: string }> }
| {
type: 'message';
role: 'assistant';
content: Array<{ type: 'output_text'; text: string }>;
}
| {
type: 'function_call';
call_id: string;
name: string;
arguments: string;
}
| { type: 'function_call_output'; call_id: string; output: string };
interface OpenAIResponsesRequest {
model: string;
input: string | ResponsesInputItem[];
instructions?: string;
max_output_tokens?: number;
temperature?: number;
top_p?: number;
tools?: OpenAITool[];
tool_choice?: OpenAIToolChoice;
parallel_tool_calls?: boolean;
store?: boolean;
stream?: boolean;
metadata?: Record<string, unknown>;
}
interface OpenAIResponsesResponse {
id: string;
object: string;
created_at: string;
model: string;
output: ResponsesOutputItem[];
status: string;
usage?: {
input_tokens: number;
output_tokens: number;
total_tokens: number;
input_tokens_details?: {
cached_tokens?: number;
};
output_tokens_details?: {
reasoning_tokens?: number;
};
};
incomplete_details?: Record<string, unknown>;
metadata?: Record<string, unknown>;
user?: string;
service_tier?: string;
}
type ResponsesOutputItem =
| {
type: 'message';
role: 'assistant';
id?: string;
content: Array<{
type: 'output_text';
text: string;
}>;
}
| {
type: 'function_call';
id?: string;
call_id: string;
name: string;
arguments: string;
}
| {
type: 'reasoning';
id?: string;
summary: Array<{
type: string;
text: string;
}>;
};
interface OpenAIStreamEvent {
type: string;
delta?: string;
output_index?: number;
item?: Record<string, unknown>;
response?: Record<string, unknown>;
}
// Helpers
async function* parseOpenAIStreamEvents(
body: AsyncIterableIterator<Buffer | Uint8Array>,
): AsyncIterable<OpenAIStreamEvent> {
for await (const message of parseSSEStream(body)) {
if (!message.data) continue;
if (message.data === '[DONE]') continue;
try {
const event = JSON.parse(message.data);
yield event as OpenAIStreamEvent;
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn('Failed to parse OpenAI SSE event:', message.data);
}
}
}
}
function genericMessagesToResponsesInput(messages: Message[]): {
instructions?: string;
input: string | ResponsesInputItem[];
} {
const instructionsParts: string[] = [];
const inputItems: ResponsesInputItem[] = [];
for (const msg of messages) {
if (msg.role === 'system') {
for (const contentPart of msg.content) {
if (contentPart.type === 'text') {
instructionsParts.push(contentPart.text);
}
}
}
if (msg.role === 'user') {
for (const contentPart of msg.content) {
if (contentPart.type === 'text') {
inputItems.push({
role: 'user',
content: contentPart.text,
});
}
}
continue;
}
if (msg.role === 'assistant') {
for (const contentPart of msg.content) {
if (contentPart.type === 'text') {
inputItems.push({
type: 'message',
role: 'assistant',
content: [
{
type: 'output_text',
text: contentPart.text,
},
],
});
} else if (contentPart.type === 'tool-call') {
if (!contentPart.toolCallId) {
throw new Error('Tool call ID is required');
}
inputItems.push({
type: 'function_call',
call_id: contentPart.toolCallId,
name: contentPart.toolName,
arguments: contentPart.input,
});
} else if (contentPart.type === 'reasoning') {
inputItems.push({
type: 'message',
role: 'assistant',
content: [
{
type: 'output_text',
text: contentPart.text,
},
],
});
}
}
}
if (msg.role === 'tool') {
for (const contentPart of msg.content) {
if (contentPart.type === 'tool-result') {
const output =
typeof contentPart.result === 'string'
? contentPart.result
: JSON.stringify(contentPart.result);
inputItems.push({
type: 'function_call_output',
call_id: contentPart.toolCallId,
output,
});
}
}
}
}
const instructions = instructionsParts.length > 0 ? instructionsParts.join('\n\n') : undefined;
const single = inputItems[0];
if (
inputItems.length === 1 &&
single &&
'role' in single &&
single.role === 'user' &&
typeof single.content === 'string'
) {
return { instructions, input: single.content };
}
return { instructions, input: inputItems };
}
function genericToolToResponsesTool(tool: Tool): OpenAITool {
if (tool.type === 'provider') {
if (tool.name === 'web_search') {
return {
type: 'web_search',
...tool.args,
};
}
throw new Error(`Unsupported provider tool: ${tool.name}`);
}
const parameters = getParametersJsonSchema(tool);
return {
type: 'function',
name: tool.name,
description: tool.description,
parameters,
strict: tool.strict,
};
}
function parseResponsesOutput(output: ResponsesOutputItem[]): {
text: string;
toolCalls: ToolCall[];
} {
let text = '';
const toolCalls: ToolCall[] = [];
for (const item of output) {
if (item.type === 'message' && item.role === 'assistant') {
for (const block of item.content) {
if (block.type === 'output_text') {
text += block.text;
}
}
}
if (item.type === 'function_call') {
try {
toolCalls.push({
id: item.call_id,
name: item.name,
arguments: JSON.parse(item.arguments) as Record<string, unknown>,
argumentsRaw: item.arguments,
});
} catch (e) {
throw new Error(`Failed to parse function call arguments: ${item.arguments}`);
}
}
}
return { text, toolCalls };
}
function parseTokenUsage(
usage: OpenAIResponsesResponse['usage'] | undefined,
): TokenUsage | undefined {
return usage
? {
promptTokens: usage.input_tokens ?? 0,
completionTokens: usage.output_tokens ?? 0,
totalTokens: usage.total_tokens ?? 0,
inputTokenDetails: {
...(!!usage.input_tokens_details?.cached_tokens && {
cacheRead: usage.input_tokens_details.cached_tokens,
}),
},
outputTokenDetails: {
...(!!usage.output_tokens_details?.reasoning_tokens && {
reasoning: usage.output_tokens_details.reasoning_tokens,
}),
},
}
: undefined;
}
interface OpenAIChatModelConfig extends ChatModelConfig {
apiKey?: string;
baseURL?: string;
providerTools?: ProviderTool[];
}
interface RequestConfig {
httpRequest: (
method: IHttpRequestMethods,
url: string,
body?: object,
headers?: Record<string, string>,
) => Promise<{ body: unknown }>;
openStream: (
method: IHttpRequestMethods,
url: string,
body?: object,
headers?: Record<string, string>,
) => Promise<{ body: AsyncIterableIterator<Buffer | Uint8Array> }>;
}
export class OpenAIChatModel extends BaseChatModel<OpenAIChatModelConfig> {
private baseURL: string;
constructor(
modelId: string = 'gpt-4o',
private requests: RequestConfig,
config?: OpenAIChatModelConfig,
) {
super('openai', modelId, config);
this.baseURL = config?.baseURL ?? 'https://api.openai.com/v1';
}
private getTools(config?: OpenAIChatModelConfig) {
const ownTools = this.tools;
const providerTools = config?.providerTools ?? this.defaultConfig?.providerTools ?? [];
return [...ownTools, ...providerTools].map(genericToolToResponsesTool);
}
async generate(messages: Message[], config?: OpenAIChatModelConfig): Promise<GenerateResult> {
const merged = this.mergeConfig(config);
const { instructions, input } = genericMessagesToResponsesInput(messages);
const tools = this.getTools(config);
const requestBody: OpenAIResponsesRequest = {
model: this.modelId,
input,
instructions,
max_output_tokens: merged.maxTokens,
temperature: merged.temperature,
top_p: merged.topP,
tools,
parallel_tool_calls: true,
store: false,
stream: false,
};
const response = await this.requests.httpRequest(
'POST',
`${this.baseURL}/responses`,
requestBody,
);
const body = response.body as OpenAIResponsesResponse;
const { text, toolCalls } = parseResponsesOutput(body.output);
const usage = parseTokenUsage(body.usage);
const responseMetadata: Record<string, unknown> = {
model_provider: 'openai',
model: body.model,
created_at: body.created_at,
id: body.id,
incomplete_details: body.incomplete_details,
metadata: body.metadata,
object: body.object,
status: body.status,
user: body.user,
service_tier: body.service_tier,
model_name: body.model,
output: body.output,
};
for (const item of body.output as unknown[]) {
const o = item as Record<string, unknown>;
if (o.type === 'reasoning') {
responseMetadata.reasoning = o;
}
}
const content: MessageContent[] = [];
if (toolCalls.length) {
for (const toolCall of toolCalls) {
content.push({
type: 'tool-call',
toolCallId: toolCall.id,
toolName: toolCall.name,
input: JSON.stringify(toolCall.arguments),
});
}
}
content.push({ type: 'text', text });
const message: Message = {
role: 'assistant',
content,
id: body.id,
};
return {
id: body.id,
finishReason: body.status === 'completed' ? 'stop' : 'other',
usage,
message,
rawResponse: body,
providerMetadata: responseMetadata,
};
}
async *stream(messages: Message[], config?: OpenAIChatModelConfig): AsyncIterable<StreamChunk> {
const merged = this.mergeConfig(config) as OpenAIChatModelConfig;
const { instructions, input } = genericMessagesToResponsesInput(messages);
const tools = this.getTools(config);
const requestBody: OpenAIResponsesRequest = {
model: this.modelId,
input,
instructions,
max_output_tokens: merged.maxTokens,
temperature: merged.temperature,
top_p: merged.topP,
tools,
parallel_tool_calls: true,
store: false,
stream: true,
};
const streamResponse = await this.requests.openStream(
'POST',
`${this.baseURL}/responses`,
requestBody,
);
const streamBody = streamResponse.body;
const toolCallBuffers: Record<number, { name: string; arguments: string }> = {};
for await (const event of parseOpenAIStreamEvents(streamBody)) {
const type = event.type;
if (type === 'response.output_text.delta') {
const delta = event.delta;
if (delta) {
yield { type: 'text-delta', delta };
}
}
if (type === 'response.output_item.added') {
const item = event.item;
if (item?.type === 'function_call') {
const idx = event.output_index ?? 0;
toolCallBuffers[idx] = {
name: (item.name as string) ?? '',
arguments: (item.arguments as string) ?? '',
};
}
if (item?.type === 'reasoning') {
const summary = (item.summary as Array<Record<string, unknown>>) ?? [];
const reasoningText = summary
.map((s) => s.text)
.filter(Boolean)
.join('');
if (reasoningText) {
yield { type: 'reasoning-delta', delta: reasoningText };
}
}
}
if (type === 'response.reasoning_summary_text.delta') {
const delta = event.delta;
if (delta) {
yield { type: 'reasoning-delta', delta };
}
}
if (type === 'response.function_call_arguments.delta') {
const idx = event.output_index ?? 0;
const delta = event.delta;
if (toolCallBuffers[idx] && delta) {
toolCallBuffers[idx].arguments += delta;
}
}
if (type === 'response.output_item.done') {
const item = event.item;
if (item?.type === 'function_call') {
const idx = event.output_index ?? 0;
const buf = toolCallBuffers[idx];
if (buf) {
yield {
type: 'tool-call-delta',
id: (item.call_id as string) ?? (item.id as string),
name: buf.name,
argumentsDelta: buf.arguments,
};
}
}
}
if (type === 'response.done' || type === 'response.completed') {
const responseData =
(event.response as unknown as OpenAIResponsesResponse) ??
(event as unknown as OpenAIResponsesResponse);
yield {
type: 'finish',
finishReason: 'stop',
usage: parseTokenUsage(responseData.usage),
};
}
}
}
}