Repository navigation
Conversation
There was a problem hiding this comment.
注:文中“V4 层”的代码位置,以及未注明 master 的 aimux 路径和行号,均以 #256 的提交 65ca6be 为准(#249–#256 按顺序叠放,这个提交包含全部各层):https://github.com/arcships/aimux/tree/65ca6be74515374ea89eb4f25717b4ac42ca2911
| |---|---|---| | ||
| | T1 | 不在公共 wire 引入 `ToolInput::Raw/Parsed`;用不同层 DTO 的 `String` 与 JSON value 区分 | #185 的具体实现方案改写,保留消除混层的目标 | | ||
| | T2 | 删除 `aimux-stream` 公共 tracker 和第二套 `ToolCallStreamPart`;在 provider-utils 保留单一协议内部累积器 | 接受 #204 的归属方向,不接受“整套累积算法纯删除即完成”;见 §4 | | ||
| | T3 | 无法唯一归属的工具增量不静默丢弃,终止该模型流并报告 `InvalidResponseData` | 与 #204 `861f26e` 的 ambiguity-drop 行为不同,需维护者明确接受及差分测试 | |
There was a problem hiding this comment.
§4 的 tracker 规则有三处和上游 StreamingToolCallTracker 不同,实现时都碰到了。我们的实现是上游该文件的移植(aimux-provider-utils/src/streaming_tool_call_tracker.rs)。
- 歧义增量(T3,第 31 行):T3 要求无法唯一归属的工具增量报 InvalidResponseData 并终止流。上游直接丢弃:@ai-sdk/provider-utils@5.0.51 src/streaming-tool-call-tracker.ts:142-144 遇到 ambiguous 直接 return;未关联的空白 name 增量也忽略(:151-153);只有新调用缺 name 才抛 InvalidResponseDataError(:379-384)。
§4.2 的七条规则没有覆盖“新 id + 空参数”:按规则 2 空参数不算开始证据,结果要么附加,要么按规则 5 报错终止。5.0.51 在这里有 bug(同 index 上新 id、同名、首片段参数为空的调用会被并进前一条,:270-278),5.0.55 修了(CHANGELOG “preserve parallel tool calls when stream labels are missing or reused”,5.0.55 :204、:280-285)。§4.2 的两种结果都和 5.0.55 不一样。我们照搬了 5.0.51 的这个 bug,修复见 Security and bug fixes needed on master #228 (B10)。 - 结束(§4.3,第 135 行):
- 上游只在流 flush 时结束全部调用(提前结束会用到被截断的参数,5.0.51 :424-427);全部调用都有 index 时按 index 排序,否则按到达顺序(:177-192),不是“依首次出现顺序”。
- 表里没有流内 error 帧:上游 OpenAI chat 发 error part、把 finishReason 记为 error,之后照常 flush 出 tool-call 和 finish(@ai-sdk/openai@4.0.80 src/chat/openai-chat-language-model.ts:560-568、:656-668),Groq、openai-compatible 相同;只有 OpenAI 对首个输出之前的 error 帧直接抛 APICallError(src/openai-stream-error.ts:39-116)。
- EOF 缺 finish_reason 各包不同:OpenAI chat、Groq 照常发 tool-call 和 finish(unified 默认 other,openai-chat-language-model.ts:522-525);openai-compatible 先 flush tool-call,再发 error part 和 finish(error)(@ai-sdk/openai-compatible@3.0.59 src/chat/openai-compatible-chat-language-model.ts:723-733)。两者都不丢已累积的 tool-call。
- 上限(第 143 行):上游 tracker 无界追加(5.0.51 :497-499),SSE 解析也不设 maxBufferSize(src/parse-json-event-stream.ts:20)。docs: full-chain AI SDK alignment design and impact map #200 的影响面地图把旧的 SSE 单事件 1 MiB 上限列为缺陷 as:STREAM-1(docs/aisdk-architecture-impact-map.md:120、:558),master 上的 feat(stream)!: decode SSE with the sse-stream crate, aligned with eventsource-parser #213 已按上游去掉(aimux-stream/src/sse.rs:19)。固定默认上限加“超限报错关流”,等于把同一个缺陷搬到 tracker 层。
建议:
- T3 和 §4.2 改为“关联语义等于登记基线版本上游 tracker 的 correlation 表(基线至少 provider-utils 5.0.55):歧义增量丢弃,不报错、不终止;差分用例取自上游该文件的测试”。第 35 行“在确定前阻塞实现合入”随之去掉。
- §4.3 完成行改为“flush 时结束;全部有 index 时按 index 排序(同 index 保持到达顺序),否则按到达顺序”;新增“流内 error 帧 / 解析失败:发 error part,finishReason=error,之后照常 flush”;EOF 行改为“按各包上游处理,已累积的 tool-call 照常 flush”。“不补发 tool-call/finish”只留给传输错误、取消和消费者 drop。这也回答了 RFC-0040 B7 留给本 RFC 的“error 后是否 Finish”。
- 上限默认不设(与上游一致),可另给可选配置;§9 不再把补齐默认值作为实施前提。
| | `123` / `true` / `null` | number / bool / null | 支持 JSON primitive;schema 是否允许另行判断 | | ||
| | `{"a":1,"a":2}` | 解析器最终对象语义 | repair 错误必须保留含重复 key 的原文 | | ||
| | ` 1e2 ` | 数字值 | 不宣称解析后保留数字拼写和空白 | | ||
| | 空文本、`{"a":` | 非法调用及错误 | 不补成 `{}`,不自动 partial-JSON repair | |
There was a problem hiding this comment.
§3.2、§3.3、§5 有几处和上游 parseToolCall 及各厂商编码器不同,实现时都碰到了。我们按上游实现(aimux-core/src/parse_tool_call.rs)。
- 空参数(第 91 行):上游在 input.trim() === '' 时用 {} 做 schema 校验(ai@7.0.122 src/generate-text/parse-tool-call.ts:251-256,注释说很多模型对无参数工具返回空串;provider-executed 动态工具同样,:202-205)。按表里“空文本 → 非法调用,不补成 {}”,无参数工具在 aimux 里会成为非法调用,在 AI SDK 里是合法调用。
- 非法调用的 core input:上游是“原文能解析就用解析值,否则用原文字符串”(parse-tool-call.ts:116-117),所有失败都一样;下一轮 responseMessages 里,非对象的非法 input 写成 {}(src/generate-text/to-response-messages.ts:93-94)。我们照上游实现后踩过一次:从非法调用重建原始调用时用了 input.to_string(),原文 {broken 变成了 JSON 字符串字面量 "{broken"(已修,见同文件 raw_from_invalid)。
- 修复后再校验(§5 第 3 条,第 151 行):上游只把 repair 函数自己抛出的异常包成 ToolCallRepairError(parse-tool-call.ts:90-96);修复结果的重新解析在这个 try 之外(:103-106),失败时返回保留原调用 id/name/input 的非法调用,error 是修复后调用的解析/校验错误本身,不包装(:112-134)。master 上 RFC-0035 §3.2 写的也是包装,会随对应的代码 PR 按上游修订。[1/8] chore: CI on every PR and test fixture cleanup #249 删除了 contract-tests/fixtures/tool-call-repair.json:它记录的是对齐前 Rust 自己的输出,其中 reply_repaired_still_invalid 钉住的正是这个包装,所以 §8.1 的“RFC-0035 全 fixtures”(第 248 行)不再适用。第 7 条的“generateObject 嵌套 raw 路径”(第 155 行)对应的字段在 docs: full-chain AI SDK alignment design and impact map #200 文档的删除清单里(docs/aisdk-architecture-alignment.md:2730-2731)。
- 非对象参数的重编码(第 99 行):primitive 作为合法工具输入时,§6.2 / §8.1 的“下一轮重编码断言”(第 187、249 行)容易让人以为 "Tokyo" 在下一轮请求里仍是 "Tokyo"。上游各厂商编码器并不保值:OpenAI chat 写成 "{}"(@ai-sdk/openai@4.0.80 src/chat/convert-to-openai-chat-messages.ts:15-21、:287);Bedrock 包成 {rawInvalidInput: input}(@ai-sdk/amazon-bedrock@5.0.100 src/convert-to-amazon-bedrock-chat-messages.ts:694-699);Groq、DeepSeek 写 JSON.stringify(input),null 变成 "null"(@ai-sdk/groq@4.0.52 src/convert-to-groq-chat-messages.ts:105)。我们逐个照搬。
建议:
- 表中空文本那行拆成“空文本或仅空白 → 以 {} 走 schema 校验(与上游一致)”和“不完整 JSON → 非法调用,不做 partial-JSON repair”。
- §3.3 加一条“非法调用的 core input 与上游一致:可解析时为解析值,否则为原文字符串;原文只从 error 的 toolInput 读取,不从 input 推断”,并加对应向量。
- §5 第 3 条改为“与上游 parseToolCall 一致:只包装 repair 函数抛出的错误;修复结果再解析失败时返回原调用的非法调用,error 为修复后调用的解析/校验错误”;第 7 条去掉 raw 路径;§8.1 修复行改为“以上游 parse-tool-call 行为为准的用例”。
- §3.3 或 §6.2 补一句:“工具参数的厂商编码遵循各包上游编码器,包括对非对象 input 的规范化;重编码断言与固定基线的上游请求比较,不要求还原 core 值。”
|
|
||
| ### 7.3 版本、确定性与门禁 | ||
|
|
||
| manifest 记录固定 SDK 基线(#200 使用 `ai@7.0.122`、`@ai-sdk/provider@4.0.19`)、生成器版本、输入 hash、schema/type revision。不得依赖在线 latest。manifest hash 是产物一致性标识,不把每次 hash 变化当成新握手协议版本。 |
There was a problem hiding this comment.
§7.3 的 manifest 只记 ai@7.0.122 和 @ai-sdk/provider@4.0.19,但本 RFC 要差分的行为(tracker、空参数、编码器、EOF)都在 @ai-sdk/provider-utils 和各厂商包里;#200 文档的基线已经列出 16 个包(docs/aisdk-architecture-alignment.md:7)。实现中我们已经遇到两套版本并存:移植代码对照 ai 7.0.122 / provider-utils 5.0.51,而 fixtures/aisdk 是在 ai 7.0.127 / provider-utils 5.0.53 / openai 4.0.83 下录的(fixtures/aisdk/VERSIONS.json)。
上游发版也很快:ai 从 7.0.122(09-28)到 7.0.137(10-09)发了 14 个补丁版;provider-utils 5.0.51 之后到 10-08 发了 7 个,其中 5.0.55 修了 tracker 的并行调用合并 bug(见 T3 的评论),我们这边的修复见 #228 (B10)。
还有一处会直接影响生成的类型:master 上的 Decision API(RFC-0037/0038)就是上游实验性的 evaluate。@ai-sdk/provider 4.0.19 里它叫 EvaluationModelV4(src/evaluation-model/v4/),4.0.22 改名为 DecisionModelV4 / doDecide(src/decision-model/v4/decision-model-v4.ts:6-17),错误名也从 AI_EvaluationUnsupportedQuestionTypeError 改为 AI_DecisionUnsupportedQuestionTypeError。§7.2 要求 manifest 覆盖各模态,如果按 4.0.19 生成,Decision 的类型名和错误名就是改名前的。
建议:manifest 按包记录完整基线(直接引用 #200 文档的清单),其中 @ai-sdk/provider 至少到 4.0.22;加一条升级规则,例如“影响已移植行为的上游修复(以 CHANGELOG 为准)单独升级并附差分,其余定期批量升级”;并写明 §8.1 的行为差分对哪一份基线。
覆盖 #185、原文/解析后工具参数、metadata 保真、D8 及旧 L2 选型节点的替代关系。
状态与依赖
沿用 #200 的四层类型和唯一生成链;与 #204 不同的歧义处理明确列为待接受提案,不作为既定实现结论。
本 PR 属于完整 roadmap RFC 批次。整批已完成节点覆盖与跨文档契约复核,再按主题分别提交 Draft。各 PR 均独立基于 master,不叠加其他未合并文档。设计齐备不表示运行时实现或验收已完成。
验证
同批设计(均独立基于 master)