From 5197f505703c653f846e669097c568aed10c2524 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:06:04 +0000 Subject: [PATCH] fix(ollama_chat): preserve per-tool-call index in streaming deltas --- litellm/llms/ollama/chat/transformation.py | 6 +- .../ollama/test_ollama_chat_transformation.py | 85 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/litellm/llms/ollama/chat/transformation.py b/litellm/llms/ollama/chat/transformation.py index 694f8cdd6c2..f9927ba237c 100644 --- a/litellm/llms/ollama/chat/transformation.py +++ b/litellm/llms/ollama/chat/transformation.py @@ -475,7 +475,11 @@ class OllamaChatCompletionResponseIterator(BaseModelResponseIterator): tool_calls = chunk["message"].get("tool_calls") if tool_calls is not None: for tool_call in tool_calls: - function_args = tool_call.get("function").get("arguments") + function = tool_call.get("function") or {} + function_index = function.get("index") + if function_index is not None: + tool_call["index"] = function_index + function_args = function.get("arguments") if function_args is not None and len(function_args) > 0: is_function_call_complete = self._is_function_call_complete(function_args) if is_function_call_complete: diff --git a/tests/test_litellm/llms/ollama/test_ollama_chat_transformation.py b/tests/test_litellm/llms/ollama/test_ollama_chat_transformation.py index 906c51d8064..e22963bca31 100644 --- a/tests/test_litellm/llms/ollama/test_ollama_chat_transformation.py +++ b/tests/test_litellm/llms/ollama/test_ollama_chat_transformation.py @@ -618,6 +618,91 @@ class TestOllamaFinishReasonLength: ), f"Expected 'stop' for natural finish, got '{result.choices[0].finish_reason}'" +class TestOllamaStreamingToolCallIndex: + """Streaming: parallel tool calls must preserve Ollama's per-call function.index.""" + + def test_parallel_tool_calls_preserve_distinct_index(self): + """ + Ollama streams one tool call per chunk with the correct index nested at + function.index (0, then 1). Previously LiteLLM dropped it and every delta + was emitted with index=0, so clients that accumulate arguments by index + merged parallel calls into one malformed tool call. + """ + iterator = OllamaChatCompletionResponseIterator( + streaming_response=iter([]), + sync_stream=True, + ) + + first_chunk = { + "model": "qwen3:14b", + "message": { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "function": { + "index": 0, + "name": "read_file", + "arguments": {"path": "a.rs"}, + } + } + ], + }, + "done": False, + } + second_chunk = { + "model": "qwen3:14b", + "message": { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "function": { + "index": 1, + "name": "read_file", + "arguments": {"path": "b.rs"}, + } + } + ], + }, + "done": False, + } + + first_result = iterator.chunk_parser(first_chunk) + second_result = iterator.chunk_parser(second_chunk) + + assert first_result.choices[0].delta.tool_calls[0].index == 0 + assert second_result.choices[0].delta.tool_calls[0].index == 1 + + def test_missing_function_index_falls_back_to_positional(self): + """When Ollama omits function.index, Delta still assigns a positional index.""" + iterator = OllamaChatCompletionResponseIterator( + streaming_response=iter([]), + sync_stream=True, + ) + + chunk = { + "model": "qwen3:14b", + "message": { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "function": { + "name": "read_file", + "arguments": {"path": "a.rs"}, + } + } + ], + }, + "done": False, + } + + result = iterator.chunk_parser(chunk) + + assert result.choices[0].delta.tool_calls[0].index == 0 + + class TestOllamaReasoningContentStreaming: """Test that reasoning_content is properly extracted from all thinking chunks."""