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):