fix(stream): prevent tool-call argument leakage into text chunks

This commit is contained in:
JOJO 2026-04-03 16:09:29 +08:00
parent 494edc53e9
commit ce02ea21fb

View File

@ -15,6 +15,7 @@ from .state import get_stop_flag, clear_stop_flag
async def run_streaming_attempts(*, web_terminal, messages, tools, sender, client_sid: str, username: str, conversation_id: Optional[str], current_iteration: int, max_api_retries: int, retry_delay_seconds: int, detected_tool_intent: Dict[str, str], full_response: str, tool_calls: list, current_thinking: str, detected_tools: Dict[str, str], last_usage_payload, in_thinking: bool, thinking_started: bool, thinking_ended: bool, text_started: bool, text_has_content: bool, text_streaming: bool, text_chunk_index: int, last_text_chunk_time, chunk_count: int, reasoning_chunks: int, content_chunks: int, tool_chunks: int, last_finish_reason: Optional[str], accumulated_response: str) -> Dict[str, Any]:
api_error = None
tool_call_stream_active = False
for api_attempt in range(max_api_retries + 1):
api_error = None
if api_attempt > 0:
@ -36,6 +37,7 @@ async def run_streaming_attempts(*, web_terminal, messages, tools, sender, clien
content_chunks = 0
tool_chunks = 0
last_finish_reason = None
tool_call_stream_active = False
# 收集流式响应
async for chunk in web_terminal.api_client.chat(messages, tools, stream=True):
@ -128,51 +130,12 @@ async def run_streaming_attempts(*, web_terminal, messages, tools, sender, clien
current_thinking += reasoning_content
sender('thinking_chunk', {'content': reasoning_content})
# 处理正常内容
if "content" in delta:
content = delta["content"]
if content:
content_chunks += 1
debug_log(f" 正式内容 #{content_chunks}: {repr(content[:100] if content else 'None')}")
if in_thinking and not thinking_ended:
in_thinking = False
thinking_ended = True
sender('thinking_end', {'full_content': current_thinking})
await asyncio.sleep(0.1)
if not text_started:
text_started = True
text_streaming = True
sender('text_start', {})
brief_log("模型输出了内容")
await asyncio.sleep(0.05)
full_response += content
accumulated_response += content
text_has_content = True
emit_time = time.time()
elapsed = 0.0 if last_text_chunk_time is None else emit_time - last_text_chunk_time
last_text_chunk_time = emit_time
text_chunk_index += 1
log_backend_chunk(
conversation_id,
current_iteration,
text_chunk_index,
elapsed,
len(content),
content[:32]
)
sender('text_chunk', {
'content': content,
'index': text_chunk_index,
'elapsed': elapsed
})
# 收集工具调用 - 实时发送准备状态
if "tool_calls" in delta:
delta_tool_calls = delta.get("tool_calls")
if isinstance(delta_tool_calls, list) and delta_tool_calls:
tool_call_stream_active = True
tool_chunks += 1
for tc in delta["tool_calls"]:
for tc in delta_tool_calls:
found = False
for existing in tool_calls:
if existing.get("index") == tc.get("index"):
@ -261,6 +224,56 @@ async def run_streaming_attempts(*, web_terminal, messages, tools, sender, clien
await asyncio.sleep(0.01)
debug_log(f" 新工具: {tool_name}")
# 处理正常内容
if "content" in delta:
content = delta["content"]
if content:
# 某些供应商在 tool_calls 流式阶段会把参数碎片误放进 content。
# 一旦进入工具调用流,抑制正文 chunk避免 write_file 参数泄露到前端。
if tool_call_stream_active:
debug_log(
" 抑制可疑正文chunktool_call阶段: "
f"{repr((content or '')[:100])}"
)
continue
content_chunks += 1
debug_log(f" 正式内容 #{content_chunks}: {repr(content[:100] if content else 'None')}")
if in_thinking and not thinking_ended:
in_thinking = False
thinking_ended = True
sender('thinking_end', {'full_content': current_thinking})
await asyncio.sleep(0.1)
if not text_started:
text_started = True
text_streaming = True
sender('text_start', {})
brief_log("模型输出了内容")
await asyncio.sleep(0.05)
full_response += content
accumulated_response += content
text_has_content = True
emit_time = time.time()
elapsed = 0.0 if last_text_chunk_time is None else emit_time - last_text_chunk_time
last_text_chunk_time = emit_time
text_chunk_index += 1
log_backend_chunk(
conversation_id,
current_iteration,
text_chunk_index,
elapsed,
len(content),
content[:32]
)
sender('text_chunk', {
'content': content,
'index': text_chunk_index,
'elapsed': elapsed
})
# 检查是否被停止
client_stop_info = get_stop_flag(client_sid, username)
if client_stop_info: