feat(bedrock): default to native structured output on Opus 5 and Opus 4.8 (#50222)

* feat(bedrock): enable native structured output for Opus 5 models

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(bedrock): keep govcloud and bare model rows off native output

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(bedrock): retry rejected native schemas with the JSON tool on default-native models

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(bedrock): keep the rejection retry on the unforceable-tool native path

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(bedrock): retry rejected schemas on the Messages path for default-native models

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(bedrock): keep JSON-tool streaming coverage on unchanged route

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style(bedrock): format Messages native output retry

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(bedrock): allow profile-only native output metadata

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(bedrock): only disable default-native output on schema-rejection retries

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(bedrock): type fixture parameters in native output retry tests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(bedrock): retry with the JSON tool when Bedrock rejects a schema's grammar size

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(bedrock): drop strict tools after a native-output retry hits the grammar limit

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style(bedrock): format native output fallback

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Mateo Wang 2026-11-05 17:22:17 -08:00 • committed by GitHub
parent 7b550cdd94
commit 14d50ed40d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 437 additions and 76 deletions

View file

@ -77,6 +77,7 @@ from litellm.llms.bedrock.common_utils import (
bedrock_model_supports_regex_lookaround,
bedrock_native_output_format,
bedrock_native_output_is_opt_in,
bedrock_native_output_retry_requested,
bedrock_supports_tool_search,
deployment_model_info,
requests_output_config_format,
@ -1425,6 +1426,16 @@ class AmazonConverseConfig(BaseConfig):
deployment_model_info(litellm_params),
)
def uses_native_output(
self, model: str, optional_params: Mapping[str, object], litellm_params: Mapping[str, object]
) -> bool:
return bedrock_native_output_retry_requested(
bedrock_capability_model(model, litellm_params.get("base_model")),
optional_params,
self.custom_llm_provider or "bedrock_converse",
deployment_model_info(litellm_params),
)
def update_optional_params_with_thinking_tokens(self, non_default_params: dict, optional_params: dict):
"""
Handles scenario where max tokens is not specified. For anthropic models (anthropic api/bedrock/vertex ai), this requires having the max tokens being set and being greater than the thinking token budget.

View file

@ -34,6 +34,7 @@ from litellm.llms.bedrock.common_utils import (
bedrock_capability_model,
bedrock_chat_uses_native_structured_output,
bedrock_cleaned_output_schema,
bedrock_native_output_retry_requested,
bedrock_supports_tool_search,
deployment_model_info,
get_anthropic_beta_from_headers,
@ -161,6 +162,13 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
) -> bool:
return requests_opt_in_native_output(model, optional_params, deployment_model_info(litellm_params))
def uses_native_output(
self, model: str, optional_params: Mapping[str, object], litellm_params: Mapping[str, object]
) -> bool:
return bedrock_native_output_retry_requested(
model, optional_params, model_info=deployment_model_info(litellm_params)
)
def map_openai_params(
self,
non_default_params: dict,

View file

@ -1,4 +1,5 @@
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from functools import partial
from typing import Final, TypeAlias
@ -6,6 +7,7 @@ from litellm.llms.anthropic.common_utils import is_claude_strict_tools_rejection
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.bedrock.common_utils import (
NativeOutputFallback,
bedrock_strict_tools_fallback,
is_bedrock_native_output_rejection,
warn_bedrock_native_output_rejected,
warn_bedrock_strict_tools_dropped,
@ -17,7 +19,15 @@ NativeOutputResponse: TypeAlias = ModelResponse | CustomStreamWrapper
NativeOutputAttemptResult: TypeAlias = NativeOutputResponse | Coroutine[object, object, NativeOutputResponse]
NativeOutputAttempt: TypeAlias = Callable[[NativeOutputFallback | None], NativeOutputAttemptResult]
NativeOutputFallbackFactory: TypeAlias = Callable[[], NativeOutputFallback | None]
_RetryFor: TypeAlias = Callable[[BaseLLMException], NativeOutputFallback | None]
@dataclass(frozen=True, slots=True)
class _SelectedFallback:
fallback: NativeOutputFallback
native_output_retry: bool
_RetryFor: TypeAlias = Callable[[BaseLLMException], _SelectedFallback | None]
def _native_output_retry(
@ -47,17 +57,42 @@ def _fallback_for(
fallback: NativeOutputFallbackFactory | None,
strict_tools_fallback: NativeOutputFallbackFactory | None,
model: str,
) -> NativeOutputFallback | None:
) -> _SelectedFallback | None:
native_output_retry: Final = _native_output_retry(error, fallback, model)
if native_output_retry is not None:
return native_output_retry
return _strict_tools_retry(error, strict_tools_fallback, model)
return _SelectedFallback(native_output_retry, native_output_retry=True)
strict_tools_retry: Final = _strict_tools_retry(error, strict_tools_fallback, model)
if strict_tools_retry is None:
return None
return _SelectedFallback(strict_tools_retry, native_output_retry=False)
def _strict_tools_retry_after_native_output_retry(
error: BaseLLMException,
retry: _SelectedFallback,
strict_tools_fallback: NativeOutputFallbackFactory | None,
model: str,
) -> NativeOutputFallback | None:
if (
not retry.native_output_retry
or strict_tools_fallback is None
or not is_claude_strict_tools_rejection(error.status_code, error.message)
):
return None
composed_retry: Final = bedrock_strict_tools_fallback(
model, retry.fallback.optional_params, retry.fallback.litellm_params
)
if composed_retry is not None:
warn_bedrock_strict_tools_dropped(model)
return composed_retry
async def _await_with_native_output_fallback(
first_attempt: Coroutine[object, object, NativeOutputResponse],
attempt: NativeOutputAttempt,
retry_for: _RetryFor,
strict_tools_fallback: NativeOutputFallbackFactory | None,
model: str,
) -> NativeOutputResponse:
try:
return await first_attempt
@ -65,8 +100,17 @@ async def _await_with_native_output_fallback(
retry: Final = retry_for(error)
if retry is None:
raise
second_attempt: Final = attempt(retry)
return await second_attempt if isinstance(second_attempt, Coroutine) else second_attempt
try:
second_attempt: Final = attempt(retry.fallback)
return await second_attempt if isinstance(second_attempt, Coroutine) else second_attempt
except BaseLLMException as error:
composed_retry: Final = _strict_tools_retry_after_native_output_retry(
error, retry, strict_tools_fallback, model
)
if composed_retry is None:
raise
third_attempt: Final = attempt(composed_retry)
return await third_attempt if isinstance(third_attempt, Coroutine) else third_attempt
def call_with_native_output_fallback(
@ -86,7 +130,15 @@ def call_with_native_output_fallback(
retry: Final = retry_for(error)
if retry is None:
raise
return attempt(retry)
try:
return attempt(retry.fallback)
except BaseLLMException as retry_error:
composed_retry: Final = _strict_tools_retry_after_native_output_retry(
retry_error, retry, strict_tools_fallback, model
)
if composed_retry is None:
raise
return attempt(composed_retry)
if isinstance(first_attempt, Coroutine):
return _await_with_native_output_fallback(first_attempt, attempt, retry_for)
return _await_with_native_output_fallback(first_attempt, attempt, retry_for, strict_tools_fallback, model)
return first_attempt

View file

@ -292,6 +292,15 @@ def _bedrock_model_supports(model: str, key: str, custom_llm_provider: str = "be
return supports_factory(model=model, custom_llm_provider=custom_llm_provider, key=key)
def _bedrock_native_output_enabled(
model: str, custom_llm_provider: str, model_info: Mapping[str, object] | None
) -> bool:
deployment_flag: Final = (model_info or {}).get("supports_native_structured_output")
if isinstance(deployment_flag, bool):
return deployment_flag
return _bedrock_model_supports(model, "supports_native_structured_output", custom_llm_provider)
_DEPLOYMENT_MODEL_INFO: Final = TypeAdapter(dict[str, object])
@ -312,9 +321,9 @@ def _bedrock_native_output_opt_in(
def bedrock_supports_native_structured_output(
model: str, custom_llm_provider: str = "bedrock", model_info: Mapping[str, object] | None = None
) -> bool:
return _bedrock_model_supports(
model, "supports_native_structured_output", custom_llm_provider
) or _bedrock_native_output_opt_in(model, custom_llm_provider, model_info)
return _bedrock_native_output_enabled(model, custom_llm_provider, model_info) or _bedrock_native_output_opt_in(
model, custom_llm_provider, model_info
)
def _bedrock_map_native_output_flag(model: str) -> object:
@ -349,7 +358,7 @@ def bedrock_chat_opts_into_native_structured_output(
custom_llm_provider: str = "bedrock",
model_info: Mapping[str, object] | None = None,
) -> bool:
return _bedrock_model_supports(model, "supports_native_structured_output", custom_llm_provider) or (
return _bedrock_native_output_enabled(model, custom_llm_provider, model_info) or (
thinking_requested and _bedrock_native_output_opt_in(model, custom_llm_provider, model_info)
)
@ -370,15 +379,15 @@ def bedrock_chat_uses_native_structured_output(
def bedrock_native_output_is_deployment_opt_in(
model: str, custom_llm_provider: str = "bedrock", model_info: Mapping[str, object] | None = None
) -> bool:
return not _bedrock_model_supports(
model, "supports_native_structured_output", custom_llm_provider
) and _bedrock_native_output_opt_in(model, custom_llm_provider, model_info)
return not _bedrock_native_output_enabled(model, custom_llm_provider, model_info) and _bedrock_native_output_opt_in(
model, custom_llm_provider, model_info
)
def bedrock_native_output_is_opt_in(
model: str, custom_llm_provider: str = "bedrock", model_info: Mapping[str, object] | None = None
) -> bool:
return not _bedrock_model_supports(model, "supports_native_structured_output", custom_llm_provider) and (
return not _bedrock_native_output_enabled(model, custom_llm_provider, model_info) and (
_bedrock_native_output_opt_in(model, custom_llm_provider, model_info)
or _bedrock_native_output_replaces_unforceable_tool(model, model_info)
)
@ -387,7 +396,21 @@ def bedrock_native_output_is_opt_in(
def without_bedrock_native_output_opt_in(litellm_params: Mapping[str, object]) -> Mapping[str, object]:
return {
**litellm_params,
"model_info": {**deployment_model_info(litellm_params), "supports_bedrock_native_structured_output": False},
"model_info": {
**deployment_model_info(litellm_params),
"supports_bedrock_native_structured_output": False,
},
}
def for_bedrock_json_tool_retry(litellm_params: Mapping[str, object]) -> Mapping[str, object]:
opt_in_disabled: Final = without_bedrock_native_output_opt_in(litellm_params)
return {
**opt_in_disabled,
"model_info": {
**deployment_model_info(opt_in_disabled),
"supports_native_structured_output": False,
},
}
@ -395,6 +418,7 @@ _BEDROCK_NATIVE_OUTPUT_UNSUPPORTED: Final = "output_config.format: Extra inputs
_BEDROCK_NATIVE_OUTPUT_REJECTIONS: Final = (
_BEDROCK_NATIVE_OUTPUT_UNSUPPORTED,
"compilation exceeded limits: Schema is too complex",
"The compiled grammar is too large",
"output_config.format.schema:",
)
@ -475,8 +499,27 @@ def requests_output_config_format(optional_params: Mapping[str, object]) -> bool
def requests_native_output(anthropic_optional_params: Mapping[str, object]) -> bool:
return isinstance(anthropic_optional_params.get("output_format"), Mapping) or requests_output_config_format(
anthropic_optional_params
output_config: Final = json_object_or_none(anthropic_optional_params.get("outputConfig"))
return (
isinstance(anthropic_optional_params.get("output_format"), Mapping)
or requests_output_config_format(anthropic_optional_params)
or (
anthropic_optional_params.get("json_mode") is True
and output_config is not None
and isinstance(output_config.get("textFormat"), Mapping)
)
)
def bedrock_native_output_retry_requested(
model: str,
optional_params: Mapping[str, object],
custom_llm_provider: str = "bedrock",
model_info: Mapping[str, object] | None = None,
) -> bool:
return requests_native_output(optional_params) and (
bedrock_native_output_is_opt_in(model, custom_llm_provider, model_info)
or _bedrock_native_output_enabled(model, custom_llm_provider, model_info)
)
@ -518,7 +561,7 @@ def bedrock_native_output_schema(
model_info: Mapping[str, object] | None = None,
) -> Mapping[str, object] | None:
native_schema: Final = bedrock_cleaned_output_schema(model, schema, custom_llm_provider)
if _bedrock_model_supports(model, "supports_native_structured_output", custom_llm_provider):
if _bedrock_native_output_enabled(model, custom_llm_provider, model_info):
return schema if native_schema is None else native_schema
if _bedrock_native_output_opt_in(model, custom_llm_provider, model_info):
return native_schema

View file

@ -40,15 +40,17 @@ from litellm.llms.bedrock.common_utils import (
BEDROCK_INVOKE_CONTEXT_MANAGEMENT_EDIT_BETAS,
BedrockError,
apply_bedrock_invoke_structured_output,
bedrock_native_output_is_deployment_opt_in,
bedrock_native_output_format,
bedrock_native_output_retry_requested,
bedrock_supports_tool_search,
ensure_bedrock_anthropic_messages_tool_names,
for_bedrock_json_tool_retry,
get_anthropic_beta_from_headers,
is_bedrock_native_output_rejection,
json_object_or_none,
normalize_bedrock_opus_output_config_effort,
normalize_custom_field_on_tools,
normalize_tool_input_schema_types_for_bedrock_invoke,
requests_native_output,
requests_output_config_format,
strip_unsupported_bedrock_invoke_output_config_keys,
supports_bedrock_prompt_cache_ttl,
@ -133,13 +135,24 @@ class AmazonAnthropicClaudeMessagesConfig(
) -> GenericLiteLLMParams | None:
model_info: Final = _MODEL_INFO.validate_python(litellm_params.model_info)
if not (
requests_native_output(anthropic_messages_optional_request_params)
and bedrock_native_output_is_deployment_opt_in(model, model_info=model_info)
bedrock_native_output_retry_requested(
model, anthropic_messages_optional_request_params, model_info=model_info
)
):
return None
return litellm_params.model_copy(
update={"model_info": {**(model_info or {}), "supports_bedrock_native_structured_output": False}}
legacy_output_format: Final = json_object_or_none(
anthropic_messages_optional_request_params.get("output_format")
)
output_config: Final = json_object_or_none(anthropic_messages_optional_request_params.get("output_config"))
output_config_format: Final = (
json_object_or_none(output_config.get("format")) if output_config is not None else None
)
output_format: Final = legacy_output_format if legacy_output_format is not None else output_config_format
if output_format is None or (bedrock_native_output_format(model, output_format, model_info=model_info) is None):
return None
retry_params: Final = for_bedrock_json_tool_retry({"model_info": model_info})
retry_model_info: Final = _MODEL_INFO.validate_python(retry_params.get("model_info"))
return litellm_params.model_copy(update={"model_info": retry_model_info})
def should_retry_anthropic_messages_without_native_output(
self, e: httpx.HTTPStatusError, request_body: Mapping[str, object], model: str

View file

@ -143,7 +143,7 @@ from litellm.llms.bedrock.common_utils import (
bedrock_route_for_request,
bedrock_strict_tools_fallback,
deployment_model_info,
without_bedrock_native_output_opt_in,
for_bedrock_json_tool_retry,
without_bedrock_route_prefix,
)
from litellm.llms.bedrock_mantle.chat.claude_transformation import bedrock_mantle_chat_config
@ -4369,7 +4369,7 @@ def _native_output_fallback(
first_attempt_params: Mapping[str, object],
first_attempt_litellm_params: Mapping[str, object],
) -> NativeOutputFallback:
litellm_params: Final = without_bedrock_native_output_opt_in(first_attempt_litellm_params)
litellm_params: Final = for_bedrock_json_tool_retry(first_attempt_litellm_params)
remapped_params: Final = ctx.optional_params_for_model_info(deployment_model_info(litellm_params))
return NativeOutputFallback(
optional_params={
@ -4500,7 +4500,7 @@ def _complete_bedrock(ctx: CompletionDispatchContext) -> _CompletionDispatchResu
converse_attempt,
(
partial(_converse_native_output_fallback, ctx, converse_params, dict(litellm_params))
if AmazonConverseConfig().uses_opt_in_native_output(converse_model, converse_params, litellm_params)
if AmazonConverseConfig().uses_native_output(converse_model, converse_params, litellm_params)
else None
),
converse_model,
@ -4556,7 +4556,7 @@ def _complete_bedrock(ctx: CompletionDispatchContext) -> _CompletionDispatchResu
(
partial(_native_output_fallback, ctx, invoke_params, dict(litellm_params))
if isinstance(provider_config, AmazonAnthropicClaudeConfig)
and provider_config.uses_opt_in_native_output(model, invoke_params, litellm_params)
and provider_config.uses_native_output(model, invoke_params, litellm_params)
else None
),
model,

View file

@ -2242,7 +2242,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
@ -2331,7 +2331,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
@ -2420,7 +2420,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
@ -2504,7 +2504,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
@ -2593,7 +2593,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
@ -2715,7 +2715,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -2759,7 +2759,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -2803,7 +2803,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -2847,7 +2847,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -2891,7 +2891,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -75504,7 +75504,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -75544,7 +75544,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,

View file

@ -2242,7 +2242,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
@ -2331,7 +2331,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
@ -2420,7 +2420,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
@ -2504,7 +2504,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
@ -2593,7 +2593,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
@ -2715,7 +2715,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -2759,7 +2759,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -2803,7 +2803,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -2847,7 +2847,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -2891,7 +2891,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -75504,7 +75504,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
@ -75544,7 +75544,7 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,

View file

@ -2047,8 +2047,9 @@ def _invoke_chat_params(
)
@pytest.mark.parametrize("model", ["us.anthropic.claude-opus-4-8", "global.anthropic.claude-sonnet-5"])
def test_invoke_uses_opt_in_native_output_when_the_opt_in_drove_native_output(
local_model_cost_map, model, thinking_params
local_model_cost_map, model, thinking_params, monkeypatch
):
monkeypatch.setitem(litellm.model_cost[model], "supports_native_structured_output", False)
optional_params: Final = _invoke_chat_params(
model, None, {"response_format": _CITY_RESPONSE_FORMAT, **thinking_params}
)
@ -2083,6 +2084,15 @@ def test_invoke_uses_opt_in_native_output_is_false_when_native_output_did_not_co
)
def test_invoke_uses_default_native_output_for_schema_retry(local_model_cost_map: None) -> None:
model: Final = "us.anthropic.claude-opus-5"
optional_params: Final = _invoke_chat_params(
model, None, {"response_format": _CITY_RESPONSE_FORMAT, "thinking": {"type": "disabled"}}
)
assert AmazonAnthropicClaudeConfig().uses_native_output(model, optional_params, {})
@pytest.mark.parametrize(
("forced_tool_use", "map_flag", "model_info", "thinking_params", "native"),
[

View file

@ -5244,6 +5244,44 @@ def test_models_whose_thinking_stops_under_native_output_keep_the_json_tool_only
assert ("toolConfig" in request) is not native
@pytest.mark.usefixtures("local_model_cost_map")
@pytest.mark.parametrize(
("model", "thinking_params", "native"),
[
("us.anthropic.claude-opus-5", None, True),
("us.anthropic.claude-opus-5", {"thinking": {"type": "adaptive"}}, True),
("us.anthropic.claude-opus-5", {"thinking": {"type": "disabled"}}, True),
("us.anthropic.claude-opus-4-8", None, True),
("us.anthropic.claude-opus-4-8", {"thinking": {"type": "adaptive"}}, True),
("us.anthropic.claude-opus-4-8", {"thinking": {"type": "disabled"}}, True),
("us-gov.anthropic.claude-opus-5", None, False),
("us-gov.anthropic.claude-opus-4-8", None, False),
],
ids=[
"opus-5-unset",
"opus-5-adaptive",
"opus-5-disabled",
"opus-4-8-unset",
"opus-4-8-adaptive",
"opus-4-8-disabled",
"opus-5-us-gov",
"opus-4-8-us-gov",
],
)
def test_opus_5_and_4_8_cost_map_defaults_use_native_output_across_thinking_modes(
model: str, thinking_params: Mapping[str, object] | None, native: bool
) -> None:
_, request = _converse_request_for_json_schema(model, False, thinking_params)
if native:
assert request["outputConfig"]["textFormat"]["type"] == "json_schema"
assert "toolConfig" not in request
return
assert "outputConfig" not in request
assert [tool["toolSpec"]["name"] for tool in request["toolConfig"]["tools"]] == ["json_tool_call"]
def _converse_capital_answer(request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
@ -11599,7 +11637,10 @@ _OUTPUT_CONFIG_FORMAT: Final = {
ids=["adaptive", "reasoning_effort", "budget", "output_config_format"],
)
@pytest.mark.parametrize("model", ["us.anthropic.claude-opus-4-8", "global.anthropic.claude-sonnet-5"])
def test_uses_opt_in_native_output_when_the_opt_in_drove_native_output(local_model_cost_map, model, request_params):
def test_uses_opt_in_native_output_when_the_opt_in_drove_native_output(
local_model_cost_map, monkeypatch, model, request_params
):
monkeypatch.setitem(litellm.model_cost[model], "supports_native_structured_output", False)
optional_params: Final = _converse_params(model, None, request_params)
assert AmazonConverseConfig().uses_opt_in_native_output(model, optional_params, {"model_info": {"id": "d1"}})
@ -11630,7 +11671,10 @@ def test_uses_opt_in_native_output_is_false_when_native_output_did_not_come_from
assert not AmazonConverseConfig().uses_opt_in_native_output(model, optional_params, {"model_info": model_info})
def test_uses_opt_in_native_output_reads_the_base_model_of_an_application_inference_profile(local_model_cost_map):
def test_uses_opt_in_native_output_reads_the_base_model_of_an_application_inference_profile(
local_model_cost_map, monkeypatch
):
monkeypatch.setitem(litellm.model_cost["us.anthropic.claude-opus-4-8"], "supports_native_structured_output", False)
profile: Final = "arn:aws:bedrock:us-east-1:000000000000:application-inference-profile/profile-id"
litellm_params: Final = {"base_model": "bedrock/us.anthropic.claude-opus-4-8"}
optional_params: Final = _converse_params(
@ -11698,8 +11742,9 @@ _BEDROCK_SCHEMA_REFUSAL: Final = "output_config.format.schema: pattern is too co
)
@pytest.mark.respx(assert_all_called=True)
def test_a_refused_native_schema_is_sent_again_only_when_the_retry_drops_native_output(
caller_params: dict, expected_calls: int, respx_mock: respx.Router, local_model_cost_map: None
caller_params: dict, expected_calls: int, respx_mock: respx.Router, local_model_cost_map: None, monkeypatch
) -> None:
monkeypatch.setitem(litellm.model_cost["us.anthropic.claude-opus-4-8"], "supports_native_structured_output", False)
route: Final = respx_mock.post(url__regex=r"/converse$").mock(
return_value=httpx.Response(400, json={"message": _BEDROCK_SCHEMA_REFUSAL})
)
@ -11722,6 +11767,61 @@ def test_a_refused_native_schema_is_sent_again_only_when_the_retry_drops_native_
assert all("outputConfig" not in json.loads(call.request.content) for call in route.calls[1:])
@pytest.mark.respx(assert_all_called=True)
def test_a_default_native_schema_rejection_retries_with_the_json_tool(
respx_mock: respx.Router, local_model_cost_map: None
) -> None:
route: Final = respx_mock.post(url__regex=r"/converse$").mock(
return_value=httpx.Response(400, json={"message": _BEDROCK_SCHEMA_REFUSAL})
)
with pytest.raises(litellm.BadRequestError, match=re.escape(_BEDROCK_SCHEMA_REFUSAL)):
completion(
model="bedrock/converse/us.anthropic.claude-opus-5",
messages=[{"role": "user", "content": "What is the capital of France?"}],
response_format=CAPITAL_RESPONSE_FORMAT,
thinking={"type": "disabled"},
num_retries=0,
aws_access_key_id="fake",
aws_secret_access_key="fake",
aws_region_name="us-east-1",
client=HTTPHandler(),
)
assert route.call_count == 2
first_request: Final = json.loads(route.calls[0].request.content)
retry_request: Final = json.loads(route.calls[1].request.content)
assert "outputConfig" in first_request and "toolConfig" not in first_request
assert "toolConfig" in retry_request and "outputConfig" not in retry_request
@pytest.mark.respx(assert_all_called=True)
def test_an_unforceable_tool_native_schema_rejection_retries_with_the_json_tool(
respx_mock: respx.Router, local_model_cost_map: None
) -> None:
route: Final = respx_mock.post(url__regex=r"/converse$").mock(
return_value=httpx.Response(400, json={"message": _BEDROCK_SCHEMA_REFUSAL})
)
with pytest.raises(litellm.BadRequestError, match=re.escape(_BEDROCK_SCHEMA_REFUSAL)):
completion(
model="bedrock/converse/anthropic.claude-fable-5-1",
messages=[{"role": "user", "content": "What is the capital of France?"}],
response_format=CAPITAL_RESPONSE_FORMAT,
num_retries=0,
aws_access_key_id="fake",
aws_secret_access_key="fake",
aws_region_name="us-east-1",
client=HTTPHandler(),
)
assert route.call_count == 2
first_request: Final = json.loads(route.calls[0].request.content)
retry_request: Final = json.loads(route.calls[1].request.content)
assert "outputConfig" in first_request and "toolConfig" not in first_request
assert "toolConfig" in retry_request and "outputConfig" not in retry_request
CONTROL_PLANE_ENDPOINT: Final = "https://bedrock-fips.us-east-1.amazonaws.com"

View file

@ -1214,7 +1214,7 @@ async def test_converse_json_schema_stream_sends_claude_prose_only_when_claude_s
transport: Final = httpx.MockTransport(handle_request)
request: Final = {
"model": "bedrock/us.anthropic.claude-opus-5",
"model": "bedrock/anthropic.claude-opus-5",
"messages": [{"role": "user", "content": "What is the LCM of 24 and 120? Reply in JSON."}],
"response_format": {
"type": "json_schema",

View file

@ -13,10 +13,33 @@ _MODEL: Final = "us.anthropic.claude-opus-4-8"
_FALLBACK: Final = NativeOutputFallback(
optional_params={"tools": "json_tool_call"}, litellm_params={"model_info": {"opted_out": True}}
)
_NATIVE_RETRY_WITH_STRICT_TOOLS: Final = NativeOutputFallback(
optional_params={
"tools": (
{"function": {"name": "json_schema", "strict": True}},
{"function": {"name": "custom", "strict": True}},
)
},
litellm_params={
"model_info": {
"supports_native_structured_output": False,
"supports_bedrock_native_structured_output": False,
}
},
)
_COMPOSED_FALLBACK: Final = NativeOutputFallback(
optional_params={"tools": [{"function": {"name": "json_schema"}}, {"function": {"name": "custom"}}]},
litellm_params=_NATIVE_RETRY_WITH_STRICT_TOOLS.litellm_params,
)
_REJECTION: Final = BedrockError(
status_code=400,
message='{"message":"The model returned the following errors: output_config.format: Extra inputs are not permitted"}',
)
_GRAMMAR_REJECTION: Final = BedrockError(
status_code=400,
message='{"message":"The model returned the following errors: The compiled grammar is too large, which would cause '
'performance issues. Simplify your tool schemas or reduce the number of strict tools."}',
)
_SCHEMA_REFUSAL: Final = BedrockError(
status_code=400, message='{"message":"output_config.format.schema: pattern is too complex for structured output"}'
)
@ -58,8 +81,15 @@ _MODES: Final = pytest.mark.parametrize("asynchronous", [False, True], ids=["syn
@_MODES
def test_a_native_output_rejection_is_sent_again_once_with_the_fallback_params(asynchronous: bool) -> None:
bedrock: Final = ScriptedBedrock(errors=(_REJECTION, None), asynchronous=asynchronous)
@pytest.mark.parametrize(
"rejection",
[_REJECTION, _GRAMMAR_REJECTION],
ids=["format_not_permitted", "compiled_grammar_too_large"],
)
def test_a_native_output_rejection_is_sent_again_once_with_the_fallback_params(
asynchronous: bool, rejection: BedrockError
) -> None:
bedrock: Final = ScriptedBedrock(errors=(rejection, None), asynchronous=asynchronous)
response: Final = _call(bedrock, _FALLBACK)
@ -77,6 +107,19 @@ def test_a_second_native_output_rejection_reaches_the_caller(asynchronous: bool)
assert len(bedrock.sent) == 2
@_MODES
def test_a_grammar_rejection_after_a_native_retry_reaches_the_caller_without_strict_tools_fallback(
asynchronous: bool,
) -> None:
bedrock: Final = ScriptedBedrock(errors=(_GRAMMAR_REJECTION, _GRAMMAR_REJECTION), asynchronous=asynchronous)
with pytest.raises(BedrockError) as raised:
_resolve(call_with_native_output_fallback(bedrock, lambda: _FALLBACK, _MODEL))
assert raised.value is _GRAMMAR_REJECTION
assert bedrock.sent == (None, _FALLBACK)
@_MODES
@pytest.mark.parametrize(
("error", "fallback"),
@ -155,13 +198,16 @@ _OTHER_400S_WITH_A_STRICT_TOOL: Final = tuple(
def _call_with_strict_tools_fallback(
bedrock: ScriptedBedrock, fallback: NativeOutputFallback | None, strict_tools_fallback: NativeOutputFallback | None
bedrock: ScriptedBedrock,
fallback: NativeOutputFallback | None,
strict_tools_fallback: NativeOutputFallback | None,
model: str = _MODEL,
) -> object:
return _resolve(
call_with_native_output_fallback(
bedrock,
None if fallback is None else lambda: fallback,
_MODEL,
model,
strict_tools_fallback=None if strict_tools_fallback is None else lambda: strict_tools_fallback,
)
)
@ -223,3 +269,18 @@ def test_each_rejection_is_sent_again_with_its_own_fallback_when_both_are_set(
_call_with_strict_tools_fallback(bedrock, _FALLBACK, _STRICT_FALLBACK)
assert bedrock.sent == (None, retried_with)
@_MODES
def test_a_strict_tools_rejection_after_native_retry_composes_both_fallbacks(asynchronous: bool) -> None:
bedrock: Final = ScriptedBedrock(
errors=(_GRAMMAR_REJECTION, _GRAMMAR_REJECTION, None),
asynchronous=asynchronous,
)
response: Final = _call_with_strict_tools_fallback(
bedrock, _NATIVE_RETRY_WITH_STRICT_TOOLS, _STRICT_FALLBACK, "us.anthropic.claude-sonnet-4-6"
)
assert isinstance(response, ModelResponse) and response.id == "answer-3"
assert bedrock.sent == (None, _NATIVE_RETRY_WITH_STRICT_TOOLS, _COMPOSED_FALLBACK)

View file

@ -2047,7 +2047,7 @@ class TestBedrockBatchAnthropicRowParams:
@pytest.mark.parametrize(
("model", "native_output"),
[("bedrock/us.anthropic.claude-opus-4-8", False), (MODEL, True)],
[("bedrock/us.anthropic.claude-sonnet-5-5", False), (MODEL, True)],
ids=["opt_in_native_output_model", "native_output_model"],
)
def test_thinking_json_schema_rows_use_native_output_only_on_models_that_always_take_it(

View file

@ -4016,7 +4016,10 @@ _NATIVE_OUTPUT_REJECTION: Final = {
"message": "The model returned the following errors: compilation exceeded limits: Schema is too complex."
}
_STALE_SIGNATURE_ERROR: Final = {"message": "messages.1.content.0: Invalid `signature` in `thinking` block"}
_OPTED_OUT: Final = {"supports_bedrock_native_structured_output": False}
_OPTED_OUT: Final = {
"supports_native_structured_output": False,
"supports_bedrock_native_structured_output": False,
}
_CONSTRAINED_SCHEMA: Final = {
"type": "object",
"properties": {
@ -4158,21 +4161,21 @@ async def test_bedrock_messages_native_output_rejection_and_signature_400_each_g
@pytest.mark.usefixtures("local_model_cost_map")
@pytest.mark.parametrize(
("model", "model_info", "error"),
("model", "model_info", "error", "should_retry"),
[
("us.anthropic.claude-opus-4-8", None, {"message": "messages.0.content: Field required"}),
("us.anthropic.claude-sonnet-4-6", None, _NATIVE_OUTPUT_REJECTION),
("us.anthropic.claude-opus-4-8", _OPTED_OUT, _NATIVE_OUTPUT_REJECTION),
("us.anthropic.claude-opus-4-8", None, {"message": "messages.0.content: Field required"}, False),
("us.anthropic.claude-sonnet-4-6", None, _NATIVE_OUTPUT_REJECTION, True),
("us.anthropic.claude-opus-4-8", _OPTED_OUT, _NATIVE_OUTPUT_REJECTION, False),
],
ids=["other_400", "native_model", "deployment_opt_out"],
ids=["other_400", "native_rejection", "deployment_opt_out"],
)
async def test_bedrock_messages_400_is_not_retried_unless_it_rejects_opt_in_native_output(
model: str, model_info: Mapping[str, object] | None, error: Mapping[str, object]
async def test_bedrock_messages_retries_only_native_output_rejections(
model: str, model_info: Mapping[str, object] | None, error: Mapping[str, object], should_retry: bool
) -> None:
bedrock, raised = await _send_bedrock_messages(model, model_info, [(400, error), (200, _ANTHROPIC_MESSAGE)])
assert isinstance(raised, litellm.BadRequestError)
assert len(bedrock.bodies) == 1
assert (raised is None) is should_retry
assert len(bedrock.bodies) == (2 if should_retry else 1)
@pytest.mark.usefixtures("local_model_cost_map")

View file

@ -13,6 +13,7 @@ from litellm.llms.bedrock.common_utils import(
bedrock_strict_tools_fallback,
deployment_model_info,
extract_model_name_from_bedrock_arn,
for_bedrock_json_tool_retry,
get_bedrock_base_model,
get_bedrock_cross_region_inference_regions,
is_bedrock_native_output_rejection,
@ -2328,6 +2329,10 @@ def test_bedrock_capability_model_reads_base_model_only_for_application_profile_
_FORMAT_REJECTION: Final = "The model returned the following errors: output_config.format: Extra inputs are not permitted"
_COMPILE_REJECTION: Final = "The model returned the following errors: compilation exceeded limits: Schema is too complex."
_GRAMMAR_REJECTION: Final = (
"The model returned the following errors: The compiled grammar is too large, which would cause performance issues. "
"Simplify your tool schemas or reduce the number of strict tools."
)
_SCHEMA_PATTERN_REJECTION: Final = (
"The model returned the following errors: output_config.format.schema: Unsupported regex feature in pattern "
"field: pattern is too complex for structured output: reduce the {n,m} upper bound, narrow the character class "
@ -2340,6 +2345,7 @@ _SCHEMA_PATTERN_REJECTION: Final = (
[
(400, f'{{"message":"{_FORMAT_REJECTION}"}}', True),
(400, f'{{"message":"{_COMPILE_REJECTION}"}}', True),
(400, _GRAMMAR_REJECTION, True),
(400, f'{{"message":"{_SCHEMA_PATTERN_REJECTION}"}}', True),
(400, "Malformed input request: #: extraneous key [output_config] is not permitted", False),
(400, "The model returned the following errors: messages.0.content: Field required", False),
@ -2349,6 +2355,7 @@ _SCHEMA_PATTERN_REJECTION: Final = (
ids=[
"format_not_permitted",
"schema_too_complex",
"compiled_grammar_too_large",
"schema_pattern_too_complex",
"other_key_not_permitted",
"other_400",
@ -2365,7 +2372,7 @@ def test_is_bedrock_native_output_rejection_matches_only_the_400s_that_reject_na
@pytest.mark.parametrize(
("model", "model_info", "expected"),
[
("us.anthropic.claude-opus-4-8", None, True),
("us.anthropic.claude-opus-4-8", None, False),
("global.anthropic.claude-sonnet-5", None, True),
("us-gov.anthropic.claude-opus-4-8", None, False),
("us.anthropic.claude-opus-4-8", {"supports_bedrock_native_structured_output": False}, False),
@ -2389,9 +2396,43 @@ def test_without_bedrock_native_output_opt_in_turns_the_opt_in_off_and_keeps_the
"us.anthropic.claude-opus-4-8", model_info=deployment_model_info(turned_off)
)
assert turned_off["api_base"] == "https://bedrock.example"
assert deployment_model_info(turned_off)["id"] == "deployment-1"
assert deployment_model_info(turned_off) == {
"id": "deployment-1",
"supports_bedrock_native_structured_output": False,
}
assert litellm_params["model_info"] == {"id": "deployment-1"}
native_default: Final = without_bedrock_native_output_opt_in(
{"model_info": {"supports_native_structured_output": True}}
)
assert deployment_model_info(native_default) == {
"supports_native_structured_output": True,
"supports_bedrock_native_structured_output": False,
}
def test_for_bedrock_json_tool_retry_disables_both_native_output_flags(local_model_cost_map: None) -> None:
litellm_params: Final = {
"model_info": {
"id": "deployment-1",
"supports_native_structured_output": True,
"supports_bedrock_native_structured_output": True,
}
}
retry_params: Final = for_bedrock_json_tool_retry(litellm_params)
assert deployment_model_info(retry_params) == {
"id": "deployment-1",
"supports_native_structured_output": False,
"supports_bedrock_native_structured_output": False,
}
assert litellm_params["model_info"] == {
"id": "deployment-1",
"supports_native_structured_output": True,
"supports_bedrock_native_structured_output": True,
}
def test_bedrock_response_stream_shape_is_unavailable_when_botocore_describes_the_service_as_a_list():
import litellm.llms.bedrock.common_utils as mod

View file

@ -332,6 +332,18 @@ def _cross_region_profiles(
)
_BEDROCK_PROFILE_ONLY_NATIVE_OUTPUT_MODELS: Final = frozenset(
{
"anthropic.claude-opus-5-5",
"anthropic.claude-fable-5-1",
"anthropic.claude-sonnet-5",
"anthropic.claude-haiku-5-5",
"anthropic.claude-opus-5",
"anthropic.claude-opus-4-8",
}
)
def test_get_model_info_bedrock_cross_region_capability_parity():
"""
Cross-region inference profiles carry litellm_provider "bedrock_converse", so the
@ -362,6 +374,13 @@ def test_get_model_info_bedrock_cross_region_capability_parity():
if _govcloud_may_lag(k, cap):
assert base_value or not v[cap], f"{cap} is on {k} but not on {base_model_key}"
continue
if (
cap == "supports_native_structured_output"
and base_model_key in _BEDROCK_PROFILE_ONLY_NATIVE_OUTPUT_MODELS
and v[cap] is True
):
assert base_value is False, f"{cap} is already enabled on bare model {base_model_key}"
continue
assert (
v[cap] == base_value
), f"{cap} is {v[cap]} on {k} but {base_value} on {base_model_key}"