issue: Frontend Display Glitch: replace Event Causes Brief Flash and Revert of Streamed Content #5434

Closed
opened 2025-11-11 16:20:57 -06:00 by GiteaMirror · 4 comments
Owner

Originally created by @olivier-lacroix on GitHub (Jun 4, 2025).

Originally assigned to: @jackthgu on GitHub.

Check Existing Issues

  • I have searched the existing issues and discussions.
  • I am using the latest version of Open WebUI.

Installation Method

Docker

Open WebUI Version

v0.6.13

Ollama Version (if applicable)

N/A

Operating System

N/A

Browser (if applicable)

Firefox

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. My steps:
  • Start with the initial platform/version/OS and dependencies used,
  • Specify exact install/launch/configure commands,
  • List URLs visited, user input (incl. example values/emails/passwords if needed),
  • Describe all options and toggles enabled or changed,
  • Include any files or environmental changes,
  • Identify the expected and actual result at each stage,
  • Ensure any reasonably skilled user can follow and hit the same issue.

Expected Behavior

I think there are some sort of 'data race' when using replace event. The dummy pipe below reproduces what I have experienced in a 'real' pipe.

In that dummy pipe, I expect the initial streamed content ("This is the first part...", "This is the second part...", "This is the third part...") appear. After the pause, "This content has been replaced!" should appear and remain on screen, replacing the original streamed content.

Actual Behavior

The initial streamed content appears correctly. After the pause, "This content has been replaced!" briefly flashes on screen, but then the display immediately reverts to showing the original streamed content. The intended replacement is not sustained.

Steps to Reproduce

Create the following dummy pipeline

import asyncio
from typing import AsyncIterator, Callable, Dict, Any, List

class Pipe:
    """
    A dummy pipeline for testing async generator and replace events.
    """

    def __init__(self):
        self.name: str = "Dummy Pipe"

    def pipes(self) -> List[Dict[str, str]]:
        """
        Returns a list of dummy models for the UI.
        """
        return [{"id": "dummy-model", "name": "Dummy Model for Testing"}]

    async def _dummy_source_iterator(self) -> AsyncIterator[str]:
        """
        A dummy async iterator to simulate an external streaming source.
        """
        source_contents = [
            "This is the first part of the content. ",
            "This is the second part of the content. ",
            "This is the third part of the content.",
        ]
        for part in source_contents:
            yield part
            await asyncio.sleep(0.5) # Simulate external source delay

    async def _dummy_streaming_response(
        self,
        __event_emitter__: Callable,
    ) -> AsyncIterator[str]:
        """
        Internal async generator for streaming dummy content, consuming an async iterator.
        """
        async for content_part in self._dummy_source_iterator():
            yield content_part

        # Introduce a pause before sending the replace event
        await asyncio.sleep(2) # Or 20 seconds, as tested by user

        # Emit a replace event
        await __event_emitter__(
            {
                "type": "replace",
                "data": {
                    "content": "This content has been replaced!",
                },
            }
        )

    async def pipe(
        self,
        body: Dict[str, Any],
        __event_emitter__: Callable,
        __metadata__: Dict[str, Any],
        __tools__: Dict[str, Any] | None,
    ) -> AsyncIterator[str]:
        """
        This dummy pipe returns an async generator and emits a replace event.
        """
        return self._dummy_streaming_response(__event_emitter__)

Logs & Screenshots

Docker logs did not seem to surface anything useful.

Relevant browser logs (screenshot, as, for the life of me, could not figure out how to export logs with objects expanded in firefox)

Image

Additional Information

No response

Originally created by @olivier-lacroix on GitHub (Jun 4, 2025). Originally assigned to: @jackthgu on GitHub. ### Check Existing Issues - [x] I have searched the existing issues and discussions. - [x] I am using the latest version of Open WebUI. ### Installation Method Docker ### Open WebUI Version v0.6.13 ### Ollama Version (if applicable) N/A ### Operating System N/A ### Browser (if applicable) Firefox ### 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**. My steps: - Start with the initial platform/version/OS and dependencies used, - Specify exact install/launch/configure commands, - List URLs visited, user input (incl. example values/emails/passwords if needed), - Describe all options and toggles enabled or changed, - Include any files or environmental changes, - Identify the expected and actual result at each stage, - Ensure any reasonably skilled user can follow and hit the same issue. ### Expected Behavior I think there are some sort of 'data race' when using `replace` event. The dummy pipe below reproduces what I have experienced in a 'real' pipe. In that dummy pipe, I expect the initial streamed content ("This is the first part...", "This is the second part...", "This is the third part...") appear. After the pause, "This content has been replaced!" should appear and remain on screen, replacing the original streamed content. ### Actual Behavior The initial streamed content appears correctly. After the pause, "This content has been replaced!" briefly flashes on screen, but then the display immediately reverts to showing the original streamed content. The intended replacement is not sustained. ### Steps to Reproduce Create the following dummy pipeline ```python import asyncio from typing import AsyncIterator, Callable, Dict, Any, List class Pipe: """ A dummy pipeline for testing async generator and replace events. """ def __init__(self): self.name: str = "Dummy Pipe" def pipes(self) -> List[Dict[str, str]]: """ Returns a list of dummy models for the UI. """ return [{"id": "dummy-model", "name": "Dummy Model for Testing"}] async def _dummy_source_iterator(self) -> AsyncIterator[str]: """ A dummy async iterator to simulate an external streaming source. """ source_contents = [ "This is the first part of the content. ", "This is the second part of the content. ", "This is the third part of the content.", ] for part in source_contents: yield part await asyncio.sleep(0.5) # Simulate external source delay async def _dummy_streaming_response( self, __event_emitter__: Callable, ) -> AsyncIterator[str]: """ Internal async generator for streaming dummy content, consuming an async iterator. """ async for content_part in self._dummy_source_iterator(): yield content_part # Introduce a pause before sending the replace event await asyncio.sleep(2) # Or 20 seconds, as tested by user # Emit a replace event await __event_emitter__( { "type": "replace", "data": { "content": "This content has been replaced!", }, } ) async def pipe( self, body: Dict[str, Any], __event_emitter__: Callable, __metadata__: Dict[str, Any], __tools__: Dict[str, Any] | None, ) -> AsyncIterator[str]: """ This dummy pipe returns an async generator and emits a replace event. """ return self._dummy_streaming_response(__event_emitter__) ``` ### Logs & Screenshots Docker logs did not seem to surface anything useful. Relevant browser logs (screenshot, as, for the life of me, could not figure out how to export logs with objects expanded in firefox) ![Image](https://github.com/user-attachments/assets/124b4daf-af17-4cda-bdeb-bcce55de7289) ### Additional Information _No response_
GiteaMirror added the bug label 2025-11-11 16:20:57 -06:00
Author
Owner

@jackthgu commented on GitHub (Jun 5, 2025):

Hello,
First of all, thank you for writing such a precise piece of code to test and point out this potential issue.
The cause of this behavior is that the replace event is triggered twice — this happens because the title also performs a replace operation.
To avoid this issue, you can try modifying the code as shown below.

Also, please pay attention to the task parameter in the pipe function — it plays a key role in how the behavior is handled depending on the request context.

import asyncio
from typing import AsyncIterator, Callable, Dict, Any, List, Optional


class Pipe:
    """
    A dummy pipeline for testing async generator and replace events.
    """

    def __init__(self):
        self.name: str = "Dummy Pipe"

    def pipes(self) -> List[Dict[str, str]]:
        """
        Returns a list of dummy models for the UI.
        """
        return [{"id": "dummy-model", "name": "Dummy Model for Testing"}]

    async def _dummy_source_iterator(self) -> AsyncIterator[str]:
        """
        A dummy async iterator to simulate an external streaming source.
        """
        source_contents = [
            "This is the first part of the content. ",
            "This is the second part of the content. ",
            "This is the third part of the content.",
        ]
        for part in source_contents:
            yield part
            await asyncio.sleep(0.5)  # Simulate external source delay

    async def _dummy_streaming_response(
        self,
        __event_emitter__: Callable,
    ):
        """
        Internal async generator for streaming dummy content, consuming an async iterator.
        """

        print("_dummy_streaming_response")
        async for content_part in self._dummy_source_iterator():
            await __event_emitter__(
                {
                    "type": "chat:message:delta",
                    "data": {
                        "content": content_part,
                    },
                }
            )

        # Introduce a pause before sending the replace event
        await asyncio.sleep(2)  # Or 20 seconds, as tested by user

        # Emit a replace event
        await __event_emitter__(
            {
                "type": "replace",
                "data": {
                    "content": "This content has been replaced!",
                },
            }
        )

        return ""

    async def pipe(
        self,
        body: Dict[str, Any],
        __task__: Optional[str],
        __event_emitter__: Callable,
        __metadata__: Dict[str, Any],
        __tools__: Dict[str, Any] | None,
    ) -> str:
        """
        This dummy pipe returns an async generator and emits a replace event.
        """

        if __task__ is None:
            return await self._dummy_streaming_response(__event_emitter__)
        else:
            return "task return value"

Since it works well in our environment, we’ll go ahead and close this issue.
If the result is not as expected or if you encounter any other issues, please feel free to let us know anytime.
Thank you!

@jackthgu commented on GitHub (Jun 5, 2025): Hello, First of all, thank you for writing such a precise piece of code to test and point out this potential issue. The cause of this behavior is that the replace event is triggered twice — this happens because the title also performs a replace operation. To avoid this issue, you can try modifying the code as shown below. Also, please pay attention to the __task__ parameter in the pipe function — it plays a key role in how the behavior is handled depending on the request context. ``` import asyncio from typing import AsyncIterator, Callable, Dict, Any, List, Optional class Pipe: """ A dummy pipeline for testing async generator and replace events. """ def __init__(self): self.name: str = "Dummy Pipe" def pipes(self) -> List[Dict[str, str]]: """ Returns a list of dummy models for the UI. """ return [{"id": "dummy-model", "name": "Dummy Model for Testing"}] async def _dummy_source_iterator(self) -> AsyncIterator[str]: """ A dummy async iterator to simulate an external streaming source. """ source_contents = [ "This is the first part of the content. ", "This is the second part of the content. ", "This is the third part of the content.", ] for part in source_contents: yield part await asyncio.sleep(0.5) # Simulate external source delay async def _dummy_streaming_response( self, __event_emitter__: Callable, ): """ Internal async generator for streaming dummy content, consuming an async iterator. """ print("_dummy_streaming_response") async for content_part in self._dummy_source_iterator(): await __event_emitter__( { "type": "chat:message:delta", "data": { "content": content_part, }, } ) # Introduce a pause before sending the replace event await asyncio.sleep(2) # Or 20 seconds, as tested by user # Emit a replace event await __event_emitter__( { "type": "replace", "data": { "content": "This content has been replaced!", }, } ) return "" async def pipe( self, body: Dict[str, Any], __task__: Optional[str], __event_emitter__: Callable, __metadata__: Dict[str, Any], __tools__: Dict[str, Any] | None, ) -> str: """ This dummy pipe returns an async generator and emits a replace event. """ if __task__ is None: return await self._dummy_streaming_response(__event_emitter__) else: return "task return value" ``` Since it works well in our environment, we’ll go ahead and close this issue. If the result is not as expected or if you encounter any other issues, please feel free to let us know anytime. Thank you!
Author
Owner

@RAPHCVR commented on GitHub (Jun 11, 2025):

Hey @jackthgu, thanks for the tip on using the event-based approach! It successfully resolved the initial "flash/revert" issue.

However, I've encountered a small follow-up problem. It seems the UI handles streamed content from yield differently than content from a chat:message:delta event.

When using yield, the <thinking> tags are correctly interpreted by the UI to show the thinking visualization. But when I send the same <thinking> tags inside a chat:message:delta event, they are printed literally as text to the screen.

The issue: The event-based method prints <thinking> tags as-is.

This code sends the literal string "<thinking>" to the UI, which is not what's intended.

# In my new event-based code
if not is_in_thinking_block:
    is_in_thinking_block = True
    # This renders the tag as plain text
    await __event_emitter__(
        {
            "type": "chat:message:delta",
            "data": {"content": "<thinking>"},
        }
    )

In a previous version using yield, this worked correctly:

# In my old yield-based code
if not is_in_thinking_block:
    is_in_thinking_block = True
    # This correctly triggered the UI visualization
    yield "<thinking>"

So what would be the correct event to send via __event_emitter__ to toggle the "thinking" UI state, instead of just printing the tag? I suspect there might be a dedicated event type for this, like {"type": "thinking", "data": ...}.

Thanks for your help

@RAPHCVR commented on GitHub (Jun 11, 2025): Hey @jackthgu, thanks for the tip on using the event-based approach! It successfully resolved the initial "flash/revert" issue. However, I've encountered a small follow-up problem. It seems the UI handles streamed content from `yield` differently than content from a `chat:message:delta` event. When using `yield`, the `<thinking>` tags are correctly interpreted by the UI to show the thinking visualization. But when I send the same `<thinking>` tags inside a `chat:message:delta` event, they are printed literally as text to the screen. **The issue:** The event-based method prints `<thinking>` tags as-is. This code sends the literal string `"<thinking>"` to the UI, which is not what's intended. ```python # In my new event-based code if not is_in_thinking_block: is_in_thinking_block = True # This renders the tag as plain text await __event_emitter__( { "type": "chat:message:delta", "data": {"content": "<thinking>"}, } ) ``` In a previous version using `yield`, this worked correctly: ```python # In my old yield-based code if not is_in_thinking_block: is_in_thinking_block = True # This correctly triggered the UI visualization yield "<thinking>" ``` So what would be the correct event to send via `__event_emitter__` to toggle the "thinking" UI state, instead of just printing the tag? I suspect there might be a dedicated event type for this, like `{"type": "thinking", "data": ...}`. Thanks for your help
Author
Owner

@olivier-lacroix commented on GitHub (Jun 20, 2025):

Thanks a lot @jackthgu ! in the actual pipeline, switching from yield to the chat:message:delta works. It is also faster :)

Looking at the console, I can see the complete content is resent each time with the original code. I am surprised both forms are not equivalent. Shouldn't openwebui send the delta only when using yield?

@olivier-lacroix commented on GitHub (Jun 20, 2025): Thanks a lot @jackthgu ! in the actual pipeline, switching from `yield` to the `chat:message:delta` works. It is also faster :) Looking at the console, I can see the complete content is resent each time with the original code. I am surprised both forms are not equivalent. Shouldn't openwebui send the delta only when using yield?
Author
Owner

@HEI204 commented on GitHub (Aug 15, 2025):

Hello, First of all, thank you for writing such a precise piece of code to test and point out this potential issue. The cause of this behavior is that the replace event is triggered twice — this happens because the title also performs a replace operation. To avoid this issue, you can try modifying the code as shown below.

Also, please pay attention to the task parameter in the pipe function — it plays a key role in how the behavior is handled depending on the request context.

import asyncio
from typing import AsyncIterator, Callable, Dict, Any, List, Optional


class Pipe:
    """
    A dummy pipeline for testing async generator and replace events.
    """

    def __init__(self):
        self.name: str = "Dummy Pipe"

    def pipes(self) -> List[Dict[str, str]]:
        """
        Returns a list of dummy models for the UI.
        """
        return [{"id": "dummy-model", "name": "Dummy Model for Testing"}]

    async def _dummy_source_iterator(self) -> AsyncIterator[str]:
        """
        A dummy async iterator to simulate an external streaming source.
        """
        source_contents = [
            "This is the first part of the content. ",
            "This is the second part of the content. ",
            "This is the third part of the content.",
        ]
        for part in source_contents:
            yield part
            await asyncio.sleep(0.5)  # Simulate external source delay

    async def _dummy_streaming_response(
        self,
        __event_emitter__: Callable,
    ):
        """
        Internal async generator for streaming dummy content, consuming an async iterator.
        """

        print("_dummy_streaming_response")
        async for content_part in self._dummy_source_iterator():
            await __event_emitter__(
                {
                    "type": "chat:message:delta",
                    "data": {
                        "content": content_part,
                    },
                }
            )

        # Introduce a pause before sending the replace event
        await asyncio.sleep(2)  # Or 20 seconds, as tested by user

        # Emit a replace event
        await __event_emitter__(
            {
                "type": "replace",
                "data": {
                    "content": "This content has been replaced!",
                },
            }
        )

        return ""

    async def pipe(
        self,
        body: Dict[str, Any],
        __task__: Optional[str],
        __event_emitter__: Callable,
        __metadata__: Dict[str, Any],
        __tools__: Dict[str, Any] | None,
    ) -> str:
        """
        This dummy pipe returns an async generator and emits a replace event.
        """

        if __task__ is None:
            return await self._dummy_streaming_response(__event_emitter__)
        else:
            return "task return value"

Since it works well in our environment, we’ll go ahead and close this issue. If the result is not as expected or if you encounter any other issues, please feel free to let us know anytime. Thank you!

First of all, thank you for writing such a precise piece of code to test and point out this potential issue.
The cause of this behavior is that the replace event is triggered twice

I have similar issue but I using Pipeline class, is any solution to solve?

Since pipeline class do not have the attribute "__task""

@HEI204 commented on GitHub (Aug 15, 2025): > Hello, First of all, thank you for writing such a precise piece of code to test and point out this potential issue. The cause of this behavior is that the replace event is triggered twice — this happens because the title also performs a replace operation. To avoid this issue, you can try modifying the code as shown below. > > Also, please pay attention to the **task** parameter in the pipe function — it plays a key role in how the behavior is handled depending on the request context. > > ``` > import asyncio > from typing import AsyncIterator, Callable, Dict, Any, List, Optional > > > class Pipe: > """ > A dummy pipeline for testing async generator and replace events. > """ > > def __init__(self): > self.name: str = "Dummy Pipe" > > def pipes(self) -> List[Dict[str, str]]: > """ > Returns a list of dummy models for the UI. > """ > return [{"id": "dummy-model", "name": "Dummy Model for Testing"}] > > async def _dummy_source_iterator(self) -> AsyncIterator[str]: > """ > A dummy async iterator to simulate an external streaming source. > """ > source_contents = [ > "This is the first part of the content. ", > "This is the second part of the content. ", > "This is the third part of the content.", > ] > for part in source_contents: > yield part > await asyncio.sleep(0.5) # Simulate external source delay > > async def _dummy_streaming_response( > self, > __event_emitter__: Callable, > ): > """ > Internal async generator for streaming dummy content, consuming an async iterator. > """ > > print("_dummy_streaming_response") > async for content_part in self._dummy_source_iterator(): > await __event_emitter__( > { > "type": "chat:message:delta", > "data": { > "content": content_part, > }, > } > ) > > # Introduce a pause before sending the replace event > await asyncio.sleep(2) # Or 20 seconds, as tested by user > > # Emit a replace event > await __event_emitter__( > { > "type": "replace", > "data": { > "content": "This content has been replaced!", > }, > } > ) > > return "" > > async def pipe( > self, > body: Dict[str, Any], > __task__: Optional[str], > __event_emitter__: Callable, > __metadata__: Dict[str, Any], > __tools__: Dict[str, Any] | None, > ) -> str: > """ > This dummy pipe returns an async generator and emits a replace event. > """ > > if __task__ is None: > return await self._dummy_streaming_response(__event_emitter__) > else: > return "task return value" > ``` > > Since it works well in our environment, we’ll go ahead and close this issue. If the result is not as expected or if you encounter any other issues, please feel free to let us know anytime. Thank you! > First of all, thank you for writing such a precise piece of code to test and point out this potential issue. > The cause of this behavior is that the replace event is triggered twice I have similar issue but I using Pipeline class, is any solution to solve? Since pipeline class do not have the attribute "__task""
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/open-webui#5434