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