If you loop over fullStream from streamText and switch on part.type with an assertNever fallback, bumping ai from 7.0.162 to anything from 7.0.163 up will break your type check. Nothing is wrong at runtime. The union just got two new members in a patch release.
I hit this on purpose to see exactly what changes, so here's the error, why it happens, and the fix.
The error
This is a normal exhaustive handler. It compiles cleanly against ai@7.0.162:
import type { TextStreamPart, ToolSet } from 'ai'; function assertNever(value: never): never { throw new Error(`Unhandled stream part: ${JSON.stringify(value)}`); } export function describePart(part: TextStreamPart<ToolSet>): string { switch (part.type) { case 'start': case 'start-step': case 'finish-step': case 'finish': return part.type; case 'text-start': case 'text-end': case 'reasoning-start': case 'reasoning-end': return part.type; case 'text-delta': case 'reasoning-delta': return part.text; case 'tool-input-start': case 'tool-input-delta': case 'tool-input-end': case 'tool-call': case 'tool-result': case 'tool-error': case 'tool-output-denied': case 'tool-approval-request': case 'tool-approval-response': return part.type; case 'source': case 'file': case 'reasoning-file': case 'custom': case 'raw': return part.type; case 'abort': return 'aborted'; case 'error': return `error: ${String(part.error)}`; default: return assertNever(part); } } Same file, same tsc (TypeScript 7.0.2), with ai@7.0.165 installed:
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'. Where the two new parts come from
Both landed in ai 7.0.163 (Nov 5, 2026):
-
pingis a typed streaming heartbeat. The release note is direct about it: "Consumers with exhaustive stream-part switches must handle the newpingunion variant when upgrading, even when the opt-in is disabled." According to the release note, Anthropic emits it only when you passincludePings: true(added in@ai-sdk/anthropic4.0.97, off by default). -
steering-messagecomes from the "stream steering messages to the ui" change. It shows up when a steering message gets added to a running agent loop, and it carriesid,message(the user message) andstepNumber.
So even if you never turn pings on and never use steering, your switch still has to account for both. The type is the contract, not what your provider happens to send today.
The fix
Add the two cases. A ping has nothing to render, so I return null and widen the return type:
export function describePart(part: TextStreamPart<ToolSet>): string | null { switch (part.type) { case 'ping': return null; // keep-alive only, nothing to render case 'steering-message': return `steering: ${part.id} (step ${part.stepNumber})`; // ...every case from before, unchanged... default: return assertNever(part); } } That compiles clean on 7.0.165.
What a ping looks like at runtime
I wanted to know where pings actually surface, so I fed streamText a MockLanguageModelV4 (from ai/test) that emits two ping chunks around some text, then read both fullStream and toUIMessageStream(). Output on ai@7.0.165, Node 22.23.4:
fullStream: start,start-step,ping,text-start,text-delta,ping,text-delta,text-end,finish-step,finish text: "Hello world" uiMessageStream: start,start-step,text-start,text-delta,text-delta,text-end,finish-step,finish That matches the release note. Pings come through fullStream, they don't end up in the generated text, and they're dropped from the UI message stream. So if your frontend only consumes useChat and the UI stream, you won't see them. The type error only bites code that walks fullStream (or the stream property) on the server.
This isn't the first time
The AI SDK changelog has flagged the same kind of break in patch releases before. 7.0.149 added interrupted to the finish-reason types and warned that exhaustive switches and Record<FinishReason, string> maps need updating. 7.0.129 did the same with a new max reasoning level. If you keep exhaustive checks (and I'd keep them, since they're how you find out about new parts at all), two habits help:
- Pin
aito an exact version and bump it in its own commit, so a redtscpoints straight at the upgrade. - Read the changelog for "exhaustive" before bumping. The maintainers have been good about calling these out.
The other option is dropping assertNever for a default: return null. That compiles forever, but then new parts like steering-message slip by silently, and that's arguably worse than a build error you fix in two lines.
Do you keep exhaustive switches over fullStream, or did you give up on them after the third patch-release break?
Tested Nov 9, 2026 with ai 7.0.162 and 7.0.165, @ai-sdk/anthropic 4.0.99 (type definitions only, no live Anthropic calls), TypeScript 7.0.2 and Node 22.23.4. The runtime output comes from a mock model, not a real provider stream.


