zai-org / zai-org/feedback

[Bug] openai-compatible 流式解析把 tool_calls:[](空数组)误判为工具调用阶段,思考过程被逐 token 碎片化(一词一行)

Open Beginner friendly
#678 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

priority: P2
Dominant language
No language data
Stars
22
Forks
1
PR merge metrics
No merged PRs in 30d

Description

类别 Category: 模型设置 / 切换 · Model config | 框架 Framework: ZCode Agent(自研) | 严重程度 Severity: 影响体验 · Major | 频率 Frequency: 必现 · Always

问题描述 · Description

配置 openai-compatible 自定义 provider 时,如果上游网关对每个 SSE chunk 都序列化全部字段(不少国产网关如此:每个 delta 都携带 tool_calls: []content: ""),ZCode 内嵌的 AI SDK openai-compatible 流式解析会把空数组 tool_calls: [] 误判为"进入工具调用阶段",导致:

  • 每输出一个 reasoning token 就强制关闭当前思考块(reasoning-end),下一个 token 再重新开启(reasoning-start,id 恒为 "reasoning-0");
  • 运行时消费侧 reasoning_end 会把 id 从 reasoningById 删除,下次 reasoning_start 重建 block 并 push 进 parts 数组 → 每个 token 一个独立 reasoning part
  • 前端把多个 reasoning part 用 \n\n 拼接渲染 → 思考过程一词一行;同时每个 token 触发一对 reasoning start/end 流事件,UI 反复开合思考行造成卡顿。

实测一次 13.8k 字符的思考内容在消息存储中被拆成 2187 个 reasoning part(平均每段 1–9 字符);正常应为个位数段落级 part。

复现步骤 · Steps to reproduce

  1. 配置一个 openai-compatible 自定义 provider,指向"全字段序列化"的上游(即流式响应每个 chunk 的 delta 都包含 tool_calls: []content: "" 字段的 reasoning 模型);
  2. 新建会话发送任意问题,开启思考模型;
  3. 观察思考过程渲染,并检查消息存储中该次响应的 reasoningText

期望表现 · Expected

整轮思考聚合为 1 个(或少量段落级)reasoning block,流式渲染连贯成段。

实际表现 · Actual

每个 token 生成独立 reasoning block:渲染一词一行;单次响应产生数千对 reasoning start/end 事件;UI 卡顿、思考区显示杂乱。

根因定位 · Root cause

zcode.cjs(Desktop 3.11.2.6792 / CLI 0.16.5)中 openai-compatible doStream 的 transform:

let fe = ce.reasoning_content ?? ce.reasoning;
if (fe && (y || (b.enqueue({type:"reasoning-start",id:"reasoning-0"}), y=!0),
           b.enqueue({type:"reasoning-delta",id:"reasoning-0",delta:fe}))),
    ce.content && (y && (b.enqueue({type:"reasoning-end",id:"reasoning-0"}), y=!1), /* text-start/delta */),
    ce.tool_calls != null) {
  y && (b.enqueue({type:"reasoning-end",id:"reasoning-0"}), y=!1);
  /* for (ue of ce.tool_calls) ... */
}

ce.tool_calls != null空数组也成立(JS 中 [] != null 为 true),于是每个 reasoning delta 之后紧跟的空 tool_calls 字段都会强制结束思考块。

建议修复 · Suggested fix

将工具调用判定改为长度判断:

ce.tool_calls?.length > 0   // 或 Array.isArray(ce.tool_calls) && ce.tool_calls.length

仅此一处改动即可根治,官方 OpenAI 兼容行为(无工具调用时省略该字段)不受影响。可选加固:对同轮相邻的多个 reasoning part 做合并,避免上游其他形式的块碎片放大存储与渲染成本。

验证证据 · Evidence

用真实捕获的 SSE 流做端到端管线模拟:

  • 原样透传 149 个含 tool_calls: [] 的 reasoning chunk → 产生 149 个 reasoning block、reasoning-start ×149(一词一行);
  • 仅剔除空数组字段后重放 → reasoning-start ×1、1 个 block、文本完全连贯。

环境 · Environment

  • ZCode Desktop 3.11.2 (6792) / CLI 0.16.5
  • macOS(darwin 25.5.0)arm64
  • 复现频率:在受影响上游上必现(Always)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in zcode.cjs at the openai-compatible doStream transform and inspect how ce.tool_calls is handled after reasoning deltas. Replay the captured SSE stream described in the issue, then verify that empty tool_calls arrays no longer end reasoning and that the response remains a single or small number of coherent reasoning blocks.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
api, stream-processing
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.