Submission checklist
Package (Required)
Related Issues / PRs
No response
Reproduction Steps / Example Code (Python)
import asyncio
from typing import Any
from langchain_core.callbacks import AsyncCallbackHandler
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import HumanMessage
from langchain_core.outputs import ChatResult
class Abort(BaseException):
"""A BaseException that is NOT an Exception (e.g., CancelledError )."""
class RaisingModel(BaseChatModel):
@property
def _llm_type(self) -> str:
return "raising"
def _generate(self, *args: Any, **kwargs: Any) -> ChatResult:
raise Abort
async def _agenerate(self, *args: Any, **kwargs: Any) -> ChatResult:
raise Abort
async def main() -> None:
model = RaisingModel()
# Attaching any callback makes run_managers truthy and triggers the on_llm_end path.
await model.agenerate([[HumanMessage("hi")]], callbacks=[AsyncCallbackHandler()])
asyncio.run(main())
Error Message and Stack Trace (if applicable)
generations=[res.generations], # type: ignore[union-attr]
^^^^^^^^^^^^^^^
AttributeError: 'Abort' object has no attribute 'generations'
Description
BaseChatModel.agenerate runs its per-message generations with asyncio.gather(..., return_exceptions=True). It collects failed results with an isinstance(res, BaseException) check, but the subsequent on_llm_end step filters results with the narrower isinstance(res, Exception).
As a result, a BaseException that is not an Exception — KeyboardInterrupt, asyncio.CancelledError, or a library's blocking/abort sentinel — slips past the on_llm_end filter. The cleanup code then reads .generations off the raised exception object and raises AttributeError, which masks the original exception. The user sees a confusing attribute error instead of the cancellation/interrupt/abort that actually occurred.
The two checks should agree on BaseException. This has been detailed and/or worked around in two different ddtrace PRs:
System Info
Package Information
langchain_core: 1.4.8
langsmith: 0.8.18
langchain_protocol: 0.0.17
langchain_tests: 1.1.9
Optional packages not installed
deepagents
deepagents-cli
Other Dependencies
httpx: 0.28.1 > pytest-socket: 0.7.0
jsonpatch: 1.33 > pyyaml: 6.0.3
numpy: 2.3.5 > requests: 2.33.0
orjson: 3.11.6 > requests-toolbelt: 1.0.0
packaging: 26.0 > rich: 14.2.0
pydantic: 2.12.5 > syrupy: 5.1.0
pytest: 9.0.3 > tenacity: 9.1.4
pytest-asyncio: 1.3.0 > typing-extensions: 4.15.0
pytest-benchmark: 5.2.3> uuid-utils: 0.16.0
pytest-codspeed: 4.3.0 > vcrpy: 8.2.1
pytest-recording: 0.13.4> websockets: 16.0
pytest-socket: 0.7.0 > wrapt: 2.0.1
> xxhash: 3.6.0
> zstandard: 0.25.0
Submission checklist
Package (Required)
Related Issues / PRs
No response
Reproduction Steps / Example Code (Python)
Error Message and Stack Trace (if applicable)
Description
BaseChatModel.agenerateruns its per-message generations withasyncio.gather(..., return_exceptions=True). It collects failed results with anisinstance(res, BaseException)check, but the subsequenton_llm_endstep filters results with the narrowerisinstance(res, Exception).As a result, a
BaseExceptionthat is not anException—KeyboardInterrupt,asyncio.CancelledError, or a library's blocking/abort sentinel — slips past theon_llm_endfilter. The cleanup code then reads.generationsoff the raised exception object and raisesAttributeError, which masks the original exception. The user sees a confusing attribute error instead of the cancellation/interrupt/abort that actually occurred.The two checks should agree on
BaseException. This has been detailed and/or worked around in two differentddtracePRs:System Info
Package Information
Optional packages not installed
Other Dependencies