Conversation
- src/handlers/chat.js: repairUnanswerableMessages drops the three tails the upstream cannot serve — a trailing assistant turn (the Anthropic prefill shape), an empty-content user turn, and a tool/function result whose tool_call_id no earlier assistant turn declared — and trims a trailing assistant run back to the last answerable turn. A chain with nothing answerable left is returned unchanged, so the explicit 400 still owns it. - test/responses-chain-scope.test.js: the trailing-assistant shape moves from the unanswerable list to a new repairable list; the rejection assertions for genuinely unanswerable chains are unchanged.
|
串行补充(已清场、无并发): |
dwgx
left a comment
There was a problem hiding this comment.
评审:三种结尾你自己打出来了,现象是真的。这版先别合。它做了两件仓库原来故意不做的事:把「只在原生工具那条路上才删」的工具返回提前整段删掉;把助手已经写出的半句丢掉,说成是接着写。
你这份说明我收下。中英都写了,三种形状是你本地打的,没有加依赖,也没有改协议字段号。
我实跑过什么
对着 head 69876a9、parent a454117,单独工作树里看的就是这一份补丁。
| 项 | 结果 |
|---|---|
test/responses-chain-scope.test.js |
17 通过 / 0 失败 |
真实 handleChatCompletions,没有账号、外网拒绝 |
[user, assistant]:原版 400,这版 401 NO_TOKEN。不是说明里的 200 |
normalizeMessagesForCascade,stripOrphans=false |
旧路径会留下被截断的那次工具返回;先过 repairUnanswerableMessages,这段内容没了 |
你写的现场 200,我这边没有原始回执,按你的报告记,不算我复现过。test:release 里原来就有的 2 个失败和 restart,原版上也有,不记成这个补丁的回归。
「不编新内容,只裁掉最后一条能回答的话之后」这句话我专门试过,不成立。
M1(必须改)— 找不到对应调用的工具返回,在选后端之前就被删了
repairUnanswerableMessages 会把整段 messages 扫一遍。工具返回的 tool_call_id 如果这次数组里没有对应的助手调用,就直接丢掉。
仓库里已经写明的规则在 src/handlers/tool-emulation.js:1290-1297:stripOrphans 默认是关的。客户端可能把更早一轮的工具调用截掉了,返回还在,这段话仍然有用。走文本模拟的时候,还要拿它去做内容清理。只有原生工具、上游要求这一轮自己对得上时,调用方才会显式传 stripOrphans:true。
你这刀在那条规则之前。后面即使选择保留,也找不回来。
我用的输入是这样:一条工具返回,它的调用在更早、已经被客户端截掉的轮次里,正文是 MARKER;后面再跟一条用户消息「总结上文」。
stripOrphans=false:旧路径留下MARKER- 先过你这个函数:
MARKER没了 - 这条工具返回在最后一条用户消息前面,所以也不是「只裁结尾」
请回到原来的边界。原生工具要删的继续删,文本模拟继续留。不要在校验前面再加一层意思不一样的删除。
M2(必须改)— 删掉结尾的助手消息,不是接着写
输入是:用户说「返回 JSON」,助手已经写出 {"approved": false, "reason": "。过了你的函数,只剩用户那句。approved=false 和写到一半的原因都没了。已经写完的助手回复,也会同样被丢掉。
原来返回 400 是故意的。responses-chain-scope.test.js 里你改掉的那段注释写着:上游不接受以助手消息结尾,本地直接拒绝,免得一次说不清的失败再记到账号上。
要改这条,先证明上游现在肯接着这段前缀写,或者明确写成「丢掉结尾,让模型重答」。不能把丢掉的内容说成「没有编新内容」。
M3(请补测试)— 新测试只证明「不是 400」
断言是 assert.notEqual(res.status, 400)。没有账号时,三条都会落到 401。找不到对应调用的那条,在原版上也不是 400,这条断言照样通过。
请把实际送出去的 messages、调用了几次、输入对象有没有被改,都钉住。至少留下这几种:文本模拟里被截断的工具上下文还在、成对的工具调用和返回不被误伤、助手已经写出的前缀还在。一旦有人把「找不到调用就删」改成一律删除,测试要失败。
wire-byte-identity 比的是默认请求的字节,不会跑到这次改的处理函数。它通过,不能算这三条已经覆盖。
这种结尾修复可以提。请对着仓库里已经写明的规则改。两条必须改的,可以分开提。改完 ping 我,我按你的新 head 再跑。
改了什么 / What changed
之前:会话历史落在三种「上游无法作答」的形状上时请求被拒 —— 以 assistant 轮结尾(Anthropic prefill 形状)、或最后一条 user 内容为空 → 本地 400;带一条孤儿 tool 结果 → 上游
invalid_argument→ 502。现在:这三种尾巴在校验/编码前被规范化(裁剪到最后一条可应答轮 / 丢弃孤儿结果 / 丢弃空 user 轮),请求照常作答;真正没有可应答轮的历史仍然返回原来的 400(
[]、[system]、[assistant]、[system, user:""])。为什么 / Why
逐条实测(本仓库
/v1/chat/completions,非流式)—— 三条形状都是我亲自复现的,不是外部清单转述:[user, assistant](尾部 assistant)The conversation must end with a user message or a tool result.[user, assistant, user:""]The last user message has empty content. Provide a non-empty user prompt.[user, {tool, tool_call_id 未被声明}]invalid_argument)[assistant]/[user:""]/[system, user:""][user, assistant(tool_calls), tool]裁剪只发生在「最后一条可应答轮之后」,system 轮原位保留,不发明任何内容;无可应答轮时原样返回,仍由既有 400 负责 —— 该契约的断言保持原样,只把「尾部 assistant」从不可修复列表移到新增的可修复列表(3 条行为断言,断言 HTTP 结果而非源码文本)。
同类先例:#270(
devin-connect同角色文本轮合并)—— 同一类「线编码前的形状修复」,落在共享校验路径上,因此/v1/chat/completions、/v1/messages、/v1/responses一并覆盖。测试 / Testing
Linux + Node 24(与 CI 同环境)、干净 clone、
npm ci;退出码直接取自命令本身(未经管道):npm run test:release(=run-test-shard.mjs 0 1,366 个测试文件)改动前后各跑一次(A/B):两者失败集合完全一致(均为本机 scratch 环境的 langserver / 进程类测试依赖所致),0 净新增失败;+2 pass 正是本 PR 新增的用例。CI 的 4 分片为权威口径。
Checklist