1717
1818from ..event_loop import streaming
1919from ..tools import convert_pydantic_to_tool_spec
20- from ..types .content import ContentBlock , Message , Messages
20+ from ..types .content import ContentBlock , Message , Messages , _ContentBlockType
2121from ..types .exceptions import (
2222 ContextWindowOverflowException ,
2323 ModelThrottledException ,
2424)
2525from ..types .streaming import CitationsDelta , StreamEvent
26- from ..types .tools import ToolResult , ToolSpec
26+ from ..types .tools import ToolSpec
2727from ._config_validation import validate_config_keys
2828from .model import Model
2929
4343 "anthropic.claude" ,
4444]
4545
46+ # Allowed fields for each Bedrock content block type to prevent validation exceptions
47+ # Bedrock strictly validates content blocks and throws exceptions for unknown fields
48+ # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ContentBlock.html
49+ _BEDROCK_CONTENT_BLOCK_FIELDS : dict [_ContentBlockType , set [str ]] = {
50+ "image" : {
51+ "format" ,
52+ "source" ,
53+ }, # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ImageBlock.html
54+ "toolResult" : {
55+ "content" ,
56+ "toolUseId" ,
57+ "status" ,
58+ }, # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolResultBlock.html
59+ "toolUse" : {
60+ "input" ,
61+ "name" ,
62+ "toolUseId" ,
63+ }, # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolUseBlock.html
64+ "document" : {
65+ "name" ,
66+ "source" ,
67+ "citations" ,
68+ "context" ,
69+ "format" ,
70+ }, # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_DocumentBlock.html
71+ "video" : {
72+ "format" ,
73+ "source" ,
74+ }, # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_VideoBlock.html
75+ "reasoningContent" : {
76+ "reasoningText" ,
77+ "redactedContent" ,
78+ }, # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ReasoningContentBlock.html
79+ "citationsContent" : {
80+ "citations" ,
81+ "content" ,
82+ }, # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_CitationsContentBlock.html
83+ "cachePoint" : {"type" }, # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_CachePointBlock.html
84+ "guardContent" : {
85+ "image" ,
86+ "text" ,
87+ }, # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_GuardrailConverseContentBlock.html
88+ # Note: text is handled as a primitive (string)
89+ }
90+ _BEDROCK_CONTENT_BLOCK_TYPES : set [_ContentBlockType ] = set (_BEDROCK_CONTENT_BLOCK_FIELDS .keys ())
91+
4692T = TypeVar ("T" , bound = BaseModel )
4793
4894DEFAULT_READ_TIMEOUT = 120
4995
96+
5097class BedrockModel (Model ):
5198 """AWS Bedrock model provider implementation.
5299
@@ -279,9 +326,7 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages:
279326
280327 This function ensures messages conform to Bedrock's expected format by:
281328 - Filtering out SDK_UNKNOWN_MEMBER content blocks
282- - Cleaning tool result content blocks by removing additional fields that may be
283- useful for retaining information in hooks but would cause Bedrock validation
284- exceptions when presented with unexpected fields
329+ - Eagerly filtering content blocks to only include Bedrock-supported fields
285330 - Ensuring all message content blocks are properly formatted for the Bedrock API
286331
287332 Args:
@@ -291,9 +336,11 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages:
291336 Messages formatted for Bedrock API compatibility
292337
293338 Note:
294- Bedrock will throw validation exceptions when presented with additional
295- unexpected fields in tool result blocks.
296- https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolResultBlock.html
339+ Unlike other APIs that ignore unknown fields, Bedrock only accepts a strict
340+ subset of fields for each content block type and throws validation exceptions
341+ when presented with unexpected fields. Therefore, we must eagerly filter all
342+ content blocks to remove any additional fields before sending to Bedrock.
343+ https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ContentBlock.html
297344 """
298345 cleaned_messages = []
299346
@@ -315,30 +362,25 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages:
315362 dropped_deepseek_reasoning_content = True
316363 continue
317364
318- if "toolResult" in content_block :
319- # Create a new content block with only the cleaned toolResult
320- tool_result : ToolResult = content_block ["toolResult" ]
365+ # Clean content blocks that need field filtering for Bedrock API compatibility
366+ filterable_block_types = set (content_block .keys ()) & _BEDROCK_CONTENT_BLOCK_TYPES
321367
322- if self ._should_include_tool_result_status ():
323- # Include status field
324- cleaned_tool_result = ToolResult (
325- content = tool_result ["content" ],
326- toolUseId = tool_result ["toolUseId" ],
327- status = tool_result ["status" ],
328- )
329- else :
330- # Remove status field
331- cleaned_tool_result = ToolResult ( # type: ignore[typeddict-item]
332- toolUseId = tool_result ["toolUseId" ], content = tool_result ["content" ]
333- )
368+ if filterable_block_types :
369+ # Should only be one block type per content block since it is a discriminated union
370+ block_type = cast (_ContentBlockType , next (iter (filterable_block_types )))
371+ block_data = content_block [block_type ]
372+ allowed_fields = _BEDROCK_CONTENT_BLOCK_FIELDS [block_type ].copy ()
373+
374+ if block_type == "toolResult" and not self ._should_include_tool_result_status ():
375+ allowed_fields .discard ("status" )
334376
335- cleaned_block : ContentBlock = {"toolResult" : cleaned_tool_result }
336- cleaned_content .append (cleaned_block )
377+ cleaned_data = {k : v for k , v in block_data . items () if k in allowed_fields }
378+ cleaned_content .append (cast ( ContentBlock , { block_type : cleaned_data }) )
337379 else :
338380 # Keep other content blocks as-is
339381 cleaned_content .append (content_block )
340382
341- # Create new message with cleaned content (skip if empty for DeepSeek )
383+ # Create new message with cleaned content (skip if empty)
342384 if cleaned_content :
343385 cleaned_message : Message = Message (content = cleaned_content , role = message ["role" ])
344386 cleaned_messages .append (cleaned_message )
0 commit comments