Skip to content

[Bug]: Streaming /v1/responses drops Anthropic redacted_thinking blocks, breaking multi-turn tool use with extended thinking #5093

Description

@fus3r

Prerequisites

  • I have searched existing issues and discussions to avoid duplicates
  • I am using the latest version (or have tested against main/nightly)

Description

When Anthropic's safety systems flag part of Claude's reasoning, the API returns it as a redacted_thinking content block (an encrypted data payload) instead of a thinking block, and requires these blocks to be passed back unmodified in multi-turn tool use; their docs call out dropping them as the most common cause of the 400 error `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified.

On the responses surface (/v1/responses with an Anthropic provider) the blocks are lost twice, in core/providers/anthropic/responses.go:

  1. Streaming responses drop them entirely. The content_block_start handler in ToBifrostResponsesStream switches over the block type with cases for text, tool_use, MCP, compaction, and thinking, but has no case for redacted_thinking, so the block is silently discarded (redacted_thinking arrives complete in content_block_start; the SDKs model it as a start-event union member with no deltas). The non-streaming converter preserves the same block as a reasoning output item carrying encrypted_content.

  2. Request conversion drops them again on replay. The reasoning items both converters produce for redacted thinking carry an empty, non-nil summary list next to encrypted_content, and convertBifrostReasoningToAnthropicThinking checks Summary != nil before falling back to encrypted_content. An empty list passes the nil check, the loop emits zero thinking blocks, and the encrypted payload is never converted back into a redacted_thinking block. So even the item the non-streaming path preserves is stripped from the outgoing request when the client echoes it back.

The result: a streaming client never receives the item (no output_item.added for it, only an orphan output_item.done carrying an empty assistant message shell, nothing in response.completed.output), the accumulated message that feeds logging, observability, and the semantic cache loses it too, and even a client that faithfully replays its history sends Anthropic an assistant message with the redacted block gone. When all of the turn's reasoning was redacted, the replayed message has no thinking block at all in a thinking-enabled tool-use conversation. The chat completions surface had the same class of bug and was fixed in #4943; the responses surface needs the equivalent treatment.

Steps to reproduce

Deterministic, no API key: replay an Anthropic SSE sequence containing a content_block_start with a redacted_thinking block through ToBifrostResponsesStream and observe that no output item is produced for it, while the equivalent non-streaming message through ToBifrostResponsesResponse produces a reasoning item with encrypted_content. Then take that non-streaming reasoning item (empty summary list plus encrypted_content), put it in front of a function_call item in a /v1/responses input, convert with ToAnthropicResponsesRequest, and observe the outgoing Anthropic request contains the tool_use block but no redacted_thinking block.

Against the live API: send a streamed /v1/responses request to an Anthropic model with reasoning enabled and the documented test string that forces redaction (ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB) as the input, and compare the stream (and the logged message) with the same request non-streamed.

Expected behavior

The streamed response carries the redacted reasoning item like the non-streamed one does: an output_item.added/output_item.done pair for a reasoning item with encrypted_content, the item present in response.completed.output and in the accumulated/logged message. Replaying a reasoning item that carries encrypted_content (with or without an empty summary list) converts it back into the original redacted_thinking block, placed before the tool_use block as Anthropic requires.

Actual behavior

The block contributes nothing to the converted stream: no output_item.added, nothing in response.completed.output, nothing in the accumulated/logged message. The output index was already allocated when the start event arrived, so the block's content_block_stop still goes through the generic done path and the stream emits an orphan output_item.done with no matching added, carrying an empty assistant message shell ({"type": "message", "status": "completed", "role": "assistant", "content": []}). Observed against the live API with the magic string: 9 done events for a single added on current dev. On the way back, replayed reasoning items with an empty summary list lose their encrypted_content and the outgoing request carries the tool_use block with no redacted_thinking before it. Multi-turn tool use with extended thinking breaks with an upstream 400 when redaction occurs and the model loses that reasoning context.

Affected area(s)

Core (Go)

Version

Current dev (reproduced at 3fbd9a4); the responses stream converter never handled redacted_thinking, and the request-side summary-list check predates it (the chat completions surface got the equivalent round-trip fix in #4943).

Severity

Medium (some functionality impaired)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions