Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion litellm/anthropic_beta_headers_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
"computer-use-2025-11-24": "computer-use-2025-11-24",
"context-1m-2025-08-07": "context-1m-2025-08-07",
"context-management-2025-06-27": null,
"effort-2025-11-24": null,
"effort-2025-11-24": "effort-2025-11-24",
"fast-mode-2026-02-01": null,
"files-api-2025-04-14": null,
"fine-grained-tool-streaming-2025-05-14": null,
Expand Down
79 changes: 76 additions & 3 deletions litellm/llms/bedrock/chat/converse_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
_bedrock_tools_pt,
)
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
from litellm.types.llms.bedrock import *
from litellm.types.llms.openai import (
Expand Down Expand Up @@ -452,6 +453,70 @@ def _handle_reasoning_effort_parameter(
optional_params["thinking"] = AnthropicConfig._map_reasoning_effort(
reasoning_effort=reasoning_effort, model=model
)
# Claude 4.6/4.7 adaptive thinking takes ``effort`` inside the
# ``thinking`` block on Bedrock Converse (Anthropic's
# ``output_config.effort`` is API-only). Surface the mapped
# effort here so it survives into ``additionalModelRequestFields``.
if (
AnthropicModelInfo._is_adaptive_thinking_model(model)
and isinstance(optional_params.get("thinking"), dict)
and optional_params["thinking"].get("type") == "adaptive"
and "effort" not in optional_params["thinking"]
):
effort_map = {
"low": "low",
"minimal": "low",
"medium": "medium",
"high": "high",
"xhigh": "xhigh",
"max": "max",
}
optional_params["thinking"]["effort"] = effort_map.get(
reasoning_effort, reasoning_effort
)

@staticmethod
def _fold_output_config_effort_into_thinking(
inference_params: dict, model: str
) -> None:
"""
Fold ``output_config.effort`` into ``thinking.effort`` for Bedrock
Converse on adaptive-thinking models (Claude 4.6 / 4.7).

Bedrock Converse exposes Anthropic's ``effort`` parameter inside
``additionalModelRequestFields.thinking`` (per the AWS adaptive
thinking docs), not as a top-level ``output_config`` block. Callers
commonly send the Anthropic Messages API shape — ``output_config:
{effort: ...}`` — when chaining a Messages-style request through
Converse, which leaves the value on the floor.

This helper preserves caller intent: if the model accepts adaptive
thinking and the caller did not already set ``thinking.effort``, we
attach the effort there. ``output_config`` is still stripped from
``inference_params`` separately so it never reaches the wire body.
"""
if not AnthropicModelInfo._is_adaptive_thinking_model(model):
return
output_config = inference_params.get("output_config")
if not isinstance(output_config, dict):
return
effort = output_config.get("effort")
if not (effort and isinstance(effort, str)):
return
thinking = inference_params.get("thinking")
if isinstance(thinking, dict):
# Don't override an explicit thinking.effort set by the caller.
if "effort" not in thinking:
thinking["effort"] = effort
# Make sure the type is adaptive — only adaptive thinking accepts
# effort. If the caller passed ``enabled``, leave it alone; the
# downstream `_translate_legacy_thinking_for_adaptive_model` flow
# owns that translation.
else:
inference_params["thinking"] = {
"type": "adaptive",
"effort": effort,
}

@staticmethod
def _clamp_thinking_budget_tokens(optional_params: dict) -> None:
Expand Down Expand Up @@ -1192,6 +1257,17 @@ def _prepare_request_params(
+ supported_config_params
)
inference_params.pop("json_mode", None) # used for handling json_schema

# Bedrock Converse exposes the Anthropic ``effort`` parameter through
# ``additionalModelRequestFields.thinking.effort`` (per AWS adaptive
# thinking docs). If the caller supplied ``output_config.effort`` —
# the Anthropic Messages API shape — fold it into ``thinking`` for
# Claude 4.6/4.7 adaptive-thinking models so the value reaches the
# wire body. For all other models the field is unsupported and gets
# stripped below.
self._fold_output_config_effort_into_thinking(
inference_params=inference_params, model=model
)
# Anthropic-only key. Bedrock expects `outputConfig` (camelCase) and
# will reject `output_config` if it leaks through pass-through routes.
inference_params.pop("output_config", None)
Expand All @@ -1204,9 +1280,6 @@ def _prepare_request_params(
output_config: Optional[OutputConfigBlock] = inference_params.pop(
"outputConfig", None
)
inference_params.pop(
"output_config", None
) # Bedrock Converse doesn't support it

# keep supported params in 'inference_params', and set all model-specific params in 'additional_request_params'
additional_request_params = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
convert_url_to_base64,
)
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation import (
AmazonInvokeConfig,
)
Expand All @@ -31,6 +32,21 @@
LiteLLMLoggingObj = Any


def _supports_effort_on_bedrock_invoke(model: str) -> bool:
"""
Bedrock Invoke (legacy /completion path) accepts ``output_config.effort``
on Claude 4.6/4.7 adaptive-thinking models (no beta header) and on Claude
Opus 4.5 with the ``effort-2025-11-24`` beta header. All other Claude
models reject the field.
"""
if AnthropicModelInfo._is_adaptive_thinking_model(model):
return True
model_lower = model.lower()
return any(
p in model_lower for p in ("opus-4.5", "opus_4.5", "opus-4-5", "opus_4_5")
)


class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
"""
Reference:
Expand Down Expand Up @@ -169,7 +185,12 @@ def _build_bedrock_anthropic_request_base(
anthropic_request.pop("model", None)
anthropic_request.pop("stream", None)
anthropic_request.pop("output_format", None)
anthropic_request.pop("output_config", None)
# Bedrock accepts ``output_config`` (carrying ``effort``) on Claude 4.6/4.7
# adaptive-thinking models natively, and on Claude Opus 4.5 with the
# ``effort-2025-11-24`` beta header. Older Claude models reject the
# field — strip it for them so the request signs cleanly.
if not _supports_effort_on_bedrock_invoke(model):
anthropic_request.pop("output_config", None)
if "anthropic_version" not in anthropic_request:
anthropic_request["anthropic_version"] = self.anthropic_version

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,19 @@ def _ensure_thinking_for_clear_thinking_context_management(
)
return True

def _supports_effort_on_bedrock(self, model: str) -> bool:
"""
Whether Bedrock accepts ``output_config.effort`` for this model.

Adaptive-thinking models (Claude 4.6 / 4.7) take ``effort`` natively
with no beta header. Claude Opus 4.5 takes ``effort`` only when the
``effort-2025-11-24`` beta header is attached. Older Claude models
reject the field outright.
"""
return AnthropicModelInfo._is_adaptive_thinking_model(
model
) or self._is_claude_opus_4_5(model)

def _is_claude_opus_4_5(self, model: str) -> bool:
"""
Check if the model is Claude Opus 4.5.
Expand Down Expand Up @@ -511,6 +524,16 @@ def transform_anthropic_messages_request(
anthropic_messages_request=anthropic_messages_request,
)

# 5b. ``output_config`` (carries the Anthropic ``effort`` parameter)
# is accepted by Bedrock on:
# - Claude 4.6/4.7 adaptive-thinking models (no beta header needed)
# - Claude Opus 4.5 with the ``effort-2025-11-24`` beta header
# Other Claude models on Bedrock reject the field, so strip it for
# them. The allowlist (step 7 below) only preserves keys that survive
# this filter.
if not self._supports_effort_on_bedrock(model):
anthropic_messages_request.pop("output_config", None)

# 5a. Remove `custom` field from tools (Bedrock doesn't support it)
# Claude Code sends `custom: {defer_loading: true}` on tool definitions,
# which causes Bedrock to reject the request with "Extra inputs are not permitted"
Expand Down
9 changes: 9 additions & 0 deletions litellm/types/llms/bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1041,3 +1041,12 @@ class BedrockInvokeAnthropicMessagesRequest(TypedDict, total=False):
# `metadata` is part of the common Anthropic Messages API shape.
thinking: dict
metadata: dict

# `output_config` carries the Anthropic ``effort`` parameter. Bedrock
# accepts it on the Invoke Messages API for Claude 4.6+ adaptive thinking
# (no beta header) and for Opus 4.5 with the ``effort-2025-11-24`` beta.
# Older Claude models on Bedrock reject this field, so the runtime strips
# it for them — see ``AmazonAnthropicClaudeMessagesConfig`` for that
# model-aware filtering. Including it here lets the allowlist preserve it
# for the supported-model path.
output_config: dict
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,51 @@ def test_output_config_removed_from_bedrock_chat_invoke_request():
assert result["max_tokens"] == 100


def test_output_config_preserved_for_adaptive_thinking_on_bedrock_invoke():
"""
Bedrock Invoke (legacy /completion path) must forward ``output_config``
for Claude 4.6/4.7 adaptive-thinking models — the field reaches the wire
body. This mirrors the /v1/messages behavior for the same models.
"""
config = AmazonAnthropicClaudeConfig()
messages = [{"role": "user", "content": "test"}]
optional_params = {
"max_tokens": 100,
"output_config": {"effort": "high"},
}

result = config.transform_request(
model="anthropic.claude-sonnet-4-6-v1:0",
messages=messages,
optional_params=optional_params,
litellm_params={},
headers={},
)
assert result.get("output_config") == {"effort": "high"}


def test_output_config_preserved_for_opus_4_5_on_bedrock_invoke():
"""
Bedrock Invoke /completion forwards ``output_config`` on Claude Opus 4.5
(gated behind the ``effort-2025-11-24`` beta header on Bedrock).
"""
config = AmazonAnthropicClaudeConfig()
messages = [{"role": "user", "content": "test"}]
optional_params = {
"max_tokens": 100,
"output_config": {"effort": "low"},
}

result = config.transform_request(
model="anthropic.claude-opus-4-5-20251101-v1:0",
messages=messages,
optional_params=optional_params,
litellm_params={},
headers={},
)
assert result.get("output_config") == {"effort": "low"}


def test_output_format_removed_from_bedrock_invoke_request():
"""
Test that output_format parameter is removed from Bedrock Invoke requests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3284,6 +3284,98 @@ def test_transform_request_with_output_config():
)


def test_converse_folds_output_config_effort_into_thinking_for_46():
"""
Bedrock Converse exposes the Anthropic ``effort`` parameter through
``additionalModelRequestFields.thinking.effort`` (per AWS adaptive thinking
docs), not as a top-level ``output_config`` block. The transform must
fold ``output_config.effort`` into ``thinking`` for adaptive-thinking
models so the value survives onto the wire body.

Customer regression: Sonnet 4.6 ``output_config.effort`` was being dropped
on Bedrock Converse and never reached the request payload.
"""
config = AmazonConverseConfig()
messages = [{"role": "user", "content": "hello"}]

result = config._transform_request(
model="us.anthropic.claude-sonnet-4-6-v1:0",
messages=messages,
optional_params={
"maxTokens": 64,
"output_config": {"effort": "low"},
},
litellm_params={},
headers={},
)

assert "outputConfig" not in result
additional_fields = result.get("additionalModelRequestFields", {})
assert "output_config" not in additional_fields
thinking = additional_fields.get("thinking")
assert isinstance(thinking, dict)
assert thinking.get("type") == "adaptive"
assert thinking.get("effort") == "low"


def test_converse_folds_output_config_effort_preserves_existing_thinking():
"""
When the caller already supplied a ``thinking`` block, fold ``effort``
into it without overriding any explicit ``thinking.effort``.
"""
config = AmazonConverseConfig()
messages = [{"role": "user", "content": "hello"}]

# Caller already set thinking with no effort — fold into it.
result = config._transform_request(
model="us.anthropic.claude-opus-4-7-v1",
messages=messages,
optional_params={
"maxTokens": 64,
"thinking": {"type": "adaptive"},
"output_config": {"effort": "high"},
},
litellm_params={},
headers={},
)
thinking = result["additionalModelRequestFields"].get("thinking")
assert thinking == {"type": "adaptive", "effort": "high"}

# Caller set thinking.effort explicitly — must not be overridden.
result = config._transform_request(
model="us.anthropic.claude-opus-4-7-v1",
messages=messages,
optional_params={
"maxTokens": 64,
"thinking": {"type": "adaptive", "effort": "max"},
"output_config": {"effort": "low"},
},
litellm_params={},
headers={},
)
thinking = result["additionalModelRequestFields"].get("thinking")
assert thinking.get("effort") == "max"


def test_converse_reasoning_effort_sets_thinking_effort_for_46():
"""
On adaptive-thinking models, OpenAI ``reasoning_effort`` should produce
``thinking.type=adaptive`` plus ``thinking.effort`` so Converse forwards
the value through ``additionalModelRequestFields``.
"""
config = AmazonConverseConfig()
optional_params: dict = {}
config._handle_reasoning_effort_parameter(
model="us.anthropic.claude-sonnet-4-6-v1:0",
reasoning_effort="medium",
optional_params=optional_params,
)
thinking = optional_params.get("thinking")
assert isinstance(thinking, dict)
assert thinking.get("type") == "adaptive"
assert thinking.get("effort") == "medium"


def test_transform_request_strips_anthropic_output_config():
"""
output_config is Anthropic-specific and must never be forwarded to Bedrock.
Expand Down
Loading
Loading