The complaint was that one model could not answer questions about email. Ask it anything about the inbox and the bubble came back empty. Ask the same question of two other models and they answered. Ask the same model about the calendar and it answered as well.

All of those observations were correct. The conclusion drawn from them was wrong in every particular.

What the server saw

Nothing. That is the entire difficulty.

The request logged a 200. There was no error line, no stack, no elevated latency and no failed invocation. Adding logging did not help, because the logging lived in the finish callback and the finish callback never ran.

The platform was not hiding anything. From where it stood, the response really did start successfully. It simply never ended.

The part that was never sent

The provider package emits two different stream parts out of a thinking block. It emits a reasoning part when text arrives inside the block, and a separate reasoning-signature part when the block is signed.

Claude 5 thinks adaptively. It is free to open a thinking block, decide there is nothing worth thinking about, and close it. Signed, but empty.

That produces a signature with no reasoning in front of it, and the SDK core throws on exactly that shape:

InvalidStreamPart: reasoning-signature without reasoning

It is a real invariant and the check is correct. The problem is where it fires.

Why it was invisible

The throw happens inside the stream transform. Not in the handler, not in a promise the route is awaiting. Inside the transform that is already piping bytes to the client.

By then the response has begun. The status line is out. So the error cannot travel back through any of the machinery built to catch errors: the error callback does not fire, the finish callback does not fire, the framework notes that it failed to pipe a response, and the request is recorded as a healthy 200 because that is genuinely the status that was sent.

Why it looked like email

This is the part I got wrong for six sessions, and it was not the SDK’s fault.

The bug fires whenever the model opens a thinking block and puts nothing in it. Nothing about that is specific to any topic. But an easy question does not make a model think at all, so it never opens the block, so it never hits the bug. Calendar questions survived because they were too easy.

The harder questions were the ones about the inbox. So a defect that depended purely on whether reasoning was triggered presented itself as a defect about subject matter, and it presented so consistently that it went into the bug list as a capability gap rather than as a crash. It stopped being a thing to debug and became a thing the model apparently could not do.

The fix is ten lines

A middleware that watches the stream and drops a reasoning-signature that has no reasoning in front of it. The rule is exactly core’s own state machine: a signature is valid only if a reasoning part has arrived since the last one. Anything else is dropped, and only that part is dropped.

Discarding it costs nothing here. The signature is an integrity token for replaying thinking blocks back to the provider on a later turn, which this codebase does not do.

It is applied to every model rather than only to Anthropic’s. Any provider that can stream reasoning can produce the same shape, and the middleware is inert for streams that do not.

Not an upgrade

The obvious alternative was to move to the next major version of the SDK, where this is fixed.

That version is a rewrite, and this codebase is deliberately built against internals of the current one. Swapping the SDK to repair a one-part stream defect would put every one of those at risk in a single change, to fix something that ten lines fix locally. The middleware also deletes itself the day the upstream fix lands on a version worth taking.

Picking the smaller change was not caution. It was scope: the bug is one stream part wide and the remedy should be too.

What I would take from this

When a stream dies with neither an error callback nor a finish callback, stop reading production logs. They do not have it. The error is being thrown somewhere the observability was never wired to reach, so more logging on the same path returns more nothing.

Pulling the real credentials down and running it locally printed the entire stack trace on the first attempt, after hours of production logs had shown a clean 200 every time.

The other half is slower to learn. A bug that reproduces reliably under some description will get filed under that description, and once it is filed as behaviour it stops being investigated. Six sessions of it being true that the model could not answer email never once made it true.