Skip to content

Python: Bug: OllamaChatCompletion streaming merges parallel tool calls into one call, so only the first function runs with combined arguments #14614

Closed

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

  1. added a commit that references this issue on Oct 26, 2026
    b11d98b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pythonPull requests for the Python Semantic Kerneltriage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions