open-webui/backend/open_webui/utils
Claude d7b36be54d
perf(stream): emit text tokens as deltas instead of re-serializing the whole chat per SSE event
The streaming hot path in `streaming_chat_response_handler.stream_body_handler`
was calling `serialize_output(full_output())` on every SSE event — rebuilding
an HTML string of the entire accumulated output (text + reasoning + tool calls
+ images + citations) and emitting it via Socket.IO.

For an N-token response this is O(N) work per token -> O(N^2) bytes across
the WebSocket, then amplified again by Socket.IO's AsyncRedisManager pub/sub
(x redis nodes x workers) and a third time by the frontend Markdown
re-parser. A 2000-token reply at normal scale could push ~10,000x more
bytes through Redis than the new tokens actually needed.

Fix: when an SSE event only appended text characters to the currently
active `message` block (the overwhelming common case), accumulate those
chars into a `pending_text_delta` buffer and emit them as a lightweight
`chat:message:delta` event. The frontend already handles that event via
string append (`Chat.svelte:472-473`), so no frontend changes are needed.

Any structural event - reasoning update, reasoning->message transition,
tool call, image attachment, tag-block append, annotation source, etc. -
still goes through the legacy full-serialize path, and when it does it
clears the pending text delta buffer (because the full-content checkpoint
already contains the accumulated text). Ordering is preserved by flushing
pending deltas before each structural emit.

The final `done: true` `chat:completion` emitted by the outer handler at
line 4793 overwrites `message.content` with the canonical
`serialize_output(output)`, reconciling the frontend to the exact backend
state at the end of every response.

Discovered and diagnosed by @Shirasawa; this is the Phase 1 minimum-viable
landing. Phase 2 (typed block model, per-block rendering, Redis op log
with resume-by-seq) is tracked as follow-up work.

Impact summary:
- Per-token WS payload: O(size_so_far) -> O(new_chars)
- Redis pub/sub bytes: drops by ~10-100x on long responses
- Frontend Markdown re-parse cost: now bounded by delta size per event,
  not full response size
- Semantics: identical. Final message content reconciles via the
  existing `done: true` full-content checkpoint.

Out of scope for this change:
- Reasoning-block content still takes the full-serialize path per token
  (Phase 2 will incrementalize it via per-block deltas)
- Tool-call argument streaming still takes the full-serialize path
- DB persistence path (REALTIME_CHAT_SAVE) is untouched — its WS path
  already emitted raw SSE deltas and is unchanged
2026-04-14 20:33:02 +00:00
..
access_control refac: async db 2026-04-12 14:22:11 -05:00
images refac 2026-03-17 17:58:01 -05:00
mcp refac: async db 2026-04-12 14:22:11 -05:00
telemetry refac: async db 2026-04-12 14:22:11 -05:00
actions.py refac: async db 2026-04-12 14:22:11 -05:00
anthropic.py refac 2026-04-13 14:51:09 -05:00
asgi_middleware.py fix(middleware): replace BaseHTTPMiddleware HTTP middlewares with pure ASGI implementations (#23709) 2026-04-14 10:47:48 -05:00
audit.py refac 2026-04-12 16:25:01 -05:00
auth.py chore: format 2026-04-12 18:12:59 -05:00
automations.py refac 2026-04-13 21:29:03 -05:00
channels.py refac 2026-03-17 17:58:01 -05:00
chat.py refac: async db 2026-04-12 14:22:11 -05:00
code_interpreter.py refac 2026-03-17 17:58:01 -05:00
embeddings.py refac: async db 2026-04-12 14:22:11 -05:00
files.py refac 2026-04-14 10:55:11 -05:00
filter.py refac: async db 2026-04-12 14:22:11 -05:00
groups.py refac: async db 2026-04-12 14:22:11 -05:00
headers.py refac 2026-03-17 17:58:01 -05:00
logger.py refac 2026-04-10 10:15:55 -07:00
middleware.py perf(stream): emit text tokens as deltas instead of re-serializing the whole chat per SSE event 2026-04-14 20:33:02 +00:00
misc.py refac 2026-04-14 01:17:39 -05:00
models.py refac: async db 2026-04-12 14:22:11 -05:00
oauth.py refac 2026-04-13 18:20:46 -05:00
payload.py refac 2026-03-24 04:49:48 -05:00
pdf_generator.py refac 2026-03-17 17:58:01 -05:00
plugin.py refac: async db 2026-04-12 14:22:11 -05:00
rate_limit.py refac 2026-03-17 17:58:01 -05:00
redis.py chore: format 2026-04-12 18:12:59 -05:00
response.py refac 2026-03-24 04:49:48 -05:00
sanitize.py refac 2026-03-17 17:58:01 -05:00
security_headers.py refac 2026-03-17 17:58:01 -05:00
session_pool.py refac 2026-04-14 01:17:39 -05:00
task.py fix: Fix the error that occurs when the task model does not exist (#23169) 2026-04-01 07:01:25 -05:00
tools.py chore: format 2026-04-12 22:11:10 -05:00
validate.py chore: format 2026-04-12 18:12:59 -05:00
webhook.py refac 2026-03-24 04:49:48 -05:00