From e6044318cc22982874cc10cfb4aaed89ede964f4 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:09:40 -0800 Subject: [PATCH] fix(ollama_chat): send {} for null replayed arguments and keep argument text out of the repair warning Replayed tool call arguments of the JSON string "null" used to reach Ollama and return 200. The shared parser turns them into None, so they now go out as {} like empty or missing arguments instead of answering 400. The parser's repair warning logged up to 200 characters of the arguments. It now logs only the tool name, context, and length. --- .../prompt_templates/tool_call_json_repair.py | 4 +--- litellm/llms/ollama/chat/transformation.py | 3 ++- .../prompt_templates/test_tool_call_json_repair.py | 13 +++++++++++++ .../llms/ollama/test_ollama_chat_transformation.py | 9 ++++++--- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/tool_call_json_repair.py b/litellm/litellm_core_utils/prompt_templates/tool_call_json_repair.py index 317ffa7b3b1..1fe57f38ee3 100644 --- a/litellm/litellm_core_utils/prompt_templates/tool_call_json_repair.py +++ b/litellm/litellm_core_utils/prompt_templates/tool_call_json_repair.py @@ -104,12 +104,10 @@ def parse_tool_call_arguments( repaired: Final = _attempt_json_repair(arguments) if repaired is not None: verbose_logger.warning( - "Repaired truncated tool call arguments for tool '%s' (%s). Original (%d chars): %.200s%s", + "Repaired truncated tool call arguments for tool '%s' (%s). Original length: %d chars", tool_name or "", context or "unknown context", len(arguments), - arguments, - "..." if len(arguments) > 200 else "", ) return repaired diff --git a/litellm/llms/ollama/chat/transformation.py b/litellm/llms/ollama/chat/transformation.py index a575a7bb42e..ee795d2f49c 100644 --- a/litellm/llms/ollama/chat/transformation.py +++ b/litellm/llms/ollama/chat/transformation.py @@ -55,11 +55,12 @@ def _to_ollama_tool_call_function(model: str, tool_call: ChatCompletionAssistant name: Final = function.get("name") or "" raw_arguments: Final[object] = function.get("arguments") try: - arguments: Final = _TOOL_CALL_ARGUMENTS.validate_python( + parsed: Final = ( parse_tool_call_arguments(raw_arguments, tool_name=name, context="Ollama chat request") if isinstance(raw_arguments, str) or raw_arguments is None else raw_arguments ) + arguments: Final = _TOOL_CALL_ARGUMENTS.validate_python({} if parsed is None else parsed) except ValueError as e: raise litellm.BadRequestError( message=f"OllamaException - arguments of assistant tool call {tool_call.get('id')!r} are not a JSON object", diff --git a/tests/unit/litellm_core_utils/prompt_templates/test_tool_call_json_repair.py b/tests/unit/litellm_core_utils/prompt_templates/test_tool_call_json_repair.py index 75f4e686137..a23dcfbb882 100644 --- a/tests/unit/litellm_core_utils/prompt_templates/test_tool_call_json_repair.py +++ b/tests/unit/litellm_core_utils/prompt_templates/test_tool_call_json_repair.py @@ -1,4 +1,6 @@ import json +import logging +from typing import Final import pytest @@ -115,3 +117,14 @@ def _distinct_json_objects(count: int) -> str: ) def test_salvage_concatenated_tool_arguments(raw: str, expected: tuple[dict[str, object], ...]) -> None: assert salvage_concatenated_tool_arguments(raw) == expected + + +def test_parse_tool_call_arguments_repair_warning_leaves_out_argument_content( + caplog: pytest.LogCaptureFixture, +) -> None: + with caplog.at_level(logging.WARNING, logger="LiteLLM"): + result: Final = parse_tool_call_arguments('{"patient": "Jane Doe"', tool_name="lookup", context="test") + + assert result == {"patient": "Jane Doe"} + assert "Repaired truncated tool call arguments for tool 'lookup'" in caplog.text + assert "Jane Doe" not in caplog.text diff --git a/tests/unit/llms/ollama/test_ollama_chat_transformation.py b/tests/unit/llms/ollama/test_ollama_chat_transformation.py index cabfdcd117c..acd52987144 100644 --- a/tests/unit/llms/ollama/test_ollama_chat_transformation.py +++ b/tests/unit/llms/ollama/test_ollama_chat_transformation.py @@ -1077,10 +1077,12 @@ class TestOllamaToolCallTransformation: [ ("", {}), (None, {}), + ("null", {}), ('{"path": "a.rs"', {"path": "a.rs"}), + ('{"path": "a.rs", "limit": 2}', {"path": "a.rs", "limit": 2}), ({"path": "a.rs"}, {"path": "a.rs"}), ], - ids=["empty", "null", "truncated", "object"], + ids=["empty", "null", "null_string", "truncated", "non_string_value", "object"], ) def test_replayed_tool_call_arguments_are_forwarded_as_an_object(self, arguments: object, expected: object): result: Final = OllamaChatConfig().transform_request( @@ -1097,8 +1099,8 @@ class TestOllamaToolCallTransformation: @pytest.mark.parametrize( "arguments", - ['{"path": "a.rs"}{"path": "b.rs"}', '["a.rs", "b.rs"]'], - ids=["glued", "array"], + ['{"path": "a.rs"}{"path": "b.rs"}', '["a.rs", "b.rs"]', '[["path", "a.rs"]]'], + ids=["glued", "array", "pairs"], ) def test_replayed_tool_call_arguments_that_are_not_an_object_answer_400(self, arguments: str): with pytest.raises(litellm.BadRequestError) as exc_info: @@ -1111,6 +1113,7 @@ class TestOllamaToolCallTransformation: assert exc_info.value.status_code == 400 assert "call_abc123" in str(exc_info.value) + class TestOllamaStreamingParallelToolCallIndex: """Regression for https://github.com/BerriAI/litellm/issues/33678.