fix(bedrock): handle adaptive-thinking interactions for prompt caching and reasoning conversion#1675
Merged
gold-silver-copper merged 2 commits into0xPlaygrounds:mainfrom Apr 28, 2026
Conversation
Contributor
|
Very clean work, Thank you! |
Open
This was referenced Apr 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(bedrock): handle adaptive-thinking interactions for prompt caching and reasoning conversion
Description
Two related, narrowly-scoped fixes to
rig-bedrockfor issues that surface when Anthropic Claude models on AWS Bedrock are used in adaptive thinking mode (additionalModelRequestFields.thinking = { "type": "adaptive" }) together with tool use across multi-turn conversations. Both reproduce reliably withclaude-sonnet-4-6andclaude-opus-4-7. Both manifest as BedrockConverseAPI errors that the currentrig-bedrockrequest builder either causes or refuses to send.The two fixes are independent code paths but stem from the same root cause: Bedrock's Anthropic backend has stricter rules around
thinkingblocks (and stricter validation of cache point placement around them) thanrig-bedrockcurrently models. They are bundled here because they trigger together in real adaptive-thinking workloads and because each individually makes the other reachable.Fixes #1673
Fixes #1674
Type of change
Fix 1 — Skip the message-level
CachePointwhen chat history contains reasoning blocksFile:
rig-integrations/rig-bedrock/src/types/completion_request.rsSymptom
With
with_prompt_caching()enabled and a chat history containing a prior reasoning turn (which is the normal shape after one tool-use round-trip in extended/adaptive thinking), Bedrock rejects the request with:This fires even when the literal trailing block of the last message is a
tool_result(i.e. aUsermessage). Empirically, Anthropic's backend on Bedrock validates the cache point's position against the whole assistant turn it follows — including any reasoning block that opened the merged turn — not just the final block adjacent to the cache point.Root cause
AwsCompletionRequest::messages()unconditionally appends aContentBlock::CachePointto the last message's content wheneverprompt_cachingis enabled. This is correct for plain text/tool-result histories but illegal once a reasoning block exists anywhere in the chat history.Fix
Before appending the message-level cache point, scan
chat_historyfor anyMessage::Assistant { content, … }whose content carries acompletion::AssistantContent::Reasoning(_). If found, skip the message-level checkpoint for this request. The system-promptSystemContentBlock::CachePointis on a separate code path and remains unconditional — it is the larger and more stable cache prefix anyway (system prompts are reused across every agent-loop turn).This is intentionally conservative: we'd rather forfeit a single message-level checkpoint occasionally than emit a request the API will reject. A future refinement could narrow the heuristic to only check the last assistant turn's content, but the broad gate is safe and cheap.
Test added
test_messages_skip_cache_point_when_history_contains_reasoning— constructs a[User, Assistant{Reasoning}, User]chat history withprompt_caching=true, callsmessages(), and asserts that the resulting last message has noContentBlock::CachePointin its content. It also confirms the system-prompt cache path is independent.The pre-existing
test_messages_append_cache_point_when_prompt_caching_enabled(plain text only) andtest_system_prompt_appends_cache_point_when_prompt_caching_enabledcontinue to pass — the new gate doesn't trip when no reasoning is present.Fix 2 — Allow empty-text reasoning blocks when a signature is present
File:
rig-integrations/rig-bedrock/src/types/assistant_content.rsSymptom
When a Claude model on Bedrock returns a reasoning block with an empty plaintext body but a real cryptographic signature (which adaptive thinking does emit on simple/trivial steps where no surfaced reasoning is needed), round-tripping that reasoning back into a follow-up turn fails inside
rig-bedrockitself, before the request leaves the client:Root cause
The
TryFrom<RigAssistantContent> for aws_bedrock::ContentBlockimplementation flattens the rigReasoninginto aReasoningTextBlockand then guards against an empty body:The guard is too strict. Bedrock's
ReasoningTextBlockaccepts an emptytextfield as long assignatureis present, which mirrors how Anthropic's underlying API encodes signature-only thinking. The signature is the load-bearing part for tool-use validation across turns — dropping it (or refusing to forward it) breaks the contract Anthropic uses to verify that a replayed tool call is genuinely the model's.Fix
Allow the conversion to proceed when
display_text()is empty as long asreasoning.first_signature()returnsSome. The resultingReasoningTextBlockis built withtext("")and.signature(sig), which Bedrock accepts and which preserves the round-trip invariant. The error remains for the truly degenerate case (no text and no signature).Tests added
rig_reasoning_with_empty_text_and_signature_is_converted— constructs aReasoning::new_with_signature("", Some("sig_empty_text")), converts toaws_bedrock::ContentBlock, and asserts the result isReasoningContent(ReasoningText { text: "", signature: Some("sig_empty_text") }).rig_reasoning_with_empty_text_and_no_signature_returns_error— confirms the original guard still rejects empty-without-signature reasoning, preserving existing behaviour for the actually-degenerate case.Why both fixes ship together
Each fix on its own is reachable by a different production workload:
In the workload that surfaced these, both occur within the same agent run: Fix 2's empty-text reasoning is what arrives back from Bedrock on iteration 1 → it gets stored in chat history → on iteration 2 the request now contains reasoning content, triggering Fix 1's cache-point rejection. They were also discovered in the same debugging session against the same test harness. Splitting them into two PRs is straightforward if preferred — happy to do that on request.
Testing
cargo fmt --check -p rig-bedrockpassescargo clippy -p rig-bedrock --all-targets --all-features -- -D warningspassescargo test -p rig-bedrock --lib— 66 passed, 0 failed (3 new tests, all pre-existing reasoning/cache-point assertions still pass)claude-sonnet-4-6andclaude-opus-4-7in adaptive thinking mode on a multi-turn agent loop with tool use; both error symptoms above no longer reproduce.Public API impact
No public API changes. Both fixes are internal request-construction logic. Public types, traits, and trait bounds are unchanged. No new dependencies, no feature flags.
Checklist:
Why:block above the conditional explaining the Bedrock-side validator behaviour they accommodate)Notes
User { ToolResult }still trip the rejection. The wider check is the safer default; happy to narrow it on request.