AI SDK 补丁升级触发穷尽分支报错
作者报告,AI SDK 从 7.0.162 升至 7.0.163 后,流事件联合类型新增两个成员,导致使用 assertNever 的穷尽 switch 无法通过类型检查。文章围绕复现代码解释类型扩展造成的编译影响。
AI 深度解读
把 ai 从 7.0.162 升到 7.0.163 及之后的版本,运行时未必出问题,TypeScript 检查却可能先失败。触发条件很具体:代码遍历 streamText 的 fullStream,用 switch 处理事件,并通过 assertNever 检查是否遗漏分支。
作者主动复现了这次变化:补丁版本给联合类型增加了两个成员,原来的完整处理器因此不再完整。这篇最有用的地方,是分清了类型契约、实际事件和前端展示之间的边界,也解释了为什么这类编译错误值得保留。
没开启新功能,为什么也得改代码
问题来自 TextStreamPart<ToolSet>。这是一个联合类型,part.type 决定当前事件是哪种成员。代码处理完全部成员后,TypeScript 会把剩余可能性收窄到 never,因此可以调用只接受 never 的 assertNever。
一旦联合类型增加成员,原来的分支就不够了。
作者使用同一份代码、同一个 TypeScript 7.0.2 编译器,在 ai@7.0.162 下通过检查,安装 ai@7.0.165 后出现以下错误。它直接指出了遗漏的类型:
src/handle.ts(43,26): error TS2345: Argument of type 'TextStreamPingPart | TextStreamSteeringMessagePart' is not assignable to parameter of type 'never'.
Type 'TextStreamPingPart' is not assignable to type 'never'.
要看的是第一行:传给 assertNever 的参数仍有两种可能,所以不能作为 never 使用。
两个成员都加入于 2026 年 11 月 5 日发布的 ai 7.0.163:
ping是带类型的流式心跳事件。按原文转述的发布说明,Anthropic 只有在传入includePings: true时才发送它;这个选项在@ai-sdk/anthropic 4.0.97加入,默认关闭。steering-message表示向运行中的 agent 循环加入引导消息,携带id、用户消息message和stepNumber。
编译器检查的是类型声明允许出现什么,而不是当前配置会发送什么。
因此,关闭心跳、不使用引导消息,都不能消除这个编译错误。它们限制了运行时行为,没有缩小处理函数接收的联合类型。
两个新分支,应该分别怎么处理
作者选择继续保留穷尽检查,明确处理新增事件。心跳没有展示内容,返回 null;引导消息返回包含标识和步骤编号的字符串:
export function describePart(part: TextStreamPart<ToolSet>): string | null {
switch (part.type) {
case 'ping':
return null; // 仅用于保活,没有需要渲染的内容
case 'steering-message':
return `steering: ${part.id} (step ${part.stepNumber})`;
// ...every case from before, unchanged...
default:
return assertNever(part);
}
}
要留意函数签名里的 string | null:调用方现在需要接受没有展示内容的结果。这段是修改示意,注释位置仍要保留原来的全部分支,不能直接当成完整处理器使用。作者补齐后,在 7.0.165 下通过了编译。
这里还有个容易忽略的区别:示例只是描述 steering-message,没有展示其 message 内容,也没有实现完整 UI。真实项目该如何记录或展示这条消息,需要由业务决定。
另一种处理是让默认分支直接返回 null。这样以后增加成员也不会触发同类检查失败,但新增事件会被静默忽略。对 ping 可能无所谓,对需要反馈给用户的消息就未必合适。
前端用 useChat,会不会收到心跳
作者用 ai/test 提供的 MockLanguageModelV4 构造事件流,在文本片段前后插入两个 ping,然后分别读取 fullStream 和 toUIMessageStream()。
在 ai@7.0.165、Node 22.23.4 下,结果是:
fullStream能读到两个ping。- 最终文本仍是
Hello world,心跳没有混进文本。 - UI 消息流过滤了
ping,只保留其他对应事件。
因此,按这次验证,仅通过 useChat 消费 UI 消息流的前端不会看到这些心跳。需要优先排查的是服务端遍历 fullStream 或 stream 属性,并进行穷尽检查的代码。
证据也有边界。测试日期是 2026 年 11 月 9 日,使用了 ai 7.0.162 和 7.0.165;@ai-sdk/anthropic 4.0.99 只用于类型定义,没有发起真实 Anthropic 请求。
运行时结果证明了模拟输入经过 SDK 后的处理行为,没有验证真实提供商的发送行为。
原文也没有提供 steering-message 的同等运行时验证,不能把心跳实验的结论直接套给它。
我的判断
保留 assertNever 的建议有道理。它让依赖升级带来的契约变化变成一个明确的编译错误,维护者可以逐项决定怎么处理。代价是升级时必须处理新增分支。
但这确实给补丁升级增加了维护成本。原文还列出两次类似变化:7.0.149 为结束原因类型增加 interrupted,影响穷尽分支和 Record<FinishReason, string> 映射;7.0.129 增加推理级别 max,也有类似提醒。
作者建议锁定 ai 的精确版本,单独提交升级,并在 changelog 中搜索 exhaustive。这套做法能帮助定位错误,但原文没有提供与宽松默认分支、其他升级流程的对照数据,应当视为维护经验。
我的取舍是:对需要理解每种事件的处理器,保留穷尽检查;对明确只关心少数事件的消费者,可以有意识地忽略其余事件。关键是把忽略变成设计决定,而不是为了让构建变绿顺手删掉检查。
能拿来做什么
- 升级
ai前,检查 Node 服务中遍历fullStream的switch,为ping和steering-message分别确定处理方式,并检查调用方能否接受null。 - 如果前端只使用
useChat和 UI 消息流,先确认事件入口,再决定是否修改展示逻辑;作者的模拟实验显示,ping已在 UI 消息流中被过滤。 - 把
ai升级放进独立提交,保留明确的版本记录,并搜索 changelog 中的exhaustive,同时检查FinishReason映射等相似位置。 - 给流处理逻辑做验证时,可以参考作者使用
MockLanguageModelV4的办法,检查事件是否进入文本或 UI 流;涉及真实提供商的发送条件时,需要另行验证。