Skip to content

fix(bedrock): handle adaptive-thinking interactions for prompt caching and reasoning conversion#1675

Merged
gold-silver-copper merged 2 commits into0xPlaygrounds:mainfrom
byQuexo:fix/bedrock-adaptive-thinking
Apr 28, 2026
Merged

fix(bedrock): handle adaptive-thinking interactions for prompt caching and reasoning conversion#1675
gold-silver-copper merged 2 commits into0xPlaygrounds:mainfrom
byQuexo:fix/bedrock-adaptive-thinking

Conversation

@byQuexo
Copy link
Copy Markdown
Contributor

@byQuexo byQuexo commented Apr 27, 2026

fix(bedrock): handle adaptive-thinking interactions for prompt caching and reasoning conversion

Description

Two related, narrowly-scoped fixes to rig-bedrock for 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 with claude-sonnet-4-6 and claude-opus-4-7. Both manifest as Bedrock Converse API errors that the current rig-bedrock request 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 thinking blocks (and stricter validation of cache point placement around them) than rig-bedrock currently 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

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Fix 1 — Skip the message-level CachePoint when chat history contains reasoning blocks

File: rig-integrations/rig-bedrock/src/types/completion_request.rs

Symptom

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:

ProviderError: Cache point cannot be inserted after reasoning block.
Please remove the invalid cache point and try again.

This fires even when the literal trailing block of the last message is a tool_result (i.e. a User message). 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 a ContentBlock::CachePoint to the last message's content whenever prompt_caching is 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_history for any Message::Assistant { content, … } whose content carries a completion::AssistantContent::Reasoning(_). If found, skip the message-level checkpoint for this request. The system-prompt SystemContentBlock::CachePoint is 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 with prompt_caching=true, calls messages(), and asserts that the resulting last message has no ContentBlock::CachePoint in 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) and test_system_prompt_appends_cache_point_when_prompt_caching_enabled continue 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.rs

Symptom

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-bedrock itself, before the request leaves the client:

ProviderError: AWS Bedrock reasoning conversion requires at least one text or summary block

Root cause

The TryFrom<RigAssistantContent> for aws_bedrock::ContentBlock implementation flattens the rig Reasoning into a ReasoningTextBlock and then guards against an empty body:

let flattened_text = reasoning.display_text();
if flattened_text.is_empty() {
    return Err(CompletionError::ProviderError(
        "AWS Bedrock reasoning conversion requires at least one text or summary block".to_owned(),
    ));
}

The guard is too strict. Bedrock's ReasoningTextBlock accepts an empty text field as long as signature is 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 as reasoning.first_signature() returns Some. The resulting ReasoningTextBlock is built with text("") 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 a Reasoning::new_with_signature("", Some("sig_empty_text")), converts to aws_bedrock::ContentBlock, and asserts the result is ReasoningContent(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:

  • Fix 1 is hit by any adaptive- or extended-thinking conversation that has done at least one tool-use round-trip and has prompt caching enabled.
  • Fix 2 is hit by any adaptive-thinking call where the model decided a step needed no surfaced reasoning text (signature only).

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-bedrock passes
  • cargo clippy -p rig-bedrock --all-targets --all-features -- -D warnings passes
  • cargo test -p rig-bedrock --lib66 passed, 0 failed (3 new tests, all pre-existing reasoning/cache-point assertions still pass)
  • End-to-end verified against AWS Bedrock with claude-sonnet-4-6 and claude-opus-4-7 in 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:

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas (both fixes carry a Why: block above the conditional explaining the Bedrock-side validator behaviour they accommodate)
  • I have made corresponding changes to the documentation (none required — internal behaviour only, public API unchanged)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Notes

  • This PR was drafted with significant AI assistance (Claude). I (the contributor) reviewed every change, ran the full quality gate locally, and validated against live Bedrock traffic.
  • The cache-point gate in Fix 1 deliberately checks the entire chat history rather than just the trailing assistant turn. Empirically Bedrock's validator behaves as if it inspects the merged assistant turn, so even chat histories where the structurally-last message is a User { ToolResult } still trip the rejection. The wider check is the safer default; happy to narrow it on request.
  • Fix 2 is intentionally permissive in one direction only: empty-text with signature → allowed; empty-text without signature → still rejected. This preserves the guard's original intent (catch truly-empty reasoning conversions) while letting the real Anthropic-on-Bedrock signature-only encoding through.

@gold-silver-copper
Copy link
Copy Markdown
Contributor

Very clean work, Thank you!
A nice follow up PR would be to apply this patch to the streaming path as well, but this is great for now. Danke

@gold-silver-copper gold-silver-copper added this pull request to the merge queue Apr 28, 2026
Merged via the queue into 0xPlaygrounds:main with commit 04e34d3 Apr 28, 2026
6 checks passed
@github-actions github-actions Bot mentioned this pull request Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants