mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(bedrock): degrade gracefully on malformed tool-call arguments
split_concatenated_json_objects re-raised JSONDecodeError on genuinely
malformed (non-concatenated) tool-call arguments, which propagated out of
_convert_to_bedrock_tool_call_invoke and turned every replayed Bedrock
conversation into a 500. Catch the decode error, keep whatever complete
objects parsed, log a warning, and let the caller fall back to input={}
so the conversation continues.
Fixes #18667
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
66dea7df8f
commit
3b843708b0
3 changed files with 89 additions and 11 deletions
|
|
@ -1679,16 +1679,19 @@ def split_concatenated_json_objects(raw: str) -> List[Dict[str, Any]]:
|
|||
This helper uses ``json.JSONDecoder.raw_decode()`` to walk the string
|
||||
and extract each JSON object individually.
|
||||
|
||||
The walk degrades gracefully: if the string is malformed or truncated
|
||||
(e.g. a stream that ended mid-tool-call), whatever complete objects were
|
||||
parsed before the bad tail are returned and the remainder is discarded
|
||||
with a warning, rather than raising. The sole caller
|
||||
(``_convert_to_bedrock_tool_call_invoke``) treats an empty result as
|
||||
``input={}`` so the conversation can continue instead of hard-failing.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[dict]
|
||||
A list of parsed dicts – one per JSON object found. If *raw* is
|
||||
empty or whitespace-only, an empty list is returned.
|
||||
|
||||
Raises
|
||||
------
|
||||
json.JSONDecodeError
|
||||
If the string contains text that cannot be parsed as JSON at all.
|
||||
empty, whitespace-only, or wholly unparseable, an empty list is
|
||||
returned.
|
||||
"""
|
||||
import json
|
||||
|
||||
|
|
@ -1708,7 +1711,17 @@ def split_concatenated_json_objects(raw: str) -> List[Dict[str, Any]]:
|
|||
if idx >= length:
|
||||
break
|
||||
|
||||
obj, end_idx = decoder.raw_decode(raw, idx)
|
||||
try:
|
||||
obj, end_idx = decoder.raw_decode(raw, idx)
|
||||
except json.JSONDecodeError as e:
|
||||
verbose_logger.warning(
|
||||
"split_concatenated_json_objects: discarding unparseable tool-call "
|
||||
"arguments tail after %d complete object(s); error=%s at char %d",
|
||||
len(results),
|
||||
e,
|
||||
idx,
|
||||
)
|
||||
break
|
||||
if isinstance(obj, dict):
|
||||
results.append(obj)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -251,10 +251,32 @@ def test_split_concatenated_json_non_dict_value():
|
|||
assert result == [{}]
|
||||
|
||||
|
||||
def test_split_concatenated_json_invalid_raises():
|
||||
"""Completely invalid JSON raises JSONDecodeError."""
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
split_concatenated_json_objects("not json at all")
|
||||
def test_split_concatenated_json_wholly_invalid_returns_empty():
|
||||
"""
|
||||
Wholly unparseable JSON degrades to an empty list instead of raising.
|
||||
|
||||
Regression for https://github.com/BerriAI/litellm/issues/18667: a raise
|
||||
here propagated out of `_convert_to_bedrock_tool_call_invoke` and turned
|
||||
every replayed conversation into a 500.
|
||||
"""
|
||||
assert split_concatenated_json_objects("not json at all") == []
|
||||
|
||||
|
||||
def test_split_concatenated_json_malformed_object_returns_empty():
|
||||
"""
|
||||
A single malformed object (missing comma between keys) degrades to an
|
||||
empty list rather than raising `Expecting ',' delimiter`.
|
||||
"""
|
||||
assert split_concatenated_json_objects('{"location": "Boston" "unit": "celsius"}') == []
|
||||
|
||||
|
||||
def test_split_concatenated_json_salvages_prefix_before_truncated_tail():
|
||||
"""
|
||||
Complete objects parsed before an unparseable/truncated tail are kept;
|
||||
only the bad tail is discarded.
|
||||
"""
|
||||
result = split_concatenated_json_objects('{"a": 1}{"b": 2}{"c":')
|
||||
assert result == [{"a": 1}, {"b": 2}]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -2286,6 +2286,49 @@ def test_bedrock_tool_call_invoke_non_dict_arguments():
|
|||
assert result[0]["toolUse"]["input"] == {}
|
||||
|
||||
|
||||
def test_bedrock_tool_call_invoke_malformed_json_does_not_raise():
|
||||
"""
|
||||
Regression for https://github.com/BerriAI/litellm/issues/18667.
|
||||
|
||||
When the model emits malformed JSON in tool-call arguments (here a
|
||||
missing comma between keys), replaying that history must NOT raise
|
||||
`Unable to convert openai tool calls ... Expecting ',' delimiter`.
|
||||
It degrades to an empty-object input so the conversation can continue.
|
||||
"""
|
||||
tool_calls = [
|
||||
{
|
||||
"id": "toolu_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": '{"location": "Boston" "unit": "celsius"}',
|
||||
},
|
||||
}
|
||||
]
|
||||
result = _convert_to_bedrock_tool_call_invoke(tool_calls)
|
||||
assert len(result) == 1
|
||||
assert result[0]["toolUse"]["toolUseId"] == "toolu_abc123"
|
||||
assert result[0]["toolUse"]["name"] == "get_weather"
|
||||
assert result[0]["toolUse"]["input"] == {}
|
||||
|
||||
|
||||
def test_bedrock_tool_call_invoke_salvages_valid_prefix_before_truncated_tail():
|
||||
"""
|
||||
A valid leading object followed by a truncated tail keeps the valid
|
||||
object rather than dropping everything or raising.
|
||||
"""
|
||||
tool_calls = [
|
||||
{
|
||||
"id": "call_partial",
|
||||
"type": "function",
|
||||
"function": {"name": "shell", "arguments": '{"cmd": "ls"}{"cmd":'},
|
||||
}
|
||||
]
|
||||
result = _convert_to_bedrock_tool_call_invoke(tool_calls)
|
||||
assert len(result) == 1
|
||||
assert result[0]["toolUse"]["input"] == {"cmd": "ls"}
|
||||
|
||||
|
||||
def test_make_valid_bedrock_tool_name_preserves_hyphens():
|
||||
assert make_valid_bedrock_tool_name("my-tool") == "my-tool"
|
||||
assert (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue