Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion rig-integrations/rig-bedrock/src/types/assistant_content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,13 @@ impl TryFrom<RigAssistantContent> 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(),
Expand Down Expand Up @@ -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<aws_bedrock::ContentBlock, _> = 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<aws_bedrock::ContentBlock, _> = 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 =
Expand Down
64 changes: 64 additions & 0 deletions rig-integrations/rig-bedrock/src/types/completion_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,23 @@ impl AwsCompletionRequest {
.map(|message| RigMessage(message).try_into())
.collect::<Result<Vec<aws_bedrock::Message>, _>>()?;

// 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();
Expand Down Expand Up @@ -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());
}
}
Loading