Skip to content

Streams

LLM tokens, file chunks, paginated rows: decorate a generator or async generator function and the policies guard the stream.

from nopanic import circuit_breaker, retry, timeout

llm_breaker = circuit_breaker(name="llm", slow_call_duration=20.0)

@retry(attempts=3, on=ConnectionError)
@timeout(10.0, first_item=30.0)   # 30 s to the first token, then 10 s per token
@llm_breaker                       # one outcome per stream
async def stream_completion(prompt):
    async for token in client.stream(prompt):
        yield token

async for token in stream_completion("Hello"):
    print(token, end="")

What each policy does with a stream

Policy Behaviour
retry Re-opens a stream that fails before its first item. After items were delivered, a failure propagates: they cannot be taken back.
timeout Async generators only. Bounds each item; first_item for the first one. The consumer's own time between items never counts.
circuit_breaker Admits once, records one outcome when the stream ends. Slow-call tracking measures time to the first item. A consumer that stops early after receiving items counts as a success; before any item, as no outcome.
bulkhead Holds its slot until the stream finishes.
rate_limit, adaptive_rate_limit One token when the stream opens; the adaptive limiter learns from how it ends.
cache, fallback, hedge, deadline Not supported: TypeError at decoration time.

A policy that cannot guard a stream says so when you decorate, instead of silently doing nothing.

Close what you abandon

As with any async generator, close a stream you stop reading early, so its policies see the end:

import contextlib

async with contextlib.aclosing(stream_completion(prompt)) as tokens:
    async for token in tokens:
        if done(token):
            break

Without it, Python finalizes the generator later from the event loop (and asyncio.run may cancel that at shutdown).