fix: create AsyncOpenAI lazily in trajectory_compressor to avoid closed event loop (#4013)

The AsyncOpenAI client was created once at __init__ and stored as an
instance attribute. process_directory() calls asyncio.run() which creates
and closes a fresh event loop. On a second call, the client's httpx
transport is still bound to the closed loop, raising RuntimeError:
"Event loop is closed" — the same pattern fixed by PR #3398 for the
main agent loop.

Create the client lazily in _get_async_client() so each asyncio.run()
gets a client bound to the current loop.

Co-authored-by: binhnt92 <binhnt.ht.92@gmail.com>
This commit is contained in:
Teknium
2026-03-30 13:16:16 -07:00
committed by GitHub
parent 158f49f19a
commit ed9af6e589
2 changed files with 138 additions and 4 deletions

View File

@@ -375,15 +375,34 @@ class TrajectoryCompressor:
raise RuntimeError(
f"Missing API key. Set {self.config.api_key_env} "
f"environment variable.")
from openai import OpenAI, AsyncOpenAI
from openai import OpenAI
self.client = OpenAI(
api_key=api_key, base_url=self.config.base_url)
self.async_client = AsyncOpenAI(
api_key=api_key, base_url=self.config.base_url)
# AsyncOpenAI is created lazily in _get_async_client() so it
# binds to the current event loop — avoids "Event loop is closed"
# when process_directory() is called multiple times (each call
# creates a new loop via asyncio.run()).
self.async_client = None
self._async_client_api_key = api_key
print(f"✅ Initialized summarizer client: {self.config.summarization_model}")
print(f" Max concurrent requests: {self.config.max_concurrent_requests}")
def _get_async_client(self):
"""Return an AsyncOpenAI client bound to the current event loop.
Created lazily so that each ``asyncio.run()`` call in
``process_directory()`` gets a client tied to its own loop,
avoiding "Event loop is closed" errors on repeated calls.
"""
from openai import AsyncOpenAI
# Always create a fresh client so it binds to the running loop.
self.async_client = AsyncOpenAI(
api_key=self._async_client_api_key,
base_url=self.config.base_url,
)
return self.async_client
def _detect_provider(self) -> str:
"""Detect the provider name from the configured base_url."""
url = (self.config.base_url or "").lower()
@@ -615,7 +634,7 @@ Write only the summary, starting with "[CONTEXT SUMMARY]:" prefix."""
max_tokens=self.config.summary_target_tokens * 2,
)
else:
response = await self.async_client.chat.completions.create(
response = await self._get_async_client().chat.completions.create(
model=self.config.summarization_model,
messages=[{"role": "user", "content": prompt}],
temperature=self.config.temperature,