An LLM response can be correct while the interface displaying it still feels broken. Text arrives in fragments. When the UI redraws in the middle of a word, the transport boundary leaks into the reading experience.

Provider chunks are delivery envelopes. Their boundaries describe transport behavior. My first choice is to append those chunks exactly as received. If the product still benefits from complete-word updates, I buffer at the presentation boundary and accept the added latency.

This walkthrough builds that buffer as a small Python generator using synthetic chunks. It preserves the incoming text. When the stream ends normally, it flushes the final fragment.

Separate text from chunk boundaries

Suppose a provider emits "Hel", "lo ", "wor", and "ld". Appending those strings produces "Hello world". If the UI displays "Hel lo wor ld", the renderer is inserting spaces between chunks. Fix the renderer first. Preserve the incoming text exactly before adding a buffer.

Word-aware buffering solves a narrower problem: delaying display until a whitespace boundary arrives. A reader sees fewer incomplete words. Every buffered character also waits longer. That tradeoff is most visible in long URLs, code, and languages that commonly omit spaces.

Emit whitespace-delimited chunks

The generator below accepts an iterable of decoded text strings. It emits application-level text events. Model tokenization is a separate layer. Whitespace stays exactly as it arrived, including repeated spaces, tabs, and newlines.

from collections.abc import Iterable, Iterator


def stream_text(parts: Iterable[str]) -> Iterator[dict[str, str]]:
    buffer: list[str] = []
    for part in parts:
        for char in part:
            buffer.append(char)
            if char.isspace():
                yield {"type": "text", "content": "".join(buffer)}
                buffer.clear()

    # A normal end of stream completes the final text fragment.
    if buffer:
        yield {"type": "text", "content": "".join(buffer)}


parts = ["Hel", "lo ", "wor", "ld"]
events = list(stream_text(parts))
print(events)
assert "".join(event["content"] for event in events) == "".join(parts)

The output is:

[{'type': 'text', 'content': 'Hello '}, {'type': 'text', 'content': 'world'}]

The final flush is required to emit "world". An empty stream yields no events. A whitespace-only stream retains all its whitespace.

Bound the wait and the buffer

Whitespace is a simple delimiter, with limited linguistic meaning. Languages that commonly omit spaces need a different segmentation rule. A URL or code fragment can remain buffered in full. Memory use grows with the longest stretch between whitespace characters.

For an interactive application, choose a maximum buffer length and a maximum wait time based on acceptable display latency. I would start with a length limit because its behavior is deterministic and easy to test. Add a time limit only when measurements show that idle gaps are holding text too long. A timer must run while the upstream source is idle. This synchronous generator has no timer. Either limit can produce partial words again.

This example handles normal completion only. If the upstream iterator raises an exception, the remaining buffer stays internal. Decide whether an interrupted response should expose that partial text and how the client will distinguish it from a completed response.

Integrate at the text layer

I keep this buffer at the text layer. Extract decoded text deltas from your provider’s events before calling it, and keep tool calls, usage metadata, and completion signals on their own paths. Byte decoding and transport framing also belong outside this helper.

At the client, append each content value exactly as received. For an asynchronous source, adapt the iteration and cancellation handling to your server framework. Test multiple chunk boundaries for the same source text and confirm that concatenating the emitted content always reproduces it on normal completion.

If you’re also collecting usage, keep presentation chunks separate from token counts. The token-usage walkthrough describes that collection boundary.