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):

  • ping is a typed streaming heartbeat. The release note is direct about it: "Consumers with exhaustive stream-part switches must handle the new ping union variant when upgrading, even when the opt-in is disabled." According to the release note, Anthropic emits it only when you pass includePings: true (added in @ai-sdk/anthropic 4.0.97, off by default).
  • steering-message comes 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 carries id, message (the user message) and stepNumber.

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 ai to an exact version and bump it in its own commit, so a red tsc points 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.