Compare commits

...

1 Commits

Author SHA1 Message Date
Mason Daugherty
d50251e787 fireworks: retain root client refs to prevent aiohttp session leak
`ChatFireworks.validate_environment` extracted `.chat.completions` from
freshly created `Fireworks` / `AsyncFireworks` instances and immediately
discarded the parents.  Starting with `fireworks-ai>=0.19`, those parents
eagerly create an `aiohttp.ClientSession` when an async event loop is
running.  Orphaning the parents means the sessions are never closed,
producing "Unclosed client session" warnings at interpreter shutdown.

Store parent instances as `root_client` / `root_async_client` (matching
the `langchain-openai` pattern) so transports stay reachable and are
cleaned up deterministically.

Adds a regression test that constructs `ChatFireworks` inside an async
context and asserts no `ResourceWarning` about unclosed sessions fires.
2026-03-24 13:33:00 -04:00
2 changed files with 57 additions and 2 deletions

View File

@@ -325,6 +325,12 @@ class ChatFireworks(BaseChatModel):
async_client: Any = Field(default=None, exclude=True)
root_client: Any = Field(default=None, exclude=True)
"""Underlying `Fireworks` client (owns the httpx transport)."""
root_async_client: Any = Field(default=None, exclude=True)
"""Underlying `AsyncFireworks` client (owns the httpx transport)."""
model_name: str = Field(alias="model")
"""Model name to use."""
@@ -416,9 +422,11 @@ class ChatFireworks(BaseChatModel):
}
if not self.client:
self.client = Fireworks(**client_params).chat.completions
self.root_client = Fireworks(**client_params)
self.client = self.root_client.chat.completions
if not self.async_client:
self.async_client = AsyncFireworks(**client_params).chat.completions
self.root_async_client = AsyncFireworks(**client_params)
self.async_client = self.root_async_client.chat.completions
if self.max_retries:
self.client._max_retries = self.max_retries
self.async_client._max_retries = self.max_retries

View File

@@ -0,0 +1,47 @@
"""Verify ChatFireworks does not orphan aiohttp.ClientSession objects.
The Fireworks SDK (>=0.19) eagerly creates an ``aiohttp.ClientSession``
inside ``FireworksClient.__init__`` when an async event loop is running.
If ``ChatFireworks`` discards the parent ``Fireworks`` /
``AsyncFireworks`` objects after extracting ``.chat.completions``, the
underlying sessions are never closed, producing ``Unclosed client
session`` warnings.
This test constructs the model inside an async context (matching how
LangGraph / Harbor invoke it) and asserts no sessions leak.
"""
import asyncio
import gc
import warnings
from pydantic import SecretStr
from langchain_fireworks import ChatFireworks
async def test_no_unclosed_aiohttp_sessions() -> None:
"""ChatFireworks must not leak aiohttp.ClientSession objects."""
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
model = ChatFireworks(
model="accounts/fireworks/models/llama-v3p1-8b-instruct",
api_key=SecretStr("test-key"),
)
del model
gc.collect()
# Yield control so weak-ref / __del__ callbacks can fire.
await asyncio.sleep(0)
gc.collect()
unclosed = [
w
for w in caught
if issubclass(w.category, ResourceWarning)
and "unclosed" in str(w.message).lower()
]
assert unclosed == [], f"Leaked {len(unclosed)} unclosed session(s):\n" + "\n".join(
str(w.message) for w in unclosed
)