litellm/tests/unit/llms/bedrock/slow_upstream.py
Mateo Wang 16645cc963
fix(router)!: bound fake-streamed calls by the request timeout, not stream_timeout (#47162)
* fix(router): bound fake-streamed calls by the request timeout, not stream_timeout

A stream=True call that the provider serves as one non-streaming request (Bedrock Converse for json_schema on models without native structured output, tools on some Llama models, ai21) gets no byte until generation ends, so stream_timeout acted as a total-duration cap. The Router now passes timeout and stream_timeout separately to chat completion calls, and completion() applies stream_timeout only when it reads a live stream.

* test(timeout): cover stream_timeout on live streams only and the router's timeout split

* fix(router): cover every fake-stream site and keep the target group's stream_timeout under prompt management

* fix(router): never shorten a fake stream below stream_timeout, and split timeouts on every surface

A faked stream now takes the longer of timeout and stream_timeout, so a caller
who raised stream_timeout to cover a slow fake stream keeps that headroom.
Azure o-series dispatch is mirrored when deciding whether a stream is faked.
text_completion, /v1/messages and /v1/responses through the Router now get
timeout and stream_timeout separately, like chat calls.

* fix(router): report the effective stream timeout, split adapter streams, and cover M365 and Rust messages

* fix(router): report and log the timeout each stream was bounded by

Native /v1/messages and /v1/responses now record the timeout they applied, so
x-litellm-timeout and the cooldown guard read the real limit instead of the raw
request value. A prompt-managed model also passes on only the caller's or its
own timeout, so the target deployment's timeout still wins.

* fix(router): keep the caller's request_timeout on a prompt-managed handoff and report the applied stream limit

* fix(router): give streams answered in one piece the longer limit and accept text timeouts on responses

* fix(router): give /v1/messages and /v1/responses streams answered in one piece the longer limit

* test(integration): cover faked, live and converted stream timeout limits per endpoint

* test(router): regression tests for faked-stream timeouts
2026-10-17 22:08:02 +00:00

52 lines
1.9 KiB
Python

"""An upstream whose first byte takes longer than the request allows, the way a slow model does.
httpx hands every transport the request's timeout in ``request.extensions["timeout"]``, so this one honours it
in process the way a socket would: a read timeout shorter than the first byte's latency times out, a longer one
gets the answer.
"""
from __future__ import annotations
import json
from collections.abc import Callable
from typing import Final
import httpx
from pydantic import TypeAdapter
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
STREAM_TIMEOUT_SECONDS: Final = 0.5
UPSTREAM_FIRST_BYTE_SECONDS: Final = 2.0
CONVERSE_ANSWER: Final = json.dumps(
{
"output": {"message": {"role": "assistant", "content": [{"text": "hi"}]}},
"stopReason": "end_turn",
"usage": {"inputTokens": 1, "outputTokens": 1, "totalTokens": 2},
"metrics": {"latencyMs": 1},
}
).encode()
_TIMEOUT_EXTENSION: Final = TypeAdapter(dict[str, float | None])
def slow_upstream_answer(body: bytes = b"") -> Callable[[httpx.Request], httpx.Response]:
def answer(request: httpx.Request) -> httpx.Response:
read_timeout: Final = _TIMEOUT_EXTENSION.validate_python(request.extensions["timeout"])["read"]
if read_timeout is not None and read_timeout < UPSTREAM_FIRST_BYTE_SECONDS:
raise httpx.ReadTimeout(f"no byte arrived within {read_timeout}s", request=request)
return httpx.Response(200, content=body, request=request)
return answer
def slow_upstream_transport(body: bytes = b"") -> httpx.MockTransport:
return httpx.MockTransport(slow_upstream_answer(body))
def slow_upstream_async_client(body: bytes = b"") -> AsyncHTTPHandler:
return AsyncHTTPHandler(transport=slow_upstream_transport(body))
def slow_upstream_sync_client(body: bytes = b"") -> HTTPHandler:
return HTTPHandler(client=httpx.Client(transport=slow_upstream_transport(body)))