mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(tools): salvage concatenated JSON in tool call arguments
Models sometimes emit several JSON objects concatenated into a single
tool-call `arguments` string. `json.loads` reports "Extra data" and
`_attempt_json_repair` cannot help because nothing is truncated, so
`parse_tool_call_arguments` raised and the chat completions caller
converted that into `{}` - silently discarding the tool call. An empty
dict is indistinguishable from the model asking for nothing, so the
failure was invisible from both ends: the tool server saw no request at
all, and the model retried the same malformed shape.
`split_concatenated_json_objects` already handles this exact provider
behaviour on the Bedrock request path, but was never wired into the
chat completions response path. Reuse it before raising, keeping the
first object to mirror the "first call keeps the original tool id"
semantics in factory.py, and warn with the number discarded.
Fixes #40582
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
218b3280d1
commit
47ce07253c
2 changed files with 65 additions and 0 deletions
|
|
@ -2360,6 +2360,30 @@ def parse_tool_call_arguments(
|
|||
)
|
||||
return repaired
|
||||
|
||||
# Some providers emit several JSON objects concatenated into a single
|
||||
# arguments string, which ``json.loads`` reports as "Extra data" and
|
||||
# ``_attempt_json_repair`` cannot fix because nothing is truncated.
|
||||
# This is the same provider behaviour already repaired on the Bedrock
|
||||
# request path (see ``_convert_to_bedrock_tool_call_invoke``), so the
|
||||
# helper is reused here rather than dropping the call: returning ``{}``
|
||||
# is indistinguishable from the model asking for nothing.
|
||||
concatenated: Final = split_concatenated_json_objects(arguments)
|
||||
if concatenated:
|
||||
verbose_logger.warning(
|
||||
"Recovered %d concatenated JSON object(s) from tool call arguments for tool '%s' (%s); "
|
||||
"using the first and discarding %d. Original (%d chars): %.200s%s",
|
||||
len(concatenated),
|
||||
tool_name or "<unknown>",
|
||||
context or "unknown context",
|
||||
len(concatenated) - 1,
|
||||
len(arguments),
|
||||
arguments,
|
||||
"..." if len(arguments) > 200 else "",
|
||||
)
|
||||
# Mirrors factory.py, where the first parsed object keeps the
|
||||
# original tool call id.
|
||||
return concatenated[0]
|
||||
|
||||
error_parts: Final = ["Failed to parse tool call arguments"]
|
||||
|
||||
if tool_name:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
handle_any_messages_to_chat_completion_str_messages_conversion,
|
||||
hoist_images_from_tool_messages,
|
||||
is_encrypted_reasoning_block,
|
||||
parse_tool_call_arguments,
|
||||
responses_reasoning_items_from_thinking_blocks,
|
||||
split_concatenated_json_objects,
|
||||
strip_encrypted_reasoning_from_messages,
|
||||
|
|
@ -268,6 +269,46 @@ def test_split_concatenated_json_salvages_prefix_before_truncated_tail():
|
|||
assert result == [{"a": 1}, {"b": 2}]
|
||||
|
||||
|
||||
def test_parse_tool_call_arguments_salvages_concatenated_objects():
|
||||
"""
|
||||
Regression test for #40582.
|
||||
|
||||
Models sometimes emit several JSON objects concatenated into a single
|
||||
tool-call ``arguments`` string. ``json.loads`` fails on this with
|
||||
``Extra data``, and ``_attempt_json_repair`` cannot help because nothing is
|
||||
truncated. Previously this raised ``ValueError``, which the chat
|
||||
completions caller converted into ``{}`` - silently discarding the tool
|
||||
call. ``split_concatenated_json_objects`` already handled this exact shape
|
||||
on the Bedrock request path (#20543); the response path must salvage it too.
|
||||
"""
|
||||
raw = (
|
||||
'{"args": "{\\"flag\\": true}"}'
|
||||
'{"args": "{\\"box\\": \\"A\\", \\"limit\\": 50}"}'
|
||||
'{"args": "{\\"since\\": \\"01-Jan-2025\\"}"}'
|
||||
)
|
||||
|
||||
result = parse_tool_call_arguments(raw, tool_name="demo", context="chat completions")
|
||||
|
||||
# The first object is kept, mirroring the "first call keeps the original
|
||||
# tool id" semantics already used in factory.py for the Bedrock path.
|
||||
assert result == {"args": '{"flag": true}'}
|
||||
|
||||
|
||||
def test_parse_tool_call_arguments_concatenated_is_not_dropped_silently():
|
||||
"""
|
||||
The chat completions caller must no longer turn a concatenated-arguments
|
||||
tool call into an empty dict, which is indistinguishable from the model
|
||||
asking for nothing.
|
||||
"""
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
_parse_tool_call_arguments,
|
||||
)
|
||||
|
||||
result = _parse_tool_call_arguments('{"a": 1}{"b": 2}', tool_name="demo", context="chat completions")
|
||||
|
||||
assert result == {"a": 1}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regression tests for non-OpenAI file content blocks.
|
||||
#
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue