"""Async utility helpers for bridging sync generators to async FastAPI routes.""" from __future__ import annotations import asyncio import threading from typing import AsyncGenerator, Generator, TypeVar T = TypeVar("T") _SENTINEL = object() async def iter_in_thread(sync_gen: Generator[T, None, None]) -> AsyncGenerator[T, None]: """Yield items from a synchronous generator without blocking the event loop. Runs the generator in a daemon thread, forwarding items via an asyncio.Queue. Use this to wrap blocking LLM streaming generators inside async FastAPI routes. """ loop = asyncio.get_running_loop() queue: asyncio.Queue = asyncio.Queue(maxsize=32) def _drain() -> None: try: for item in sync_gen: future = asyncio.run_coroutine_threadsafe(queue.put(item), loop) future.result() finally: asyncio.run_coroutine_threadsafe(queue.put(_SENTINEL), loop).result() thread = threading.Thread(target=_drain, daemon=True) thread.start() while True: item = await queue.get() if item is _SENTINEL: break yield item # type: ignore[misc]