2121
2222from .._exception_notes import add_exception_note
2323from ..event_loop import streaming
24- from ..tools import convert_pydantic_to_tool_spec
24+ from ..tools import convert_pydantic_to_json_schema , convert_pydantic_to_tool_spec
2525from ..tools ._tool_helpers import noop_tool
2626from ..types .content import ContentBlock , Messages , SystemContentBlock
2727from ..types .exceptions import (
@@ -98,6 +98,9 @@ class BedrockConfig(TypedDict, total=False):
9898 Please check https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html for
9999 supported service tiers, models, and regions
100100 stop_sequences: List of sequences that will stop generation when encountered
101+ structured_output_mode: Mode for structured output. "tool" (default) uses tool-based approach,
102+ "native" uses Bedrock's outputConfig.textFormat for schema-constrained responses.
103+ Native mode requires a model that supports structured output.
101104 streaming: Flag to enable/disable streaming. Defaults to True.
102105 temperature: Controls randomness in generation (higher = more random)
103106 top_p: Controls diversity via nucleus sampling (alternative to temperature)
@@ -123,6 +126,7 @@ class BedrockConfig(TypedDict, total=False):
123126 include_tool_result_status : Literal ["auto" ] | bool | None
124127 service_tier : str | None
125128 stop_sequences : list [str ] | None
129+ structured_output_mode : Literal ["tool" , "native" ] | None
126130 streaming : bool | None
127131 temperature : float | None
128132 top_p : float | None
@@ -218,6 +222,7 @@ def _format_request(
218222 tool_specs : list [ToolSpec ] | None = None ,
219223 system_prompt_content : list [SystemContentBlock ] | None = None ,
220224 tool_choice : ToolChoice | None = None ,
225+ output_config : dict [str , Any ] | None = None ,
221226 ) -> dict [str , Any ]:
222227 """Format a Bedrock converse stream request.
223228
@@ -226,6 +231,7 @@ def _format_request(
226231 tool_specs: List of tool specifications to make available to the model.
227232 tool_choice: Selection strategy for tool invocation.
228233 system_prompt_content: System prompt content blocks to provide context to the model.
234+ output_config: Output configuration for structured output (JSON schema).
229235
230236 Returns:
231237 A Bedrock converse stream request.
@@ -251,6 +257,20 @@ def _format_request(
251257 "messages" : self ._format_bedrock_messages (messages ),
252258 "system" : system_blocks ,
253259 ** ({"serviceTier" : {"type" : self .config ["service_tier" ]}} if self .config .get ("service_tier" ) else {}),
260+ ** (
261+ {
262+ "outputConfig" : {
263+ "textFormat" : {
264+ "type" : "json_schema" ,
265+ "structure" : {
266+ "jsonSchema" : output_config ,
267+ },
268+ },
269+ }
270+ }
271+ if output_config
272+ else {}
273+ ),
254274 ** (
255275 {
256276 "toolConfig" : {
@@ -747,6 +767,7 @@ async def stream(
747767 * ,
748768 tool_choice : ToolChoice | None = None ,
749769 system_prompt_content : list [SystemContentBlock ] | None = None ,
770+ output_config : dict [str , Any ] | None = None ,
750771 ** kwargs : Any ,
751772 ) -> AsyncGenerator [StreamEvent , None ]:
752773 """Stream conversation with the Bedrock model.
@@ -760,6 +781,7 @@ async def stream(
760781 system_prompt: System prompt to provide context to the model.
761782 tool_choice: Selection strategy for tool invocation.
762783 system_prompt_content: System prompt content blocks to provide context to the model.
784+ output_config: Output configuration for structured output (JSON schema).
763785 **kwargs: Additional keyword arguments for future extensibility.
764786
765787 Yields:
@@ -782,7 +804,9 @@ def callback(event: StreamEvent | None = None) -> None:
782804 if system_prompt and system_prompt_content is None :
783805 system_prompt_content = [{"text" : system_prompt }]
784806
785- thread = asyncio .to_thread (self ._stream , callback , messages , tool_specs , system_prompt_content , tool_choice )
807+ thread = asyncio .to_thread (
808+ self ._stream , callback , messages , tool_specs , system_prompt_content , tool_choice , output_config
809+ )
786810 task = asyncio .create_task (thread )
787811
788812 while True :
@@ -801,6 +825,7 @@ def _stream(
801825 tool_specs : list [ToolSpec ] | None = None ,
802826 system_prompt_content : list [SystemContentBlock ] | None = None ,
803827 tool_choice : ToolChoice | None = None ,
828+ output_config : dict [str , Any ] | None = None ,
804829 ) -> None :
805830 """Stream conversation with the Bedrock model.
806831
@@ -813,14 +838,15 @@ def _stream(
813838 tool_specs: List of tool specifications to make available to the model.
814839 system_prompt_content: System prompt content blocks to provide context to the model.
815840 tool_choice: Selection strategy for tool invocation.
841+ output_config: Output configuration for structured output (JSON schema).
816842
817843 Raises:
818844 ContextWindowOverflowException: If the input exceeds the model's context window.
819845 ModelThrottledException: If the model service is throttling requests.
820846 """
821847 try :
822848 logger .debug ("formatting request" )
823- request = self ._format_request (messages , tool_specs , system_prompt_content , tool_choice )
849+ request = self ._format_request (messages , tool_specs , system_prompt_content , tool_choice , output_config )
824850 logger .debug ("request=<%s>" , request )
825851
826852 logger .debug ("invoking model" )
@@ -1032,6 +1058,10 @@ async def structured_output(
10321058 ) -> AsyncGenerator [dict [str , T | Any ], None ]:
10331059 """Get structured output from the model.
10341060
1061+ Supports two modes controlled by `structured_output_mode` config:
1062+ - "tool" (default): Converts the Pydantic model to a tool spec and forces tool use.
1063+ - "native": Uses Bedrock's outputConfig.textFormat with JSON schema for guaranteed schema compliance.
1064+
10351065 Args:
10361066 output_model: The output model to use for the agent.
10371067 prompt: The prompt messages to use for the agent.
@@ -1041,6 +1071,21 @@ async def structured_output(
10411071 Yields:
10421072 Model events with the last being the structured output.
10431073 """
1074+ if self .config .get ("structured_output_mode" ) == "native" :
1075+ async for event in self ._structured_output_native (output_model , prompt , system_prompt , ** kwargs ):
1076+ yield event
1077+ else :
1078+ async for event in self ._structured_output_tool (output_model , prompt , system_prompt , ** kwargs ):
1079+ yield event
1080+
1081+ async def _structured_output_tool (
1082+ self ,
1083+ output_model : type [T ],
1084+ prompt : Messages ,
1085+ system_prompt : str | None = None ,
1086+ ** kwargs : Any ,
1087+ ) -> AsyncGenerator [dict [str , T | Any ], None ]:
1088+ """Structured output using tool-based approach."""
10441089 tool_spec = convert_pydantic_to_tool_spec (output_model )
10451090
10461091 response = self .stream (
@@ -1073,6 +1118,40 @@ async def structured_output(
10731118
10741119 yield {"output" : output_model (** output_response )}
10751120
1121+ async def _structured_output_native (
1122+ self ,
1123+ output_model : type [T ],
1124+ prompt : Messages ,
1125+ system_prompt : str | None = None ,
1126+ ** kwargs : Any ,
1127+ ) -> AsyncGenerator [dict [str , T | Any ], None ]:
1128+ """Structured output using Bedrock's native outputConfig.textFormat."""
1129+ output_config = convert_pydantic_to_json_schema (output_model )
1130+
1131+ response = self .stream (
1132+ messages = prompt ,
1133+ system_prompt = system_prompt ,
1134+ output_config = output_config ,
1135+ ** kwargs ,
1136+ )
1137+ async for event in streaming .process_stream (response ):
1138+ yield event
1139+
1140+ _ , messages , _ , _ = event ["stop" ]
1141+
1142+ content = messages ["content" ]
1143+ text_content : str | None = None
1144+ for block in content :
1145+ if "text" in block :
1146+ text_content = block ["text" ]
1147+ break
1148+
1149+ if text_content is None :
1150+ raise ValueError ("No text content found in the Bedrock response for native structured output." )
1151+
1152+ output_response = json .loads (text_content )
1153+ yield {"output" : output_model (** output_response )}
1154+
10761155 @staticmethod
10771156 def _get_default_model_with_warning (region_name : str , model_config : BedrockConfig | None = None ) -> str :
10781157 """Get the default Bedrock modelId based on region.
0 commit comments