mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(core): keep valid JSON arrays out of tool-call expansion
A valid JSON array of objects took the json.loads success path and was expanded into multiple calls, bypassing MAX_RECOVERED_ARGUMENT_OBJECTS and the historical object-only contract for non-object roots. Parse valid JSON directly with object-only semantics, and hold any sequence produced by malformed-input recovery to the same per-call bound.
This commit is contained in:
parent
3d4e3549fe
commit
0343c96558
2 changed files with 84 additions and 1 deletions
|
|
@ -5366,9 +5366,20 @@ def _parse_tool_call_arguments(
|
|||
return {}
|
||||
normalized_raw: Final = "{}" if raw == REDACTED_BY_LITELLM else raw
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
MAX_RECOVERED_ARGUMENT_OBJECTS,
|
||||
parse_tool_call_arguments,
|
||||
)
|
||||
|
||||
try:
|
||||
direct: Final = json.loads(normalized_raw)
|
||||
except json.JSONDecodeError:
|
||||
pass # malformed JSON: fall through to repair / concatenated recovery
|
||||
else:
|
||||
# Valid JSON keeps the historical object-only contract: a non-object
|
||||
# root (e.g. a JSON array of objects) degrades to {} rather than
|
||||
# becoming an uncapped expansion vector.
|
||||
return direct if isinstance(direct, dict) else {}
|
||||
|
||||
try:
|
||||
parsed: Final = parse_tool_call_arguments(
|
||||
normalized_raw,
|
||||
|
|
@ -5381,7 +5392,15 @@ def _parse_tool_call_arguments(
|
|||
return {}
|
||||
if isinstance(parsed, dict):
|
||||
return parsed
|
||||
if isinstance(parsed, list) and parsed and all(isinstance(item, dict) for item in parsed):
|
||||
# Only malformed-input recovery (repair or concatenated split) can yield a
|
||||
# sequence here; hold it to the same per-call bound as concatenated
|
||||
# recovery so expansion is never an amplification vector.
|
||||
if (
|
||||
isinstance(parsed, list)
|
||||
and parsed
|
||||
and all(isinstance(item, dict) for item in parsed)
|
||||
and len(parsed) <= MAX_RECOVERED_ARGUMENT_OBJECTS
|
||||
):
|
||||
return parsed
|
||||
return {}
|
||||
|
||||
|
|
|
|||
|
|
@ -3524,6 +3524,70 @@ def test_get_tool_calls_from_response_caps_concatenated_expansion():
|
|||
assert tool_calls == [{"id": "call_1", "name": "search", "arguments": {}}]
|
||||
|
||||
|
||||
def test_get_tool_calls_from_response_valid_json_array_not_expanded():
|
||||
"""
|
||||
A valid JSON array of objects is not concatenated recovery: it keeps the
|
||||
historical object-only semantics (degrades to {}) and never expands, so
|
||||
it cannot bypass the per-call expansion cap.
|
||||
"""
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
get_tool_calls_from_response,
|
||||
)
|
||||
|
||||
response: Final = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"function": {
|
||||
"name": "search",
|
||||
"arguments": json.dumps(
|
||||
[{"query": f"q{index}"} for index in range(9)]
|
||||
),
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
tool_calls: Final = get_tool_calls_from_response(response)
|
||||
|
||||
assert tool_calls == [{"id": "call_1", "name": "search", "arguments": {}}]
|
||||
|
||||
|
||||
def test_get_tool_calls_from_response_valid_json_array_of_two_not_expanded():
|
||||
"""Even a small valid JSON array degrades to {} (object-only contract)."""
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
get_tool_calls_from_response,
|
||||
)
|
||||
|
||||
response: Final = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"function": {
|
||||
"name": "search",
|
||||
"arguments": json.dumps([{"query": "a"}, {"query": "b"}]),
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
tool_calls: Final = get_tool_calls_from_response(response)
|
||||
|
||||
assert tool_calls == [{"id": "call_1", "name": "search", "arguments": {}}]
|
||||
|
||||
|
||||
def test_get_tool_calls_from_response_non_object_arguments_returns_empty():
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
get_tool_calls_from_response,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue