fix(bedrock): keep native structured output in Mantle requests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mateo 2026-11-07 02:46:50 +00:00
parent 6faf6a2592
commit 750536e78e
3 changed files with 20 additions and 8 deletions

View file

@ -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,

View file

@ -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

View file

@ -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"