From 5fbf8f93e28e419dd16dba0b16e23ed260b54ac0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:22:04 -0800 Subject: [PATCH] fix: add explicit timeouts to httpx clients to prevent ReadTimeout errors (#8538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses the httpx.ReadTimeout error detected in production by adding explicit timeout configurations to several httpx client usages: 1. MCP SSE client: Pass mcp_connect_to_server_timeout (30s) to sse_client() 2. MCP StreamableHTTP client: Pass mcp_connect_to_server_timeout (30s) to streamablehttp_client() 3. OpenAI model list API: Add 30s timeout with 10s connect timeout 4. Google AI model list/details API: Add 30s timeout with 10s connect timeout Previously, these httpx clients were created without explicit timeouts, which could cause ReadTimeout errors when remote servers are slow to respond. Fixes #8073 🤖 Generated with [Letta Code](https://letta.com) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: datadog-official[bot] Co-authored-by: Kian Jones <11655409+kianjones9@users.noreply.github.com> --- letta/llm_api/google_ai_client.py | 6 ++++-- letta/llm_api/openai.py | 3 ++- letta/services/mcp/sse_client.py | 7 +++++-- letta/services/mcp/streamable_http_client.py | 10 +++++++--- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/letta/llm_api/google_ai_client.py b/letta/llm_api/google_ai_client.py index 8a90e7cb..b1281b62 100644 --- a/letta/llm_api/google_ai_client.py +++ b/letta/llm_api/google_ai_client.py @@ -82,7 +82,8 @@ async def google_ai_get_model_list_async( # Determine if we need to close the client at the end close_client = False if client is None: - client = httpx.AsyncClient() + # Use explicit timeout to prevent httpx.ReadTimeout errors + client = httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=10.0)) close_client = True try: @@ -129,7 +130,8 @@ async def google_ai_get_model_details_async( # Determine if we need to close the client at the end close_client = False if client is None: - client = httpx.AsyncClient() + # Use explicit timeout to prevent httpx.ReadTimeout errors + client = httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=10.0)) close_client = True try: diff --git a/letta/llm_api/openai.py b/letta/llm_api/openai.py index 8c8692b8..8dc2318d 100644 --- a/letta/llm_api/openai.py +++ b/letta/llm_api/openai.py @@ -72,7 +72,8 @@ async def openai_get_model_list_async( # Use provided client or create a new one close_client = False if client is None: - client = httpx.AsyncClient() + # Use explicit timeout to prevent httpx.ReadTimeout errors + client = httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=10.0)) close_client = True try: diff --git a/letta/services/mcp/sse_client.py b/letta/services/mcp/sse_client.py index 0327fc26..ee8dfc17 100644 --- a/letta/services/mcp/sse_client.py +++ b/letta/services/mcp/sse_client.py @@ -7,6 +7,7 @@ from mcp.client.sse import sse_client from letta.functions.mcp_client.types import SSEServerConfig from letta.log import get_logger from letta.services.mcp.base_client import AsyncBaseMCPClient +from letta.settings import tool_settings # see: https://modelcontextprotocol.io/quickstart/user MCP_CONFIG_TOPLEVEL_KEY = "mcpServers" @@ -33,10 +34,12 @@ class AsyncSSEMCPClient(AsyncBaseMCPClient): headers[self.AGENT_ID_HEADER] = self.agent_id # Use OAuth provider if available, otherwise use regular headers + # Pass timeout to prevent httpx.ReadTimeout errors on slow connections + timeout = tool_settings.mcp_connect_to_server_timeout if self.oauth_provider: - sse_cm = sse_client(url=server_config.server_url, headers=headers if headers else None, auth=self.oauth_provider) + sse_cm = sse_client(url=server_config.server_url, headers=headers if headers else None, auth=self.oauth_provider, timeout=timeout) else: - sse_cm = sse_client(url=server_config.server_url, headers=headers if headers else None) + sse_cm = sse_client(url=server_config.server_url, headers=headers if headers else None, timeout=timeout) sse_transport = await self.exit_stack.enter_async_context(sse_cm) self.stdio, self.write = sse_transport diff --git a/letta/services/mcp/streamable_http_client.py b/letta/services/mcp/streamable_http_client.py index e2f256f5..9d29b4a6 100644 --- a/letta/services/mcp/streamable_http_client.py +++ b/letta/services/mcp/streamable_http_client.py @@ -1,3 +1,4 @@ +from datetime import timedelta from typing import Optional from mcp import ClientSession @@ -7,6 +8,7 @@ from mcp.client.streamable_http import streamablehttp_client from letta.functions.mcp_client.types import BaseServerConfig, StreamableHTTPServerConfig from letta.log import get_logger from letta.services.mcp.base_client import AsyncBaseMCPClient +from letta.settings import tool_settings logger = get_logger(__name__) @@ -38,16 +40,18 @@ class AsyncStreamableHTTPMCPClient(AsyncBaseMCPClient): headers[self.AGENT_ID_HEADER] = self.agent_id # Use OAuth provider if available, otherwise use regular headers + # Pass timeout to prevent httpx.ReadTimeout errors on slow connections + timeout = timedelta(seconds=tool_settings.mcp_connect_to_server_timeout) if self.oauth_provider: streamable_http_cm = streamablehttp_client( - server_config.server_url, headers=headers if headers else None, auth=self.oauth_provider + server_config.server_url, headers=headers if headers else None, auth=self.oauth_provider, timeout=timeout ) else: # Use streamablehttp_client context manager with headers if provided if headers: - streamable_http_cm = streamablehttp_client(server_config.server_url, headers=headers) + streamable_http_cm = streamablehttp_client(server_config.server_url, headers=headers, timeout=timeout) else: - streamable_http_cm = streamablehttp_client(server_config.server_url) + streamable_http_cm = streamablehttp_client(server_config.server_url, timeout=timeout) read_stream, write_stream, _ = await self.exit_stack.enter_async_context(streamable_http_cm)