Prerequisites
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:
-
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.
-
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)
Prerequisites
Description
When Anthropic's safety systems flag part of Claude's reasoning, the API returns it as a
redacted_thinkingcontent block (an encrypteddatapayload) instead of athinkingblock, 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/responseswith an Anthropic provider) the blocks are lost twice, incore/providers/anthropic/responses.go:Streaming responses drop them entirely. The
content_block_starthandler inToBifrostResponsesStreamswitches over the block type with cases for text, tool_use, MCP, compaction, and thinking, but has no case forredacted_thinking, so the block is silently discarded (redacted_thinkingarrives complete incontent_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 carryingencrypted_content.Request conversion drops them again on replay. The reasoning items both converters produce for redacted thinking carry an empty, non-nil
summarylist next toencrypted_content, andconvertBifrostReasoningToAnthropicThinkingchecksSummary != nilbefore falling back toencrypted_content. An empty list passes the nil check, the loop emits zero thinking blocks, and the encrypted payload is never converted back into aredacted_thinkingblock. 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.addedfor it, only an orphanoutput_item.donecarrying an empty assistant message shell, nothing inresponse.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_startwith aredacted_thinkingblock throughToBifrostResponsesStreamand observe that no output item is produced for it, while the equivalent non-streaming message throughToBifrostResponsesResponseproduces a reasoning item withencrypted_content. Then take that non-streaming reasoning item (emptysummarylist plusencrypted_content), put it in front of afunction_callitem in a/v1/responsesinput, convert withToAnthropicResponsesRequest, and observe the outgoing Anthropic request contains thetool_useblock but noredacted_thinkingblock.Against the live API: send a streamed
/v1/responsesrequest to an Anthropic model withreasoningenabled 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.donepair for a reasoning item withencrypted_content, the item present inresponse.completed.outputand in the accumulated/logged message. Replaying a reasoning item that carriesencrypted_content(with or without an empty summary list) converts it back into the originalredacted_thinkingblock, placed before thetool_useblock as Anthropic requires.Actual behavior
The block contributes nothing to the converted stream: no
output_item.added, nothing inresponse.completed.output, nothing in the accumulated/logged message. The output index was already allocated when the start event arrived, so the block'scontent_block_stopstill goes through the generic done path and the stream emits an orphanoutput_item.donewith 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 theirencrypted_contentand the outgoing request carries thetool_useblock with noredacted_thinkingbefore 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)