mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(bedrock): propagate timeout to streaming requests
This commit is contained in:
parent
31a67561ab
commit
507b00ea02
13 changed files with 160 additions and 1 deletions
|
|
@ -381,6 +381,7 @@ class BaseConfig(ABC):
|
|||
client: AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> "CustomStreamWrapper":
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
@ -396,6 +397,7 @@ class BaseConfig(ABC):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> "CustomStreamWrapper":
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
|
|||
|
|
@ -640,6 +640,7 @@ class AmazonAgentCoreConfig(BaseConfig, BaseAWSLLM):
|
|||
client: Union[HTTPHandler, "AsyncHTTPHandler"] | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> "CustomStreamWrapper":
|
||||
"""
|
||||
Simplified sync streaming - returns a generator that yields ModelResponse chunks.
|
||||
|
|
@ -853,6 +854,7 @@ class AmazonAgentCoreConfig(BaseConfig, BaseAWSLLM):
|
|||
client: Optional["AsyncHTTPHandler"] = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> "CustomStreamWrapper":
|
||||
"""
|
||||
Simplified async streaming - returns an async generator that yields ModelResponse chunks.
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ def make_sync_call(
|
|||
json_mode: bool | None = False,
|
||||
fake_stream: bool = False,
|
||||
stream_chunk_size: int | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> tuple[Any, httpx.Headers]:
|
||||
if client is None:
|
||||
client = _get_httpx_client() # Create a new client if none provided
|
||||
|
|
@ -45,6 +46,7 @@ def make_sync_call(
|
|||
data=data,
|
||||
stream=not fake_stream,
|
||||
logging_obj=logging_obj,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
|
|
@ -145,6 +147,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
fake_stream=fake_stream,
|
||||
json_mode=json_mode,
|
||||
stream_chunk_size=stream_chunk_size,
|
||||
timeout=timeout,
|
||||
)
|
||||
streaming_response: Final = CustomStreamWrapper(
|
||||
completion_stream=completion_stream,
|
||||
|
|
@ -555,6 +558,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
json_mode=json_mode,
|
||||
fake_stream=fake_stream,
|
||||
stream_chunk_size=stream_chunk_size,
|
||||
timeout=timeout,
|
||||
)
|
||||
streaming_response: Final = CustomStreamWrapper(
|
||||
completion_stream=completion_stream,
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ async def make_call(
|
|||
json_mode: bool | None = False,
|
||||
bedrock_invoke_provider: litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL | None = None,
|
||||
stream_chunk_size: int | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> tuple[Any, httpx.Headers]:
|
||||
try:
|
||||
if client is None:
|
||||
|
|
@ -181,6 +182,7 @@ async def make_call(
|
|||
data=data,
|
||||
stream=not fake_stream,
|
||||
logging_obj=logging_obj,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
|
|
@ -248,6 +250,7 @@ def make_sync_call(
|
|||
json_mode: bool | None = False,
|
||||
bedrock_invoke_provider: litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL | None = None,
|
||||
stream_chunk_size: int | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> tuple[Any, httpx.Headers]:
|
||||
try:
|
||||
if client is None:
|
||||
|
|
@ -265,6 +268,7 @@ def make_sync_call(
|
|||
data=signed_json_body if signed_json_body is not None else data,
|
||||
stream=not fake_stream,
|
||||
logging_obj=logging_obj,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
|
|
|
|||
|
|
@ -444,6 +444,7 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
|
|||
client: AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> CustomStreamWrapper:
|
||||
completion_stream, response_headers = await make_call(
|
||||
client=client,
|
||||
|
|
@ -456,6 +457,7 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
|
|||
fake_stream=True if "ai21" in api_base else False,
|
||||
bedrock_invoke_provider=self.get_bedrock_invoke_provider(model),
|
||||
json_mode=json_mode,
|
||||
timeout=timeout,
|
||||
)
|
||||
streaming_response: Final = CustomStreamWrapper(
|
||||
completion_stream=completion_stream,
|
||||
|
|
@ -479,6 +481,7 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> CustomStreamWrapper:
|
||||
sync_client: Final = (
|
||||
_get_httpx_client(params={}) if client is None or isinstance(client, AsyncHTTPHandler) else client
|
||||
|
|
@ -495,6 +498,7 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
|
|||
fake_stream=True if "ai21" in api_base else False,
|
||||
bedrock_invoke_provider=self.get_bedrock_invoke_provider(model),
|
||||
json_mode=json_mode,
|
||||
timeout=timeout,
|
||||
)
|
||||
streaming_response: Final = CustomStreamWrapper(
|
||||
completion_stream=completion_stream,
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ class BytezChatConfig(BaseConfig):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> "BytezCustomStreamWrapper":
|
||||
if client is None or isinstance(client, AsyncHTTPHandler):
|
||||
client = _get_httpx_client(params={})
|
||||
|
|
@ -299,6 +300,7 @@ class BytezChatConfig(BaseConfig):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> "BytezCustomStreamWrapper":
|
||||
if client is None or isinstance(client, HTTPHandler):
|
||||
client = get_async_httpx_client(llm_provider=LlmProviders.BYTEZ, params={})
|
||||
|
|
|
|||
|
|
@ -619,6 +619,7 @@ class BaseLLMHTTPHandler:
|
|||
messages=messages,
|
||||
client=client,
|
||||
json_mode=json_mode,
|
||||
timeout=timeout,
|
||||
)
|
||||
completion_stream, headers = self.make_sync_call(
|
||||
provider_config=provider_config,
|
||||
|
|
@ -782,6 +783,7 @@ class BaseLLMHTTPHandler:
|
|||
client=client,
|
||||
json_mode=json_mode,
|
||||
signed_json_body=signed_json_body,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
completion_stream, _response_headers = await self.make_async_call_stream_helper(
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ class LangGraphConfig(BaseConfig):
|
|||
client: Union[HTTPHandler, "AsyncHTTPHandler"] | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> CustomStreamWrapper:
|
||||
"""
|
||||
Get a CustomStreamWrapper for synchronous streaming.
|
||||
|
|
@ -343,6 +344,7 @@ class LangGraphConfig(BaseConfig):
|
|||
client: Optional["AsyncHTTPHandler"] = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> CustomStreamWrapper:
|
||||
"""
|
||||
Get a CustomStreamWrapper for asynchronous streaming.
|
||||
|
|
|
|||
|
|
@ -641,6 +641,7 @@ class OCIChatConfig(BaseConfig):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> "OCIStreamWrapper":
|
||||
if client is None or isinstance(client, AsyncHTTPHandler):
|
||||
client = _get_httpx_client(params={})
|
||||
|
|
@ -680,6 +681,7 @@ class OCIChatConfig(BaseConfig):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> "OCIStreamWrapper":
|
||||
if client is None or isinstance(client, HTTPHandler):
|
||||
client = get_async_httpx_client(llm_provider=LlmProviders.OCI, params={})
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ class SagemakerChatConfig(OpenAIGPTConfig, BaseAWSLLM):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> CustomStreamWrapper:
|
||||
if client is None or isinstance(client, AsyncHTTPHandler):
|
||||
client = _get_httpx_client(params={})
|
||||
|
|
@ -189,6 +190,7 @@ class SagemakerChatConfig(OpenAIGPTConfig, BaseAWSLLM):
|
|||
client: HTTPHandler | AsyncHTTPHandler | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> CustomStreamWrapper:
|
||||
if client is None or isinstance(client, HTTPHandler):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -364,6 +364,7 @@ class VertexAgentEngineConfig(BaseConfig, VertexBase):
|
|||
client: Union[HTTPHandler, "AsyncHTTPHandler"] | None = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> "CustomStreamWrapper":
|
||||
"""Get a CustomStreamWrapper for synchronous streaming."""
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
|
|
@ -422,6 +423,7 @@ class VertexAgentEngineConfig(BaseConfig, VertexBase):
|
|||
client: Optional["AsyncHTTPHandler"] = None,
|
||||
json_mode: bool | None = None,
|
||||
signed_json_body: bytes | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> "CustomStreamWrapper":
|
||||
"""Get a CustomStreamWrapper for asynchronous streaming."""
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ extension, and AWS credential resolution is stubbed so nothing reaches STS.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from botocore.credentials import Credentials
|
||||
from litellm.llms.bedrock.chat.converse_handler import BedrockConverseLLM
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
||||
|
|
@ -487,3 +488,66 @@ def test_post_call_is_not_logged_twice_when_the_sync_rust_call_declines():
|
|||
assert response.choices[0].message.content == "hi"
|
||||
assert len(calls["post_call"]) == 1
|
||||
assert "hi" in calls["post_call"][0]["original_response"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"timeout",
|
||||
[
|
||||
pytest.param(2.25, id="numeric"),
|
||||
pytest.param(httpx.Timeout(2.5), id="httpx-timeout"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_converse_streaming_forwards_timeout_to_existing_client(timeout):
|
||||
async def _no_bytes(chunk_size=None):
|
||||
return
|
||||
yield b""
|
||||
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.aiter_bytes = _no_bytes
|
||||
response.headers = httpx.Headers()
|
||||
client = AsyncHTTPHandler()
|
||||
client.post = AsyncMock(return_value=response)
|
||||
|
||||
await litellm.acompletion(
|
||||
model="bedrock/anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream=True,
|
||||
timeout=timeout,
|
||||
client=client,
|
||||
aws_access_key_id="fake",
|
||||
aws_secret_access_key="fake",
|
||||
aws_region_name="us-east-1",
|
||||
)
|
||||
|
||||
assert client.post.await_args.kwargs["timeout"] == timeout
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"timeout",
|
||||
[
|
||||
pytest.param(3.25, id="numeric"),
|
||||
pytest.param(httpx.Timeout(3.5), id="httpx-timeout"),
|
||||
],
|
||||
)
|
||||
def test_sync_converse_streaming_forwards_timeout_to_existing_client(timeout):
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.iter_bytes = MagicMock(return_value=iter(()))
|
||||
response.headers = httpx.Headers()
|
||||
client = HTTPHandler()
|
||||
client.post = MagicMock(return_value=response)
|
||||
|
||||
litellm.completion(
|
||||
model="bedrock/anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream=True,
|
||||
timeout=timeout,
|
||||
client=client,
|
||||
aws_access_key_id="fake",
|
||||
aws_secret_access_key="fake",
|
||||
aws_region_name="us-east-1",
|
||||
)
|
||||
|
||||
assert client.post.call_args.kwargs["timeout"] == timeout
|
||||
|
|
|
|||
|
|
@ -473,3 +473,70 @@ async def test_async_invoke_streaming_forwards_bedrock_response_headers():
|
|||
|
||||
assert stream._hidden_params["additional_headers"]["llm_provider-x-amzn-requestid"] == "req-987"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"timeout",
|
||||
[
|
||||
pytest.param(1.25, id="numeric"),
|
||||
pytest.param(httpx.Timeout(1.5), id="httpx-timeout"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_invoke_streaming_forwards_timeout_to_existing_client(timeout):
|
||||
async def _no_bytes(chunk_size=None):
|
||||
return
|
||||
yield b""
|
||||
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.aiter_bytes = _no_bytes
|
||||
response.headers = httpx.Headers()
|
||||
client = AsyncHTTPHandler()
|
||||
client.post = AsyncMock(return_value=response)
|
||||
|
||||
await litellm.acompletion(
|
||||
model="bedrock/invoke/anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream=True,
|
||||
timeout=timeout,
|
||||
client=client,
|
||||
aws_access_key_id="fake",
|
||||
aws_secret_access_key="fake",
|
||||
aws_region_name="us-east-1",
|
||||
)
|
||||
|
||||
assert client.post.await_args.kwargs["timeout"] == timeout
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_call_sets_timeout_on_httpx_request_transport():
|
||||
transport = MagicMock(return_value=httpx.Response(200, content=b""))
|
||||
timeout = httpx.Timeout(connect=1.0, read=2.0, write=3.0, pool=4.0)
|
||||
client = AsyncHTTPHandler()
|
||||
await client.client.aclose()
|
||||
client.client = httpx.AsyncClient(transport=httpx.MockTransport(transport))
|
||||
|
||||
try:
|
||||
await make_call(
|
||||
client=client,
|
||||
api_base=(
|
||||
"https://bedrock-runtime.us-east-1.amazonaws.com/model/"
|
||||
"anthropic.claude-sonnet-4-6/invoke-with-response-stream"
|
||||
),
|
||||
headers={},
|
||||
data="{}",
|
||||
model="anthropic.claude-sonnet-4-6",
|
||||
messages=[],
|
||||
logging_obj=MagicMock(),
|
||||
timeout=timeout,
|
||||
)
|
||||
finally:
|
||||
await client.client.aclose()
|
||||
|
||||
assert transport.call_args.args[0].extensions["timeout"] == {
|
||||
"connect": 1.0,
|
||||
"read": 2.0,
|
||||
"write": 3.0,
|
||||
"pool": 4.0,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue