You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Missing tool-call name validation.@earendil-works/pi-ai/dist/api/openai-completions.js:298 coerces a missing tool name to "" (function?.name ?? custom?.name ?? ""). DSH then persists that block (append-only) and replays it verbatim; the upstream provider rejects every subsequent request with 400 ... messages[N].tool_calls[0] is missing a function name, making the session permanently unusable. Suggested fix: drop/reject blocks whose function.name is empty or not in the declared tool list, before persistence/serialization.
strict sampling never engages.resolveJsonSchemaStrictSampling only applies to tools declaring constrainedSampling:{type:"json_schema"}, which no DSH tool does, so strict:false is always sent — even for providers declaring compat.supportsStrictMode: true.
Regression note.dsh-tools/lib/invariant.js and dsh-session/lib/invariant.js are gone in 0.2.1-alpha.1; the former contained a dev-only exec.name.length === 0 ⇒ fail(...) assertion, so the only pre-existing empty-name guard disappeared with them. Please confirm that is intentional.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
工具调用名校验缺失:空名
tool_call会被落盘并在回放时把整个会话锁死(附:strict采样开关永不生效)背景(一句)
某第三方 OpenAI 兼容端点会把模型写在正文里的原生文本方言(尖括号
tool_call+function=前缀)当成真调用执行,并返回一条tool_calls[],其中name取了正文里的占位符或空串、arguments为{}。DSH 侧对此没有任何校验。问题 1(主项):空名工具调用无任何校验 → 脏消息落盘 → 会话永久报废
现场(不是理论风险,真实发生过)
unknown tool;name为空串的tool_calls[];400 Param Incorrect: messages[N].tool_calls[0] is missing a function name;同一次事故中,25 次工具调用里有 7 次是这种伪造调用(6 次占位名 + 1 次空名)。
代码坐标(0.2.1-alpha.1 实读)
@earendil-works/pi-ai/dist/api/openai-completions.js:298const name = toolCall.function?.name ?? toolCall.custom?.name ?? "";—— 空名兜底成空串,既不丢弃也不报错@deepseek-ai/dsh-llm-pi-ai/lib/index.js?? "unknown"兜底;序列化不校验、不过滤@deepseek-ai/dsh-tools/lib/index.js:2545UNKNOWN_TOOL只在「名字查不到」时抛出 —— 属事后路径;名字为空串的块在落盘/序列化前没有任何闸门@earendil-works/pi-ai/dist/api/openai-completions.js:1057if (!hasContent && !assistantMsg.tool_calls) {—— 只有 reasoning、无 content 无 tool_calls 的 assistant 消息被整条丢弃(模型看不到自己上一轮想过什么)期望
在工具调用进入管线时(落盘前 / 序列化前)补一条不变量:
短拦截即可(序列化时过滤空名块);若愿意做长拦截(dispatch 前拒绝未声明名),连
unknown tool那一类也能提前变成明确错误。关键是别让一条脏消息把整会话锁死。版本回归检查(0.2.0-rc.2 → 0.2.1-alpha.1)
v0.87.1),dist/api/openai-completions.js逐字节相同(SHA256A2397CB3…,64,355 B)⇒ 空名兜底行仍在dsh-tools/lib/index.jsrun_code的描述文案 / 参数顺序;无任何工具名校验dsh-session/lib/index.jsdsh-tools/lib/invariant.js与dsh-session/lib/invariant.js在 0.2.1-alpha.1 中已不存在(全树 grepinvariants.register零命中)。其中 dsh-tools 那份原本含一条exec.name.length === 0 ⇒ fail("tools/result execution must carry non-empty name and callId")的空名断言 —— 也就是说旧版至少还有一条 dev-only 的空名断言,新版连它都没了。若是随 invariant 服务一并有意外移,请确认这是有意的;如果不是,建议恢复这条断言。问题 2:
strict采样开关存在,但永不生效dsh-llm-pi-ai/lib/index.js在两版之间逐字节相同(SHA256D46DBC30…);resolveJsonSchemaStrictSampling首行即if (!config || config.type !== "json_schema") return undefined;⇒ 只对自行声明constrainedSampling: {type:"json_schema"}的工具生效;{name, description, parameters}(dsh-llm-pi-ai/lib/index.js约:1232-1234),没有任何工具声明constrainedSampling⇒ 所有工具都以strict: false发出;compat.supportsStrictMode: true(例如小米 mimo 的目录条目),这条能力永远走不到。期望:provider 支持时,允许 / 默认对 DSH 工具启用严格 schema 约束(至少提供打开方式),让端点能对参数生成施加约束。(它治不了上面的名字错位——那种错位发生在端点的文本解析层——但属于同一类的"把可用的约束真正用起来"。)
影响面
strict通道的收益。复现 / 验证建议
tool_calls[0].function.name === ""的 assistant 消息 → 断言序列化阶段被丢弃或拒绝(现行为是原样发出)。集成层面:用一次带空名块的回放请求观察端点 400 是否仍必然发生。supportsStrictMode的 provider 打桩 → 断言发出的tools[]中strict为true(或至少不恒为false)。我们这边的责任
我们承认客户端可以更早拦截(也已在自建规则层面做了概率性止损)。提这条 issue 的定位是纵深防御:根因在上游端点,但只要 DSH 侧加一道名字校验,同类端点故障的后果就从「会话报废」降到「少一次工具调用」。
English summary (for triage)
@earendil-works/pi-ai/dist/api/openai-completions.js:298coerces a missing tool name to""(function?.name ?? custom?.name ?? ""). DSH then persists that block (append-only) and replays it verbatim; the upstream provider rejects every subsequent request with400 ... messages[N].tool_calls[0] is missing a function name, making the session permanently unusable. Suggested fix: drop/reject blocks whosefunction.nameis empty or not in the declared tool list, before persistence/serialization.strictsampling never engages.resolveJsonSchemaStrictSamplingonly applies to tools declaringconstrainedSampling:{type:"json_schema"}, which no DSH tool does, sostrict:falseis always sent — even for providers declaringcompat.supportsStrictMode: true.dsh-tools/lib/invariant.jsanddsh-session/lib/invariant.jsare gone in0.2.1-alpha.1; the former contained a dev-onlyexec.name.length === 0 ⇒ fail(...)assertion, so the only pre-existing empty-name guard disappeared with them. Please confirm that is intentional.All reactions