Skip to content

@ai-sdk/anthropic: message_start for a different message while the previous message is still open is silently merged into one generation, corrupting thinking blocks and failing all subsequent requests #18331

Description

@ro-hansolo

Description

When a streaming intermediary loses its upstream Anthropic connection mid-generation and retries, the retried generation's SSE events can be spliced into the same HTTP response body. The stream then contains a second message_start (with a new message id) while the first message is still open. We observed this in production behind Google Vertex via @ai-sdk/google-vertex/anthropic: the first generation's tool_use block was cut mid input_json_delta and never received content_block_stop, and a full second generation followed on the same stream.

The @ai-sdk/anthropic parser accepts this silently. The message_start handler tracks no message lifecycle state and only overwrites usage counters. The unterminated tool_use block is dropped without a trace, because a tool-call part is only emitted on content_block_stop. Both generations' signed thinking blocks stream through as if they were one response, and streamText records them into a single step.

That step cannot be replayed. The assistant turn now contains a signed thinking block from a generation that, from Anthropic's perspective, never completed. Every subsequent request in the tool loop is rejected with:

AI_APICallError: messages.N.content.M: `thinking` or `redacted_thinking` blocks in the
latest assistant message cannot be modified. These blocks must remain as they were in
the original response.

This is a non-retryable 400, so maxRetries does not engage and the run ends. In our incident this happened 27 steps into a 34-minute agentic run with adaptive thinking (about $20 of API spend), which then failed without producing its deliverable. The corruption is persistent: once the merged step is in the run's history, every later step fails identically.

A related, milder shape exists that the fix must not break: some Anthropic-compatible gateways duplicate the message_start frame of a single response (same message id). Today that duplicate produces a spurious second response-metadata part and overwrites usage counters, but the stream otherwise works.

Root cause

packages/anthropic/src/anthropic-language-model.ts, doStream transform, case 'message_start'.

Multiple message_start/message_stop sequences per stream are legitimate (programmatic tool calling streams several complete messages per response). What cannot occur in a valid response is a message_start while the previous message is still open. The parser does not detect this. By the time the second message_start arrives, the earlier generation's completed blocks have already been emitted downstream, so the stream cannot be repaired at this layer, but it can be failed explicitly instead of merging two generations into one.

Reproduction

Raw SSE sequence, same shape as the incident. Also included as failing tests in the linked PR; both fail on current main.

message_start (msg_first)
content_block_start    thinking
thinking_delta / signature_delta / content_block_stop
content_block_start    tool_use (toolu_first)
input_json_delta       {"value":"Spark        <- stream cut mid-input, no content_block_stop
message_start (msg_second)                     <- spliced retry, previous message still open
content_block_start    thinking ... (complete)
content_block_start    tool_use (toolu_second) ... (complete)
message_delta stop_reason=tool_use / message_stop

For the benign gateway shape: two identical message_start frames (same id) followed by a normal single-message stream.

Actual

The spliced stream emits msg_second's response-metadata, both thinking blocks, and toolu_second as one merged generation, with no error. The next request built from this history fails deterministically with the 400 above. The same-id duplicate emits a spurious second response-metadata part.

Expected

A duplicated message_start frame for the already-open message is ignored. A message_start for a different message while the previous message is still open fails the stream with an explicit error part (InvalidResponseDataError), so the step errors cleanly and no corrupted history is recorded. Programmatic tool calling (message_stop followed by message_start) is unaffected.

Corroboration

This is not specific to one intermediary. The Anthropic streaming spec allows exactly one message_start per response, but gateway implementations have been documented violating it: maximhq/bifrost#4556 (two message_start frames, same message id, on the Anthropic passthrough) and BerriAI/litellm#32479 (duplicate message_start per bridged stream, since fixed). The spliced-generation variant reported here was reconstructed from the persisted parts of a production incident: an unterminated tool_use input frozen mid-JSON, followed by a regenerated near-duplicate thinking block and a second tool call with a different id, with no step boundary between them. We did not capture the raw wire stream. Handling of Vertex's vertex_event (#13576) is precedent for accommodating intermediary stream behavior in this package.

Environment

ai 7.0.41, @ai-sdk/anthropic 4.0.23 (via @ai-sdk/google-vertex 5.0.34). Reproduced against current main (@ai-sdk/anthropic 4.0.27).

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions