llm-pi-ai 跨 provider 重放丢失 reasoning_content,DeepSeek 兼容路由多轮工具调用 400 #4745
Replies: 5 comments
|
上游 pi 库同步报告:https://github.com/earendil-works/pi/issues/8728(根因一半在 pi-ai 的 detectCompat / transformMessages)。 附最小复现配置(settings.yaml,密钥字段已省略): llm-pi-ai:
providers:
b-ai:
api: openai-completions
baseURL: https://api.b.ai/v1
apiKeyEnv: B_AI_API_KEY
models:
- id: deepseek-v4-flash
openrouter:
apiKeyEnv: OPENROUTER_API_KEY
models:
- id: stealth/ox-alpha触发路径:同一会话先走 openrouter 产生带思考的 assistant 消息,再切到 b-ai 继续多轮工具调用 → b-ai 请求中这些历史消息变为"带 tool_calls、无 reasoning_content",API 返回 400。 复现日志关键片段(会话事件序列): |
|
这个根因定位和 rc.2 的 DSH 边界是吻合的。补充一个当前版本可以直接做的配置级 A/B: providers:
b-ai:
api: openai-completions
baseURL: https://api.b.ai/v1
compat:
thinkingFormat: deepseek
requiresReasoningContentOnAssistantMessages: true
models:
- id: deepseek-v4-flash
# 其余容量与 reasoningEfforts 按端点真实契约填写建议先在复制的 Session / 临时 Profile 中验证四组对照:fresh Session、同 Provider 重放、跨 Provider 纯文本、跨 Provider reasoning + tool call。还要抓取脱敏后的目标请求键,确认字段确实出现在第一条失败历史消息上。 这里需要区分两个结论:
所以不建议直接修改 Session JSONL、复制跨 Provider 签名或伪造隐藏 reasoning。如果显式字段存在后 Endpoint 仍拒绝,应把它归类为更强的语义 passback 要求,优先保持同 Provider 历史或新建目标 Provider Session。 我依据 rc.2 的 其中私有网关行为仍标记为社区观察,没有把空字段能否满足具体网关写成通用结论。 |
|
补一个可执行的 exact-head 证据,避免把配置建议停留在推测层面。 我在官方 同一条历史 assistant 消息来自
还有一个必要前提:所选自定义模型必须通过 catalog 或 验证:目标 adapter suite 48/48、package TypeScript build、定向 lint、 因此,rc.2 当前最安全的 DSH 处置是显式描述目标 endpoint 的 compat 契约与模型 reasoning 能力,而不是按 状态更新:上游 pi PR #8732 已提交,但随即被 contributor gate 自动关闭,尚未获得 maintainer approval 或合并。该 PR 的 DeepSeek-family 自动识别与 signed-thinking 保留方案可作为后续候选对照,但不等同于这里对“目录未识别的自定义 route”所验证的显式 compat 路径。 |
|
上游 pi 库已提交修复 PR:https://github.com/earendil-works/pi/pull/8732(跨模型重放进 DeepSeek 系端点时保留 |
|
上游 PR 之外补一组 transform 层的系统测量,把这个 400 的"哪层能修/哪层修不了"钉死(同族 #3857/#1850/#231 我们贴过完整版): 把带 thinking 块的跨 provider 助手历史喂进真 pi-ai 含义:思考模式后端要求的是那个字段,而现有 transform 从设计上就不产它——所以你观察到的 400 在"换 provider 组合"层面无解,必须等你引的上游修复(按字段回传)落地;楼上给的 exact-head 证据 + 上游 PR 正是对的路径。我们的数据可作该 PR 的回归夹具参考(含 toolCall+thinking 的边界形状)。 临时缓解(不修根因):把该会话切到非思考模式的路由继续用——内联形态的历史它是接受的(我们的 live 腿就是这么跑通的)。 |
Uh oh!
There was an error while loading. Please reload this page.
使用 llm-pi-ai 配置的 DeepSeek 兼容 provider(如 api.b.ai)时,若会话历史中混有其它 provider(如 openrouter)产生的思考消息,重放时这些消息丢失
reasoning_content,API 在思考模式下返回 400The reasoning_content in the thinking mode must be passed back to the API,且该错误不可重试,整轮直接失败。复现、预期与验收
settings.yaml中配置两个 pi-ai provider:b-ai(api: openai-completions,baseURL: https://api.b.ai/v1,模型deepseek-v4-flash)和openrouter(模型stealth/ox-alpha)。turn/end报 400。inference tpm exhausted,与本文无关)。reasoning_content(或对 DeepSeek 兼容端点自动补空reasoning_content),不触发 400。@deepseek-ai/dsh-llm-pi-ai0.1.1-rc.2;@earendil-works/pi-ai0.82.1。reasoning_content;deepseek.com域名)自动启用requiresReasoningContentOnAssistantMessages兜底。排查到的根因(供维护者参考)
pi-ai 序列化只对"同模型 + 带 thinkingSignature"的消息回传
reasoning_contentopenai-completions.js的 wire 序列化中,只有 thinking 块带非空thinkingSignature时才写assistantMsg[signature] = ...;而transform-messages.js对跨模型 assistant 消息(来源 provider ≠ 当前请求 provider)把 thinking 块降级为纯文本("convert others to plain text"),于是产生"带 tool_calls 但无 reasoning_content"的 assistant 消息——正是 DeepSeek 思考模式下拒绝的形状。requiresReasoningContentOnAssistantMessages兜底只对deepseek.com域名自动开启detectCompat()中该字段 =provider === "deepseek" || baseUrl.includes("deepseek.com")。api.b.ai、token.sensenova.cn等 DeepSeek 兼容端点不满足,没有兜底,历史一旦含裸 tool-call 消息即 400。对照实现:
dsh-llm-deepseek(deepseek-official 路由)是无条件回传其
serializeAssistant直接reasoning.length > 0 ? { reasoning_content: reasoning } : {},不依赖签名与来源,因此官方路由无此问题。上游已知同类问题:earendil-works/pi [Bug] 工作区选择根目录后无法在该工作区创建对话,并跳转至其他工作区 #7702、按场景注入 vs 隔离在存储层:两种记忆注入范式的本质差异 #3668、dsh-xray — 给你的 Harness 拍 X 光:组合树归因、依赖级联、上下文成本 / Composition X-ray: layer attribution, disable-cascade, context cost #3743 及 PR [Bug] 0.1.7-rc.1:首次切换推理档位后 sessionController 失效,后续切换失败 #7701 均处理过 "reasoning_content must be passed back";DSH 侧 llm-pi-ai 仍会经跨模型降级路径触发。
建议修复方向
requiresReasoningContentOnAssistantMessages的自动开启条件从"域名含 deepseek.com"扩展为按模型/厂商判断(如 deepseek-v4-flash 及其兼容端点);或reasoning_content字段而非并入content。All reactions