diff --git a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude_transformation.py index d52720865a3..36206f45d31 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude_transformation.py @@ -360,6 +360,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): model=model, request_body=anthropic_request, model_info=model_info, + custom_llm_provider=self.custom_llm_provider or "bedrock", ) strip_unsupported_bedrock_invoke_output_config_keys( model=model, diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index 5f3729eda9a..a4c008e7e3d 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -561,16 +561,16 @@ def apply_bedrock_invoke_structured_output( model: str, request_body: dict[str, object], # mutable-ok: edited in place like siblings model_info: Mapping[str, object] | None = None, + custom_llm_provider: str = "bedrock", ) -> None: """ - Route Anthropic structured-output params to what the Bedrock model supports. + Route Anthropic structured-output params to the provider's native format. Consumes the legacy top-level ``output_format`` and the newer ``output_config.format``, keeping the pre-existing precedence of the legacy - field when a request carries both. Models flagged - ``supports_native_structured_output`` in the model map get the schema - forwarded as ``output_config.format``, which Bedrock relays to the model for - enforced structured output. For every other model the schema is inlined into + field when a request carries both. Native Mantle models keep ``output_format`` + in the Anthropic Messages body. Other native Bedrock models receive + ``output_config.format``. For every other model the schema is inlined into the last user message as best-effort text, with a warning because nothing enforces it. """ @@ -584,9 +584,13 @@ def apply_bedrock_invoke_structured_output( native_format: Final = bedrock_native_output_format( model, schema_format, + custom_llm_provider=custom_llm_provider, model_info=model_info, ) if native_format is not None: + if custom_llm_provider == "bedrock_mantle": + request_body["output_format"] = native_format # rebind-ok: mutate the caller's request body + return existing_output_config: Final = request_body.get("output_config") if isinstance(existing_output_config, dict): existing_output_config["format"] = native_format diff --git a/tests/unit/llms/bedrock/chat/mantle/test_bedrock_chat_mantle_transformation.py b/tests/unit/llms/bedrock/chat/mantle/test_bedrock_chat_mantle_transformation.py index eed217a53f8..13338c089ef 100644 --- a/tests/unit/llms/bedrock/chat/mantle/test_bedrock_chat_mantle_transformation.py +++ b/tests/unit/llms/bedrock/chat/mantle/test_bedrock_chat_mantle_transformation.py @@ -286,10 +286,10 @@ def test_mantle_claude_that_cannot_force_a_tool_keeps_json_schema_on_the_json_to @pytest.mark.usefixtures("local_model_cost_map") def test_mantle_uses_native_output_only_for_schemas_within_the_property_limit() -> None: - from litellm.llms.bedrock.chat.mantle.transformation import AmazonMantleConfig + from litellm.llms.bedrock_mantle.chat.claude_transformation import BedrockMantleClaudeChatConfig model: Final = "anthropic.claude-haiku-4-5" - config: Final = AmazonMantleConfig() + config: Final = BedrockMantleClaudeChatConfig() def transform(size: int) -> dict[str, object]: response_format: Final = { @@ -310,11 +310,18 @@ def test_mantle_uses_native_output_only_for_schemas_within_the_property_limit() model=model, drop_params=False, ) - return optional_params + return config.transform_request( + model=model, + messages=[{"role": "user", "content": "Return a JSON object."}], + optional_params=optional_params, + litellm_params={}, + headers={}, + ) requests: Final = tuple(transform(size) for size in (2, 120)) assert "output_format" in requests[0] assert "tools" not in requests[0] + assert "field_000" not in json.dumps(requests[0]["messages"]) assert "output_format" not in requests[1] assert requests[1]["tool_choice"] == {"type": "tool", "name": "json_tool_call"} assert requests[1]["tools"][0]["name"] == "json_tool_call"