diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 5b7a2f34e..75af10fc7 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -368,8 +368,24 @@ def _inject_cache_point(self, messages: list[dict[str, Any]]) -> None: last_user_idx = msg_idx if last_user_idx is not None and messages[last_user_idx].get("content"): - messages[last_user_idx]["content"].append({"cachePoint": {"type": "default"}}) - logger.debug("msg_idx=<%s> | added cache point to last user message", last_user_idx) + content = messages[last_user_idx]["content"] + # Walk backwards to find insertion point: cachePoint must not immediately follow a + # non-PDF document block because Bedrock cannot translate that combination to the + # Anthropic API format (non-PDF documents are not natively supported by Anthropic, + # so the conversion path fails when a cachePoint trails one). + insert_idx = len(content) + for i in range(len(content) - 1, -1, -1): + doc = content[i].get("document", {}) + if doc and doc.get("format") != "pdf": + insert_idx = i + else: + break + content.insert(insert_idx, {"cachePoint": {"type": "default"}}) + logger.debug( + "msg_idx=<%s>, insert_idx=<%s> | added cache point to last user message", + last_user_idx, + insert_idx, + ) def _find_last_user_text_message_index(self, messages: Messages) -> int | None: """Find the index of the last user message containing text or image content. diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 9c565d4f4..0c9ed2d21 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -2735,6 +2735,84 @@ def test_inject_cache_point_auto_strategy_resolves_to_anthropic_for_claude(bedro assert len(formatted[1]["content"]) == 1 +def test_inject_cache_point_before_non_pdf_document(bedrock_client): + """Test that cachePoint is inserted before trailing non-PDF document blocks.""" + model = BedrockModel( + model_id="us.anthropic.claude-sonnet-4-20250514-v1:0", cache_config=CacheConfig(strategy="auto") + ) + + cleaned_messages = [ + { + "role": "user", + "content": [ + {"text": "Analyze this file\n\n[Attached files: README.md]"}, + {"document": {"format": "md", "name": "readme", "source": {"bytes": b"..."}}}, + ], + } + ] + + model._inject_cache_point(cleaned_messages) + + content = cleaned_messages[0]["content"] + assert len(content) == 3 + # cachePoint must be inserted before the non-PDF document block + cache_idx = next(i for i, b in enumerate(content) if "cachePoint" in b) + doc_idx = next(i for i, b in enumerate(content) if "document" in b) + assert cache_idx < doc_idx, "cachePoint must come before the non-PDF document block" + assert content[cache_idx]["cachePoint"]["type"] == "default" + + +def test_inject_cache_point_after_pdf_document(bedrock_client): + """Test that cachePoint is appended after a PDF document block (PDF is supported by Anthropic).""" + model = BedrockModel( + model_id="us.anthropic.claude-sonnet-4-20250514-v1:0", cache_config=CacheConfig(strategy="auto") + ) + + cleaned_messages = [ + { + "role": "user", + "content": [ + {"text": "Summarize this PDF"}, + {"document": {"format": "pdf", "name": "report", "source": {"bytes": b"..."}}}, + ], + } + ] + + model._inject_cache_point(cleaned_messages) + + content = cleaned_messages[0]["content"] + assert len(content) == 3 + # PDF documents are safe to precede a cachePoint, so cachePoint goes at the end + assert "cachePoint" in content[-1] + assert content[-1]["cachePoint"]["type"] == "default" + + +def test_inject_cache_point_before_multiple_non_pdf_documents(bedrock_client): + """Test that cachePoint is inserted before multiple trailing non-PDF document blocks.""" + model = BedrockModel( + model_id="us.anthropic.claude-sonnet-4-20250514-v1:0", cache_config=CacheConfig(strategy="auto") + ) + + cleaned_messages = [ + { + "role": "user", + "content": [ + {"text": "Compare these files"}, + {"document": {"format": "csv", "name": "data1", "source": {"bytes": b"..."}}}, + {"document": {"format": "xlsx", "name": "data2", "source": {"bytes": b"..."}}}, + ], + } + ] + + model._inject_cache_point(cleaned_messages) + + content = cleaned_messages[0]["content"] + assert len(content) == 4 + cache_idx = next(i for i, b in enumerate(content) if "cachePoint" in b) + first_doc_idx = next(i for i, b in enumerate(content) if "document" in b) + assert cache_idx < first_doc_idx, "cachePoint must come before all non-PDF document blocks" + + def test_find_last_user_text_message_index_no_user_messages(bedrock_client): """Test _find_last_user_text_message_index returns None when no user text messages exist.""" model = BedrockModel(model_id="test-model")