@@ -92,6 +92,20 @@ enum BedrockContentBlock {
9292 #[ serde( rename = "toolResult" ) ]
9393 tool_result : BedrockToolResult ,
9494 } ,
95+ // Bedrock Converse representation of Anthropic's `redacted_thinking`.
96+ // The encrypted blob is echoed back verbatim under
97+ // reasoningContent.redactedContent so Claude extended-thinking history
98+ // is not rejected on resubmission.
99+ ReasoningContent {
100+ #[ serde( rename = "reasoningContent" ) ]
101+ reasoning_content : BedrockReasoningContent ,
102+ } ,
103+ }
104+
105+ #[ derive( Debug , Serialize ) ]
106+ struct BedrockReasoningContent {
107+ #[ serde( rename = "redactedContent" ) ]
108+ redacted_content : String ,
95109}
96110
97111#[ derive( Debug , Serialize ) ]
@@ -299,10 +313,23 @@ fn convert_content_block(block: &ContentBlock) -> Option<BedrockContentBlock> {
299313 } ,
300314 } ,
301315 } ) ,
302- // Image, Thinking, RedactedThinking, and Unknown are not supported — silently drop
316+ // Echo redacted_thinking verbatim. Bedrock Converse rejects history
317+ // that drops these blocks on Claude extended-thinking models, mirroring
318+ // the anthropic.rs path. Drop empty blobs (e.g. interrupted stream).
319+ ContentBlock :: RedactedThinking { data } => {
320+ if data. is_empty ( ) {
321+ None
322+ } else {
323+ Some ( BedrockContentBlock :: ReasoningContent {
324+ reasoning_content : BedrockReasoningContent {
325+ redacted_content : data. clone ( ) ,
326+ } ,
327+ } )
328+ }
329+ }
330+ // Image, Thinking, and Unknown are not supported — silently drop
303331 ContentBlock :: Image { .. }
304332 | ContentBlock :: Thinking { .. }
305- | ContentBlock :: RedactedThinking { .. }
306333 | ContentBlock :: Unknown => None ,
307334 }
308335}
@@ -1126,6 +1153,53 @@ mod tests {
11261153 assert ! ( text_at_3 >= 1 ) ;
11271154 }
11281155
1156+ /// Issue #1187 — Bedrock Converse history must preserve
1157+ /// `redacted_thinking` blocks on Claude extended-thinking models.
1158+ /// A message containing only RedactedThinking must round-trip through
1159+ /// `convert_content_block` without being silently dropped, and the wire
1160+ /// format must use `reasoningContent.redactedContent`.
1161+ #[ test]
1162+ fn test_bedrock_redacted_thinking_round_trip ( ) {
1163+ let msg = Message :: assistant_with_blocks ( vec ! [ ContentBlock :: RedactedThinking {
1164+ data: "encrypted-blob-abc123" . to_string( ) ,
1165+ } ] ) ;
1166+
1167+ let bedrock_blocks = convert_message_content ( & msg. content ) ;
1168+ assert_eq ! (
1169+ bedrock_blocks. len( ) ,
1170+ 1 ,
1171+ "RedactedThinking must survive convert_content_block"
1172+ ) ;
1173+ match & bedrock_blocks[ 0 ] {
1174+ BedrockContentBlock :: ReasoningContent { reasoning_content } => {
1175+ assert_eq ! ( reasoning_content. redacted_content, "encrypted-blob-abc123" ) ;
1176+ }
1177+ other => panic ! ( "expected ReasoningContent block, got {other:?}" ) ,
1178+ }
1179+
1180+ // Wire format check: serialized JSON must carry
1181+ // reasoningContent.redactedContent so Bedrock accepts the history.
1182+ let json = serde_json:: to_value ( & bedrock_blocks[ 0 ] ) . unwrap ( ) ;
1183+ assert_eq ! (
1184+ json[ "reasoningContent" ] [ "redactedContent" ] ,
1185+ "encrypted-blob-abc123"
1186+ ) ;
1187+ }
1188+
1189+ /// Empty RedactedThinking blobs (interrupted stream) must be dropped on
1190+ /// outbound, matching the anthropic.rs behavior.
1191+ #[ test]
1192+ fn test_bedrock_redacted_thinking_empty_dropped ( ) {
1193+ let msg = Message :: assistant_with_blocks ( vec ! [ ContentBlock :: RedactedThinking {
1194+ data: String :: new( ) ,
1195+ } ] ) ;
1196+ let bedrock_blocks = convert_message_content ( & msg. content ) ;
1197+ assert ! (
1198+ bedrock_blocks. is_empty( ) ,
1199+ "empty redacted_thinking must be dropped"
1200+ ) ;
1201+ }
1202+
11291203 #[ test]
11301204 fn test_validate_tool_pairing_noop_on_correct ( ) {
11311205 // already correct 2-for-2 → no change
0 commit comments