Repository navigation
Replies: 2 comments
|
当前 master 仍保留报告中的三条空值路径:DeepSeek adapter 的 CallId(block.callId ?? '')、pi-ai toolcall_end 直接采用 event.toolCall.id,以及通用 BlockAssembler 无条件用 delta 的空 id 覆盖已有值。因此根因判断成立。 修复职责建议再收紧一些:
P1 的 reasoning_content 回传是另一个协议修复,最好不要与 call-id 迁移绑定成一个不可拆补丁。 历史坏日志需要单独的离线修复器:先停止 Web/Agent 写入并备份,按一次 assistant tool-call 与对应 tool/result 建立映射,同时重写成唯一 id,然后跑 Session replay/invariant 验证再替换原文件。直接热改 node_modules/lib 或在 live Session 上重写,会继续被旧进程追加的数据覆盖。 回归应覆盖直连与 pi-ai 两条 adapter:id 为 null/undefined/空串、空后出现真 id、两个并行调用、连续两轮 index 重复、block-end 空值,以及修复后的日志重启可加载并能继续发下一轮请求。 |
|
你这份诊断的根因链路(流式 delta 空 id/name → 累积覆盖 → 第一层:故障注入打真累加器(针对"全程无 id"的畸形流本身)。 用 第二层:真机端到端(stock 边界说明:① 真机那轮模型选择了顺序调用而非同 step 并行(记录里 |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
DSH DeepSeek adapter 统一修复诊断(2026-08-17)
1. 故障现象(两类,同根)
运行
deepseek-official / deepseek-v4-flash|pro时,DeepSeek API 偶发在并行 ≥2 个tool call 的流式 delta 里发空
id/name(违反 OpenAI"id/name 只在首个 delta 出现"的约定),甚至整次调用全程不带 id。由此产生两类故障:
unknown tool ""→UNKNOWN_TOOL;agent-loop 整步拒掉重试tool_call_idDuplicate value for 'tool_call_id' of in message[N]→INVALID_REQUEST(上一轮内存里是旧 adapter;新写入仍空 id)callId的 tool/result 事件session event at seq N message must have tool source (internal)(dsh-session 回放校验拒绝)时间线(本地时间,2026-08-16 → 08-17)
session-d3a71f45/fcb10cee等补丁前会话已出现空 callId 存档写入空 callId → 00:17 / 00:23 重启后 无法加载(dsh-session 校验)
2f256497 / 83e6c5d0 / 6d14b4b3 / e7935da8 / b8c7279b 被旧代码持续追加空 callId,
修复反复被覆盖
结论:补丁 P1/P2 不是故障根源,也没有解决它 —— 它们只防"空串覆盖已设的 id",
防不了"API 全程不给 id"。坏数据在补丁前就已产生。
2. 根因链(两处,都在 bundle,源码
deepseek-harness未动)关键细节:
assemble()优先返回它),所以光改 dsh-llm 的??兜底救不了DeepSeek —— 必须在 adapter 的 closeBlock 之前保证 callId 非空。
lib/index.js(内联 BlockAssembler, L673+),lib/types/assembler.js是同源码的独立文件,两处都要打。
2.1 扩展根因:
dsh-llm-pi-ai(opencode-go / pi-ai 路径)settings.yaml 的
llm-pi-ai.providers.opencode-go走https://opencode.ai/zen/go/v1反代,该反代同时提供 deepseek-v4-flash / pro 等模型。用户在 GUI 模型选择器里切到这类 deepseek
走的不是
dsh-llm-deepseek(直连 DeepSeek API,需要llm-deepseek.providers.deepseek.apiKeyEnv,settings 里没配),而是
dsh-llm-pi-ai,它内部用@earendil-works/pi-ai通用库。dsh-llm-pi-ai的toStreamChunks用的是带 block-end 的 chunk 协议:toolcall_start→ yieldblock-starttoolcall_delta→ yieldtool-call-delta(携带id: CallId(known?.id ?? ""))toolcall_end→ yieldblock-end(携带block: { id: CallId(event.toolCall.id), ... })P4 的
BlockAssembler.assemble()(L739)只在partial.block未设时走兜底:而
push()L713partial.block = chunk.block;会把toolcall_end携带的空 id block直接覆盖 partial.block。P4 兜底对 dsh-llm-pi-ai 这条路径完全失效。
P5 修复:在源头合成 id(toStreamChunks 入口生成 per-stream
callSalt,toolcall_start/delta/end 三处空 id 都兜底合成
call-<index>-<callSalt>),从源头让 block.id 永不为空,partial.block 永远是非空 brand,assemble() 返回它。
2.2 扩展根因:
dsh-llm-deepseek的 P3 null 漏洞(OpenCode 反代路径)settings.yaml 里
llm-deepseek.baseURL = https://opencode.ai/zen/go/v1(孤儿配置,没
llm-deepseek.providers块),用户切到 deepseek 时dsh-llm-deepseek通过 OpenCode 反代拿模型,API key 走环境变量(
DEEPSEEK_API_KEY,settings 未显式配,反代侧校验)。OpenCode 反代的 SSE delta 里
tool_calls[].id可能是null(而非 undefined 或空字符串)。P3 的条件
call.id === void 0 || call.id === ""对 null 全部不成立:null === void 0→ falsenull === ""→ falseelse if (call.id !== void 0 && call.id !== ""):null !== void 0→ truenull !== ""→ trueblock.callId = null!然后
closeBlockL213id: CallId(block.callId ?? ""):??对 null 返回""→ block id = ""!P6 修复:把
=== void 0改成== null(同时匹配 undefined 和 null),!== void 0改成!= null。P6 已在
E:\caches\bun\global\node_modules\@deepseek-ai\dsh-llm-deepseek\lib\index.jsL321 落盘,4 case 验证(
id: null/ 无 id 字段 / 真 id 保留 / null 后真 id 切换)全部产出非空 brand。3. 统一补丁(P1-P6,一个脚本全打)
脚本:
patch-dsh-llm-unified.ps1(幂等,自动跳过已应用项)reasoning_contentINVALID_REQUEST(无 reasoning 块)id不覆盖已设值;name真值守卫(空串/null 均不覆盖,2026-08-17 升级)unknown tool ""/ id 被空串清掉call-<index>-<盐>,盐每流随机(callSalt)if (chunk.id)才赋值;兜底改||且带随机盐callSalt;toolcall_start/delta/end 三处空 id 都兜底合成call-<index>-<盐>llm-pi-ai.providers.opencode-go走 opencode.ai/zen/go/v1 的 deepseek 模型=== void 0→== null,!== void 0→!= nulltool_calls[].id为null时 P3 漏掉,null进入 else if 设block.callId=null,closeBlock 的?? ""把 null 当空,产出空 idP3 语义(seed 行):
call-<index>-<盐>,不依赖每次流从 0 开始的 index(避免两轮都叫call-0→ duplicate)4. 剩余已知风险(未修,文档备案)
index字段toolBlocks.get(undefined)会拆块for (chunk.choices)同状态块交错name:null且后续也无 nameunknown tool ""name字段同样可能为空dsh-llm-deepseek-lib-index.js.diff.txt
dsh-llm-lib-index.js.diff.txt
All reactions