diff --git a/src/main/agent/model-runtime.test.ts b/src/main/agent/model-runtime.test.ts index 9d3a2d5..61f6c14 100644 --- a/src/main/agent/model-runtime.test.ts +++ b/src/main/agent/model-runtime.test.ts @@ -119,6 +119,19 @@ function createResponsesEventStream( ].join('\n') } +function createSseEventStream( + events: ReadonlyArray> +): string { + return [ + ...events.flatMap((event) => [ + `event: ${event.type as string}`, + `data: ${JSON.stringify(event)}`, + '' + ]), + '' + ].join('\n') +} + function createToolProvider( overrides: Partial = {} ): ModelToolProviderLike { @@ -1808,6 +1821,227 @@ describe('ModelAgentRuntime', () => { expect(events.at(-1)).toMatchObject({ type: 'done' }) }) + it('streams OpenAI Responses text and reasoning through tool rounds', async () => { + const streams = [ + createSseEventStream([ + { + type: 'response.reasoning_summary_text.delta', + delta: '先分析。' + }, + { + type: 'response.output_text.delta', + delta: '准备读取。' + }, + { + type: 'response.completed', + response: { + id: 'resp-stream-tool-1', + model: 'gpt-5', + status: 'completed', + output: [ + { + id: 'reasoning-stream-1', + type: 'reasoning', + summary: [ + { type: 'summary_text', text: '先分析。' } + ] + }, + { + id: 'message-stream-1', + type: 'message', + role: 'assistant', + status: 'completed', + content: [ + { type: 'output_text', text: '准备读取。' } + ] + }, + { + id: 'function-stream-1', + type: 'function_call', + call_id: 'call-stream-1', + name: 'workspace_read_text', + arguments: '{"path":"README.md"}' + } + ], + usage: { input_tokens: 12, output_tokens: 4 } + } + } + ]), + createSseEventStream([ + { + type: 'response.output_text.delta', + delta: '读取完成。' + }, + { + type: 'response.completed', + response: { + id: 'resp-stream-tool-2', + model: 'gpt-5', + status: 'completed', + output: [ + { + id: 'message-stream-2', + type: 'message', + role: 'assistant', + status: 'completed', + content: [ + { type: 'output_text', text: '读取完成。' } + ] + } + ], + usage: { input_tokens: 20, output_tokens: 5 } + } + } + ]) + ] + const fetcher = vi.fn(async () => + new Response(streams.shift(), { + status: 200, + headers: { 'content-type': 'text/event-stream' } + }) + ) + const toolProvider = createToolProvider() + const runtime = new ModelAgentRuntime({ + apiKey: 'test-key', + baseUrl: 'https://api.openai.com/v1', + model: 'gpt-5', + protocol: 'openai-responses', + authentication: 'api-key', + fetcher, + toolProvider + }) + const events = [] + + for await (const event of runtime.run( + { + requestId: 'a431666e-5ec8-45e6-beb4-654132eed151', + conversationId: 'conversation-responses-streaming-tools', + prompt: '读取 README', + workMode: 'execute' + }, + new AbortController().signal, + async () => 'once' + )) { + events.push(event) + } + + for (const [, init] of fetcher.mock.calls) { + expect(JSON.parse(init?.body as string)).toMatchObject({ + stream: true + }) + } + expect( + events + .filter((event) => event.type === 'reasoning') + .map((event) => event.delta) + ).toEqual(['先分析。']) + expect( + events + .filter((event) => event.type === 'text') + .map((event) => event.delta) + ).toEqual(['准备读取。', '读取完成。']) + expect( + events.findIndex((event) => event.type === 'text') + ).toBeLessThan( + events.findIndex( + (event) => + event.type === 'tool' && event.state === 'running' + ) + ) + const secondBody = JSON.parse( + fetcher.mock.calls[1]?.[1]?.body as string + ) as { input: Array> } + expect(secondBody.input).toEqual([ + { + role: 'user', + content: '读取 README' + }, + { + id: 'reasoning-stream-1', + type: 'reasoning', + summary: [{ type: 'summary_text', text: '先分析。' }] + }, + { + id: 'message-stream-1', + type: 'message', + role: 'assistant', + status: 'completed', + content: [ + { type: 'output_text', text: '准备读取。' } + ] + }, + { + id: 'function-stream-1', + type: 'function_call', + call_id: 'call-stream-1', + name: 'workspace_read_text', + arguments: '{"path":"README.md"}' + }, + { + type: 'function_call_output', + call_id: 'call-stream-1', + output: [ + { + type: 'input_text', + text: 'tool result' + } + ] + } + ]) + expect(toolProvider.callTool).toHaveBeenCalledOnce() + expect(events.at(-1)).toMatchObject({ type: 'done' }) + }) + + it('rejects an incomplete OpenAI Responses tool stream', async () => { + const fetcher = vi.fn(async () => + new Response( + [ + 'event: response.output_text.delta', + `data: ${JSON.stringify({ + type: 'response.output_text.delta', + delta: 'partial' + })}`, + '', + 'data: [DONE]', + '', + '' + ].join('\n'), + { + status: 200, + headers: { 'content-type': 'text/event-stream' } + } + ) + ) + const toolProvider = createToolProvider() + const runtime = new ModelAgentRuntime({ + apiKey: 'test-key', + baseUrl: 'https://api.openai.com/v1', + model: 'gpt-5', + protocol: 'openai-responses', + authentication: 'api-key', + fetcher, + toolProvider + }) + const consume = async (): Promise => { + for await (const _event of runtime.run( + { + requestId: 'a431666e-5ec8-45e6-beb4-654132eed153', + conversationId: 'conversation-responses-incomplete-tools', + prompt: '读取 README', + workMode: 'execute' + }, + new AbortController().signal, + async () => 'once' + )) { + void _event + } + } + + await expect(consume()).rejects.toThrow('流式响应意外中断') + expect(toolProvider.callTool).not.toHaveBeenCalled() + expect(fetcher).toHaveBeenCalledOnce() + }) + it('continues OpenAI Responses with function_call_output', async () => { const responses = [ { @@ -1902,7 +2136,7 @@ describe('ModelAgentRuntime', () => { ) as Record expect(firstBody).toMatchObject({ model: 'gpt-5', - stream: false, + stream: true, tools: [ { type: 'function', @@ -2197,7 +2431,7 @@ describe('ModelAgentRuntime', () => { fetcher.mock.calls[0]?.[1]?.body as string ) as Record expect(firstBody).toMatchObject({ - stream: false, + stream: true, tools: [ { name: 'workspace_read_text', @@ -2233,6 +2467,372 @@ describe('ModelAgentRuntime', () => { }) }) + it('streams Anthropic text and thinking through tool rounds', async () => { + const streams = [ + createSseEventStream([ + { + type: 'message_start', + message: { + id: 'message-stream-tool-1', + model: 'claude', + usage: { input_tokens: 10 } + } + }, + { + type: 'content_block_start', + index: 0, + content_block: { type: 'thinking', thinking: '' } + }, + { + type: 'content_block_delta', + index: 0, + delta: { type: 'thinking_delta', thinking: '先分析。' } + }, + { + type: 'content_block_delta', + index: 0, + delta: { type: 'signature_delta', signature: 'signed' } + }, + { type: 'content_block_stop', index: 0 }, + { + type: 'content_block_start', + index: 1, + content_block: { type: 'text', text: '' } + }, + { + type: 'content_block_delta', + index: 1, + delta: { type: 'text_delta', text: '准备读取。' } + }, + { type: 'content_block_stop', index: 1 }, + { + type: 'content_block_start', + index: 2, + content_block: { + type: 'tool_use', + id: 'toolu-stream-1', + name: 'workspace_read_text', + input: {} + } + }, + { + type: 'content_block_delta', + index: 2, + delta: { + type: 'input_json_delta', + partial_json: '{"path":' + } + }, + { + type: 'content_block_delta', + index: 2, + delta: { + type: 'input_json_delta', + partial_json: '"notes.md"}' + } + }, + { type: 'content_block_stop', index: 2 }, + { + type: 'message_delta', + delta: { stop_reason: 'tool_use' }, + usage: { output_tokens: 4 } + }, + { type: 'message_stop' } + ]), + createSseEventStream([ + { + type: 'message_start', + message: { + id: 'message-stream-tool-2', + model: 'claude', + usage: { input_tokens: 18 } + } + }, + { + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' } + }, + { + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text: '读取完成。' } + }, + { type: 'content_block_stop', index: 0 }, + { + type: 'message_delta', + delta: { stop_reason: 'end_turn' }, + usage: { output_tokens: 5 } + }, + { type: 'message_stop' } + ]) + ] + const fetcher = vi.fn(async () => + new Response(streams.shift(), { + status: 200, + headers: { 'content-type': 'text/event-stream' } + }) + ) + const toolProvider = createToolProvider() + const runtime = new ModelAgentRuntime({ + apiKey: 'test-key', + baseUrl: 'https://api.anthropic.com', + model: 'claude', + protocol: 'anthropic-messages', + authentication: 'api-key', + fetcher, + toolProvider + }) + const events = [] + + for await (const event of runtime.run( + { + requestId: 'a431666e-5ec8-45e6-beb4-654132eed152', + conversationId: 'conversation-anthropic-streaming-tools', + prompt: '读取 notes', + workMode: 'execute' + }, + new AbortController().signal, + async () => 'once' + )) { + events.push(event) + } + + for (const [, init] of fetcher.mock.calls) { + expect(JSON.parse(init?.body as string)).toMatchObject({ + stream: true + }) + } + expect( + events + .filter((event) => event.type === 'reasoning') + .map((event) => event.delta) + ).toEqual(['先分析。']) + expect( + events + .filter((event) => event.type === 'text') + .map((event) => event.delta) + ).toEqual(['准备读取。', '读取完成。']) + expect( + events.findIndex((event) => event.type === 'text') + ).toBeLessThan( + events.findIndex( + (event) => + event.type === 'tool' && event.state === 'running' + ) + ) + const secondBody = JSON.parse( + fetcher.mock.calls[1]?.[1]?.body as string + ) as { messages: Array> } + expect(secondBody.messages.at(-2)).toEqual({ + role: 'assistant', + content: [ + { + type: 'thinking', + thinking: '先分析。', + signature: 'signed' + }, + { + type: 'text', + text: '准备读取。' + }, + { + type: 'tool_use', + id: 'toolu-stream-1', + name: 'workspace_read_text', + input: { path: 'notes.md' } + } + ] + }) + expect(secondBody.messages.at(-1)).toEqual({ + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'toolu-stream-1', + content: [{ type: 'text', text: 'tool result' }] + } + ] + }) + expect(toolProvider.callTool).toHaveBeenCalledOnce() + expect(events.at(-1)).toMatchObject({ type: 'done' }) + }) + + it('rejects malformed streamed Anthropic tool arguments', async () => { + const stream = createSseEventStream([ + { + type: 'message_start', + message: { + id: 'message-invalid-tool-1', + model: 'claude', + usage: { input_tokens: 8 } + } + }, + { + type: 'content_block_start', + index: 0, + content_block: { + type: 'tool_use', + id: 'toolu-invalid-1', + name: 'workspace_read_text', + input: {} + } + }, + { + type: 'content_block_delta', + index: 0, + delta: { + type: 'input_json_delta', + partial_json: '{"path":' + } + }, + { type: 'content_block_stop', index: 0 }, + { type: 'message_stop' } + ]) + const fetcher = vi.fn(async () => + new Response(stream, { + status: 200, + headers: { 'content-type': 'text/event-stream' } + }) + ) + const toolProvider = createToolProvider() + const runtime = new ModelAgentRuntime({ + apiKey: 'test-key', + baseUrl: 'https://api.anthropic.com', + model: 'claude', + protocol: 'anthropic-messages', + authentication: 'api-key', + fetcher, + toolProvider + }) + const consume = async (): Promise => { + for await (const _event of runtime.run( + { + requestId: 'a431666e-5ec8-45e6-beb4-654132eed154', + conversationId: 'conversation-anthropic-invalid-tools', + prompt: '读取 notes', + workMode: 'execute' + }, + new AbortController().signal, + async () => 'once' + )) { + void _event + } + } + + await expect(consume()).rejects.toThrow( + '模型返回了无效的工具参数 JSON' + ) + expect(toolProvider.callTool).not.toHaveBeenCalled() + expect(fetcher).toHaveBeenCalledOnce() + }) + + it('rejects a truncated streamed Anthropic tool round', async () => { + const stream = createSseEventStream([ + { + type: 'message_start', + message: { + id: 'message-truncated-stream-1', + model: 'claude', + usage: { input_tokens: 8 } + } + }, + { + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' } + }, + { + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text: 'partial' } + }, + { type: 'content_block_stop', index: 0 }, + { + type: 'message_delta', + delta: { stop_reason: 'max_tokens' }, + usage: { output_tokens: 4 } + }, + { type: 'message_stop' } + ]) + const fetcher = vi.fn(async () => + new Response(stream, { + status: 200, + headers: { 'content-type': 'text/event-stream' } + }) + ) + const toolProvider = createToolProvider() + const runtime = new ModelAgentRuntime({ + apiKey: 'test-key', + baseUrl: 'https://api.anthropic.com', + model: 'claude', + protocol: 'anthropic-messages', + authentication: 'api-key', + fetcher, + toolProvider + }) + const consume = async (): Promise => { + for await (const _event of runtime.run( + { + requestId: 'a431666e-5ec8-45e6-beb4-654132eed155', + conversationId: 'conversation-anthropic-truncated-stream', + prompt: '读取 notes', + workMode: 'execute' + }, + new AbortController().signal, + async () => 'once' + )) { + void _event + } + } + + await expect(consume()).rejects.toThrow( + 'Anthropic 返回未完成结果:max_tokens' + ) + expect(toolProvider.callTool).not.toHaveBeenCalled() + }) + + it('rejects a truncated Anthropic JSON fallback', async () => { + const fetcher = vi.fn(async () => + Response.json({ + id: 'message-truncated-json-1', + model: 'claude', + content: [{ type: 'text', text: 'partial' }], + stop_reason: 'model_context_window_exceeded', + usage: { input_tokens: 8, output_tokens: 4 } + }) + ) + const toolProvider = createToolProvider() + const runtime = new ModelAgentRuntime({ + apiKey: 'test-key', + baseUrl: 'https://api.anthropic.com', + model: 'claude', + protocol: 'anthropic-messages', + authentication: 'api-key', + fetcher, + toolProvider + }) + const consume = async (): Promise => { + for await (const _event of runtime.run( + { + requestId: 'a431666e-5ec8-45e6-beb4-654132eed156', + conversationId: 'conversation-anthropic-truncated-json', + prompt: '读取 notes', + workMode: 'execute' + }, + new AbortController().signal, + async () => 'once' + )) { + void _event + } + } + + await expect(consume()).rejects.toThrow( + 'Anthropic 返回未完成结果:model_context_window_exceeded' + ) + expect(toolProvider.callTool).not.toHaveBeenCalled() + }) + it('synthesizes and pairs a missing Anthropic tool_use id', async () => { const responses = [ { diff --git a/src/main/agent/model-runtime.ts b/src/main/agent/model-runtime.ts index 511c3f7..98ce147 100644 --- a/src/main/agent/model-runtime.ts +++ b/src/main/agent/model-runtime.ts @@ -799,6 +799,20 @@ function parseModelToolResponse( throw new Error('模型接口返回格式无效') } if (protocol === 'anthropic') { + const stopReason = payload.stop_reason + if ( + stopReason !== undefined && + stopReason !== null && + typeof stopReason !== 'string' + ) { + throw new Error('Anthropic 模型接口返回了无效停止原因') + } + if ( + stopReason === 'max_tokens' || + stopReason === 'model_context_window_exceeded' + ) { + throw new Error(`Anthropic 返回未完成结果:${stopReason}`) + } if (!Array.isArray(payload.content)) { throw new Error('Anthropic 模型接口未返回 content') } @@ -1124,6 +1138,349 @@ async function* readBoundedSseBlocks( } } +async function* readOpenAIResponsesToolStream( + response: Response, + requestId: string +): AsyncGenerator { + let answer = '' + let reasoning = '' + const usage: ModelUsageAccumulator = { + reported: false + } + + for await (const block of readBoundedSseBlocks(response)) { + const parsed = parseSseData(block) + if (parsed.stopped) { + break + } + if (parsed.event === undefined) { + continue + } + const providerError = getErrorMessage(parsed.event) + if (providerError) { + throw new Error(providerError) + } + const event = getRecord(parsed.event) + if (!event) { + throw new Error('OpenAI Responses 返回了无效流式事件') + } + if (event.type === 'response.failed') { + const failedResponse = getRecord(event.response) + throw new Error( + getErrorMessage(failedResponse) ?? 'OpenAI Responses 请求失败' + ) + } + if (event.type === 'response.incomplete') { + const incompleteResponse = getRecord(event.response) + const details = getRecord(incompleteResponse?.incomplete_details) + const reason = + typeof details?.reason === 'string' + ? `:${details.reason.slice(0, 200)}` + : '' + throw new Error(`OpenAI Responses 返回未完成结果${reason}`) + } + applyUsageUpdate(usage, getUsageUpdate(event, 'openai')) + + const reasoningDelta = getOpenAIResponsesReasoningDelta(event) + if (reasoningDelta) { + reasoning += reasoningDelta + yield { + requestId, + type: 'reasoning', + delta: reasoningDelta + } + } + const textDelta = getOpenAIResponsesTextDelta(event) + if (textDelta) { + answer += textDelta + yield { + requestId, + type: 'text', + delta: textDelta + } + } + if (event.type !== 'response.completed') { + continue + } + + const completedResponse = getRecord(event.response) + const result = parseModelToolResponse( + completedResponse, + 'openai-responses' + ) + applyUsageUpdate(usage, result.usage) + if (result.reasoning !== reasoning) { + if (!result.reasoning.startsWith(reasoning)) { + throw new Error( + 'OpenAI Responses 流式推理与完成结果不一致' + ) + } + const remainingReasoning = result.reasoning.slice(reasoning.length) + if (remainingReasoning) { + reasoning += remainingReasoning + yield { + requestId, + type: 'reasoning', + delta: remainingReasoning + } + } + } + if (result.text !== answer) { + if (!result.text.startsWith(answer)) { + throw new Error('OpenAI Responses 流式文本与完成结果不一致') + } + const remainingText = result.text.slice(answer.length) + if (remainingText) { + answer += remainingText + yield { + requestId, + type: 'text', + delta: remainingText + } + } + } + return { + ...result, + text: answer, + reasoning, + usage, + streamed: true + } + } + + throw new Error('模型接口流式响应意外中断') +} + +type AnthropicStreamBlock = { + content: Record + initialInput?: unknown + kind: 'other' | 'text' | 'thinking' | 'tool_use' + open: boolean + partialJson: string +} + +function getAnthropicStreamIndex( + event: Record +): number { + if ( + !Number.isSafeInteger(event.index) || + (event.index as number) < 0 + ) { + throw new Error('Anthropic 返回了无效流式内容块序号') + } + return event.index as number +} + +async function* readAnthropicToolStream( + response: Response, + requestId: string +): AsyncGenerator { + const blocks = new Map() + const usage: ModelUsageAccumulator = { + reported: false + } + let answer = '' + let reasoning = '' + let stopReason: unknown + let toolCallCount = 0 + + for await (const block of readBoundedSseBlocks(response)) { + const parsed = parseSseData(block) + if (parsed.stopped) { + break + } + if (parsed.event === undefined) { + continue + } + const providerError = getErrorMessage(parsed.event) + if (providerError) { + throw new Error(providerError) + } + const event = getRecord(parsed.event) + if (!event || typeof event.type !== 'string') { + throw new Error('Anthropic 返回了无效流式事件') + } + applyUsageUpdate(usage, getUsageUpdate(event, 'anthropic')) + + if (event.type === 'message_delta') { + const delta = getRecord(event.delta) + if (delta?.stop_reason !== undefined) { + stopReason = delta.stop_reason + } + } + + if (event.type === 'content_block_start') { + const index = getAnthropicStreamIndex(event) + const content = getRecord(event.content_block) + if (!content || blocks.has(index)) { + throw new Error('Anthropic 返回了无效流式内容块') + } + const next: AnthropicStreamBlock = { + content: { ...content }, + kind: 'other', + open: true, + partialJson: '' + } + if (content.type === 'text') { + if ( + content.text !== undefined && + typeof content.text !== 'string' + ) { + throw new Error('Anthropic 返回了无效流式文本块') + } + const text = typeof content.text === 'string' ? content.text : '' + next.kind = 'text' + next.content.text = text + if (text) { + answer += text + yield { + requestId, + type: 'text', + delta: text + } + } + } else if (content.type === 'thinking') { + if ( + content.thinking !== undefined && + typeof content.thinking !== 'string' + ) { + throw new Error('Anthropic 返回了无效流式推理块') + } + const thinking = + typeof content.thinking === 'string' ? content.thinking : '' + next.kind = 'thinking' + next.content.thinking = thinking + if (thinking) { + reasoning += thinking + yield { + requestId, + type: 'reasoning', + delta: thinking + } + } + } else if (content.type === 'tool_use') { + toolCallCount += 1 + if (toolCallCount > maxToolCallsPerRun) { + throw new Error('模型单轮工具调用超过安全限制') + } + const identity = parseToolCallIdentity(content.id, content.name) + next.kind = 'tool_use' + next.initialInput = content.input + next.content.id = identity.id + next.content.name = identity.name + next.content.input = {} + } + blocks.set(index, next) + continue + } + + if (event.type === 'content_block_delta') { + const index = getAnthropicStreamIndex(event) + const current = blocks.get(index) + const delta = getRecord(event.delta) + if (!current?.open || !delta || typeof delta.type !== 'string') { + throw new Error('Anthropic 返回了无效流式内容增量') + } + if (delta.type === 'text_delta') { + if (current.kind !== 'text' || typeof delta.text !== 'string') { + throw new Error('Anthropic 返回了无效流式文本增量') + } + current.content.text = + `${current.content.text as string}${delta.text}` + if (delta.text) { + answer += delta.text + yield { + requestId, + type: 'text', + delta: delta.text + } + } + } else if (delta.type === 'thinking_delta') { + if ( + current.kind !== 'thinking' || + typeof delta.thinking !== 'string' + ) { + throw new Error('Anthropic 返回了无效流式推理增量') + } + current.content.thinking = + `${current.content.thinking as string}${delta.thinking}` + if (delta.thinking) { + reasoning += delta.thinking + yield { + requestId, + type: 'reasoning', + delta: delta.thinking + } + } + } else if (delta.type === 'signature_delta') { + if ( + current.kind !== 'thinking' || + typeof delta.signature !== 'string' + ) { + throw new Error('Anthropic 返回了无效流式签名增量') + } + current.content.signature = + `${typeof current.content.signature === 'string' + ? current.content.signature + : ''}${delta.signature}` + } else if (delta.type === 'input_json_delta') { + if ( + current.kind !== 'tool_use' || + typeof delta.partial_json !== 'string' + ) { + throw new Error('Anthropic 返回了无效流式工具参数增量') + } + current.partialJson += delta.partial_json + if ( + Buffer.byteLength(current.partialJson) > + maxToolArgumentBytes + ) { + throw new Error('模型工具参数超过 128KB 安全限制') + } + } + continue + } + + if (event.type === 'content_block_stop') { + const index = getAnthropicStreamIndex(event) + const current = blocks.get(index) + if (!current?.open) { + throw new Error('Anthropic 返回了无效流式内容结束事件') + } + current.open = false + if (current.kind === 'tool_use') { + current.content.input = parseToolArguments( + current.partialJson || current.initialInput || {} + ) + } + continue + } + + if (event.type !== 'message_stop') { + continue + } + if ([...blocks.values()].some((item) => item.open)) { + throw new Error('Anthropic 流式响应包含未结束的内容块') + } + const content = [...blocks.entries()] + .sort(([left], [right]) => left - right) + .map(([, item]) => item.content) + const result = parseModelToolResponse( + { content, stop_reason: stopReason }, + 'anthropic' + ) + return { + ...result, + text: answer, + reasoning, + usage, + streamed: true + } + } + + throw new Error('模型接口流式响应意外中断') +} + export class ModelAgentRuntime implements AgentRuntime { readonly runtimeId = 'model' readonly requiresToolApproval = false @@ -1784,7 +2141,7 @@ export class ModelAgentRuntime implements AgentRuntime { ? { model: this.options.model, max_output_tokens: this.maxOutputTokens, - stream: false, + stream: true, instructions: system, input: messages, tools: providerTools @@ -1793,7 +2150,7 @@ export class ModelAgentRuntime implements AgentRuntime { ? { model: this.options.model, max_tokens: this.maxOutputTokens, - stream: false, + stream: true, system, messages, tools: providerTools @@ -1843,12 +2200,22 @@ export class ModelAgentRuntime implements AgentRuntime { detail ?? `模型接口请求失败(HTTP ${response.status})` ) } + const isEventStream = response.headers + .get('content-type') + ?.toLocaleLowerCase() + .includes('text/event-stream') === true + if (responses && isEventStream) { + return yield* readOpenAIResponsesToolStream( + response, + requestId + ) + } + if (anthropic && isEventStream) { + return yield* readAnthropicToolStream(response, requestId) + } if ( streamOpenAIChat && - response.headers - .get('content-type') - ?.toLocaleLowerCase() - .includes('text/event-stream') + isEventStream ) { const streamedToolCalls = new Map< number,