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.
This commit is contained in:
mateo-berri 2026-11-06 18:09:40 -08:00
parent 0ae806cce4
commit e6044318cc
4 changed files with 22 additions and 7 deletions

View file

@ -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 "<unknown>",
context or "unknown context",
len(arguments),
arguments,
"..." if len(arguments) > 200 else "",
)
return repaired

View file

@ -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",

View file

@ -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

View file

@ -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.