Repository navigation
Python: Bug: OllamaChatCompletion streaming merges parallel tool calls into one call, so only the first function runs with combined arguments #14614
Description
What happens
When an Ollama model returns several tool calls in a streaming response, the Python OllamaChatCompletion connector loses all but one of them. With auto function invocation, only the first call name runs, with the arguments of all calls merged into it.
Where
python/semantic_kernel/connectors/ai/ollama/services/ollama_chat_completion.py, _inner_get_streaming_chat_message_contents and _create_streaming_chat_message_content*.
Ollama streams each tool call complete in its own chunk, with no id and no index. The connector builds a FunctionCallContent from each one without setting id or index:
FunctionCallContent(
inner_content=tool_call,
ai_model_id=self.ai_model_id,
name=tool_call.get("function").get("name"),
arguments=tool_call.get("function").get("arguments"),
)When the stream is combined (reduce(lambda x, y: x + y, all_messages) in ChatCompletionClientBase.get_streaming_chat_message_contents), StreamingContentMixin._merge_items_lists adds items of the same type with +. FunctionCallContent.__add__ only refuses to merge when ids or indexes differ. Both are None here, so every tool call collapses into the first one: the name of the first call is kept and combine_arguments merges the argument dicts.
Reproduce
No Ollama server needed. Current main (7a92cd4):
import asyncio
from functools import reduce
from unittest.mock import AsyncMock, patch
from semantic_kernel.connectors.ai.ollama import OllamaChatCompletion, OllamaChatPromptExecutionSettings
from semantic_kernel.contents import ChatHistory, FunctionCallContent
def chunk(name, args):
return {"model": "m", "message": {"role": "assistant", "content": "",
"tool_calls": [{"function": {"name": name, "arguments": args}}]}}
async def stream():
yield chunk("Weather-get_weather", {"city": "Paris"})
yield chunk("Time-get_time", {"zone": "Tokyo"})
yield {"model": "m", "message": {"role": "assistant", "content": ""}, "done": True}
async def main():
with patch("ollama.AsyncClient.chat", new_callable=AsyncMock) as chat:
chat.return_value = stream()
svc = OllamaChatCompletion(ai_model_id="m")
h = ChatHistory(); h.add_user_message("hi")
chunks = []
async for batch in svc.get_streaming_chat_message_contents(h, OllamaChatPromptExecutionSettings()):
chunks.extend(batch)
full = reduce(lambda x, y: x + y, chunks)
for item in full.items:
if isinstance(item, FunctionCallContent):
print(item.name, item.arguments)
asyncio.run(main())Actual output:
Weather-get_weather {'city': 'Paris', 'zone': 'Tokyo'}
Expected output:
Weather-get_weather {'city': 'Paris'}
Time-get_time {'zone': 'Tokyo'}
I also checked this against a real local Ollama (qwen2.5:7b, FunctionChoiceBehavior.Auto(), a plugin with get_weather(city), prompt asking for Paris and Tokyo). Ollama sent two chunks, each with one tool call (index 0 and 1 inside the function object). The kernel ran the function once, with Tokyo (the later argument overwrote the earlier one). Paris was never called.
Suggested fix
Number the tool calls as they stream in, so they stay separate when combined. Bedrock and Mistral already set index on their streamed function calls. I have a small fix with tests and will link it here.
Activity
Python: fix(ollama): preserve streamed parallel tool calls by assigni…