From 64ff29df4d4fc1172765fe43cbfa1857578625f5 Mon Sep 17 00:00:00 2001
From: DrMelone <27028174+Classic298@users.noreply.github.com>
Date: Fri, 3 Apr 2026 21:27:10 +0200
Subject: [PATCH] =?UTF-8?q?refactor(middleware):=20optimize=20fence-stripp?=
=?UTF-8?q?ing=20O(n=C2=B2)=20=E2=86=92=20O(n)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replace repeated string concatenation in serialize_output with
list-append + single join, eliminating quadratic copying.
Inner accumulators (reasoning_content, result_text) receive the
same treatment. Fence-stripping for code_interpreter is rewritten
to operate on the parts list instead of the monolithic string.
Co-authored-by: alifurkanstahl <180474740+alifurkanstahl@users.noreply.github.com>
---
backend/open_webui/utils/middleware.py | 84 ++++++++++++++++----------
1 file changed, 51 insertions(+), 33 deletions(-)
diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py
index fb4912bef5..9ab5c18b74 100644
--- a/backend/open_webui/utils/middleware.py
+++ b/backend/open_webui/utils/middleware.py
@@ -389,16 +389,15 @@ def get_citation_source_from_tool_result(
]
-def split_content_and_whitespace(content):
- content_stripped = content.rstrip()
- original_whitespace = content[len(content_stripped) :] if len(content) > len(content_stripped) else ''
- return content_stripped, original_whitespace
+def content_endswith_newline(parts):
+ if not parts:
+ return False
+ return parts[-1].endswith('\n')
-def is_opening_code_block(content):
- backtick_segments = content.split('```')
- # Even number of segments means the last backticks are opening a new block
- return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0
+def ensure_trailing_newline(parts):
+ if parts and not content_endswith_newline(parts):
+ parts.append('\n')
def serialize_output(output: list) -> str:
@@ -406,7 +405,13 @@ def serialize_output(output: list) -> str:
Convert OR-aligned output items to HTML for display.
For LLM consumption, use convert_output_to_messages() instead.
"""
- content = ''
+ content_parts = []
+ fence_count = 0
+
+ def append_part(part):
+ nonlocal fence_count
+ content_parts.append(part)
+ fence_count += part.count('```')
# First pass: collect function_call_output items by call_id for lookup
tool_outputs = {}
@@ -423,12 +428,12 @@ def serialize_output(output: list) -> str:
if 'text' in content_part:
text = content_part.get('text', '').strip()
if text:
- content = f'{content}{text}\n'
+ append_part(text)
+ append_part('\n')
elif item_type == 'function_call':
# Render tool call inline with its result (if available)
- if content and not content.endswith('\n'):
- content += '\n'
+ ensure_trailing_newline(content_parts)
call_id = item.get('call_id', '')
name = item.get('name', '')
@@ -436,33 +441,34 @@ def serialize_output(output: list) -> str:
result_item = tool_outputs.get(call_id)
if result_item:
- result_text = ''
+ result_text_parts = []
for result_output in result_item.get('output', []):
if 'text' in result_output:
output_text = result_output.get('text', '')
- result_text += str(output_text) if not isinstance(output_text, str) else output_text
+ result_text_parts.append(str(output_text))
+ result_text = ''.join(result_text_parts)
files = result_item.get('files')
embeds = result_item.get('embeds', '')
- content += f'\nTool Executed
\n \n'
+ append_part(f'\nTool Executed
\n \n')
else:
- content += f'\nExecuting...
\n \n'
+ append_part(f'\nExecuting...
\n \n')
elif item_type == 'function_call_output':
# Already handled inline with function_call above
pass
elif item_type == 'reasoning':
- reasoning_content = ''
+ reasoning_parts = []
# Check for 'summary' (new structure) or 'content' (legacy/fallback)
source_list = item.get('summary', []) or item.get('content', [])
for content_part in source_list:
if 'text' in content_part:
- reasoning_content += content_part.get('text', '')
+ reasoning_parts.append(content_part.get('text', ''))
elif 'summary' in content_part: # Handle potential nested logic if any
pass
- reasoning_content = reasoning_content.strip()
+ reasoning_content = ''.join(reasoning_parts).strip()
duration = item.get('duration')
status = item.get('status', 'in_progress')
@@ -471,8 +477,7 @@ def serialize_output(output: list) -> str:
# render as done (a subsequent item means reasoning is complete)
is_last_item = idx == len(output) - 1
- if content and not content.endswith('\n'):
- content += '\n'
+ ensure_trailing_newline(content_parts)
display = html.escape(
'\n'.join(
@@ -481,19 +486,32 @@ def serialize_output(output: list) -> str:
)
if status == 'completed' or duration is not None or not is_last_item:
- content = f'{content}\nThought for {duration or 0} seconds
\n{display}\n \n'
+ append_part(f'\nThought for {duration or 0} seconds
\n{display}\n \n')
else:
- content = f'{content}\nThinking…
\n{display}\n \n'
+ append_part(f'\nThinking…
\n{display}\n \n')
elif item_type == 'open_webui:code_interpreter':
- content_stripped, original_whitespace = split_content_and_whitespace(content)
- if is_opening_code_block(content_stripped):
- content = content_stripped.rstrip('`').rstrip() + original_whitespace
- else:
- content = content_stripped + original_whitespace
+ # Check if previous content ends with an opening code fence (e.g. ```python)
+ # If so, strip the fence characters since code_interpreter replaces it
+ # Odd fence_count means an unclosed (opening) fence
+ if fence_count % 2 == 1:
+ for i in range(len(content_parts) - 1, -1, -1):
+ part = content_parts[i]
+ stripped = part.rstrip()
+ if stripped:
+ trailing_ws = part[len(stripped):]
+ new_stripped = re.sub(r'```[^\n]*$', '', stripped).rstrip()
+ new_part = new_stripped + trailing_ws
+ fence_count += new_part.count('```') - part.count('```')
+ content_parts[i] = new_part
+ break
- if content and not content.endswith('\n'):
- content += '\n'
+ # Remove trailing empty/whitespace-only parts left
+ # after fence removal to avoid extra blank lines.
+ while content_parts and not content_parts[-1].strip():
+ fence_count -= content_parts.pop().count('```')
+
+ ensure_trailing_newline(content_parts)
# Render the code_interpreter item as a block
# so the frontend Collapsible renders "Analyzing..."/"Analyzed".
@@ -519,11 +537,11 @@ def serialize_output(output: list) -> str:
output_attr = f' output="{html.escape(output_json)}"'
if status == 'completed' or duration is not None or not is_last_item:
- content += f'\nAnalyzed
\n{display}\n \n'
+ append_part(f'\nAnalyzed
\n{display}\n \n')
else:
- content += f'\nAnalyzing…
\n{display}\n \n'
+ append_part(f'\nAnalyzing…
\n{display}\n \n')
- return content.strip()
+ return ''.join(content_parts).strip()
def deep_merge(target, source):