From ac091273b7133299ff5e8bfa3b132a5cc15fe808 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 23 Aug 2026 21:35:53 +0200 Subject: [PATCH] fix: keep streamed text when a filter or provider sends non-string content (#28840) A stream filter function, or a provider that puts something other than a string in a delta, makes the streaming handler concatenate a string with a non-string. That raises TypeError, and the broad handler wrapped around the whole per-chunk block swallows it at debug level and moves on. The chunk's text never reaches the message the user sees, and nothing above debug level says why. The content and reasoning fields are now coerced to text once, where they are read off the delta, ahead of every consumer. The coercion is guarded on truthiness, so falsy values such as an empty list still skip the block exactly as before, and the accumulated content receives byte for byte what it received previously. Checked against 14 delta shapes covering strings, empty values, numbers, booleans, None, lists, dicts and a content array: the truthiness gate and the accumulated content are identical before and after. --- backend/open_webui/utils/middleware.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 12584752b7..ebb51cb559 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -5064,13 +5064,18 @@ async def streaming_chat_response_handler(response, ctx): } ) + # content and reasoning deltas are raw JSON: a stream filter can make them any type value = delta.get('content') + if value and not isinstance(value, str): + value = f'{value}' reasoning_content = ( delta.get('reasoning_content') or delta.get('reasoning') or delta.get('thinking') ) + if reasoning_content and not isinstance(reasoning_content, str): + reasoning_content = f'{reasoning_content}' reasoning_details = get_reasoning_details(delta) reasoning_detail_items = ( [item for item in reasoning_details if isinstance(item, dict)] @@ -5201,7 +5206,7 @@ async def streaming_chat_response_handler(response, ctx): ) # closure-cell str += recopies per chunk; append + join once at read is O(n) - content_parts.append(value if isinstance(value, str) else f'{value}') + content_parts.append(value) # Check if we're inside a tag-based block # (reasoning, code_interpreter, or solution).