From c02f11dd7edb783a220ed6c49922b3b8de6dc9af Mon Sep 17 00:00:00 2001 From: sclfcz Date: Sat, 26 Sep 2026 09:19:57 +0800 Subject: [PATCH] chore: keep this PR to the shift fix only Two commits that belong to the malformed-payload PR (#43173) had leaked onto this branch: the widened except clause and its test file. Restore the clause to main's version and drop those tests here, and keep the regression test only in tests/test_litellm/litellm_core_utils/llm_response_utils/ where the unit coverage job runs. --- .../convert_dict_to_response.py | 2 +- .../test_convert_dict_to_chat_completion.py | 82 ------------------- ...test_handle_invalid_parallel_tool_calls.py | 59 ------------- 3 files changed, 1 insertion(+), 142 deletions(-) delete mode 100644 tests/test_litellm/litellm_core_utils/llm_response_utils/test_handle_invalid_parallel_tool_calls.py diff --git a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py index 5395ba973d3..ff624a3de8b 100644 --- a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py +++ b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py @@ -413,7 +413,7 @@ def _handle_invalid_parallel_tool_calls( shift += len(replacement) - 1 return tool_calls - except (json.JSONDecodeError, KeyError, TypeError, AttributeError): + except json.JSONDecodeError: # if there is a JSONDecodeError, return the original tool_calls return tool_calls diff --git a/tests/llm_translation/test_llm_response_utils/test_convert_dict_to_chat_completion.py b/tests/llm_translation/test_llm_response_utils/test_convert_dict_to_chat_completion.py index ff0bd628ab3..31c554985a7 100644 --- a/tests/llm_translation/test_llm_response_utils/test_convert_dict_to_chat_completion.py +++ b/tests/llm_translation/test_llm_response_utils/test_convert_dict_to_chat_completion.py @@ -2486,85 +2486,3 @@ class TestConvertToModelResponseObjectCompletion: }, model_response_object=None, ) - - -def test_convert_to_model_response_object_expands_every_parallel_tool_call(): - """ - Every hallucinated `multi_tool_use.parallel` entry in one message must be - expanded. Replacing one entry with its expansions moves the offsets of the - entries that follow it by `len(expansions) - 1`; advancing by the full - length skipped one entry per expansion, so with two such calls the second - one stayed in place and the call after it was overwritten. - """ - - def parallel(call_id, *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, name): - return { - "id": call_id, - "type": "function", - "function": {"name": name, "arguments": "{}"}, - } - - 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 splice 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", - ] - assert "multi_tool_use.parallel" not in names diff --git a/tests/test_litellm/litellm_core_utils/llm_response_utils/test_handle_invalid_parallel_tool_calls.py b/tests/test_litellm/litellm_core_utils/llm_response_utils/test_handle_invalid_parallel_tool_calls.py deleted file mode 100644 index 09ef7bca2d2..00000000000 --- a/tests/test_litellm/litellm_core_utils/llm_response_utils/test_handle_invalid_parallel_tool_calls.py +++ /dev/null @@ -1,59 +0,0 @@ -"""The hallucinated multi_tool_use.parallel expansion must not fail the response.""" - -import json - -import pytest - -from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import ( - _handle_invalid_parallel_tool_calls, -) -from litellm.types.utils import ChatCompletionMessageToolCall, Function - - -def _parallel_instance(arguments: str): - return [ - ChatCompletionMessageToolCall( - id="call_1", - type="function", - function=Function(name="multi_tool_use.parallel", arguments=arguments), - ) - ] - - -@pytest.mark.parametrize( - "arguments", - [ - '{"tool_uses": [{"recipient_name": "functions.get_weather"}]}', - '{"tool_uses": "nope"}', - '{"tool_uses": [42]}', - "{}", - ], -) -def test_malformed_tool_uses_returns_original_calls(arguments): - """A hallucinated payload we cannot expand must come back untouched.""" - tool_calls = _parallel_instance(arguments) - - result = _handle_invalid_parallel_tool_calls(tool_calls) - - assert len(result) == 1 - assert result[0].function.name == "multi_tool_use.parallel" - assert result[0].id == "call_1" - assert result[0].function.arguments == arguments - - -def test_valid_parallel_payload_still_expands(): - """The guard must not swallow well-formed payloads.""" - tool_calls = _parallel_instance( - json.dumps( - { - "tool_uses": [ - {"recipient_name": "functions.get_weather", "parameters": {"city": "NYC"}}, - {"recipient_name": "functions.get_time", "parameters": {"tz": "EST"}}, - ] - } - ) - ) - - result = _handle_invalid_parallel_tool_calls(tool_calls) - - assert [c.function.name for c in result] == ["get_weather", "get_time"]