Skip to content
Open
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
37 changes: 34 additions & 3 deletions litellm/llms/bedrock/chat/converse_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import json
import time
import types
from typing import List, Literal, Optional, Tuple, Union, cast, overload
from typing import Any, List, Literal, Optional, Tuple, Union, cast, overload

import httpx

Expand Down Expand Up @@ -411,6 +411,28 @@ def _transform_reasoning_effort_to_reasoning_config(
}
}

@staticmethod
def _coerce_reasoning_effort(value: Any) -> Optional[str]:
"""
Normalize `reasoning_effort` into the bare-string shape that
`_handle_reasoning_effort_parameter` and `AnthropicConfig._map_reasoning_effort`
expect.

Accepts:
- bare string ("low" / "high" / ...)
- OpenAI Responses `Reasoning(effort, summary)` dict — extract `effort`
(see #25359 / #28196)

Returns the effort string, or None if `value` is None / malformed
(e.g. dict without an `effort` key). Malformed values are dropped
silently to match the direct Anthropic adapter's behavior.
"""
if value is None:
return None
if isinstance(value, dict):
value = value.get("effort")
return value if isinstance(value, str) else None

def _handle_reasoning_effort_parameter(
self, model: str, reasoning_effort: str, optional_params: dict
) -> None:
Expand Down Expand Up @@ -946,9 +968,18 @@ def map_openai_params(
}
if param == "thinking":
optional_params["thinking"] = value
elif param == "reasoning_effort" and isinstance(value, str):
elif (
param == "reasoning_effort"
and (effort_value := self._coerce_reasoning_effort(value)) is not None
):
# See `_coerce_reasoning_effort` — accepts both bare string
# and the OpenAI Responses `{effort, summary}` dict shape
# (#25359 / #28196). Same coercion the direct Anthropic
# adapter already does.
self._handle_reasoning_effort_parameter(
model=model, reasoning_effort=value, optional_params=optional_params
model=model,
reasoning_effort=effort_value,
optional_params=optional_params,
)
elif param == "context_management" and isinstance(value, (dict, list)):
self._map_context_management_param(value, optional_params)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5268,3 +5268,50 @@ def text(self):
msg = str(exc_info.value)
assert "secret content" not in msg
assert "Error converting to valid response block" in msg


@pytest.mark.parametrize(
"model",
[
"bedrock/converse/us.anthropic.claude-opus-4-7",
"bedrock/converse/us.anthropic.claude-sonnet-4-6",
],
)
def test_reasoning_effort_accepts_dict_shape_on_bedrock_converse(model):
"""Regression for #28196 — OpenAI Responses callers send
``reasoning_effort={'effort': 'low', 'summary': 'concise'}``; the
Bedrock Converse adapter must coerce the dict to ``low`` instead of
silently dropping it (the way the direct Anthropic path already does).
"""
config = AmazonConverseConfig()

optional_params = config.map_openai_params(
non_default_params={
"reasoning_effort": {"effort": "low", "summary": "concise"},
},
optional_params={},
model=model,
drop_params=False,
)

assert (
"thinking" in optional_params
), f"reasoning_effort dict was dropped on {model}: {optional_params!r}"
# Adaptive Claude 4.6 / 4.7: dict effort should drive output_config too.
assert optional_params.get("output_config") == {"effort": "low"}


def test_reasoning_effort_invalid_dict_does_not_crash_or_emit_thinking():
"""Defensive: a malformed dict (missing ``effort`` key) must silently
drop instead of raising, matching the direct Anthropic path."""
config = AmazonConverseConfig()

optional_params = config.map_openai_params(
non_default_params={"reasoning_effort": {"summary": "concise"}},
optional_params={},
model="bedrock/converse/us.anthropic.claude-opus-4-7",
drop_params=False,
)

assert "thinking" not in optional_params
assert "output_config" not in optional_params
Loading