This commit is contained in:
IronMurphy 2026-10-09 16:17:26 +03:00 • committed by GitHub
commit f5e2d95ce9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 93 additions and 1 deletions

View file

@ -427,7 +427,11 @@ def handle_invalid_parallel_tool_calls(
shift = 0
for i, replacement in replacements.items():
tool_calls[:] = tool_calls[: i + shift] + replacement + tool_calls[i + shift + 1 :]
shift += len(replacement)
# One entry is replaced by ``len(replacement)`` entries, so the
# offsets of everything after it move by the difference - not by
# the full length, which would skip one entry per expansion and
# leave the next ``multi_tool_use.parallel`` call in place.
shift += len(replacement) - 1
return tool_calls
except json.JSONDecodeError:

View file

@ -0,0 +1,88 @@
"""Every hallucinated multi_tool_use.parallel call in one message must expand.
Replacing one entry with len(expansions) entries moves everything after it by
len(expansions) - 1; advancing by the full length skipped one entry per
expansion, so with two such calls the second stayed in place (and the call
after it was overwritten).
"""
import json
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
convert_to_model_response_object,
)
from litellm.types.utils import ModelResponse
def _parallel(call_id: str, *calls):
return {
"id": call_id,
"type": "function",
"function": {
"name": "multi_tool_use.parallel",
"arguments": json.dumps(
{
"tool_uses": [
{"recipient_name": name, "parameters": params} for name, params in calls
]
}
),
},
}
def _plain(call_id: str, name: str):
return {
"id": call_id,
"type": "function",
"function": {"name": name, "arguments": "{}"},
}
def test_every_parallel_tool_call_expands():
response_object = {
"id": "chatcmpl-parallel",
"object": "chat.completion",
"created": 1728933352,
"model": "gpt-4o-2024-08-06",
"choices": [
{
"index": 0,
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
_plain("0", "get_weather"),
# Two expansions here and one below, so the first splice
# changes the list length and the second has to land at
# the shifted offset.
_parallel(
"m1",
("functions.get_time", {"tz": "UTC"}),
("functions.get_date", {"tz": "UTC"}),
),
_plain("2", "get_news"),
_parallel("m2", ("functions.get_quote", {"sym": "AAPL"})),
_plain("4", "get_forecast"),
],
},
}
],
}
result = convert_to_model_response_object(
response_object=response_object,
model_response_object=ModelResponse(),
response_type="completion",
)
names = [tc.function.name for tc in result.choices[0].message.tool_calls]
assert names == [
"get_weather",
"get_time",
"get_date",
"get_news",
"get_quote",
"get_forecast",
]