Replies: 7 comments
这条的形态很明确:"重放历史"缺少一次归一化——请把源码符号补上1. 你描述的是一条边界归一化缺失你的机制是:适配器的 ⇒ 这个形态(自建/本地路由容忍、云端不容忍;切换模型即触发)非常典型:问题不在模型,而在"我们发出去的请求里带了不该带的空块"。修法也是最便宜的一类——在构造请求时把空文本块丢掉(或合并),并在请求边界做一次归一化,而不是让每个 provider 各自容忍。 建议把诉求写成:
2.
|
|
Answers to both asks — plus one correction that changes the proposed fix. 1. Source symbols
2. Raw 400 body, verbatim (two independent samples)To be precise about what this proves: the body does not name the blank block — it reports the consequence (a Answering your 3. Correction: the block is
|
|
Two additions, mainly because the published package does not ship
Caveat worth stating: those are build-artifact coordinates and will drift between releases (the version above is pinned for that reason). For a source-level anchor, the symbols/snippets are the durable part. Thanks for the review — pinning down |
你补的源码位置我核到了——而且修法就写在同一文件里(我第一次找错了文件,向你说明)1. 先认错:我第一次检索错了文件我是在 packages/llm/llm-deepseek/src/serialize.ts
:26 function assistant(message: Message, model: string, onReplayDegrade?: (reason: string) => void): WireBlock[] {
:30 case 'text': return { type: 'text', text: block.text } ← 无守卫(你指的那处)
:119 ? assistant(message, options.model, onReplayDegrade) ← 历史重放的调用点2. ⭐ 而且同一文件里已经有一个正确的写法我在同一文件里核到 case 'text': return block.text.length === 0 ? [] : [{ type: 'text', text: block.text }]⇒ developer 路径已经过滤了空文本块,assistant 路径没有。 这就是确切的不对称,也让修法变成一句话:
建议你把这一点写进报告的诉求里——"同一文件已有正确写法,只差应用到重放路径"是维护者最容易接受的形式;比"请过滤空文本块"更强,因为它证明了团队自己认可的语义(developer 路径的行为就是规范)。 3. 关于你第二条补充(包内不含
|
|
Answer to your §4 (where the blank blocks come from) — plus a live before/after, because the same thing happened again in the same session while we were talking. 1. Origin: they come in on the provider stream, and only on our local route
We cannot see the raw SSE any more, but DSH has nothing to invent a 2. Live before/after, one session, same cloud provider and model
Why this is a real test rather than luck:
3. One correction to your §2The asymmetry you found is the right framing, but copying |
|
|
Closing the loop with a ready-to-apply source change, so nobody has to re-derive it. 1. The three sites, all with the same gap
2. Suggested diff- return message.content.map((block, index): WireBlock => {
+ return message.content.flatMap((block, index): WireBlock | [] => {
switch (block.type) {
- case 'text': return { type: 'text', text: block.text }
+ case 'text': {
+ const text = block.text ?? ''
+ // whitespace-only text between tool calls makes the Messages API split the
+ // message, so the earlier tool_use loses its immediately-following tool_result
+ if (text.trim().length === 0 && message.content.some(b => b.type === 'tool-call')) return []
+ return { type: 'text', text: block.text }
+ }plus Two notes from running this guard in production:
3. The dual-route contrast, as numbers
4. On the 400 bodyWhat was pasted is the complete record DSH retains: |
Uh oh!
There was an error while loading. Please reload this page.
Summary
assistant()in the DeepSeek adapter emits{ type: "text", text: "" }for empty text blocks when replaying history. Cloud endpoints reject such a request with HTTP 400, so any session whose retained history contains an empty text block breaks as soon as it is routed to a cloud model — switching models mid-session is enough to hit it.Where
@deepseek-ai/dsh-llm-deepseek→lib/index.js, functionassistant()(around L1539); the text case is the first branch of itsmessage.content.map(...):Why this looks like an oversight, not a decision
Two other content-to-block converters in the same file already guard the identical case:
if (block.type === "text") return block.text ? [{ type: "text", text: block.text }] : [];case "text": return block.text.length === 0 ? [] : [{ type: "text", text: block.text }];assistant()is the only one of the three without the guard, which matches "blank text block reaches the wire".Observed impact
invalid_request_errorclass) — the turn cannot be retried into success while the shape stays inside the retained window.Minimal repro
Suggested fix (one line)
(also worth dropping whitespace-only blocks, e.g.
block.text.trim().length === 0)Version matrix checked
assistant()guard0.2.0-rc.20.2.1-alpha.1lib/index.js, 101,115 bytes)So this is not fixed by upgrading within the published line. Happy to test a build with the guard if you want a confirming report.
All reactions