Calling Coroutines from Sync Hooks
Calling async code from a sync callback raises `RuntimeError` inside a running event loop. The fix branches on `asyncio.get_running_loop()` — fire-and-forget if a loop exists, `asyncio.run()` if it doesn't.

The problem
You have a sync callback hook — LiteLLM's log_success_event, a Django signal handler, a Celery task — and you need to call async code from it. The standard advice is asyncio.get_event_loop().run_until_complete(coro), but this raises RuntimeError: This event loop is already running when called from inside a running async context, which is exactly where these hooks fire.
The approach
The correct pattern depends on whether you're inside a running event loop or not:
def _dispatch_sync(self, coro) -> None:
import asyncio
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop is not None:
loop.create_task(coro)
else:
asyncio.run(coro)asyncio.get_running_loop() raises RuntimeError if there's no running loop (not deprecated, safe to use). If a loop is running, create_task schedules the coroutine without blocking. If there's no running loop (pure sync context), asyncio.run() spins one up.
The tradeoff: create_task is fire-and-forget. If the coroutine raises, the exception lands in the event loop's exception handler, not your caller. For logging hooks where you want best-effort delivery and don't want to block, this is the right choice. For anything where you need the result or need to guarantee delivery, you need a different pattern (thread pool, queue, or refactor to async).
What I learned
asyncio.get_event_loop() is the source of most of the confusion here — it creates a new loop in some contexts and returns the running one in others, and its behavior changed across Python versions. get_running_loop() is unambiguous: it either returns the running loop or raises. Use that and branch on the result.
The other thing: LiteLLM provides async variants of all sync hooks (async_log_success_event). If you control the hook registration, prefer those. The sync-to-async bridge is only necessary when you're forced into the sync API.
