From 73df7125839f26fd1e9b0b5ff973dd4c8efbf0b6 Mon Sep 17 00:00:00 2001 From: kimnamu Date: Wed, 23 Sep 2026 18:14:08 +0900 Subject: [PATCH 1/4] Python: Send Bedrock guardrailConfig and additionalModelRequestFields options to Converse BedrockChatOptions declares guardrailConfig, performanceConfig, requestMetadata and promptVariables, but _prepare_options never copied them into the Converse request, so they were silently dropped. There was also no way to pass additionalModelRequestFields (for example the reasoning effort of OpenAI GPT-6 models). Forward these keys as top-level Converse fields when they are set, and declare additionalModelRequestFields on BedrockChatOptions. Requests that don't set them are unchanged. Co-Authored-By: Claude --- .../agent_framework_bedrock/_chat_client.py | 17 +++++++++++- .../bedrock/tests/test_bedrock_client.py | 26 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index 6d2d222d1dd..a782b10f604 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -136,9 +136,10 @@ class BedrockChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], t user: Not supported. store: Not supported. logit_bias: Not supported. - metadata: Not supported (use additional_properties for additionalModelRequestFields). + metadata: Not supported (use requestMetadata). # Bedrock-specific options: + additionalModelRequestFields: Model-specific request fields not covered by the Converse API. guardrailConfig: Guardrails configuration for content filtering. performanceConfig: Performance optimization settings. requestMetadata: Key-value metadata for the request. @@ -146,6 +147,10 @@ class BedrockChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], t """ # Bedrock-specific options + additionalModelRequestFields: dict[str, Any] + """Model-specific request fields passed through as ``additionalModelRequestFields`` + (e.g. ``{"reasoning": {"effort": "low"}}``).""" + guardrailConfig: BedrockGuardrailConfig """Guardrails configuration for content filtering and safety.""" @@ -461,6 +466,16 @@ def _prepare_options( if output_config := self._prepare_output_config(options.get("response_format")): run_options["outputConfig"] = output_config + for key in ( + "additionalModelRequestFields", + "guardrailConfig", + "performanceConfig", + "requestMetadata", + "promptVariables", + ): + if (value := options.get(key)) is not None: + run_options[key] = value + return run_options def _prepare_bedrock_messages( diff --git a/python/packages/bedrock/tests/test_bedrock_client.py b/python/packages/bedrock/tests/test_bedrock_client.py index ee17483e84d..681da0dafdb 100644 --- a/python/packages/bedrock/tests/test_bedrock_client.py +++ b/python/packages/bedrock/tests/test_bedrock_client.py @@ -14,7 +14,7 @@ from boto3.session import Session as Boto3Session from botocore.client import BaseClient -from agent_framework_bedrock import BedrockChatClient, BedrockEmbeddingClient +from agent_framework_bedrock import BedrockChatClient, BedrockChatOptions, BedrockEmbeddingClient from agent_framework_bedrock._chat_client import BedrockSettings from agent_framework_bedrock._feature_usage import FeatureIndex @@ -387,6 +387,30 @@ def test_prepare_options_adds_instructions_and_sampling_settings() -> None: } +async def test_get_response_forwards_bedrock_specific_options() -> None: + """Bedrock-specific options should reach the Converse request as top-level fields.""" + stub = _StubBedrockRuntime() + client = BedrockChatClient( + model="us.openai.gpt-6-sol", + region="us-east-1", + client=stub, # pyrefly: ignore[bad-argument-type] # ty: ignore[invalid-argument-type] # pyright: ignore[reportArgumentType] + ) + bedrock_options: BedrockChatOptions = { + "additionalModelRequestFields": {"reasoning": {"effort": "low"}}, + "guardrailConfig": {"guardrailIdentifier": "gr-123", "guardrailVersion": "1"}, + "performanceConfig": {"latency": "optimized"}, + "requestMetadata": {"tenant": "contoso"}, + "promptVariables": {"topic": {"text": "hash maps"}}, + } + + await client.get_response( + [Message(role="user", contents=[Content.from_text(text="hello")])], options=bedrock_options + ) + + payload = stub.calls[0] + assert {key: payload.get(key) for key in bedrock_options} == bedrock_options + + def test_prepare_options_unsupported_tool_mode_raises(monkeypatch: pytest.MonkeyPatch) -> None: """Unexpected tool modes should raise a clear error.""" from agent_framework_bedrock import _chat_client as chat_client_module From 4d3b964804c12c092fdbf438c6ed466ae3a9872b Mon Sep 17 00:00:00 2001 From: kimnamu Date: Thu, 24 Sep 2026 13:14:22 +0900 Subject: [PATCH 2/4] Python: Leave streamProcessingMode out of Bedrock Converse guardrailConfig BedrockGuardrailConfig declares streamProcessingMode, but only ConverseStream accepts it. Now that guardrailConfig is forwarded, setting it made botocore reject every Converse call with ParamValidationError (this client sends stream=True through Converse too). Drop that one key when building the request; the rest of the guardrail config is still sent and the caller's options are not modified. Co-Authored-By: Claude --- .../bedrock/agent_framework_bedrock/_chat_client.py | 3 +++ .../packages/bedrock/tests/test_bedrock_client.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index a782b10f604..4f2b3ffe18e 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -475,6 +475,9 @@ def _prepare_options( ): if (value := options.get(key)) is not None: run_options[key] = value + if guardrail_config := run_options.get("guardrailConfig"): + # streamProcessingMode is only valid for ConverseStream; Converse rejects requests that include it. + run_options["guardrailConfig"] = {k: v for k, v in guardrail_config.items() if k != "streamProcessingMode"} return run_options diff --git a/python/packages/bedrock/tests/test_bedrock_client.py b/python/packages/bedrock/tests/test_bedrock_client.py index 681da0dafdb..41eea9df359 100644 --- a/python/packages/bedrock/tests/test_bedrock_client.py +++ b/python/packages/bedrock/tests/test_bedrock_client.py @@ -411,6 +411,19 @@ async def test_get_response_forwards_bedrock_specific_options() -> None: assert {key: payload.get(key) for key in bedrock_options} == bedrock_options +def test_prepare_options_omits_stream_processing_mode_from_guardrail_config() -> None: + """Converse rejects streamProcessingMode, so it should be left out while the guardrail still applies.""" + client = _make_client() + options: BedrockChatOptions = { + "guardrailConfig": {"guardrailIdentifier": "gr-123", "guardrailVersion": "1", "streamProcessingMode": "async"} + } + + request = client._prepare_options([Message(role="user", contents=[Content.from_text(text="hello")])], options) + + assert request["guardrailConfig"] == {"guardrailIdentifier": "gr-123", "guardrailVersion": "1"} + assert options["guardrailConfig"]["streamProcessingMode"] == "async" + + def test_prepare_options_unsupported_tool_mode_raises(monkeypatch: pytest.MonkeyPatch) -> None: """Unexpected tool modes should raise a clear error.""" from agent_framework_bedrock import _chat_client as chat_client_module From 54a941cfae2ddb6f7e11890bbe7517785b52f803 Mon Sep 17 00:00:00 2001 From: kimnamu Date: Thu, 24 Sep 2026 19:20:39 +0900 Subject: [PATCH 3/4] Python: Omit fields Converse rejects for Bedrock Prompt Management ARNs Review (eavanvalkenburg): forwarding promptVariables still left managed-prompt requests invalid. Per the Converse API reference, a request whose modelId is a Prompt Management ARN can't include additionalModelRequestFields, inferenceConfig, system or toolConfig, and _prepare_options always sends inferenceConfig (default maxTokens). When modelId contains ":prompt/", leave those four fields out. The default maxTokens is dropped quietly; anything the caller set (instructions, tools, sampling options, additionalModelRequestFields) is dropped with one warning, the same way the Foundry agent client strips tools the service won't accept (#5130). Other model IDs, including prompt-router ARNs, are unchanged. Co-Authored-By: Claude --- .../agent_framework_bedrock/_chat_client.py | 14 +++++++++++ .../bedrock/tests/test_bedrock_client.py | 25 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index 4f2b3ffe18e..cfcbb9aa3ce 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -478,6 +478,20 @@ def _prepare_options( if guardrail_config := run_options.get("guardrailConfig"): # streamProcessingMode is only valid for ConverseStream; Converse rejects requests that include it. run_options["guardrailConfig"] = {k: v for k, v in guardrail_config.items() if k != "streamProcessingMode"} + if ":prompt/" in model: + # A Prompt Management ARN takes these fields from the prompt, and Converse rejects requests that set them. + if run_options["inferenceConfig"] == {"maxTokens": DEFAULT_MAX_TOKENS}: + del run_options["inferenceConfig"] # only the client default, nothing the caller asked for + if omitted := [ + key + for key in ("inferenceConfig", "system", "toolConfig", "additionalModelRequestFields") + if run_options.pop(key, None) is not None + ]: + logger.warning( + "Converse does not accept %s with a Prompt Management prompt; they are omitted from the request. " + "Define them on the prompt in Prompt Management instead.", + ", ".join(omitted), + ) return run_options diff --git a/python/packages/bedrock/tests/test_bedrock_client.py b/python/packages/bedrock/tests/test_bedrock_client.py index 41eea9df359..e4c39c20461 100644 --- a/python/packages/bedrock/tests/test_bedrock_client.py +++ b/python/packages/bedrock/tests/test_bedrock_client.py @@ -424,6 +424,31 @@ def test_prepare_options_omits_stream_processing_mode_from_guardrail_config() -> assert options["guardrailConfig"]["streamProcessingMode"] == "async" +def test_prepare_options_prompt_management_arn_omits_fields_converse_rejects(caplog: pytest.LogCaptureFixture) -> None: + """Converse rejects inferenceConfig, system, toolConfig and additionalModelRequestFields with a prompt ARN.""" + client = _make_client() + client.model = "arn:aws:bedrock:us-east-1:123456789012:prompt/PROMPT1234:1" + messages = [Message(role="user", contents=[Content.from_text(text="hello")])] + variables: BedrockChatOptions = {"promptVariables": {"topic": {"text": "hash maps"}}} + + with caplog.at_level("WARNING", logger="agent_framework.bedrock"): + request = client._prepare_options(messages, variables) + assert set(request) == {"modelId", "messages", "promptVariables"} + assert not caplog.records # the client's default maxTokens is dropped without a warning + + caller_set: BedrockChatOptions = { + **variables, + "instructions": "Be brief.", + "temperature": 0.2, + "tools": [{"toolSpec": {"name": "get_weather", "description": "Get weather", "inputSchema": {"json": {}}}}], + "additionalModelRequestFields": {"reasoning": {"effort": "low"}}, + } + with caplog.at_level("WARNING", logger="agent_framework.bedrock"): + request = client._prepare_options(messages, caller_set) + assert set(request) == {"modelId", "messages", "promptVariables"} + assert "inferenceConfig, system, toolConfig, additionalModelRequestFields" in caplog.text + + def test_prepare_options_unsupported_tool_mode_raises(monkeypatch: pytest.MonkeyPatch) -> None: """Unexpected tool modes should raise a clear error.""" from agent_framework_bedrock import _chat_client as chat_client_module From 66ba4f3f220db7b54eada76e11975b179a96fdfb Mon Sep 17 00:00:00 2001 From: kimnamu Date: Tue, 29 Sep 2026 07:52:18 +0900 Subject: [PATCH 4/4] Python: Keep guardrail streamProcessingMode on Bedrock ConverseStream requests Review (eavanvalkenburg): main now streams through converse_stream, where guardrailConfig.streamProcessingMode is valid, but _prepare_options still dropped it for every request, so stream=True silently lost the requested mode. Drop the key only on the non-streaming path, right before Converse is called. botocore 1.43.104 models guardrailConfig as GuardrailStreamConfiguration for ConverseStream (streamProcessingMode accepted) and GuardrailConfiguration for Converse (ParamValidationError). The regression test sends the same options through both paths and checks each request shape. Co-Authored-By: Claude --- .../agent_framework_bedrock/_chat_client.py | 7 ++++--- .../bedrock/tests/test_bedrock_client.py | 20 ++++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index b9b1dbcdac9..ea67ecdb3d4 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -392,6 +392,10 @@ async def _stream() -> AsyncIterable[ChatResponseUpdate]: return self._build_response_stream(_stream(), response_format=options.get("response_format")) # Non-streaming mode + if guardrail_config := request.get("guardrailConfig"): + # streamProcessingMode is only valid for ConverseStream; Converse rejects requests that include it. + request["guardrailConfig"] = {k: v for k, v in guardrail_config.items() if k != "streamProcessingMode"} + async def _get_response() -> ChatResponse: raw_response = await asyncio.to_thread(self._invoke_converse, request) return self._process_converse_response(raw_response, options) @@ -472,9 +476,6 @@ def _prepare_options( ): if (value := options.get(key)) is not None: run_options[key] = value - if guardrail_config := run_options.get("guardrailConfig"): - # streamProcessingMode is only valid for ConverseStream; Converse rejects requests that include it. - run_options["guardrailConfig"] = {k: v for k, v in guardrail_config.items() if k != "streamProcessingMode"} if ":prompt/" in model: # A Prompt Management ARN takes these fields from the prompt, and Converse rejects requests that set them. if run_options["inferenceConfig"] == {"maxTokens": DEFAULT_MAX_TOKENS}: diff --git a/python/packages/bedrock/tests/test_bedrock_client.py b/python/packages/bedrock/tests/test_bedrock_client.py index d3bdeb67284..5e25cad9692 100644 --- a/python/packages/bedrock/tests/test_bedrock_client.py +++ b/python/packages/bedrock/tests/test_bedrock_client.py @@ -489,16 +489,26 @@ async def test_get_response_forwards_bedrock_specific_options() -> None: assert {key: payload.get(key) for key in bedrock_options} == bedrock_options -def test_prepare_options_omits_stream_processing_mode_from_guardrail_config() -> None: - """Converse rejects streamProcessingMode, so it should be left out while the guardrail still applies.""" - client = _make_client() +async def test_guardrail_stream_processing_mode_is_sent_only_to_converse_stream() -> None: + """ConverseStream accepts streamProcessingMode and Converse rejects it, so only the Converse request drops it.""" + stub = _StubBedrockStreamRuntime([{"messageStop": {"stopReason": "end_turn"}}]) + client = BedrockChatClient( + model="us.openai.gpt-6-sol", + region="us-east-1", + client=stub, # pyrefly: ignore[bad-argument-type] # ty: ignore[invalid-argument-type] # pyright: ignore[reportArgumentType] + ) + messages = [Message(role="user", contents=[Content.from_text(text="hello")])] options: BedrockChatOptions = { "guardrailConfig": {"guardrailIdentifier": "gr-123", "guardrailVersion": "1", "streamProcessingMode": "async"} } - request = client._prepare_options([Message(role="user", contents=[Content.from_text(text="hello")])], options) + await client.get_response(messages, options=options) + stream = client._inner_get_response(messages=messages, options=options, stream=True) + assert isinstance(stream, ResponseStream) + _ = [update async for update in stream] - assert request["guardrailConfig"] == {"guardrailIdentifier": "gr-123", "guardrailVersion": "1"} + assert stub.calls[0]["guardrailConfig"] == {"guardrailIdentifier": "gr-123", "guardrailVersion": "1"} + assert stub.calls[1]["guardrailConfig"] == options["guardrailConfig"] assert options["guardrailConfig"]["streamProcessingMode"] == "async"