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_model_supports_regex_lookaround,
bedrock_native_output_format, bedrock_native_output_format,
bedrock_native_output_is_opt_in, bedrock_native_output_is_opt_in,
bedrock_native_output_retry_requested,
bedrock_supports_tool_search, bedrock_supports_tool_search,
deployment_model_info, deployment_model_info,
requests_output_config_format, requests_output_config_format,
@ -1425,6 +1426,16 @@ class AmazonConverseConfig(BaseConfig):
deployment_model_info(litellm_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(
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): 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. 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_capability_model,
bedrock_chat_uses_native_structured_output, bedrock_chat_uses_native_structured_output,
bedrock_cleaned_output_schema, bedrock_cleaned_output_schema,
bedrock_native_output_retry_requested,
bedrock_supports_tool_search, bedrock_supports_tool_search,
deployment_model_info, deployment_model_info,
get_anthropic_beta_from_headers, get_anthropic_beta_from_headers,
@ -161,6 +162,13 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
) -> bool: ) -> bool:
return requests_opt_in_native_output(model, optional_params, deployment_model_info(litellm_params)) 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( def map_openai_params(
self, self,
non_default_params: dict, non_default_params: dict,

View file

@ -1,4 +1,5 @@
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from functools import partial from functools import partial
from typing import Final, TypeAlias 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.base_llm.chat.transformation import BaseLLMException
from litellm.llms.bedrock.common_utils import ( from litellm.llms.bedrock.common_utils import (
NativeOutputFallback, NativeOutputFallback,
bedrock_strict_tools_fallback,
is_bedrock_native_output_rejection, is_bedrock_native_output_rejection,
warn_bedrock_native_output_rejected, warn_bedrock_native_output_rejected,
warn_bedrock_strict_tools_dropped, warn_bedrock_strict_tools_dropped,
@ -17,7 +19,15 @@ NativeOutputResponse: TypeAlias = ModelResponse | CustomStreamWrapper
NativeOutputAttemptResult: TypeAlias = NativeOutputResponse | Coroutine[object, object, NativeOutputResponse] NativeOutputAttemptResult: TypeAlias = NativeOutputResponse | Coroutine[object, object, NativeOutputResponse]
NativeOutputAttempt: TypeAlias = Callable[[NativeOutputFallback | None], NativeOutputAttemptResult] NativeOutputAttempt: TypeAlias = Callable[[NativeOutputFallback | None], NativeOutputAttemptResult]
NativeOutputFallbackFactory: TypeAlias = Callable[[], NativeOutputFallback | None] 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( def _native_output_retry(
@ -47,17 +57,42 @@ def _fallback_for(
fallback: NativeOutputFallbackFactory | None, fallback: NativeOutputFallbackFactory | None,
strict_tools_fallback: NativeOutputFallbackFactory | None, strict_tools_fallback: NativeOutputFallbackFactory | None,
model: str, model: str,
) -> NativeOutputFallback | None: ) -> _SelectedFallback | None:
native_output_retry: Final = _native_output_retry(error, fallback, model) native_output_retry: Final = _native_output_retry(error, fallback, model)
if native_output_retry is not None: if native_output_retry is not None:
return native_output_retry return _SelectedFallback(native_output_retry, native_output_retry=True)
return _strict_tools_retry(error, strict_tools_fallback, model) 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( async def _await_with_native_output_fallback(
first_attempt: Coroutine[object, object, NativeOutputResponse], first_attempt: Coroutine[object, object, NativeOutputResponse],
attempt: NativeOutputAttempt, attempt: NativeOutputAttempt,
retry_for: _RetryFor, retry_for: _RetryFor,
strict_tools_fallback: NativeOutputFallbackFactory | None,
model: str,
) -> NativeOutputResponse: ) -> NativeOutputResponse:
try: try:
return await first_attempt return await first_attempt
@ -65,8 +100,17 @@ async def _await_with_native_output_fallback(
retry: Final = retry_for(error) retry: Final = retry_for(error)
if retry is None: if retry is None:
raise raise
second_attempt: Final = attempt(retry) try:
return await second_attempt if isinstance(second_attempt, Coroutine) else second_attempt 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( def call_with_native_output_fallback(
@ -86,7 +130,15 @@ def call_with_native_output_fallback(
retry: Final = retry_for(error) retry: Final = retry_for(error)
if retry is None: if retry is None:
raise 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): 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 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) 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]) _DEPLOYMENT_MODEL_INFO: Final = TypeAdapter(dict[str, object])
@ -312,9 +321,9 @@ def _bedrock_native_output_opt_in(
def bedrock_supports_native_structured_output( def bedrock_supports_native_structured_output(
model: str, custom_llm_provider: str = "bedrock", model_info: Mapping[str, object] | None = None model: str, custom_llm_provider: str = "bedrock", model_info: Mapping[str, object] | None = None
) -> bool: ) -> bool:
return _bedrock_model_supports( return _bedrock_native_output_enabled(model, custom_llm_provider, model_info) or _bedrock_native_output_opt_in(
model, "supports_native_structured_output", custom_llm_provider 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: 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", custom_llm_provider: str = "bedrock",
model_info: Mapping[str, object] | None = None, model_info: Mapping[str, object] | None = None,
) -> bool: ) -> 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) 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( def bedrock_native_output_is_deployment_opt_in(
model: str, custom_llm_provider: str = "bedrock", model_info: Mapping[str, object] | None = None model: str, custom_llm_provider: str = "bedrock", model_info: Mapping[str, object] | None = None
) -> bool: ) -> bool:
return not _bedrock_model_supports( return not _bedrock_native_output_enabled(model, custom_llm_provider, model_info) and _bedrock_native_output_opt_in(
model, "supports_native_structured_output", custom_llm_provider 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( def bedrock_native_output_is_opt_in(
model: str, custom_llm_provider: str = "bedrock", model_info: Mapping[str, object] | None = None model: str, custom_llm_provider: str = "bedrock", model_info: Mapping[str, object] | None = None
) -> bool: ) -> 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) _bedrock_native_output_opt_in(model, custom_llm_provider, model_info)
or _bedrock_native_output_replaces_unforceable_tool(model, 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]: def without_bedrock_native_output_opt_in(litellm_params: Mapping[str, object]) -> Mapping[str, object]:
return { return {
**litellm_params, **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_REJECTIONS: Final = (
_BEDROCK_NATIVE_OUTPUT_UNSUPPORTED, _BEDROCK_NATIVE_OUTPUT_UNSUPPORTED,
"compilation exceeded limits: Schema is too complex", "compilation exceeded limits: Schema is too complex",
"The compiled grammar is too large",
"output_config.format.schema:", "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: 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( output_config: Final = json_object_or_none(anthropic_optional_params.get("outputConfig"))
anthropic_optional_params 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, model_info: Mapping[str, object] | None = None,
) -> Mapping[str, object] | None: ) -> Mapping[str, object] | None:
native_schema: Final = bedrock_cleaned_output_schema(model, schema, custom_llm_provider) 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 return schema if native_schema is None else native_schema
if _bedrock_native_output_opt_in(model, custom_llm_provider, model_info): if _bedrock_native_output_opt_in(model, custom_llm_provider, model_info):
return native_schema return native_schema

View file

@ -40,15 +40,17 @@ from litellm.llms.bedrock.common_utils import (
BEDROCK_INVOKE_CONTEXT_MANAGEMENT_EDIT_BETAS, BEDROCK_INVOKE_CONTEXT_MANAGEMENT_EDIT_BETAS,
BedrockError, BedrockError,
apply_bedrock_invoke_structured_output, 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, bedrock_supports_tool_search,
ensure_bedrock_anthropic_messages_tool_names, ensure_bedrock_anthropic_messages_tool_names,
for_bedrock_json_tool_retry,
get_anthropic_beta_from_headers, get_anthropic_beta_from_headers,
is_bedrock_native_output_rejection, is_bedrock_native_output_rejection,
json_object_or_none,
normalize_bedrock_opus_output_config_effort, normalize_bedrock_opus_output_config_effort,
normalize_custom_field_on_tools, normalize_custom_field_on_tools,
normalize_tool_input_schema_types_for_bedrock_invoke, normalize_tool_input_schema_types_for_bedrock_invoke,
requests_native_output,
requests_output_config_format, requests_output_config_format,
strip_unsupported_bedrock_invoke_output_config_keys, strip_unsupported_bedrock_invoke_output_config_keys,
supports_bedrock_prompt_cache_ttl, supports_bedrock_prompt_cache_ttl,
@ -133,13 +135,24 @@ class AmazonAnthropicClaudeMessagesConfig(
) -> GenericLiteLLMParams | None: ) -> GenericLiteLLMParams | None:
model_info: Final = _MODEL_INFO.validate_python(litellm_params.model_info) model_info: Final = _MODEL_INFO.validate_python(litellm_params.model_info)
if not ( if not (
requests_native_output(anthropic_messages_optional_request_params) bedrock_native_output_retry_requested(
and bedrock_native_output_is_deployment_opt_in(model, model_info=model_info) model, anthropic_messages_optional_request_params, model_info=model_info
)
): ):
return None return None
return litellm_params.model_copy( legacy_output_format: Final = json_object_or_none(
update={"model_info": {**(model_info or {}), "supports_bedrock_native_structured_output": False}} 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( def should_retry_anthropic_messages_without_native_output(
self, e: httpx.HTTPStatusError, request_body: Mapping[str, object], model: str 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_route_for_request,
bedrock_strict_tools_fallback, bedrock_strict_tools_fallback,
deployment_model_info, deployment_model_info,
without_bedrock_native_output_opt_in, for_bedrock_json_tool_retry,
without_bedrock_route_prefix, without_bedrock_route_prefix,
) )
from litellm.llms.bedrock_mantle.chat.claude_transformation import bedrock_mantle_chat_config 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_params: Mapping[str, object],
first_attempt_litellm_params: Mapping[str, object], first_attempt_litellm_params: Mapping[str, object],
) -> NativeOutputFallback: ) -> 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)) remapped_params: Final = ctx.optional_params_for_model_info(deployment_model_info(litellm_params))
return NativeOutputFallback( return NativeOutputFallback(
optional_params={ optional_params={
@ -4500,7 +4500,7 @@ def _complete_bedrock(ctx: CompletionDispatchContext) -> _CompletionDispatchResu
converse_attempt, converse_attempt,
( (
partial(_converse_native_output_fallback, ctx, converse_params, dict(litellm_params)) 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 else None
), ),
converse_model, converse_model,
@ -4556,7 +4556,7 @@ def _complete_bedrock(ctx: CompletionDispatchContext) -> _CompletionDispatchResu
( (
partial(_native_output_fallback, ctx, invoke_params, dict(litellm_params)) partial(_native_output_fallback, ctx, invoke_params, dict(litellm_params))
if isinstance(provider_config, AmazonAnthropicClaudeConfig) 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 else None
), ),
model, model,

View file

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

View file

@ -2242,7 +2242,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
"supports_parallel_tool_use_config": true, "supports_parallel_tool_use_config": true,
@ -2331,7 +2331,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
"supports_parallel_tool_use_config": true, "supports_parallel_tool_use_config": true,
@ -2420,7 +2420,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
"supports_parallel_tool_use_config": true, "supports_parallel_tool_use_config": true,
@ -2504,7 +2504,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
"supports_parallel_tool_use_config": true, "supports_parallel_tool_use_config": true,
@ -2593,7 +2593,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
"supports_parallel_tool_use_config": true, "supports_parallel_tool_use_config": true,
@ -2715,7 +2715,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true, "supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
@ -2759,7 +2759,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true, "supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
@ -2803,7 +2803,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true, "supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
@ -2847,7 +2847,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true, "supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
@ -2891,7 +2891,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true, "supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
@ -75504,7 +75504,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_bedrock_native_structured_output": true, "supports_bedrock_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
@ -75544,7 +75544,7 @@
"supports_tool_choice": true, "supports_tool_choice": true,
"supports_vision": true, "supports_vision": true,
"supports_xhigh_reasoning_effort": true, "supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false, "supports_native_structured_output": true,
"supports_max_reasoning_effort": true, "supports_max_reasoning_effort": true,
"supports_output_config": true, "supports_output_config": true,
"supports_parallel_tool_use_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"]) @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( 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( optional_params: Final = _invoke_chat_params(
model, None, {"response_format": _CITY_RESPONSE_FORMAT, **thinking_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( @pytest.mark.parametrize(
("forced_tool_use", "map_flag", "model_info", "thinking_params", "native"), ("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 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: def _converse_capital_answer(request: httpx.Request) -> httpx.Response:
return httpx.Response( return httpx.Response(
200, 200,
@ -11599,7 +11637,10 @@ _OUTPUT_CONFIG_FORMAT: Final = {
ids=["adaptive", "reasoning_effort", "budget", "output_config_format"], ids=["adaptive", "reasoning_effort", "budget", "output_config_format"],
) )
@pytest.mark.parametrize("model", ["us.anthropic.claude-opus-4-8", "global.anthropic.claude-sonnet-5"]) @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) optional_params: Final = _converse_params(model, None, request_params)
assert AmazonConverseConfig().uses_opt_in_native_output(model, optional_params, {"model_info": {"id": "d1"}}) 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}) 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" 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"} litellm_params: Final = {"base_model": "bedrock/us.anthropic.claude-opus-4-8"}
optional_params: Final = _converse_params( 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) @pytest.mark.respx(assert_all_called=True)
def test_a_refused_native_schema_is_sent_again_only_when_the_retry_drops_native_output( 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: ) -> 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( route: Final = respx_mock.post(url__regex=r"/converse$").mock(
return_value=httpx.Response(400, json={"message": _BEDROCK_SCHEMA_REFUSAL}) 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:]) 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" 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) transport: Final = httpx.MockTransport(handle_request)
request: Final = { 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."}], "messages": [{"role": "user", "content": "What is the LCM of 24 and 120? Reply in JSON."}],
"response_format": { "response_format": {
"type": "json_schema", "type": "json_schema",

View file

@ -13,10 +13,33 @@ _MODEL: Final = "us.anthropic.claude-opus-4-8"
_FALLBACK: Final = NativeOutputFallback( _FALLBACK: Final = NativeOutputFallback(
optional_params={"tools": "json_tool_call"}, litellm_params={"model_info": {"opted_out": True}} 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( _REJECTION: Final = BedrockError(
status_code=400, status_code=400,
message='{"message":"The model returned the following errors: output_config.format: Extra inputs are not permitted"}', 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( _SCHEMA_REFUSAL: Final = BedrockError(
status_code=400, message='{"message":"output_config.format.schema: pattern is too complex for structured output"}' 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 @_MODES
def test_a_native_output_rejection_is_sent_again_once_with_the_fallback_params(asynchronous: bool) -> None: @pytest.mark.parametrize(
bedrock: Final = ScriptedBedrock(errors=(_REJECTION, None), asynchronous=asynchronous) "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) 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 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 @_MODES
@pytest.mark.parametrize( @pytest.mark.parametrize(
("error", "fallback"), ("error", "fallback"),
@ -155,13 +198,16 @@ _OTHER_400S_WITH_A_STRICT_TOOL: Final = tuple(
def _call_with_strict_tools_fallback( 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: ) -> object:
return _resolve( return _resolve(
call_with_native_output_fallback( call_with_native_output_fallback(
bedrock, bedrock,
None if fallback is None else lambda: fallback, 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, 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) _call_with_strict_tools_fallback(bedrock, _FALLBACK, _STRICT_FALLBACK)
assert bedrock.sent == (None, retried_with) 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( @pytest.mark.parametrize(
("model", "native_output"), ("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"], 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( 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." "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"} _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 = { _CONSTRAINED_SCHEMA: Final = {
"type": "object", "type": "object",
"properties": { "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.usefixtures("local_model_cost_map")
@pytest.mark.parametrize( @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-opus-4-8", None, {"message": "messages.0.content: Field required"}, False),
("us.anthropic.claude-sonnet-4-6", None, _NATIVE_OUTPUT_REJECTION), ("us.anthropic.claude-sonnet-4-6", None, _NATIVE_OUTPUT_REJECTION, True),
("us.anthropic.claude-opus-4-8", _OPTED_OUT, _NATIVE_OUTPUT_REJECTION), ("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( async def test_bedrock_messages_retries_only_native_output_rejections(
model: str, model_info: Mapping[str, object] | None, error: Mapping[str, object] model: str, model_info: Mapping[str, object] | None, error: Mapping[str, object], should_retry: bool
) -> None: ) -> None:
bedrock, raised = await _send_bedrock_messages(model, model_info, [(400, error), (200, _ANTHROPIC_MESSAGE)]) bedrock, raised = await _send_bedrock_messages(model, model_info, [(400, error), (200, _ANTHROPIC_MESSAGE)])
assert isinstance(raised, litellm.BadRequestError) assert (raised is None) is should_retry
assert len(bedrock.bodies) == 1 assert len(bedrock.bodies) == (2 if should_retry else 1)
@pytest.mark.usefixtures("local_model_cost_map") @pytest.mark.usefixtures("local_model_cost_map")

View file

@ -13,6 +13,7 @@ from litellm.llms.bedrock.common_utils import(
bedrock_strict_tools_fallback, bedrock_strict_tools_fallback,
deployment_model_info, deployment_model_info,
extract_model_name_from_bedrock_arn, extract_model_name_from_bedrock_arn,
for_bedrock_json_tool_retry,
get_bedrock_base_model, get_bedrock_base_model,
get_bedrock_cross_region_inference_regions, get_bedrock_cross_region_inference_regions,
is_bedrock_native_output_rejection, 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" _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." _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 = ( _SCHEMA_PATTERN_REJECTION: Final = (
"The model returned the following errors: output_config.format.schema: Unsupported regex feature in pattern " "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 " "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":"{_FORMAT_REJECTION}"}}', True),
(400, f'{{"message":"{_COMPILE_REJECTION}"}}', True), (400, f'{{"message":"{_COMPILE_REJECTION}"}}', True),
(400, _GRAMMAR_REJECTION, True),
(400, f'{{"message":"{_SCHEMA_PATTERN_REJECTION}"}}', True), (400, f'{{"message":"{_SCHEMA_PATTERN_REJECTION}"}}', True),
(400, "Malformed input request: #: extraneous key [output_config] is not permitted", False), (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), (400, "The model returned the following errors: messages.0.content: Field required", False),
@ -2349,6 +2355,7 @@ _SCHEMA_PATTERN_REJECTION: Final = (
ids=[ ids=[
"format_not_permitted", "format_not_permitted",
"schema_too_complex", "schema_too_complex",
"compiled_grammar_too_large",
"schema_pattern_too_complex", "schema_pattern_too_complex",
"other_key_not_permitted", "other_key_not_permitted",
"other_400", "other_400",
@ -2365,7 +2372,7 @@ def test_is_bedrock_native_output_rejection_matches_only_the_400s_that_reject_na
@pytest.mark.parametrize( @pytest.mark.parametrize(
("model", "model_info", "expected"), ("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), ("global.anthropic.claude-sonnet-5", None, True),
("us-gov.anthropic.claude-opus-4-8", None, False), ("us-gov.anthropic.claude-opus-4-8", None, False),
("us.anthropic.claude-opus-4-8", {"supports_bedrock_native_structured_output": False}, 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) "us.anthropic.claude-opus-4-8", model_info=deployment_model_info(turned_off)
) )
assert turned_off["api_base"] == "https://bedrock.example" 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"} 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(): def test_bedrock_response_stream_shape_is_unavailable_when_botocore_describes_the_service_as_a_list():
import litellm.llms.bedrock.common_utils as mod 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(): def test_get_model_info_bedrock_cross_region_capability_parity():
""" """
Cross-region inference profiles carry litellm_provider "bedrock_converse", so the 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): if _govcloud_may_lag(k, cap):
assert base_value or not v[cap], f"{cap} is on {k} but not on {base_model_key}" assert base_value or not v[cap], f"{cap} is on {k} but not on {base_model_key}"
continue 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 ( assert (
v[cap] == base_value v[cap] == base_value
), f"{cap} is {v[cap]} on {k} but {base_value} on {base_model_key}" ), f"{cap} is {v[cap]} on {k} but {base_value} on {base_model_key}"