[GH-ISSUE #22177] bug: Streaming tool call function name doubled by delta accumulation (GPT-5/5.1) #58315

Closed
opened 2026-05-05 22:52:40 -05:00 by GiteaMirror · 11 comments
Owner

Originally created by @madnight on GitHub (Mar 3, 2026).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/22177

Check Existing Issues

  • I have searched for any existing and/or related issues.
  • I have searched for any existing and/or related discussions.
  • I have also searched in the CLOSED issues AND CLOSED discussions and found no related items (your issue might already be addressed on the development branch!).
  • I am using the latest version of Open WebUI.

Installation Method

Docker (Cloud Run)

Open WebUI Version

v0.8.5

Ollama Version (if applicable)

No response

Operating System

Linux (GCP Cloud Run)

Browser (if applicable)

Chrome

Confirmation

  • I have read and followed all instructions in README.md.
  • I am using the latest version of both Open WebUI and Ollama.
  • I have included the browser console logs.
  • I have included the Docker container logs.
  • I have provided every relevant configuration, setting, and environment variable used in my setup.
  • I have clearly listed every relevant configuration, custom setting, environment variable, and command-line option that influences my setup (such as Docker Compose overrides, .env values, browser settings, authentication configurations, etc).
  • I have documented step-by-step reproduction instructions that are precise, sequential, and leave nothing to interpretation.

Expected Behavior

When using native function calling with streaming enabled, the tool call function name should be captured correctly from the streaming deltas, and the tool should execute successfully.

For example, an MCP tool registered as my_server_search should be called as my_server_search.

Actual Behavior

The function name is doubled during streaming delta accumulation. For example, my_server_search becomes my_server_searchmy_server_search. This causes a silent failure because the doubled name does not match any key in tools_dict, so tool_function_name in tools evaluates to False and the tool is never executed.

The user sees "Tool Executed" in the UI but the tool was never actually called — a silent failure with no error message.

Root Cause

In backend/open_webui/utils/middleware.py, the streaming tool call delta handler accumulates the function name with +=:

# ~line 3718 (current dev branch)
if delta_name:
    current_response_tool_call["function"]["name"] += delta_name

The first delta for a tool call index creates the entry with name = "my_server_search" (via response_tool_calls.append(delta_tool_call) at line ~3704). When a subsequent delta for the same index also includes function.name = "my_server_search", the += operator concatenates it: "my_server_search" + "my_server_search" = "my_server_searchmy_server_search".

This is model-dependent. Some API providers (notably GPT-5 and GPT-5.1 via Azure) redundantly include the function name in follow-up deltas, while others (Claude, GPT-4o) only send it in the first delta.

Arguments are correctly accumulated with += because they are genuinely split across deltas. But function names are always sent complete in the first delta per the OpenAI streaming spec — they should not be accumulated.

Steps to Reproduce

  1. Configure an MCP tool server (or any tool) with native function calling enabled
  2. Use a model whose streaming API re-sends function.name in follow-up delta chunks (GPT-5 or GPT-5.1 via Azure OpenAI)
  3. Ask the model to call a tool
  4. Observe in stored chat data or debug logs that the tool name is doubled (e.g., my_server_searchmy_server_search instead of my_server_search)
  5. The tool silently fails — "Tool Executed" appears in the UI but the tool was never actually called

The bug does not reproduce with Claude models or GPT-4o because their APIs do not re-send the function name in subsequent deltas.

Data from Production Database

Query across 18 chats containing MCP tool calls shows a clear model-specific pattern:

Model Tool Name Stored Doubled?
gpt-5.1 (2 chats) <name><name> Always
gpt-5 (4 chats) Mixed Sometimes
gpt-4o (2 chats) <name> Never
claude-* (10 chats) <name> Never

Proposed Fix

Change the name accumulation to a conditional assignment — only set the name if it has not been set yet:

# Before (bug):
if delta_name:
    current_response_tool_call["function"]["name"] += delta_name

# After (fix):
if delta_name and not current_response_tool_call["function"].get("name"):
    current_response_tool_call["function"]["name"] = delta_name

This is safe because:

  • If the name arrives in the first delta (normal case): it is already set when the entry is created via append(), so the conditional does not overwrite it
  • If the name is absent from the first delta (edge case): the setdefault("name", "") at entry creation sets it to "", which is falsy, so the subsequent delta correctly sets it
  • If the name is re-sent in a follow-up delta (GPT-5/5.1 behavior): the conditional skips it because the name is already set
  • #16138 — "tools names are doubled when calling" (closed as "intended behaviour", but the root cause is this bug)
  • #19656 — "Tool call response tokens are duplicated" (different manifestation of duplication in middleware.py, fixed by commit 52ccab8)

Logs & Screenshots

No response

Additional Information

This bug affects any tool (MCP or otherwise) when used with native function calling and a model whose streaming API includes function.name in multiple delta chunks. It is not specific to MCP or any particular tool — it is in the generic streaming tool call accumulation logic.

Originally created by @madnight on GitHub (Mar 3, 2026). Original GitHub issue: https://github.com/open-webui/open-webui/issues/22177 ### Check Existing Issues - [x] I have searched for any existing and/or related issues. - [x] I have searched for any existing and/or related discussions. - [x] I have also searched in the CLOSED issues AND CLOSED discussions and found no related items (your issue might already be addressed on the development branch!). - [x] I am using the latest version of Open WebUI. ### Installation Method Docker (Cloud Run) ### Open WebUI Version v0.8.5 ### Ollama Version (if applicable) _No response_ ### Operating System Linux (GCP Cloud Run) ### Browser (if applicable) Chrome ### Confirmation - [x] I have read and followed all instructions in `README.md`. - [x] I am using the latest version of **both** Open WebUI and Ollama. - [x] I have included the browser console logs. - [x] I have included the Docker container logs. - [x] I have **provided every relevant configuration, setting, and environment variable used in my setup.** - [x] I have clearly **listed every relevant configuration, custom setting, environment variable, and command-line option that influences my setup** (such as Docker Compose overrides, .env values, browser settings, authentication configurations, etc). - [x] I have documented **step-by-step reproduction instructions that are precise, sequential, and leave nothing to interpretation**. ### Expected Behavior When using native function calling with streaming enabled, the tool call function name should be captured correctly from the streaming deltas, and the tool should execute successfully. For example, an MCP tool registered as `my_server_search` should be called as `my_server_search`. ### Actual Behavior The function name is **doubled** during streaming delta accumulation. For example, `my_server_search` becomes `my_server_searchmy_server_search`. This causes a silent failure because the doubled name does not match any key in `tools_dict`, so `tool_function_name in tools` evaluates to `False` and the tool is never executed. The user sees "Tool Executed" in the UI but the tool was never actually called — a silent failure with no error message. ### Root Cause In `backend/open_webui/utils/middleware.py`, the streaming tool call delta handler accumulates the function name with `+=`: ```python # ~line 3718 (current dev branch) if delta_name: current_response_tool_call["function"]["name"] += delta_name ``` The first delta for a tool call index creates the entry with `name = "my_server_search"` (via `response_tool_calls.append(delta_tool_call)` at line ~3704). When a subsequent delta for the **same index** also includes `function.name = "my_server_search"`, the `+=` operator concatenates it: `"my_server_search" + "my_server_search"` = `"my_server_searchmy_server_search"`. This is model-dependent. Some API providers (notably GPT-5 and GPT-5.1 via Azure) redundantly include the function name in follow-up deltas, while others (Claude, GPT-4o) only send it in the first delta. **Arguments** are correctly accumulated with `+=` because they are genuinely split across deltas. But function **names** are always sent complete in the first delta per the OpenAI streaming spec — they should not be accumulated. ### Steps to Reproduce 1. Configure an MCP tool server (or any tool) with native function calling enabled 2. Use a model whose streaming API re-sends `function.name` in follow-up delta chunks (GPT-5 or GPT-5.1 via Azure OpenAI) 3. Ask the model to call a tool 4. Observe in stored chat data or debug logs that the tool name is doubled (e.g., `my_server_searchmy_server_search` instead of `my_server_search`) 5. The tool silently fails — "Tool Executed" appears in the UI but the tool was never actually called The bug does **not** reproduce with Claude models or GPT-4o because their APIs do not re-send the function name in subsequent deltas. ### Data from Production Database Query across 18 chats containing MCP tool calls shows a clear model-specific pattern: | Model | Tool Name Stored | Doubled? | |-------|-----------------|----------| | `gpt-5.1` (2 chats) | `<name><name>` | Always | | `gpt-5` (4 chats) | Mixed | Sometimes | | `gpt-4o` (2 chats) | `<name>` | Never | | `claude-*` (10 chats) | `<name>` | Never | ### Proposed Fix Change the name accumulation to a conditional assignment — only set the name if it has not been set yet: ```python # Before (bug): if delta_name: current_response_tool_call["function"]["name"] += delta_name # After (fix): if delta_name and not current_response_tool_call["function"].get("name"): current_response_tool_call["function"]["name"] = delta_name ``` This is safe because: - If the name arrives in the first delta (normal case): it is already set when the entry is created via `append()`, so the conditional does not overwrite it - If the name is absent from the first delta (edge case): the `setdefault("name", "")` at entry creation sets it to `""`, which is falsy, so the subsequent delta correctly sets it - If the name is re-sent in a follow-up delta (GPT-5/5.1 behavior): the conditional skips it because the name is already set ### Related Issues - #16138 — "tools names are doubled when calling" (closed as "intended behaviour", but the root cause is this bug) - #19656 — "Tool call response tokens are duplicated" (different manifestation of duplication in middleware.py, fixed by commit 52ccab8) ### Logs & Screenshots _No response_ ### Additional Information This bug affects any tool (MCP or otherwise) when used with native function calling and a model whose streaming API includes `function.name` in multiple delta chunks. It is not specific to MCP or any particular tool — it is in the generic streaming tool call accumulation logic.
Author
Owner

@Classic298 commented on GitHub (Mar 3, 2026):

is this reproducible on latest? i cant reproduce with gpt 5.2

<!-- gh-comment-id:3989949323 --> @Classic298 commented on GitHub (Mar 3, 2026): is this reproducible on latest? i cant reproduce with gpt 5.2
Author
Owner

@madnight commented on GitHub (Mar 3, 2026):

@Classic298 I currently do not have access to GPT 5.2 for testing, but I am able to consistently reproduce the issue with GPT 5.1 from Azure (via LiteLLM) and the Atlassian MCP Server (Rovo), which is configured using OAuth 2.1 and DCR (https://mcp.atlassian.com/v1/mcp). Although the Issue is independent of the concrete MCP Tool Server.

I'm also not sure why @tjbck said in https://github.com/open-webui/open-webui/issues/16138 that double naming is "Intended behaviour for external tool servers". The problem is that, even if this were intended behavior, the tool calling simply does not work in this case, so it is not just a naming issue.

<!-- gh-comment-id:3990005965 --> @madnight commented on GitHub (Mar 3, 2026): @Classic298 I currently do not have access to GPT 5.2 for testing, but I am able to consistently reproduce the issue with GPT 5.1 from Azure (via LiteLLM) and the Atlassian MCP Server (Rovo), which is configured using OAuth 2.1 and DCR (https://mcp.atlassian.com/v1/mcp). Although the Issue is independent of the concrete MCP Tool Server. I'm also not sure why @tjbck said in https://github.com/open-webui/open-webui/issues/16138 that double naming is "Intended behaviour for external tool servers". The problem is that, even if this were intended behavior, the tool calling simply does not work in this case, so it is not just a naming issue.
Author
Owner

@madnight commented on GitHub (Mar 3, 2026):

I just noticed that the Code:

if delta_name:
    current_response_tool_call["function"]["name"] += delta_name

Under certain conditions, can also generate quadruple names.

Image

I think the doubling pattern is like so:

<name><name>
<name><name><name><name>
<name><name><name><name><name><name><name><name>

Whereas longer names are increasingly rare.

<!-- gh-comment-id:3990326989 --> @madnight commented on GitHub (Mar 3, 2026): I just noticed that the Code: ```python if delta_name: current_response_tool_call["function"]["name"] += delta_name ``` Under certain conditions, can also generate quadruple names. <img width="1344" height="300" alt="Image" src="https://github.com/user-attachments/assets/c48d838c-54e4-4a1c-99b2-9ab711fcd9ce" /> I think the doubling pattern is like so: ``` <name><name> <name><name><name><name> <name><name><name><name><name><name><name><name> ``` Whereas longer names are increasingly rare.
Author
Owner

@theepicsaxguy commented on GitHub (Mar 4, 2026):

I also experience this with 5.2/5.2-codex and 5.3-codex.

Models from other providers works fine. but OpenAI models specifically seems to have this issue.

<!-- gh-comment-id:3996192706 --> @theepicsaxguy commented on GitHub (Mar 4, 2026): I also experience this with 5.2/5.2-codex and 5.3-codex. Models from other providers works fine. but OpenAI models specifically seems to have this issue.
Author
Owner

@theepicsaxguy commented on GitHub (Mar 4, 2026):

A temporary Filter workaround I managed to get working with GPT 5.3-codex. @madnight

from pydantic import BaseModel
from typing import Optional


class Filter:
    class Valves(BaseModel):
        pass

    def __init__(self):
        self.valves = self.Valves()
        self._seen = {}  # call_id -> name

    def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
        self._seen = {}
        return body

    async def stream(self, event: dict) -> dict:
        choices = event.get("choices") or []
        for choice in choices:
            delta = choice.get("delta") or {}
            tool_calls = delta.get("tool_calls") or []
            for tc in tool_calls:
                fn = tc.get("function")
                if not fn:
                    continue
                name = fn.get("name")
                if not name:
                    continue
                call_id = tc.get("id")
                idx = tc.get("index")
                print(f"[tools-dedupe] chunk: id={call_id} idx={idx} name={name}")
                if call_id in self._seen:
                    print(f"[tools-dedupe] DROP id={call_id} idx={idx} name={name}")
                    fn["name"] = ""
                else:
                    self._seen[call_id] = name
                    print(f"[tools-dedupe] KEEP id={call_id} idx={idx} name={name}")
        return event

    def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
        return body
<!-- gh-comment-id:3997137272 --> @theepicsaxguy commented on GitHub (Mar 4, 2026): A temporary Filter workaround I managed to get working with GPT 5.3-codex. @madnight ```` from pydantic import BaseModel from typing import Optional class Filter: class Valves(BaseModel): pass def __init__(self): self.valves = self.Valves() self._seen = {} # call_id -> name def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict: self._seen = {} return body async def stream(self, event: dict) -> dict: choices = event.get("choices") or [] for choice in choices: delta = choice.get("delta") or {} tool_calls = delta.get("tool_calls") or [] for tc in tool_calls: fn = tc.get("function") if not fn: continue name = fn.get("name") if not name: continue call_id = tc.get("id") idx = tc.get("index") print(f"[tools-dedupe] chunk: id={call_id} idx={idx} name={name}") if call_id in self._seen: print(f"[tools-dedupe] DROP id={call_id} idx={idx} name={name}") fn["name"] = "" else: self._seen[call_id] = name print(f"[tools-dedupe] KEEP id={call_id} idx={idx} name={name}") return event def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict: return body ````
Author
Owner

@Classic298 commented on GitHub (Mar 4, 2026):

hey guys

can you all please test this?

https://github.com/open-webui/open-webui/pull/22235

and also test for regressions, but there should be none.

I also researched about how providers handle these things and it seems like the OpenAI spec demands the function call to be sent AS ONE even if longer - so the function call is ONE delta. Any provider not doing that correctly is violating the spec, but all are doing it correctly as far as i can tell

so yeah please go ahead and test it @theepicsaxguy @madnight

we need confirmation here and also ideally tested on multiple models and providers

thanks!

<!-- gh-comment-id:3999011317 --> @Classic298 commented on GitHub (Mar 4, 2026): hey guys can you all please test this? https://github.com/open-webui/open-webui/pull/22235 and also test for regressions, but there should be none. I also researched about how providers handle these things and it seems like the OpenAI spec demands the function call to be sent AS ONE even if longer - so the function call is ONE delta. Any provider not doing that correctly is violating the spec, but all are doing it correctly as far as i can tell so yeah please go ahead and test it @theepicsaxguy @madnight we need confirmation here and also ideally tested on multiple models and providers thanks!
Author
Owner

@Classic298 commented on GitHub (Mar 6, 2026):

i can reproduce this now also on gpt 5.4

<!-- gh-comment-id:4012363594 --> @Classic298 commented on GitHub (Mar 6, 2026): i can reproduce this now also on gpt 5.4
Author
Owner

@Classic298 commented on GitHub (Mar 6, 2026):

Tested my PR in production. Works perfectly.

<!-- gh-comment-id:4012406013 --> @Classic298 commented on GitHub (Mar 6, 2026): Tested my PR in production. Works perfectly.
Author
Owner

@Classic298 commented on GitHub (Mar 6, 2026):

observed new and very weird behaviour with gpt-5.4 via api

i have expanded the PR to catch this weird behaviour - works so far.

<!-- gh-comment-id:4012696377 --> @Classic298 commented on GitHub (Mar 6, 2026): observed new and very weird behaviour with gpt-5.4 via api i have expanded the PR to catch this weird behaviour - works so far.
Author
Owner

@Classic298 commented on GitHub (Mar 8, 2026):

should be fixed by d7efdcce2b

Testing wanted

<!-- gh-comment-id:4017853889 --> @Classic298 commented on GitHub (Mar 8, 2026): should be fixed by https://github.com/open-webui/open-webui/commit/d7efdcce2b1cdbe1637a469294bf9d52dbacab53 Testing wanted
Author
Owner

@Classic298 commented on GitHub (Mar 8, 2026):

459a60a242

<!-- gh-comment-id:4017857527 --> @Classic298 commented on GitHub (Mar 8, 2026): https://github.com/open-webui/open-webui/commit/459a60a24240eab33441ed50f4f68cc27e65a037
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/open-webui#58315