mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(bedrock): neutralize orphaned tool blocks instead of raising or injecting dummy tool (#24158, #27138)
This commit is contained in:
parent
a5b465b588
commit
600924af32
3 changed files with 183 additions and 27 deletions
|
|
@ -66,9 +66,7 @@ from litellm.types.utils import (
|
|||
Usage,
|
||||
)
|
||||
from litellm.utils import (
|
||||
add_dummy_tool,
|
||||
any_assistant_message_has_thinking_blocks,
|
||||
has_tool_call_blocks,
|
||||
last_assistant_with_tool_calls_has_no_thinking_blocks,
|
||||
supports_reasoning,
|
||||
token_counter,
|
||||
|
|
@ -1647,26 +1645,6 @@ class AmazonConverseConfig(BaseConfig):
|
|||
headers: Optional[dict] = None,
|
||||
drop_params: bool = False,
|
||||
) -> CommonRequestObject:
|
||||
## VALIDATE REQUEST
|
||||
"""
|
||||
Bedrock doesn't support tool calling without `tools=` param specified.
|
||||
"""
|
||||
if (
|
||||
"tools" not in optional_params
|
||||
and messages is not None
|
||||
and has_tool_call_blocks(messages)
|
||||
):
|
||||
if litellm.modify_params:
|
||||
optional_params["tools"] = add_dummy_tool(
|
||||
custom_llm_provider="bedrock_converse"
|
||||
)
|
||||
else:
|
||||
raise litellm.UnsupportedParamsError(
|
||||
message="Bedrock doesn't support tool calling without `tools=` param specified. Pass `tools=` param OR set `litellm.modify_params = True` // `litellm_settings::modify_params: True` to add dummy tool to the request.",
|
||||
model="",
|
||||
llm_provider="bedrock",
|
||||
)
|
||||
|
||||
# Drop thinking param if thinking is enabled but thinking_blocks are missing
|
||||
# This prevents the error: "Expected thinking or redacted_thinking, but found tool_use"
|
||||
#
|
||||
|
|
@ -1763,6 +1741,8 @@ class AmazonConverseConfig(BaseConfig):
|
|||
messages, model=model
|
||||
)
|
||||
|
||||
messages = self._neutralize_orphaned_tool_blocks(messages, optional_params)
|
||||
|
||||
# Convert last user message to guarded_text if guardrailConfig is present
|
||||
messages = self._convert_consecutive_user_messages_to_guarded_text(
|
||||
messages, optional_params
|
||||
|
|
@ -1822,6 +1802,8 @@ class AmazonConverseConfig(BaseConfig):
|
|||
messages, model=model
|
||||
)
|
||||
|
||||
messages = self._neutralize_orphaned_tool_blocks(messages, optional_params)
|
||||
|
||||
# Convert last user message to guarded_text if guardrailConfig is present
|
||||
messages = self._convert_consecutive_user_messages_to_guarded_text(
|
||||
messages, optional_params
|
||||
|
|
|
|||
|
|
@ -301,11 +301,11 @@ _PARALLEL_TOOL_HISTORY_MESSAGES = [
|
|||
@pytest.mark.parametrize(
|
||||
"model, messages, expect_unsupported_params_error",
|
||||
[
|
||||
# Bedrock Converse still requires modify_params to inject the dummy tool.
|
||||
# Bedrock Converse neutralizes orphaned tool blocks to text; no error.
|
||||
(
|
||||
"anthropic.claude-3-sonnet-20240229-v1:0",
|
||||
_PARALLEL_TOOL_HISTORY_MESSAGES,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
# Anthropic Messages API: dummy tool is injected without modify_params.
|
||||
(
|
||||
|
|
@ -341,12 +341,12 @@ def test_parallel_function_call_anthropic_error_msg(
|
|||
"""
|
||||
Tool history without an explicit ``tools`` param:
|
||||
|
||||
- Bedrock **Converse** still raises ``UnsupportedParamsError`` unless
|
||||
``litellm.modify_params`` is enabled (dummy tool is only added there).
|
||||
- Bedrock **Converse** neutralizes the orphaned tool blocks into plain text
|
||||
and sends no ``toolConfig`` (see #24158, #27138). It no longer raises.
|
||||
- **Anthropic** (and Bedrock Invoke via ``AnthropicConfig.transform_request``)
|
||||
always get a dummy tool so CLIs work with ``modify_params`` left off.
|
||||
|
||||
Reference Issue: https://github.com/BerriAI/litellm/issues/5747, https://github.com/BerriAI/litellm/issues/5388
|
||||
Reference Issue: https://github.com/BerriAI/litellm/issues/24158, https://github.com/BerriAI/litellm/issues/27138
|
||||
"""
|
||||
# Ensure modify_params is False so Bedrock Converse path still raises.
|
||||
# (other tests in this file set it to True and don't reset it)
|
||||
|
|
|
|||
|
|
@ -5582,3 +5582,177 @@ def test_neutralize_orphaned_tool_blocks_logs_warning(caplog):
|
|||
"neutralizing orphaned tool blocks" in record.getMessage()
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
|
||||
def _assert_no_structured_tool_blocks(result):
|
||||
"""A valid Bedrock body for a neutralized request has no tool config AND no
|
||||
structured tool blocks in messages. Checking only toolConfig is insufficient:
|
||||
deleting the raise without rewriting still leaves toolUse/toolResult, the
|
||||
exact shape Bedrock rejects."""
|
||||
assert "toolConfig" not in result
|
||||
serialized = json.dumps(result)
|
||||
assert "toolUse" not in serialized
|
||||
assert "toolResult" not in serialized
|
||||
|
||||
|
||||
def test_transform_request_no_tools_with_tool_history_succeeds_24158(monkeypatch):
|
||||
"""#24158: a compaction-style call (tool blocks in history, no tools=) must
|
||||
not raise and must send no toolConfig or structured tool blocks, on
|
||||
default settings."""
|
||||
monkeypatch.setattr(litellm, "modify_params", False)
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
result = config.transform_request(
|
||||
model="us.anthropic.claude-opus-4-5-20251101-v1:0",
|
||||
messages=_orphaned_tool_history_messages(),
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
_assert_no_structured_tool_blocks(result)
|
||||
serialized = json.dumps(result)
|
||||
assert "get_weather" in serialized
|
||||
assert "Sunny, 25C" in serialized
|
||||
|
||||
|
||||
def test_transform_request_tool_unsupported_model_no_toolconfig_27138(monkeypatch):
|
||||
"""#27138: a tool-incapable model with tool blocks in history and no tools=
|
||||
must not get a toolConfig/toolUse/toolResult injected (which Bedrock would
|
||||
400 on), even with modify_params on."""
|
||||
monkeypatch.setattr(litellm, "modify_params", True)
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
result = config.transform_request(
|
||||
model="meta.llama3-2-3b-instruct-v1:0",
|
||||
messages=_orphaned_tool_history_messages(),
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
_assert_no_structured_tool_blocks(result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tools_value", [[], None])
|
||||
def test_transform_request_empty_tools_with_tool_history(monkeypatch, tools_value):
|
||||
"""tools=[] / tools=None must be neutralized like no tools at all; a
|
||||
key-presence gate would skip them and emit toolUse/toolResult with no
|
||||
toolConfig."""
|
||||
monkeypatch.setattr(litellm, "modify_params", False)
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
result = config.transform_request(
|
||||
model="us.anthropic.claude-opus-4-5-20251101-v1:0",
|
||||
messages=_orphaned_tool_history_messages(),
|
||||
optional_params={"tools": tools_value},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
_assert_no_structured_tool_blocks(result)
|
||||
|
||||
|
||||
def test_transform_request_tool_result_only_history(monkeypatch):
|
||||
"""A role:"tool"-only history (no assistant tool_calls) currently emits a
|
||||
lone toolResult with no toolConfig; it must be neutralized."""
|
||||
monkeypatch.setattr(litellm, "modify_params", False)
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
result = config.transform_request(
|
||||
model="us.anthropic.claude-opus-4-5-20251101-v1:0",
|
||||
messages=[
|
||||
{"role": "user", "content": "hi"},
|
||||
{"role": "tool", "tool_call_id": "call_xyz", "content": "lookup result"},
|
||||
],
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
_assert_no_structured_tool_blocks(result)
|
||||
assert "lookup result" in json.dumps(result)
|
||||
|
||||
|
||||
def test_transform_request_neutralized_tool_output_is_guarded(monkeypatch):
|
||||
"""With guardrailConfig present, a neutralized tool result that becomes the
|
||||
trailing user turn must be emitted as guardContent, not plain text, so
|
||||
untrusted tool output does not bypass the guardrail (neutralize must run
|
||||
before guarded-text conversion)."""
|
||||
monkeypatch.setattr(litellm, "modify_params", False)
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
result = config.transform_request(
|
||||
model="us.anthropic.claude-opus-4-5-20251101-v1:0",
|
||||
messages=[
|
||||
{"role": "user", "content": "look it up"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "c1",
|
||||
"type": "function",
|
||||
"function": {"name": "lookup", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "c1", "content": "secret tool output"},
|
||||
],
|
||||
optional_params={
|
||||
"guardrailConfig": {"guardrailIdentifier": "gid", "guardrailVersion": "1"}
|
||||
},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
_assert_no_structured_tool_blocks(result)
|
||||
serialized = json.dumps(result)
|
||||
assert "guardContent" in serialized
|
||||
assert "secret tool output" in serialized
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_transform_request_no_tools_with_tool_history(monkeypatch):
|
||||
"""Async is a separate request assembler; it must neutralize identically."""
|
||||
monkeypatch.setattr(litellm, "modify_params", False)
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
result = await config._async_transform_request(
|
||||
model="us.anthropic.claude-opus-4-5-20251101-v1:0",
|
||||
messages=_orphaned_tool_history_messages(),
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
_assert_no_structured_tool_blocks(result)
|
||||
assert "get_weather" in json.dumps(result)
|
||||
|
||||
|
||||
def test_transform_request_with_tools_still_builds_toolconfig(monkeypatch):
|
||||
"""Guard: when a non-empty tools= IS provided, tool blocks are legitimate and
|
||||
a toolConfig must still be produced (neutralization must not regress this)."""
|
||||
monkeypatch.setattr(litellm, "modify_params", False)
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
result = config.transform_request(
|
||||
model="us.anthropic.claude-opus-4-5-20251101-v1:0",
|
||||
messages=_orphaned_tool_history_messages(),
|
||||
optional_params={
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert "toolConfig" in result
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue