Skip to main content
Post-generation validation waits until the model has finished writing before checking the output. That is fine for short responses, but wastes time and compute when a violation appears in the first sentence of a ten-paragraph reply. Streaming validation moves the check into the generation loop: each chunk is validated as soon as it arrives, and generation is cancelled the moment a requirement fails. By the end you will have covered:
  • stream_with_chunking() — the streaming validation entry point
  • The typed event vocabulary (ChunkEvent, QuickCheckEvent, …) from result.events()
  • Early-exit cancellation and reading streaming_failures
  • Choosing between "word", "sentence", and "paragraph" chunking
  • Subclassing ChunkingStrategy to define a custom split boundary
  • result.astream() for consumers that only need the validated chunks
Prerequisites: Tutorial 02 (async and streaming), Tutorial 04 (requirements and validation), pip install mellea, Ollama running locally with granite4.1:3b downloaded.

Step 1: Your first streaming validation call

stream_with_chunking() returns a StreamChunkingResult immediately. The orchestrator runs in the background, splitting accumulated text into chunks and calling stream_validate() on each one. Consume events with result.events(), then call result.acomplete() to wait for the orchestrator to finish and raise any exception it stored.
Sample output
Note: LLM output is non-deterministic. Your result will vary in wording.
Three things to notice:
  • stream_with_chunking() is called with await but returns immediately — the orchestrator runs as a background task.
  • result.events() is an async iterator that yields one event per semantic unit. The loop ends when the CompletedEvent is delivered.
  • result.acomplete() must be called after the event loop drains to propagate any orchestrator exception and to ensure the background task has fully settled.

Step 2: Early exit on failure

When stream_validate() returns "fail", the orchestrator cancels the backend immediately and stops the stream. No further chunks are delivered, and the failure is recorded in result.streaming_failures. Lower the sentence limit so the model is likely to exceed it:
Sample output
Note: Whether the stream is cancelled depends on whether the model exceeds the limit. If the model happens to comply, streaming_failures will be empty and result.completed will be True.
result.full_text always contains the text accumulated up to the point where generation stopped — useful for debugging what the model produced before the requirement failed.

Step 3: Choosing a chunking strategy

The built-in strategies cover a coarse-to-fine spectrum: The trade-off is latency vs context. Word chunking fires after every token — maximum reaction speed, but each chunk carries only a single word. Paragraph chunking waits for blank lines — full paragraph context for the validator, but detection is later and may happen after the model has produced a large amount of invalid content. To see the granularity difference concretely, switch to word chunking and print every fifth word — so you can count how many more validation events fire compared to Step 1’s two sentences:
Sample output
Note: LLM output is non-deterministic. Your result will vary in wording.
The same two-sentence response that produced 2 ChunkEvent items with sentence chunking now produces 38. The validator fires on every word — maximum reaction speed at the cost of per-chunk context. If a forbidden word appears, the stream stops at that word and no further ChunkEvent items are emitted. To see early exit in action, change _FORBIDDEN to include a common English word like "and" or "the".

Step 4: Raw chunk access with astream()

If you only need the validated chunks and do not want event metadata, use result.astream() instead of result.events(). It yields the text of each validated chunk as a plain string — useful for streaming output directly to a UI buffer or building the response incrementally without a match dispatch:
Sample output
Note: LLM output is non-deterministic. Your result will vary in wording.
astream() and events() are independent — both are available on the same result object and can even be consumed concurrently with asyncio.gather. Each is single-consumer: calling either iterator a second time raises RuntimeError. If you need chunks after the fact, capture them to a list during iteration or read result.full_text after acomplete().

Step 5: A custom chunking strategy

The built-in strategies cover the most common boundaries. For structured output — numbered lists, code blocks, CSV rows — you can subclass ChunkingStrategy and define your own split boundary. Two methods to implement:
  • split(accumulated_text) — called on every new token delta. Return all complete chunks found so far; withhold any trailing fragment. Must be stateless: it receives the full accumulated text each time, not a delta.
  • flush(accumulated_text) — called once at natural end of stream. Release the withheld trailing fragment, or return [] to discard it.
Here is a LineChunker that splits on single newlines — natural for numbered list output where each line is one item:
Sample output
Note: LLM output is non-deterministic. Your result will vary in wording.
validate() on NumberedLineReq always returns True because all format checking happens during streaming. If any line fails, the stream is cancelled before reaching validate(). Lines that do reach it have already passed stream_validate(). This pattern — enforce in stream_validate, pass in validate — is common for requirements whose invariant is a property of individual chunks rather than the full output. Pass a ChunkingStrategy instance (not a string alias) to use a custom chunker. The built-in chunkers (WordChunker, SentenceChunker, ParagraphChunker) are also available as instances if you need to pass one explicitly or subclass to override flush().
See also: docs/examples/streaming/custom_chunking.py for an annotated version of this pattern with a more detailed split()/flush() contract walkthrough.

What you built


See also: How-to: Streaming with per-chunk validation | Concepts: The Requirements System — Streaming validation | Examples: streaming/