Skip to content

Commit 8f1341e

Browse files
committed
Add configurable timeout for passthrough endpoints
- Fix hardcoded 600-second timeout on pass-through endpoints that caused TimeoutError for long-running streaming requests (e.g. Anthropic API calls with extended thinking/tool use) - Add timeout field to PassThroughGenericEndpoint config, allowing per-endpoint timeout configuration - Fix aiohttp compatibility issue with ConnectionTimeoutError/SocketTimeoutError on older versions (not necessary for this PRs purpose, but good to have) - Added tests
1 parent a915d89 commit 8f1341e

7 files changed

Lines changed: 250 additions & 132 deletions

File tree

litellm/llms/custom_httpx/aiohttp_transport.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
# Timeout related exceptions
2222
asyncio.TimeoutError: httpx.TimeoutException,
2323
aiohttp.ServerTimeoutError: httpx.TimeoutException,
24-
aiohttp.ConnectionTimeoutError: httpx.ConnectTimeout,
25-
aiohttp.SocketTimeoutError: httpx.ReadTimeout,
2624
# Proxy related exceptions
2725
aiohttp.ClientProxyConnectionError: httpx.ProxyError,
2826
# SSL related exceptions
@@ -46,6 +44,14 @@
4644
aiohttp.ClientError: httpx.RequestError,
4745
}
4846

47+
# Add timeout exception classes that may not exist in older aiohttp versions
48+
_connection_timeout_error = getattr(aiohttp, "ConnectionTimeoutError", None)
49+
if _connection_timeout_error is not None:
50+
AIOHTTP_EXC_MAP[_connection_timeout_error] = httpx.ConnectTimeout
51+
_socket_timeout_error = getattr(aiohttp, "SocketTimeoutError", None)
52+
if _socket_timeout_error is not None:
53+
AIOHTTP_EXC_MAP[_socket_timeout_error] = httpx.ReadTimeout
54+
4955
# Add client_exceptions module exceptions
5056
try:
5157
import aiohttp.client_exceptions

litellm/proxy/_types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1931,6 +1931,10 @@ class PassThroughGenericEndpoint(LiteLLMPydanticObjectBase):
19311931
default=False,
19321932
description="If True, requests to subpaths of the path will be forwarded to the target endpoint. For example, if the path is /bria and include_subpath is True, requests to /bria/v1/text-to-image/base/2.3 will be forwarded to the target endpoint.",
19331933
)
1934+
timeout: Optional[float] = Field(
1935+
default=None,
1936+
description="Timeout in seconds for requests to the target endpoint. Defaults to 600 seconds if not specified.",
1937+
)
19341938
cost_per_request: float = Field(
19351939
default=0.0,
19361940
description="The USD cost per request to the target endpoint. This is used to calculate the cost of the request to the target endpoint.",

litellm/proxy/pass_through_endpoints/pass_through_endpoints.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,7 @@ async def pass_through_request( # noqa: PLR0915
611611
cost_per_request: Optional[float] = None,
612612
custom_llm_provider: Optional[str] = None,
613613
guardrails_config: Optional[dict] = None,
614+
timeout: Optional[float] = None,
614615
):
615616
"""
616617
Pass through endpoint handler, makes the httpx request for pass-through endpoints and ensures logging hooks are called
@@ -733,9 +734,10 @@ async def pass_through_request( # noqa: PLR0915
733734
data=_parsed_body,
734735
call_type="pass_through_endpoint",
735736
)
737+
_timeout = timeout if timeout is not None else 600
736738
async_client_obj = get_async_httpx_client(
737739
llm_provider=httpxSpecialProvider.PassThroughEndpoint,
738-
params={"timeout": 600},
740+
params={"timeout": _timeout},
739741
)
740742
async_client = async_client_obj.client
741743
passthrough_logging_payload = PassthroughStandardLoggingPayload(
@@ -1101,6 +1103,7 @@ def create_pass_through_route(
11011103
query_params: Optional[dict] = None,
11021104
default_query_params: Optional[dict] = None,
11031105
guardrails: Optional[Dict[str, Any]] = None,
1106+
timeout: Optional[float] = None,
11041107
):
11051108
# check if target is an adapter.py or a url
11061109
from litellm._uuid import uuid
@@ -1174,6 +1177,7 @@ async def endpoint_func( # type: ignore
11741177
"merge_query_params": _merge_query_params,
11751178
"cost_per_request": cost_per_request,
11761179
"guardrails": None,
1180+
"timeout": timeout,
11771181
}
11781182

11791183
if passthrough_params is not None:
@@ -1193,6 +1197,7 @@ async def endpoint_func( # type: ignore
11931197
)
11941198
param_guardrails = target_params.get("guardrails", None)
11951199
param_default_query_params = target_params.get("default_query_params", None)
1200+
param_timeout = target_params.get("timeout", None)
11961201

11971202
# Construct the full target URL with subpath if needed
11981203
full_target = (
@@ -1236,6 +1241,7 @@ async def endpoint_func( # type: ignore
12361241
cost_per_request=cast(Optional[float], param_cost_per_request),
12371242
custom_llm_provider=custom_llm_provider,
12381243
guardrails_config=cast(Optional[dict], param_guardrails),
1244+
timeout=cast(Optional[float], param_timeout),
12391245
)
12401246

12411247
return endpoint_func
@@ -1881,6 +1887,7 @@ def add_exact_path_route(
18811887
guardrails: Optional[dict] = None,
18821888
methods: Optional[List[str]] = None,
18831889
default_query_params: Optional[dict] = None,
1890+
timeout: Optional[float] = None,
18841891
):
18851892
"""Add exact path route for pass-through endpoint"""
18861893
# Default to all methods if none specified (backward compatibility)
@@ -1920,6 +1927,7 @@ def add_exact_path_route(
19201927
cost_per_request=cost_per_request,
19211928
default_query_params=default_query_params,
19221929
guardrails=guardrails,
1930+
timeout=timeout,
19231931
),
19241932
methods=methods,
19251933
dependencies=dependencies,
@@ -1940,6 +1948,7 @@ def add_exact_path_route(
19401948
"dependencies": dependencies,
19411949
"cost_per_request": cost_per_request,
19421950
"guardrails": guardrails,
1951+
"timeout": timeout,
19431952
},
19441953
}
19451954

@@ -1957,6 +1966,7 @@ def add_subpath_route(
19571966
guardrails: Optional[dict] = None,
19581967
methods: Optional[List[str]] = None,
19591968
default_query_params: Optional[dict] = None,
1969+
timeout: Optional[float] = None,
19601970
):
19611971
"""Add wildcard route for sub-paths"""
19621972
# Default to all methods if none specified (backward compatibility)
@@ -1997,6 +2007,7 @@ def add_subpath_route(
19972007
cost_per_request=cost_per_request,
19982008
default_query_params=default_query_params,
19992009
guardrails=guardrails,
2010+
timeout=timeout,
20002011
),
20012012
methods=methods,
20022013
dependencies=dependencies,
@@ -2017,6 +2028,7 @@ def add_subpath_route(
20172028
"dependencies": dependencies,
20182029
"cost_per_request": cost_per_request,
20192030
"guardrails": guardrails,
2031+
"timeout": timeout,
20202032
},
20212033
}
20222034

@@ -2230,6 +2242,9 @@ async def initialize_pass_through_endpoints(
22302242
# Get guardrails config if present
22312243
_guardrails = endpoint.get("guardrails", None)
22322244

2245+
# Get timeout if present
2246+
_timeout = endpoint.get("timeout", None)
2247+
22332248
# Get methods list if present (None means all methods for backward compatibility)
22342249
_methods = endpoint.get("methods", None)
22352250

@@ -2250,6 +2265,7 @@ async def initialize_pass_through_endpoints(
22502265
guardrails=_guardrails,
22512266
methods=_methods,
22522267
default_query_params=_default_query_params,
2268+
timeout=_timeout,
22532269
)
22542270

22552271
# Generate route key with methods for tracking
@@ -2274,6 +2290,7 @@ async def initialize_pass_through_endpoints(
22742290
guardrails=_guardrails,
22752291
methods=_methods,
22762292
default_query_params=_default_query_params,
2293+
timeout=_timeout,
22772294
)
22782295

22792296
visited_endpoints.add(f"{endpoint_id}:subpath:{_path}:{methods_str}")

package-lock.json

Lines changed: 19 additions & 57 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)