Repository navigation
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 再跑。
|
谢谢你把三种现场自己先打出来,也谢谢你按我们的门槛走了「先跑、再改」。上一轮提的两点我逐条驱动过,都成立;这一轮又量了四条上一轮没量到的。结论不变:先别合,但改法很小,方向要换一下。 三件要改的1. 「对不上调用的工具返回」被整条删了 —— 可仓库在文本模拟那条路上是特意留着它的。
还有一处:你判断「有没有对应的调用」时只记前面走过的 id,于是调用排在结果后面的那种也会被删 —— 这一支建议回到原位:原生工具该删的继续删,文本模拟继续留。 2. 结尾那条助手的半句被丢掉,然后回了一个「正常」的答案。
要保留这个行为,就得把它写成契约(注释 + README 相应段落),并在测试里钉住真正发出去的 3. 三条新用例里有一条在没打补丁的树上也是绿的。 尾部 assistant、尾部空 user 两条能区分;但「对不上调用的返回」那条断言的是「不是 400」,而那个形状在原版上本来就不返回 400,所以把这段逻辑整个删掉它照样绿。另外我把全部变异锚点提出来对了一遍,没有一个落在这一段,所以现在的绿说明不了机制被钉住。 我实跑过什么head
我攻过、攻不动的: 改完 ping 我,我按你的新 head 再跑一遍。 |
|
补一句,免得你卡在一个本来就该我们决定的问题上。 上一轮我提的第二件里,有两个形状(请求以空 user 结尾、以助手半句结尾)到底该「拒绝 400」还是「照常回答」,是我这边的政策选择,不是你需要去设计的东西。你如果不想替我们决定,说一声,我把这份契约写下来(哪种形状我们服务、哪种我们继续拒),那你这边的活就只剩第一件(工具返回那一支)。 这不是催 —— 只是把它从「等你猜」变成「等我写」。推上来 ping 我。 |
… stripOrphans Per review M1 on PR dwgx#274: the repairUnanswerableMessages pre-pass stripped orphan tool/function results across the whole array (seen-so-far ids) before stripOrphans could decide. That broke the documented opt-in rule (tool-emulation.js:1290-1297: off by default, native path passes stripOrphans:true at chat.js:3451, text emulation at 4425 passes nothing) and reintroduced the seen-so-far trap the helper comment at 1106-1108 warns about (a result whose call appears later was dropped while its call survived). This change removes the shared-layer sweep entirely (repair function, call site, and its tests), restoring base 400 behaviour for tails. Tail policy (empty user / trailing assistant, review M2) is the maintainer's call and is left for a separate contract, not decided here. M3 regression pins added to test/tool-emulation-boundary.test.js: a result whose call appears later survives, and a mid-array orphan survives the text-emulation encoder.
|
新 head M1(已改)—— 共享层孤儿清洗整段删 M2(本轮不动,等你写契约) M3(M1 相关部分已钉住)
本轮验证(干净 clone、无账号,与你同读法:400→不再400 那半句本轮不适用,repair 已删):
ping 我时按 |
|
新 head 收到,逐条回。M1 核过了,做得对。 独立复验(干净 clone、无账号、没碰你的分支):
M2 的契约直接写在这里,你不用等我另发文档(文档 + 测试正在落地,落地后这里补链接): 两条 400 都留。 理由不是口味,是实测: M3 有一处要改:第二个守卫站错了层。 它直接调 实测:把那一刀原样装回去( 改法二选一,我建议第一个:
另外提一句:你的 base 是 改完 ping 我,按新 head 跑。 |
|
补一句我上一条承诺过的:契约已经落到 master 了。
里面写清了:两条 400 各自拦什么、为什么留( 写这份文档的过程里还抓到两个真东西:
所以 M2 现在是可执行的契约,不是一段话。你那边的活还是上一条说的那处(M1 的第二个守卫站错了层)。 |
Write down which conversation shapes the proxy serves and which it keeps rejecting. The two local 400s in the shared chat guard stay: 8334cea moved the check into the shared layer and measured that an assistant-terminated conversation comes back UPSTREAM_INTERNAL with account penalties, and 79cd990 (fix dwgx#28) measured the empty-user shape as answered against the system prompt. State where the block is enforced (before streaming/backend/account/cache, so no surface bypasses it), the per-surface difference (Anthropic and Gemini converters drop textless turns before the guard) as documented-not-endorsed, what is deliberately not promised (/v1/completions), and the measured evidence bar for changing any of it.
改了什么 / 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