Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e7358a6
feat: support goal mode for claude on bedrock
dennishenry May 26, 2026
cd33bf1
Merge upstream/litellm_internal_staging
dennishenry May 26, 2026
457477d
fix failing lint test
dennishenry May 26, 2026
ad2420e
addressing greptile comments
dennishenry May 26, 2026
a1281b0
fixing failed test
dennishenry May 26, 2026
355fd04
address greptile: copy output_config and warn on dropped converse format
mateo-berri May 26, 2026
5560766
fix(bedrock): skip redundant output_config normalization on Converse …
cursoragent May 26, 2026
cb28240
test(reasoning-effort-grid): reflect Bedrock opus-4-6 xhigh→max clamping
mateo-berri May 26, 2026
ceb727f
fix(bedrock): stop leaking output_config marker and message-content m…
mateo-berri May 26, 2026
011b71b
fix(bedrock): guard effort key access in normalize_bedrock_opus_outpu…
cursoragent May 26, 2026
1a53107
fix(bedrock): drop dead second clause in effort normalization guard
mateo-berri May 26, 2026
287c49c
fix(bedrock): clamp output_config.effort against ceiling for any know…
mateo-berri May 26, 2026
78a5115
test(grid_spec): use _CAPS_OPUS_4_7 for non-Bedrock opus-4-6 entries
cursoragent May 26, 2026
64a0851
test(grid_spec): revert xhigh caps for non-Bedrock opus-4-6
mateo-berri May 26, 2026
abec9cb
fix: stop advertising xhigh effort on Opus 4.5/4.6
mateo-berri May 27, 2026
6542c8e
fix: clamp xhigh effort on Bedrock Invoke /v1/messages instead of rej…
mateo-berri May 28, 2026
1333799
fix(bedrock): preserve user output_config when applying reasoning_effort
cursoragent May 28, 2026
d8c24e8
fix(bedrock): pre-clamp reasoning_effort for chat invoke; correct tes…
cursoragent May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1793,7 +1793,10 @@ def update_headers_with_optional_anthropic_beta(
self._ensure_context_management_beta_header(
headers, optional_params["context_management"]
)
if optional_params.get("output_format") is not None:
output_config = optional_params.get("output_config")
if optional_params.get("output_format") is not None or (
isinstance(output_config, dict) and output_config.get("format") is not None
):
self._ensure_beta_header(
headers, ANTHROPIC_BETA_HEADER_VALUES.STRUCTURED_OUTPUT_2025_09_25.value
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,13 @@ def _update_headers_with_anthropic_beta(
ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value
)

# Check for structured outputs
if optional_params.get("output_format") is not None:
# Check for structured outputs. Anthropic's newer request shape nests
# the schema under output_config.format; the older top-level
# output_format remains supported for backwards compatibility.
output_config = optional_params.get("output_config")
if optional_params.get("output_format") is not None or (
isinstance(output_config, dict) and output_config.get("format") is not None
):
beta_values.add(
ANTHROPIC_BETA_HEADER_VALUES.STRUCTURED_OUTPUT_2025_09_25.value
)
Expand Down
57 changes: 54 additions & 3 deletions litellm/llms/bedrock/chat/converse_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
get_anthropic_beta_from_headers,
get_bedrock_tool_name,
is_claude_4_5_on_bedrock,
normalize_bedrock_opus_output_config_effort,
)

# Computer use tool prefixes supported by Bedrock
Expand Down Expand Up @@ -447,10 +448,17 @@ def _handle_reasoning_effort_parameter(
value=reasoning_effort,
llm_provider="bedrock_converse",
)
output_config = {"effort": mapped_effort}
normalize_bedrock_opus_output_config_effort(
model=model,
output_config=output_config,
)
mapped_effort = output_config["effort"]
self._validate_anthropic_adaptive_effort(
model=model, effort=mapped_effort
)
optional_params["output_config"] = {"effort": mapped_effort}
optional_params["output_config"] = output_config
optional_params["_output_config_normalized"] = True
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

@staticmethod
def _validate_anthropic_adaptive_effort(model: str, effort: str) -> None:
Expand Down Expand Up @@ -1198,6 +1206,12 @@ def _prepare_request_params(
self, optional_params: dict, model: str
) -> Tuple[dict, dict, dict, Optional[OutputConfigBlock]]:
"""Prepare and separate request parameters."""
# Consume the internal ``_output_config_normalized`` marker set by
# ``_handle_reasoning_effort_parameter`` so it does not linger on the
# caller's ``optional_params`` after the transformation returns.
anthropic_output_config_already_normalized = bool(
optional_params.pop("_output_config_normalized", False)
)
# Filter out exception objects before deepcopy to prevent deepcopy failures
# Exceptions should not be stored in optional_params (this is a defensive fix)
cleaned_params = filter_exceptions_from_params(optional_params)
Expand All @@ -1216,8 +1230,17 @@ def _prepare_request_params(

# Anthropic-only ``output_config`` (snake_case) — re-attached to
# ``additionalModelRequestFields`` for Anthropic models below. The
# Bedrock-native ``outputConfig`` (camelCase) is handled separately.
# structured-output ``format`` subfield is consumed into Bedrock's
# native ``outputConfig`` (camelCase), which is handled separately.
anthropic_output_config = inference_params.pop("output_config", None)
output_config_format = None
if isinstance(anthropic_output_config, dict):
anthropic_output_config = dict(anthropic_output_config)
candidate_output_config_format = anthropic_output_config.pop("format", None)
if isinstance(candidate_output_config_format, dict):
output_config_format = candidate_output_config_format
if not anthropic_output_config:
anthropic_output_config = None

# Extract requestMetadata before processing other parameters
request_metadata = inference_params.pop("requestMetadata", None)
Expand All @@ -1227,6 +1250,30 @@ def _prepare_request_params(
output_config: Optional[OutputConfigBlock] = inference_params.pop(
"outputConfig", None
)
base_model = BedrockModelInfo.get_base_model(model)
if (
output_config is None
and output_config_format is not None
and output_config_format.get("type") == "json_schema"
and base_model.startswith("anthropic")
and self._supports_native_structured_outputs(
model, self.custom_llm_provider
)
):
output_config = self._create_output_config_for_response_format(
json_schema=output_config_format.get("schema"),
name=output_config_format.get("name"),
description=output_config_format.get("description"),
)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
elif output_config is None and output_config_format is not None:
litellm.verbose_logger.warning(
"Bedrock Converse: dropping `output_config.format` for model=%s — "
"model does not advertise `supports_native_structured_output` in "
"model_prices_and_context_window.json. The schema will not be "
"enforced; pass `response_format` to use the synthetic tool-call "
"fallback.",
model,
)

# keep supported params in 'inference_params', and set all model-specific params in 'additional_request_params'
additional_request_params = {
Expand Down Expand Up @@ -1272,7 +1319,6 @@ def _prepare_request_params(
if anthropic_output_config is not None and isinstance(
anthropic_output_config, dict
):
base_model = BedrockModelInfo.get_base_model(model)
if base_model.startswith("anthropic"):
if (
litellm.drop_params is True
Expand All @@ -1283,6 +1329,11 @@ def _prepare_request_params(
model,
)
else:
if not anthropic_output_config_already_normalized:
normalize_bedrock_opus_output_config_effort(
model=model,
output_config=anthropic_output_config,
)
effort = anthropic_output_config.get("effort")
if effort is not None:
self._validate_anthropic_adaptive_effort(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@
AmazonInvokeConfig,
)
from litellm.llms.bedrock.common_utils import (
convert_bedrock_invoke_output_format_to_inline_schema,
get_anthropic_beta_from_headers,
normalize_bedrock_opus_output_config_effort,
normalize_tool_input_schema_types_for_bedrock_invoke,
pop_bedrock_invoke_output_config_format,
remove_custom_field_from_tools,
)
from litellm.types.llms.anthropic import ANTHROPIC_TOOL_SEARCH_BETA_HEADER
Expand Down Expand Up @@ -157,6 +160,13 @@ def _build_bedrock_anthropic_request_base(
for k, v in optional_params.items()
if k not in self.aws_authentication_params
}
output_config = filtered_params.get("output_config")
if isinstance(output_config, dict):
filtered_params["output_config"] = dict(output_config)
normalize_bedrock_opus_output_config_effort(
model=model,
output_config=filtered_params["output_config"],
)
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
filtered_params = self._normalize_bedrock_tool_search_tools(filtered_params)

anthropic_request = AnthropicConfig.transform_request(
Expand All @@ -170,7 +180,20 @@ def _build_bedrock_anthropic_request_base(

anthropic_request.pop("model", None)
anthropic_request.pop("stream", None)
anthropic_request.pop("output_format", None)
output_format = anthropic_request.pop("output_format", None)
output_config_format = pop_bedrock_invoke_output_config_format(
anthropic_request
)
if output_format:
convert_bedrock_invoke_output_format_to_inline_schema(
output_format=output_format,
request_body=anthropic_request,
)
elif output_config_format:
convert_bedrock_invoke_output_format_to_inline_schema(
output_format=output_config_format,
request_body=anthropic_request,
)
if not (
_supports_factory(
model=model,
Expand Down
138 changes: 138 additions & 0 deletions litellm/llms/bedrock/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ class BedrockError(BaseLLMException):
# Lazy import cache to avoid circular imports and performance impact
_get_model_info = None

BedrockOutputConfigEffort = Literal["low", "medium", "high", "max", "xhigh"]
_BEDROCK_OUTPUT_CONFIG_EFFORT_ORDER: Dict[BedrockOutputConfigEffort, int] = {
"low": 0,
"medium": 1,
"high": 2,
"max": 3,
"xhigh": 4,
}


def get_cached_model_info():
"""
Expand All @@ -51,6 +60,79 @@ def get_cached_model_info():
return _get_model_info


@functools.lru_cache(maxsize=1)
def _get_local_model_cost_map() -> Dict:
from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap

return GetModelCostMap.load_local_model_cost_map()
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved


def pop_bedrock_invoke_output_config_format(request_body: Dict) -> Optional[Dict]:
"""
Remove and return Anthropic's nested ``output_config.format`` field.

Bedrock Invoke paths convert the schema to inline message text. Any remaining
``output_config`` keys, such as ``effort``, are left in place.
"""
output_config = request_body.get("output_config")
if not isinstance(output_config, dict):
return None

output_format = output_config.pop("format", None)
if not output_config:
request_body.pop("output_config", None)

if isinstance(output_format, dict):
return output_format
return None
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved


def convert_bedrock_invoke_output_format_to_inline_schema(
output_format: Dict,
request_body: Dict,
) -> None:
"""
Embed an Anthropic structured-output schema into the last user message.

Bedrock Invoke does not support ``output_format`` directly, so the schema is
appended to the final user message for prompt-engineered structured output.
The caller's ``messages`` list, message dict, and content list are not
mutated; a fresh ``messages`` list with a copied final user message is
written back to ``request_body``.
"""
schema = output_format.get("schema")
if not schema:
return

messages = request_body.get("messages")
if not isinstance(messages, list) or not messages:
return

last_user_idx = None
for i in range(len(messages) - 1, -1, -1):
message = messages[i]
if isinstance(message, dict) and message.get("role") == "user":
last_user_idx = i
break

if last_user_idx is None:
return

original = messages[last_user_idx]
content = original.get("content", [])
schema_block = {"type": "text", "text": json.dumps(schema)}
if isinstance(content, str):
new_content = [{"type": "text", "text": content}, schema_block]
elif isinstance(content, list):
new_content = [*content, schema_block]
else:
return

new_messages = list(messages)
new_messages[last_user_idx] = {**original, "content": new_content}
request_body["messages"] = new_messages


def remove_custom_field_from_tools(request_body: dict) -> None:
"""
Remove ``custom`` field from each tool in the request body.
Expand Down Expand Up @@ -603,6 +685,62 @@ def is_claude_4_5_on_bedrock(model: str) -> bool:
return any(pattern in model_lower for pattern in claude_4_5_patterns)


def normalize_bedrock_opus_output_config_effort(model: str, output_config: Any) -> None:
"""
Normalize Anthropic ``output_config.effort`` values for Bedrock Opus ids.

Bedrock's Claude Opus request validator can accept a narrower effort
vocabulary than Anthropic's compatibility surface. The Bedrock ceiling is
read from ``model_prices_and_context_window.json`` via
``bedrock_output_config_effort_ceiling``.

Mutates ``output_config`` in place so callers can accept Claude Code's
``xhigh`` input without forwarding a provider-invalid value.
"""
if not isinstance(output_config, dict):
return

effort = output_config.get("effort")
if effort not in _BEDROCK_OUTPUT_CONFIG_EFFORT_ORDER:
return
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

ceiling = _get_bedrock_output_config_effort_ceiling(model)
if ceiling is None:
return

if (
_BEDROCK_OUTPUT_CONFIG_EFFORT_ORDER[effort]
> _BEDROCK_OUTPUT_CONFIG_EFFORT_ORDER[ceiling]
):
output_config["effort"] = ceiling


def _get_bedrock_output_config_effort_ceiling(
model: str,
) -> Optional[BedrockOutputConfigEffort]:
try:
model_info = get_cached_model_info()(
model=model,
custom_llm_provider="bedrock",
)
except Exception:
return None

ceiling = model_info.get("bedrock_output_config_effort_ceiling")
if isinstance(ceiling, str) and ceiling in _BEDROCK_OUTPUT_CONFIG_EFFORT_ORDER:
return ceiling # type: ignore[return-value]

model_cost_key = model_info.get("key")
if not isinstance(model_cost_key, str):
return None

local_model_info = _get_local_model_cost_map().get(model_cost_key, {})
ceiling = local_model_info.get("bedrock_output_config_effort_ceiling")
if isinstance(ceiling, str) and ceiling in _BEDROCK_OUTPUT_CONFIG_EFFORT_ORDER:
return ceiling # type: ignore[return-value]
return None


# Import after standalone functions to avoid circular imports
from litellm.llms.bedrock.count_tokens.bedrock_token_counter import BedrockTokenCounter

Expand Down
Loading
Loading