mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-15 09:31:21 +00:00
perf: buffer the streamed response text and join it once
When a chat has API outlet filters configured the streaming handler keeps its own copy of the assistant message, and every delta rebuilt that copy by concatenating the whole text again. The cost of a delta grows with the text already accumulated, so a long response gets slower the longer it runs, and a provider that sends large batched chunks makes it much worse. Deltas are now buffered in a list and joined once, when the stream has finished and the message is handed to the outlet filters. The join reproduces what the concatenation did without assuming the deltas are text: a stream filter can make a delta any type, and two lists used to merge into a list, so a buffer that is not all strings is folded with the same `+`. The buffered parts are tracked as they are created, so nothing walks the output tree at the end and a malformed part from a provider cannot raise where it did not before. Replaying streams through both revisions with 400-character deltas, 10000 deltas fall from 2317ms to 22ms and 50000 from 66.7s to 154ms; with 6-character deltas, 100000 deltas fall from 551ms to 148ms. Per-delta cost stops growing with the accumulated text. Holding the deltas until the stream ends costs about 49 bytes per delta, so half a megabyte on a ten thousand delta response and five megabytes on a hundred thousand. The other streaming paths still concatenate and are left alone here.
This commit is contained in:
parent
7cf6051a74
commit
b8f9bdcd31
1 changed files with 33 additions and 2 deletions
|
|
@ -3364,7 +3364,13 @@ def update_assistant_message_from_stream(assistant_message, raw):
|
|||
def append_output_text(item, text):
|
||||
parts = item.setdefault('content', [])
|
||||
if parts and parts[-1].get('type') == 'output_text':
|
||||
parts[-1]['text'] += text
|
||||
part = parts[-1]
|
||||
buffer = part.get('_text_buffer')
|
||||
if buffer is None:
|
||||
buffer = []
|
||||
part['_text_buffer'] = buffer
|
||||
assistant_message.setdefault('_buffered_parts', []).append(part)
|
||||
buffer.append(text)
|
||||
else:
|
||||
parts.append({'type': 'output_text', 'text': text})
|
||||
|
||||
|
|
@ -3382,6 +3388,7 @@ def update_assistant_message_from_stream(assistant_message, raw):
|
|||
continue
|
||||
|
||||
if data.get('type', '').startswith('response.'):
|
||||
flush_buffered_parts(assistant_message)
|
||||
output, meta = handle_responses_streaming_event(data, assistant_message.get('output', []))
|
||||
if output:
|
||||
assistant_message['output'] = output
|
||||
|
|
@ -3439,7 +3446,30 @@ def update_assistant_message_from_stream(assistant_message, raw):
|
|||
|
||||
append_output_text(output[-1], content)
|
||||
|
||||
assistant_message['content'] = assistant_message.get('content', '') + content
|
||||
assistant_message.setdefault('_content_buffer', []).append(content)
|
||||
|
||||
|
||||
def join_buffered_text(text, buffer: list):
|
||||
# A stream filter can make a delta any type, and the concatenation this replaces merged non-strings too.
|
||||
if isinstance(text, str) and all(isinstance(chunk, str) for chunk in buffer):
|
||||
return text + ''.join(buffer)
|
||||
for chunk in buffer:
|
||||
text += chunk
|
||||
return text
|
||||
|
||||
|
||||
def flush_buffered_parts(assistant_message: dict) -> None:
|
||||
"""Write the buffered text back into the output parts holding it."""
|
||||
for part in assistant_message.pop('_buffered_parts', []):
|
||||
part['text'] = join_buffered_text(part['text'], part.pop('_text_buffer'))
|
||||
|
||||
|
||||
def finalize_assistant_message(assistant_message: dict) -> None:
|
||||
"""Join the text buffered by update_assistant_message_from_stream."""
|
||||
flush_buffered_parts(assistant_message)
|
||||
buffer = assistant_message.pop('_content_buffer', None)
|
||||
if buffer:
|
||||
assistant_message['content'] = join_buffered_text(assistant_message.get('content', ''), buffer)
|
||||
|
||||
|
||||
async def get_system_oauth_token(request, user):
|
||||
|
|
@ -6108,6 +6138,7 @@ async def streaming_chat_response_handler(response, ctx):
|
|||
yield data
|
||||
|
||||
if has_api_outlet_filters and assistant_message:
|
||||
finalize_assistant_message(assistant_message)
|
||||
ctx['assistant_message'] = assistant_message
|
||||
await outlet_filter_handler(ctx)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue