fix: handle httpx.ReadError, WriteError, and ConnectError in LLM streaming clients (#8243)
Adds explicit handling for httpx network errors (ReadError, WriteError, ConnectError) in AnthropicClient, OpenAIClient, and GoogleVertexClient. These errors can occur during streaming when the connection is unexpectedly closed while reading/writing data. Maps these errors to LLMConnectionError for consistent error handling. Fixes #8221 (and duplicate #8156) 🤖 Generated with [Letta Code](https://letta.com) Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com> Co-authored-by: Letta <noreply@letta.com> Co-authored-by: Kian Jones <11655409+kianjones9@users.noreply.github.com>
This commit is contained in:
committed by
Caren Thomas
parent
e60e8ed670
commit
f2171447a8
@@ -3,7 +3,7 @@ import httpx
|
||||
import pytest
|
||||
|
||||
from letta.adapters.letta_llm_stream_adapter import LettaLLMStreamAdapter
|
||||
from letta.errors import ContextWindowExceededError, LLMServerError
|
||||
from letta.errors import ContextWindowExceededError, LLMConnectionError, LLMServerError
|
||||
from letta.llm_api.anthropic_client import AnthropicClient
|
||||
from letta.schemas.llm_config import LLMConfig
|
||||
|
||||
@@ -91,6 +91,74 @@ async def test_letta_llm_stream_adapter_converts_anthropic_413_request_too_large
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_letta_llm_stream_adapter_converts_httpx_read_error(monkeypatch):
|
||||
"""Regression: httpx.ReadError raised during streaming should be converted to LLMConnectionError."""
|
||||
|
||||
class FakeAsyncStream:
|
||||
"""Mimics anthropic.AsyncStream enough for AnthropicStreamingInterface (async cm + async iterator)."""
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return None
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
raise httpx.ReadError("Connection closed unexpectedly")
|
||||
|
||||
async def fake_stream_async(self, request_data: dict, llm_config: LLMConfig):
|
||||
return FakeAsyncStream()
|
||||
|
||||
monkeypatch.setattr(AnthropicClient, "stream_async", fake_stream_async, raising=True)
|
||||
|
||||
llm_client = AnthropicClient()
|
||||
llm_config = LLMConfig(model="claude-sonnet-4-5-20250929", model_endpoint_type="anthropic", context_window=200000)
|
||||
adapter = LettaLLMStreamAdapter(llm_client=llm_client, llm_config=llm_config)
|
||||
|
||||
gen = adapter.invoke_llm(request_data={}, messages=[], tools=[], use_assistant_message=True)
|
||||
with pytest.raises(LLMConnectionError):
|
||||
async for _ in gen:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_letta_llm_stream_adapter_converts_httpx_write_error(monkeypatch):
|
||||
"""Regression: httpx.WriteError raised during streaming should be converted to LLMConnectionError."""
|
||||
|
||||
class FakeAsyncStream:
|
||||
"""Mimics anthropic.AsyncStream enough for AnthropicStreamingInterface (async cm + async iterator)."""
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return None
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
raise httpx.WriteError("Failed to write to connection")
|
||||
|
||||
async def fake_stream_async(self, request_data: dict, llm_config: LLMConfig):
|
||||
return FakeAsyncStream()
|
||||
|
||||
monkeypatch.setattr(AnthropicClient, "stream_async", fake_stream_async, raising=True)
|
||||
|
||||
llm_client = AnthropicClient()
|
||||
llm_config = LLMConfig(model="claude-sonnet-4-5-20250929", model_endpoint_type="anthropic", context_window=200000)
|
||||
adapter = LettaLLMStreamAdapter(llm_client=llm_client, llm_config=llm_config)
|
||||
|
||||
gen = adapter.invoke_llm(request_data={}, messages=[], tools=[], use_assistant_message=True)
|
||||
with pytest.raises(LLMConnectionError):
|
||||
async for _ in gen:
|
||||
pass
|
||||
|
||||
|
||||
def test_anthropic_client_handle_llm_error_413_status_code():
|
||||
"""Test that handle_llm_error correctly converts 413 status code to ContextWindowExceededError."""
|
||||
client = AnthropicClient()
|
||||
|
||||
Reference in New Issue
Block a user