Skip to content

RFC-0042: V4 类型边界与统一生成 - #212

Draft
eric8810 wants to merge 1 commit into
masterfrom
docs/rfc-0042-v4-type-boundaries
Draft

eric8810 wants to merge 1 commit into
masterfrom
docs/rfc-0042-v4-type-boundaries

Conversation

@eric8810

@eric8810 eric8810 commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

覆盖 #185、原文/解析后工具参数、metadata 保真、D8 及旧 L2 选型节点的替代关系。

状态与依赖

沿用 #200 的四层类型和唯一生成链;与 #204 不同的歧义处理明确列为待接受提案,不作为既定实现结论。

本 PR 属于完整 roadmap RFC 批次。整批已完成节点覆盖与跨文档契约复核,再按主题分别提交 Draft。各 PR 均独立基于 master,不叠加其他未合并文档。设计齐备不表示运行时实现或验收已完成。

验证

  • 文档编号、代码围栏、相对链接、空白检查通过
  • 本 PR 仅修改 rfc/0042-v4-type-boundaries.md
  • 本地未运行构建、运行时测试、性能实测或真实 provider 探测;CI 状态以本 PR 当前 head 的检查结果为准

同批设计(均独立基于 master)

@cunninghamcard-bit cunninghamcard-bit left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注:文中“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 行为不同,需维护者明确接受及差分测试 |

@cunninghamcard-bit cunninghamcard-bit Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

§4 的 tracker 规则有三处和上游 StreamingToolCallTracker 不同,实现时都碰到了。我们的实现是上游该文件的移植(aimux-provider-utils/src/streaming_tool_call_tracker.rs)。

  1. 歧义增量(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)。
  2. 结束(§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。
  3. 上限(第 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 |

@cunninghamcard-bit cunninghamcard-bit Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

§3.2、§3.3、§5 有几处和上游 parseToolCall 及各厂商编码器不同,实现时都碰到了。我们按上游实现(aimux-core/src/parse_tool_call.rs)。

  1. 空参数(第 91 行):上游在 input.trim() === '' 时用 {} 做 schema 校验(ai@7.0.122 src/generate-text/parse-tool-call.ts:251-256,注释说很多模型对无参数工具返回空串;provider-executed 动态工具同样,:202-205)。按表里“空文本 → 非法调用,不补成 {}”,无参数工具在 aimux 里会成为非法调用,在 AI SDK 里是合法调用。
  2. 非法调用的 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)。
  3. 修复后再校验(§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)。
  4. 非对象参数的重编码(第 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 变化当成新握手协议版本。

@cunninghamcard-bit cunninghamcard-bit Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

§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 的行为差分对哪一份基线。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants