From 484fb6174381ccf0d8d7d11780166027fe7576df Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 24 Jul 2026 01:17:41 +0200 Subject: [PATCH] perf: make streamed content accumulation genuinely linear (#27359) The streaming handler's content accumulator is a closure cell (declared nonlocal in stream_body_handler), and CPython's in-place string append optimization only applies to plain local variables (STORE_FAST), never to cell variables (STORE_DEREF). The content += value form introduced in #27231 therefore still allocates and copies the full accumulated string on every delta, exactly like the f-string it replaced; whether that copy is cheap or expensive is up to the allocator, and measurements swing accordingly (6 to 83 ms of pure copying for a 400 KB response on Python 3.12, against 0.3 ms for this patch). Accumulate the deltas in a list instead and join once at the single read site (publish_chat_finished_event at stream end). List append is amortized O(1) with no dependence on reference counts, bytecode specialization or allocator behaviour, so accumulation is O(n) by construction. The non-str fallback keeps the previous f-string coercion semantics. Verified end to end against a mock SSE upstream: a streamed chat with a think-tag block plus 40 content deltas produces output items, message text, reasoning text and usage identical to current dev, with no errors in the server log. --- backend/open_webui/utils/middleware.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 319407e4cf..5544888813 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -3974,9 +3974,10 @@ async def streaming_chat_response_handler(response, ctx): except Exception as e: pass - content = ( + initial_content = ( message.get('content', '') if message else last_assistant_message if last_assistant_message else '' ) + content_parts = [initial_content] if initial_content else [] # Initialize output: use existing from message if continuing, else create new existing_output = message.get('output') if message else None @@ -3984,14 +3985,14 @@ async def streaming_chat_response_handler(response, ctx): output = existing_output else: # Only create an initial message item if there is content to initialize with - if content: + if initial_content: output = [ { 'type': 'message', 'id': output_id('msg'), 'status': 'in_progress', 'role': 'assistant', - 'content': [{'type': 'output_text', 'text': content}], + 'content': [{'type': 'output_text', 'text': initial_content}], } ] else: @@ -4053,7 +4054,7 @@ async def streaming_chat_response_handler(response, ctx): ) async def stream_body_handler(response, form_data): - nonlocal content + nonlocal content_parts nonlocal usage nonlocal output nonlocal prior_output @@ -4504,12 +4505,8 @@ async def streaming_chat_response_handler(response, ctx): user, ) - if isinstance(value, str): - # In-place append — avoids copying the full - # accumulated response on every chunk. - content += value - else: - content = f'{content}{value}' + # 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}') # Check if we're inside a tag-based block # (reasoning, code_interpreter, or solution). @@ -5374,7 +5371,7 @@ async def streaming_chat_response_handler(response, ctx): {'done': True}, ) - await publish_chat_finished_event(request, user, metadata, title, content, output) + await publish_chat_finished_event(request, user, metadata, title, ''.join(content_parts), output) await event_emitter( {