From 4fee8c084077650c441f973d41008d0857134b7a Mon Sep 17 00:00:00 2001 From: byQuexo Date: Mon, 27 Apr 2026 16:20:38 +0200 Subject: [PATCH 1/2] fix(bedrock): accept empty-text reasoning blocks when signature present --- .../src/types/assistant_content.rs | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/rig-integrations/rig-bedrock/src/types/assistant_content.rs b/rig-integrations/rig-bedrock/src/types/assistant_content.rs index 5fa993c3e..97a3a49a0 100644 --- a/rig-integrations/rig-bedrock/src/types/assistant_content.rs +++ b/rig-integrations/rig-bedrock/src/types/assistant_content.rs @@ -199,7 +199,13 @@ impl TryFrom for aws_bedrock::ContentBlock { } let flattened_text = reasoning.display_text(); - if flattened_text.is_empty() { + let has_signature = reasoning.first_signature().is_some(); + // Adaptive thinking on Bedrock can emit a reasoning block whose + // plaintext body is empty but with a real cryptographic + // signature attached. The signature is what Anthropic uses to + // verify tool_use round-trips, so we must preserve it. Only + // reject when there's neither text nor signature to send. + if flattened_text.is_empty() && !has_signature { return Err(CompletionError::ProviderError( "AWS Bedrock reasoning conversion requires at least one text or summary block" .to_owned(), @@ -546,6 +552,43 @@ mod tests { } } + #[test] + fn rig_reasoning_with_empty_text_and_signature_is_converted() { + // Adaptive thinking on Bedrock can emit a reasoning block whose + // plaintext body is empty but with a real cryptographic signature + // attached. Verify we forward this as a `ReasoningTextBlock` with + // empty text + signature instead of rejecting it. + let reasoning = + rig::message::Reasoning::new_with_signature("", Some("sig_empty_text".to_string())); + let rig_content = RigAssistantContent(AssistantContent::Reasoning(reasoning)); + + let aws_content_block: Result = rig_content.try_into(); + assert!(aws_content_block.is_ok()); + + match aws_content_block.unwrap() { + aws_bedrock::ContentBlock::ReasoningContent( + aws_bedrock::ReasoningContentBlock::ReasoningText(reasoning_text), + ) => { + assert_eq!(reasoning_text.text, ""); + assert_eq!(reasoning_text.signature, Some("sig_empty_text".to_string())); + } + _ => panic!("Expected ContentBlock::ReasoningContent"), + } + } + + #[test] + fn rig_reasoning_with_empty_text_and_no_signature_returns_error() { + let reasoning = rig::message::Reasoning::new_with_signature("", None); + let rig_content = RigAssistantContent(AssistantContent::Reasoning(reasoning)); + + let aws_content_block: Result = rig_content.try_into(); + assert!(matches!( + aws_content_block, + Err(completion::CompletionError::ProviderError(message)) + if message.contains("at least one text or summary block") + )); + } + #[test] fn rig_reasoning_with_multiple_signed_text_blocks_returns_error() { let mut reasoning = From 19a8bf6162513fd265dbd1337784cf458710ce2f Mon Sep 17 00:00:00 2001 From: byQuexo Date: Mon, 27 Apr 2026 16:20:38 +0200 Subject: [PATCH 2/2] fix(bedrock): skip message cache point when history contains reasoning --- .../src/types/completion_request.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/rig-integrations/rig-bedrock/src/types/completion_request.rs b/rig-integrations/rig-bedrock/src/types/completion_request.rs index 900193bb5..365437dda 100644 --- a/rig-integrations/rig-bedrock/src/types/completion_request.rs +++ b/rig-integrations/rig-bedrock/src/types/completion_request.rs @@ -163,7 +163,23 @@ impl AwsCompletionRequest { .map(|message| RigMessage(message).try_into()) .collect::, _>>()?; + // Bedrock rejects cache points placed after reasoning blocks + // ("Cache point cannot be inserted after reasoning block"). When the + // request carries any reasoning content (round-tripped from a prior + // turn), Anthropic's backend treats the trailing cache point as + // following reasoning even when the literal previous block is a tool + // result. Skip the message-level checkpoint in that case; the + // system-prompt cache point still applies and captures the largest + // stable prefix. + let has_reasoning = self.inner.chat_history.iter().any(|message| match message { + Message::Assistant { content, .. } => content + .iter() + .any(|c| matches!(c, rig::completion::AssistantContent::Reasoning(_))), + _ => false, + }); + if self.prompt_caching + && !has_reasoning && let Some(last_msg) = messages.last_mut() { let mut content = last_msg.content.clone(); @@ -544,4 +560,52 @@ mod tests { Some(aws_bedrock::ContentBlock::CachePoint(_)) )); } + + #[test] + fn test_messages_skip_cache_point_when_history_contains_reasoning() { + // Bedrock's Anthropic backend rejects "Cache point cannot be inserted + // after reasoning block" whenever the chat history carries a prior + // reasoning turn, even if the literal trailing block is a tool result. + // Verify the message-level checkpoint is suppressed in that case. + let reasoning = + rig::message::Reasoning::new_with_signature("thinking", Some("sig".to_string())); + let request = CompletionRequest { + chat_history: OneOrMany::many(vec![ + Message::User { + content: OneOrMany::one(UserContent::Text(Text { + text: "user prompt".to_string(), + })), + }, + Message::Assistant { + id: None, + content: OneOrMany::one(rig::completion::AssistantContent::Reasoning( + reasoning, + )), + }, + Message::User { + content: OneOrMany::one(UserContent::Text(Text { + text: "follow up".to_string(), + })), + }, + ]) + .expect("history should be non-empty"), + ..minimal_request() + }; + + let aws_request = aws_request(request, true); + let messages = aws_request.messages().expect("messages should convert"); + + let last_message = messages.last().expect("messages should not be empty"); + assert!( + !last_message + .content + .iter() + .any(|c| matches!(c, aws_bedrock::ContentBlock::CachePoint(_))), + "message-level cache point should be skipped when chat history contains reasoning" + ); + + // The system-prompt cache point path is independent and unaffected. + let system_only = aws_request.system_prompt().expect("system prompt builds"); + assert!(system_only.is_none() || !system_only.unwrap().is_empty()); + } }