Files
hermes-web-ui/tests/server/agent-runner-adapters.test.ts
2026-06-20 11:04:03 +08:00

676 lines
26 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
anthropicMessageToResponses,
openAiChatToResponses,
responsesToAnthropicMessages,
responsesToOpenAiChat,
} from '../../packages/server/src/services/agent-runner/adapters/responses'
import {
anthropicToOpenAiChat,
anthropicToOpenAiResponses,
openAiResponsesToAnthropicMessage,
openAiToAnthropicMessage,
} from '../../packages/server/src/services/agent-runner/adapters/anthropic'
import {
openAiChatSseToAnthropicEvents,
openAiResponsesSseToAnthropicEvents,
type AnthropicStreamEvent,
} from '../../packages/server/src/services/agent-runner/adapters/anthropic-stream'
import {
anthropicMessagesSseToResponsesEvents,
openAiChatSseToResponsesEvents,
openAiResponsesSseToResponsesEvents,
type CanonicalResponsesEvent,
} from '../../packages/server/src/services/agent-runner/adapters/responses-stream'
const target = { model: 'test-model' }
const codexTarget = { model: 'test-model', annotateMcpToolNamespaces: true }
const anthropicTarget = { provider: 'deepseek', model: 'deepseek-reasoner', baseUrl: 'https://api.deepseek.com/v1' }
describe('agent runner Responses adapters', () => {
it('converts Responses input to OpenAI Chat messages and tools', () => {
const body = {
instructions: 'be terse',
max_output_tokens: 16,
temperature: 0.2,
top_p: 0.9,
input: [
{ role: 'user', content: [{ type: 'input_text', text: 'hello' }] },
{ role: 'developer', content: [{ type: 'input_text', text: 'rules' }] },
{ type: 'function_call', call_id: 'call_1', name: 'search', arguments: '{"q":"x"}' },
{ type: 'function_call_output', call_id: 'call_1', output: 'found' },
],
tools: [{ type: 'function', name: 'search', description: 'Search', parameters: { type: 'object' } }],
}
expect(responsesToOpenAiChat(body, target)).toMatchObject({
model: 'test-model',
max_tokens: 16,
temperature: 0.2,
top_p: 0.9,
stream: false,
messages: [
{ role: 'system', content: 'be terse' },
{ role: 'user', content: 'hello' },
{ role: 'system', content: 'rules' },
{
role: 'assistant',
content: null,
tool_calls: [{
id: 'call_1',
type: 'function',
function: { name: 'search', arguments: '{"q":"x"}' },
}],
},
{ role: 'tool', tool_call_id: 'call_1', content: 'found' },
],
tools: [{
type: 'function',
function: { name: 'search', description: 'Search', parameters: { type: 'object' } },
}],
})
})
it('groups parallel Responses function calls before Chat tool results', () => {
const body = {
input: [
{ role: 'user', content: [{ type: 'input_text', text: 'check repo' }] },
{ type: 'function_call', call_id: 'call_1', name: 'read_file', arguments: '{"path":"a.ts"}' },
{ type: 'function_call', call_id: 'call_2', name: 'search', arguments: '{"q":"todo"}' },
{ type: 'function_call_output', call_id: 'call_2', output: 'matches' },
{ type: 'function_call_output', call_id: 'call_1', output: 'file text' },
{ role: 'user', content: [{ type: 'input_text', text: 'continue' }] },
],
}
expect(responsesToOpenAiChat(body, target).messages).toEqual([
{ role: 'user', content: 'check repo' },
{
role: 'assistant',
content: null,
tool_calls: [
{
id: 'call_1',
type: 'function',
function: { name: 'read_file', arguments: '{"path":"a.ts"}' },
},
{
id: 'call_2',
type: 'function',
function: { name: 'search', arguments: '{"q":"todo"}' },
},
],
},
{ role: 'tool', tool_call_id: 'call_1', content: 'file text' },
{ role: 'tool', tool_call_id: 'call_2', content: 'matches' },
{ role: 'user', content: 'continue' },
])
})
it('drops incomplete Responses function call history for Chat providers', () => {
const body = {
input: [
{ role: 'user', content: [{ type: 'input_text', text: 'hello' }] },
{ type: 'function_call', call_id: 'call_missing', name: 'search', arguments: '{"q":"x"}' },
{ role: 'user', content: [{ type: 'input_text', text: 'next turn' }] },
{ type: 'function_call_output', call_id: 'orphan_call', output: 'orphan' },
],
}
expect(responsesToOpenAiChat(body, target).messages).toEqual([
{ role: 'user', content: 'hello' },
{ role: 'user', content: 'next turn' },
])
})
it('converts Responses input to Anthropic messages', () => {
const body = {
instructions: 'system text',
input: [
{ role: 'user', content: [{ type: 'input_text', text: 'hello' }] },
{ type: 'function_call', call_id: 'call_1', name: 'lookup', arguments: '{"id":1}' },
{ type: 'function_call_output', call_id: 'call_1', output: [{ text: 'ok' }] },
],
tools: [{ type: 'function', name: 'lookup', description: 'Lookup', parameters: { type: 'object' } }],
}
expect(responsesToAnthropicMessages(body, target, true)).toMatchObject({
model: 'test-model',
system: 'system text',
max_tokens: 4096,
stream: true,
messages: [
{ role: 'user', content: [{ type: 'text', text: 'hello' }] },
{ role: 'assistant', content: [{ type: 'tool_use', id: 'call_1', name: 'lookup', input: { id: 1 } }] },
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_1', content: 'ok' }] },
],
tools: [{ name: 'lookup', description: 'Lookup', input_schema: { type: 'object' } }],
})
})
it('expands Hermes MCP namespace tools for Chat and Anthropic providers', () => {
const body = {
input: [{ role: 'user', content: [{ type: 'input_text', text: 'list devices' }] }],
tools: [{ type: 'namespace', name: 'mcp__hermes_studio', description: 'Hermes tools' }],
}
expect(responsesToOpenAiChat(body, target).tools).toEqual(expect.arrayContaining([
expect.objectContaining({
type: 'function',
function: expect.objectContaining({
name: 'hermes_studio_lan_devices_scan',
parameters: expect.objectContaining({
properties: expect.objectContaining({
profile: expect.any(Object),
token: expect.any(Object),
}),
}),
}),
}),
]))
expect(responsesToAnthropicMessages(body, target).tools).toEqual(expect.arrayContaining([
expect.objectContaining({
name: 'hermes_studio_lan_devices_scan',
input_schema: expect.objectContaining({
properties: expect.objectContaining({
profile: expect.any(Object),
token: expect.any(Object),
}),
}),
}),
]))
})
it('keeps unknown MCP namespaces callable through a generic function fallback', () => {
const body = {
input: [{ role: 'user', content: [{ type: 'input_text', text: 'call custom mcp' }] }],
tools: [{ type: 'namespace', name: 'mcp__custom_server', description: 'Custom server tools' }],
}
expect(responsesToOpenAiChat(body, target).tools).toEqual(expect.arrayContaining([
expect.objectContaining({
type: 'function',
function: expect.objectContaining({
name: 'mcp__custom_server',
parameters: expect.objectContaining({
required: ['tool', 'arguments'],
}),
}),
}),
]))
})
it('converts OpenAI Chat responses to Responses output', () => {
expect(openAiChatToResponses({
id: 'chatcmpl_1',
created: 123,
choices: [{
message: {
reasoning_content: 'think',
content: 'hi',
tool_calls: [{
id: 'call_1',
function: { name: 'lookup', arguments: '{"id":1}' },
}],
},
}],
usage: { prompt_tokens: 2, completion_tokens: 3, total_tokens: 5 },
}, target)).toMatchObject({
id: 'chatcmpl_1',
object: 'response',
created_at: 123,
model: 'test-model',
output: [
{ type: 'reasoning', summary: [{ type: 'summary_text', text: 'think' }] },
{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'hi', annotations: [] }] },
{ type: 'function_call', call_id: 'call_1', name: 'lookup', arguments: '{"id":1}' },
],
usage: { input_tokens: 2, output_tokens: 3, total_tokens: 5 },
})
})
it('marks expanded Hermes MCP Chat tool calls with their Responses namespace', () => {
expect(openAiChatToResponses({
id: 'chatcmpl_1',
created: 123,
choices: [{
message: {
tool_calls: [{
id: 'call_1',
function: { name: 'hermes_studio_lan_devices_scan', arguments: '{"profile":"default"}' },
}],
},
}],
}, target)).toMatchObject({
output: [{
type: 'function_call',
call_id: 'call_1',
name: 'hermes_studio_lan_devices_scan',
namespace: 'mcp__hermes_studio',
}],
})
})
it('normalizes generic MCP namespace function calls back to Responses MCP calls', () => {
expect(openAiChatToResponses({
id: 'chatcmpl_1',
created: 123,
choices: [{
message: {
tool_calls: [{
id: 'call_1',
function: {
name: 'mcp__custom_server',
arguments: '{"tool":"custom_lookup","arguments":{"id":1}}',
},
}],
},
}],
}, target)).toMatchObject({
output: [{
type: 'function_call',
call_id: 'call_1',
name: 'custom_lookup',
arguments: '{"id":1}',
namespace: 'mcp__custom_server',
}],
})
})
it('converts Anthropic messages to Responses output', () => {
expect(anthropicMessageToResponses({
id: 'msg_1',
content: [
{ type: 'thinking', thinking: 'anthropic think' },
{ type: 'text', text: 'hi' },
{ type: 'tool_use', id: 'toolu_1', name: 'lookup', input: { id: 1 } },
],
usage: { input_tokens: 4, output_tokens: 5 },
}, target)).toMatchObject({
id: 'msg_1',
object: 'response',
model: 'test-model',
output: [
{ type: 'reasoning', summary: [{ type: 'summary_text', text: 'anthropic think' }] },
{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'hi', annotations: [] }] },
{ type: 'function_call', call_id: 'toolu_1', name: 'lookup', arguments: '{"id":1}' },
],
usage: { input_tokens: 4, output_tokens: 5, total_tokens: 9 },
})
})
it('marks expanded Hermes MCP Anthropic tool calls with their Responses namespace', () => {
expect(anthropicMessageToResponses({
id: 'msg_1',
content: [
{ type: 'tool_use', id: 'toolu_1', name: 'hermes_studio_lan_devices_list', input: { profile: 'default' } },
],
usage: { input_tokens: 1, output_tokens: 1 },
}, target)).toMatchObject({
output: [{
type: 'function_call',
call_id: 'toolu_1',
name: 'hermes_studio_lan_devices_list',
namespace: 'mcp__hermes_studio',
}],
})
})
})
async function* encodedChunks(chunks: string[]): AsyncGenerator<Uint8Array> {
const encoder = new TextEncoder()
for (const chunk of chunks) yield encoder.encode(chunk)
}
async function collectEvents(events: AsyncIterable<CanonicalResponsesEvent>): Promise<CanonicalResponsesEvent[]> {
const collected: CanonicalResponsesEvent[] = []
for await (const event of events) collected.push(event)
return collected
}
async function collectAnthropicEvents(events: AsyncIterable<AnthropicStreamEvent>): Promise<AnthropicStreamEvent[]> {
const collected: AnthropicStreamEvent[] = []
for await (const event of events) collected.push(event)
return collected
}
describe('agent runner Responses stream adapters', () => {
it('normalizes OpenAI Chat SSE text and tool calls to Responses events', async () => {
const events = await collectEvents(openAiChatSseToResponsesEvents(encodedChunks([
'data: {"choices":[{"delta":{"reasoning_content":"think"}}]}\n\n',
'data: {"choices":[{"delta":{"content":"he"}}]}\n\n',
'data: {"choices":[{"delta":{"content":"llo"}}]}\r\n\r\n',
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"lookup","arguments":"{\\"id\\":"}}]}}]}\n\n',
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"1}"}}]}}]}\n\n',
'data: [DONE]\n\n',
]), codexTarget))
expect(events.map(event => event.type)).toEqual([
'response.created',
'response.reasoning.delta',
'response.output_item.added',
'response.content_part.added',
'response.output_text.delta',
'response.output_text.delta',
'response.output_item.added',
'response.function_call_arguments.delta',
'response.function_call_arguments.delta',
'response.output_text.done',
'response.content_part.done',
'response.output_item.done',
'response.output_item.done',
'response.completed',
])
expect(events[1].data).toMatchObject({ delta: 'think' })
expect(events[4].data).toMatchObject({ delta: 'he' })
expect(events[5].data).toMatchObject({ delta: 'llo' })
expect(events[6].data).toMatchObject({
item: { type: 'function_call', call_id: 'call_1', name: 'lookup' },
})
expect(events[13].data).toMatchObject({
response: {
model: 'test-model',
status: 'completed',
output: [
{ type: 'reasoning', summary: [{ type: 'summary_text', text: 'think' }] },
{ type: 'message', content: [{ type: 'output_text', text: 'hello' }] },
{ type: 'function_call', call_id: 'call_1', name: 'lookup', arguments: '{"id":1}' },
],
},
})
})
it('marks expanded Hermes MCP Chat SSE tool calls with their Responses namespace', async () => {
const events = await collectEvents(openAiChatSseToResponsesEvents(encodedChunks([
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"hermes_studio_lan_devices_scan","arguments":"{}"}}]}}]}\n\n',
'data: [DONE]\n\n',
]), codexTarget))
expect(events).toEqual(expect.arrayContaining([
expect.objectContaining({
type: 'response.output_item.done',
data: expect.objectContaining({
item: expect.objectContaining({
type: 'function_call',
call_id: 'call_1',
name: 'hermes_studio_lan_devices_scan',
namespace: 'mcp__hermes_studio',
}),
}),
}),
]))
})
it('normalizes Anthropic Messages SSE text and tool calls to Responses events', async () => {
const events = await collectEvents(anthropicMessagesSseToResponsesEvents(encodedChunks([
'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_1"}}\n\n',
'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"think"}}\n\n',
'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}\n\n',
'event: content_block_start\ndata: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_1","name":"lookup","input":{}}}\r\n\r\n',
'event: content_block_delta\ndata: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\\"id\\":1}"}}\n\n',
]), codexTarget))
expect(events.map(event => event.type)).toEqual([
'response.created',
'response.reasoning.delta',
'response.output_item.added',
'response.content_part.added',
'response.output_text.delta',
'response.output_item.added',
'response.function_call_arguments.delta',
'response.output_text.done',
'response.content_part.done',
'response.output_item.done',
'response.output_item.done',
'response.completed',
])
expect(events[1].data).toMatchObject({ delta: 'think' })
expect(events[2].data).toMatchObject({ item: { id: 'msg_msg_1' } })
expect(events[5].data).toMatchObject({
item: { type: 'function_call', call_id: 'toolu_1', name: 'lookup' },
})
expect(events[11].data).toMatchObject({
response: {
id: 'msg_1',
output: [
{ type: 'reasoning', summary: [{ type: 'summary_text', text: 'think' }] },
{ type: 'message', content: [{ type: 'output_text', text: 'hi' }] },
{ type: 'function_call', call_id: 'toolu_1', name: 'lookup', arguments: '{"id":1}' },
],
},
})
})
it('marks expanded Hermes MCP Anthropic SSE tool calls with their Responses namespace', async () => {
const events = await collectEvents(anthropicMessagesSseToResponsesEvents(encodedChunks([
'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_1"}}\n\n',
'event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_1","name":"hermes_studio_lan_devices_list","input":{}}}\n\n',
'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\\"profile\\":\\"default\\"}"}}\n\n',
'event: message_stop\ndata: {"type":"message_stop"}\n\n',
]), codexTarget))
expect(events).toEqual(expect.arrayContaining([
expect.objectContaining({
type: 'response.output_item.done',
data: expect.objectContaining({
item: expect.objectContaining({
type: 'function_call',
call_id: 'toolu_1',
name: 'hermes_studio_lan_devices_list',
namespace: 'mcp__hermes_studio',
}),
}),
}),
]))
})
it('passes native Responses SSE events through as canonical events', async () => {
const events = await collectEvents(openAiResponsesSseToResponsesEvents(encodedChunks([
'event: response.created\r\ndata: {"response":{"id":"resp_1"}}\r\n\r\n',
'data: {"type":"response.output_text.delta","delta":"hi"}\n\n',
'data: [DONE]\n\n',
])))
expect(events).toEqual([
{
type: 'response.created',
data: { type: 'response.created', response: { id: 'resp_1' } },
},
{
type: 'response.output_text.delta',
data: { type: 'response.output_text.delta', delta: 'hi' },
},
])
})
})
describe('agent runner Anthropic adapters', () => {
it('converts Anthropic messages to OpenAI Chat with reasoning_content', () => {
const body = {
system: 'system text',
max_tokens: 32,
temperature: 0.1,
messages: [
{ role: 'user', content: [{ type: 'text', text: 'hello' }] },
{
role: 'assistant',
content: [
{ type: 'thinking', thinking: 'need tool' },
{ type: 'tool_use', id: 'toolu_1', name: 'lookup', input: { id: 1 } },
],
},
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'ok' }] },
],
tools: [{ name: 'lookup', description: 'Lookup', input_schema: { type: 'object' } }],
}
expect(anthropicToOpenAiChat(body, anthropicTarget)).toMatchObject({
model: 'deepseek-reasoner',
max_tokens: 32,
temperature: 0.1,
stream: false,
messages: [
{ role: 'system', content: 'system text' },
{ role: 'user', content: 'hello' },
{
role: 'assistant',
content: null,
reasoning_content: 'need tool',
tool_calls: [{
id: 'toolu_1',
type: 'function',
function: { name: 'lookup', arguments: '{"id":1}' },
}],
},
{ role: 'tool', tool_call_id: 'toolu_1', content: 'ok' },
],
tools: [{
type: 'function',
function: { name: 'lookup', description: 'Lookup', parameters: { type: 'object' } },
}],
})
})
it('converts Anthropic messages to Responses input', () => {
expect(anthropicToOpenAiResponses({
system: 'system text',
max_tokens: 64,
messages: [
{ role: 'user', content: 'hello' },
{ role: 'assistant', content: [{ type: 'tool_use', id: 'toolu_1', name: 'lookup', input: { id: 1 } }] },
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'ok' }] },
],
tools: [{ name: 'lookup', input_schema: { type: 'object' } }],
}, anthropicTarget, true)).toMatchObject({
model: 'deepseek-reasoner',
instructions: 'system text',
max_output_tokens: 64,
stream: true,
store: false,
input: [
{ role: 'user', content: 'hello' },
{ type: 'function_call', call_id: 'toolu_1', name: 'lookup', arguments: '{"id":1}' },
{ type: 'function_call_output', call_id: 'toolu_1', output: 'ok' },
],
tools: [{ type: 'function', name: 'lookup', parameters: { type: 'object' } }],
})
})
it('converts OpenAI Chat responses to Anthropic messages', () => {
expect(openAiToAnthropicMessage({
id: 'chatcmpl_1',
choices: [{
finish_reason: 'tool_calls',
message: {
reasoning_content: 'thinking',
content: 'hi',
tool_calls: [{ id: 'call_1', function: { name: 'lookup', arguments: '{"id":1}' } }],
},
}],
usage: { prompt_tokens: 3, completion_tokens: 4 },
}, anthropicTarget)).toMatchObject({
id: 'chatcmpl_1',
type: 'message',
role: 'assistant',
model: 'deepseek-reasoner',
content: [
{ type: 'thinking', thinking: 'thinking' },
{ type: 'text', text: 'hi' },
{ type: 'tool_use', id: 'call_1', name: 'lookup', input: { id: 1 } },
],
stop_reason: 'tool_use',
usage: { input_tokens: 3, output_tokens: 4 },
})
})
it('converts Responses output to Anthropic messages', () => {
expect(openAiResponsesToAnthropicMessage({
id: 'resp_1',
status: 'completed',
output: [
{ type: 'message', content: [{ type: 'output_text', text: 'hi' }] },
{ type: 'function_call', call_id: 'call_1', name: 'lookup', arguments: '{"id":1}' },
],
usage: { input_tokens: 5, output_tokens: 6 },
}, anthropicTarget)).toMatchObject({
id: 'resp_1',
type: 'message',
role: 'assistant',
model: 'deepseek-reasoner',
content: [
{ type: 'text', text: 'hi' },
{ type: 'tool_use', id: 'call_1', name: 'lookup', input: { id: 1 } },
],
stop_reason: 'tool_use',
usage: { input_tokens: 5, output_tokens: 6 },
})
})
})
describe('agent runner Anthropic stream adapters', () => {
it('normalizes OpenAI Chat SSE to Anthropic Messages events', async () => {
const events = await collectAnthropicEvents(openAiChatSseToAnthropicEvents(encodedChunks([
'data: {"choices":[{"delta":{"reasoning_content":"think"}}]}\n\n',
'data: {"choices":[{"delta":{"content":"hi"}}]}\n\n',
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"lookup","arguments":"{\\"id\\":"}}]}}]}\r\n\r\n',
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"1}"}}]},"finish_reason":"tool_calls"}],"usage":{"completion_tokens":7}}\n\n',
]), anthropicTarget))
expect(events.map(event => event.type)).toEqual([
'message_start',
'content_block_start',
'content_block_delta',
'content_block_stop',
'content_block_start',
'content_block_delta',
'content_block_stop',
'content_block_start',
'content_block_delta',
'content_block_delta',
'content_block_stop',
'message_delta',
'message_stop',
])
expect(events[1].data).toMatchObject({ content_block: { type: 'thinking' } })
expect(events[2].data).toMatchObject({ delta: { type: 'thinking_delta', thinking: 'think' } })
expect(events[5].data).toMatchObject({ delta: { type: 'text_delta', text: 'hi' } })
expect(events[7].data).toMatchObject({ content_block: { type: 'tool_use', id: 'call_1', name: 'lookup' } })
expect(events[11].data).toMatchObject({
delta: { stop_reason: 'tool_use', stop_sequence: null },
usage: { output_tokens: 7 },
})
})
it('normalizes Responses SSE to Anthropic Messages events', async () => {
const events = await collectAnthropicEvents(openAiResponsesSseToAnthropicEvents(encodedChunks([
'data: {"type":"response.created","response":{"id":"resp_1"}}\n\n',
'data: {"type":"response.output_text.delta","delta":"hi"}\n\n',
'data: {"type":"response.output_text.done"}\n\n',
'data: {"type":"response.output_item.added","output_index":1,"item":{"type":"function_call","call_id":"call_1","name":"lookup"}}\n\n',
'data: {"type":"response.function_call_arguments.delta","item_id":"call_1","delta":"{\\"id\\":1}"}\n\n',
'data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_1","name":"lookup","arguments":"{\\"id\\":1}"}}\n\n',
'data: {"type":"response.completed","response":{"status":"completed","usage":{"output_tokens":3}}}\n\n',
]), anthropicTarget))
expect(events.map(event => event.type)).toEqual([
'message_start',
'content_block_start',
'content_block_delta',
'content_block_stop',
'content_block_start',
'content_block_delta',
'content_block_stop',
'message_delta',
'message_stop',
])
expect(events[2].data).toMatchObject({ delta: { type: 'text_delta', text: 'hi' } })
expect(events[4].data).toMatchObject({ content_block: { type: 'tool_use', id: 'call_1', name: 'lookup' } })
expect(events[5].data).toMatchObject({ delta: { type: 'input_json_delta', partial_json: '{"id":1}' } })
expect(events[7].data).toMatchObject({
delta: { stop_reason: 'tool_use', stop_sequence: null },
usage: { output_tokens: 3 },
})
})
})