mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
Non-streaming responses served by the Rust core already carry x-litellm-rust: true through _hidden_params.additional_headers, which the SDK exposes and the gateway renders as a response header. Native streams did not, because the lifecycle Stream and SyncStream objects had nowhere to hold hidden params and the marker writer skips objects without them. Give both stream classes the same _hidden_params bag every other litellm response has, so the existing marker attaches without wrapping the stream or changing its identity. Co-authored-by: Yujong Lee <yujong@berri.ai>
188 lines
7.1 KiB
Python
188 lines
7.1 KiB
Python
from collections.abc import AsyncIterator, Iterator
|
|
from typing import Final
|
|
|
|
import pytest
|
|
|
|
import litellm
|
|
from litellm.integrations.custom_logger import CustomLogger
|
|
from litellm.router_utils.add_retry_fallback_headers import get_hidden_params_dict
|
|
from litellm.rust_bridge import catalog
|
|
from litellm.rust_bridge.catalog import Route, RouteRule
|
|
from litellm.rust_bridge.configuration import Rollout
|
|
from tests.test_litellm_rust.support.callback_recorder import RecordingLogger, drain_logging
|
|
from tests.test_litellm_rust.support.isolation import rebound
|
|
from tests.test_litellm_rust.support.recording_server import RecordingServer, ResponseSpec
|
|
from tests.test_litellm_rust.support.requests import (
|
|
MESSAGES,
|
|
MESSAGES_EVENTS,
|
|
MESSAGES_MODEL,
|
|
MESSAGES_RESPONSE,
|
|
request_body,
|
|
)
|
|
|
|
pytestmark = pytest.mark.requires_rust_extension
|
|
|
|
STREAM: Final = ResponseSpec(body=None, events=MESSAGES_EVENTS)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def opt_messages_into_rust() -> Iterator[None]:
|
|
with rebound(catalog, "RULES", (RouteRule(Route.MESSAGES, Rollout.RUST_OPT_IN), *catalog.RULES)):
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def messages_server(recording_server: RecordingServer) -> RecordingServer:
|
|
recording_server.default_response = ResponseSpec(body=MESSAGES_RESPONSE)
|
|
return recording_server
|
|
|
|
|
|
def arguments(server: RecordingServer, **kwargs: object) -> dict[str, object]:
|
|
return {
|
|
"model": MESSAGES_MODEL,
|
|
"messages": [dict(message) for message in MESSAGES],
|
|
"max_tokens": 64,
|
|
"api_key": "test-key",
|
|
"api_base": server.base_url,
|
|
**kwargs,
|
|
}
|
|
|
|
|
|
def assert_served_natively(server: RecordingServer) -> None:
|
|
assert len(server.requests) == 1
|
|
assert not server.requests[0].headers.get("user-agent", "").startswith("python-httpx")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_native_messages_callbacks_see_the_provider_request_and_the_public_response(
|
|
messages_server: RecordingServer,
|
|
) -> None:
|
|
recorder: Final = RecordingLogger()
|
|
|
|
response: Final = await litellm.anthropic.messages.acreate(
|
|
**arguments(messages_server, callbacks=[recorder], litellm_call_id="messages-success")
|
|
)
|
|
|
|
assert_served_natively(messages_server)
|
|
assert response["content"] == MESSAGES_RESPONSE["content"]
|
|
sent: Final = messages_server.requests[0]
|
|
assert sent.path == "/v1/messages"
|
|
assert sent.body == {"model": "claude-sonnet-5", "messages": list(MESSAGES), "max_tokens": 64, "stream": False}
|
|
pre_call: Final = recorder.wait_for("log_pre_api_call")
|
|
assert request_body(pre_call[0].kwargs) == sent.body
|
|
success: Final = await recorder.wait_for_async("async_log_success_event")
|
|
assert len(success) == 1
|
|
assert success[0].call_type == "anthropic_messages"
|
|
assert success[0].kwargs["litellm_call_id"] == "messages-success"
|
|
assert success[0].response.choices[0].message.content == "Hello from native Messages"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_native_messages_pre_call_body_edit_reaches_the_provider(messages_server: RecordingServer) -> None:
|
|
class Edit(CustomLogger):
|
|
def log_pre_api_call(self, model, messages, kwargs):
|
|
request_body(kwargs)["temperature"] = 0.25
|
|
|
|
await litellm.anthropic.messages.acreate(**arguments(messages_server, callbacks=[Edit()]))
|
|
|
|
assert messages_server.requests[0].body["temperature"] == 0.25
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_native_messages_provider_error_reaches_caller_and_failure_callbacks_as_one_public_error(
|
|
messages_server: RecordingServer,
|
|
) -> None:
|
|
messages_server.enqueue(
|
|
ResponseSpec(body={"type": "error", "error": {"type": "invalid_request_error", "message": "bad"}}, status=400)
|
|
)
|
|
observed: Final = []
|
|
|
|
class Observe(CustomLogger):
|
|
def log_failure_event(self, kwargs, response_obj, start_time, end_time):
|
|
observed.append(("sync", kwargs["exception"]))
|
|
|
|
async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time):
|
|
observed.append(("async", kwargs["exception"]))
|
|
|
|
with pytest.raises(litellm.BadRequestError) as raised:
|
|
await litellm.anthropic.messages.acreate(**arguments(messages_server, callbacks=[Observe()]))
|
|
|
|
assert_served_natively(messages_server)
|
|
assert [phase for phase, _ in observed] == ["sync", "async"]
|
|
assert all(error is raised.value for _, error in observed)
|
|
|
|
|
|
def sse_payload() -> bytes:
|
|
return b"".join(STREAM.payloads())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_native_messages_stream_relays_provider_events_and_logs_success_once_after_the_last_chunk(
|
|
messages_server: RecordingServer,
|
|
) -> None:
|
|
messages_server.enqueue(STREAM)
|
|
recorder: Final = RecordingLogger()
|
|
|
|
stream: Final = await litellm.anthropic.messages.acreate(
|
|
**arguments(messages_server, stream=True, callbacks=[recorder])
|
|
)
|
|
assert isinstance(stream, AsyncIterator)
|
|
assert get_hidden_params_dict(stream) == {"additional_headers": {"x-litellm-rust": "true"}}
|
|
first: Final = await anext(stream)
|
|
await drain_logging()
|
|
assert "async_log_success_event" not in recorder.names
|
|
rest: Final = [chunk async for chunk in stream]
|
|
|
|
assert first + b"".join(rest) == sse_payload()
|
|
assert_served_natively(messages_server)
|
|
assert messages_server.requests[0].body["stream"] is True
|
|
success: Final = await recorder.wait_for_async("async_log_success_event")
|
|
assert len(success) == 1
|
|
assert success[0].kwargs["stream"] is True
|
|
assert success[0].kwargs["completion_start_time"] is not None
|
|
assert "log_failure_event" not in recorder.names
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_native_messages_stream_closed_early_logs_success_once_for_what_was_delivered(
|
|
messages_server: RecordingServer,
|
|
) -> None:
|
|
messages_server.enqueue(STREAM)
|
|
recorder: Final = RecordingLogger()
|
|
|
|
stream: Final = await litellm.anthropic.messages.acreate(
|
|
**arguments(messages_server, stream=True, callbacks=[recorder])
|
|
)
|
|
assert isinstance(stream, AsyncIterator)
|
|
await anext(stream)
|
|
await stream.aclose()
|
|
|
|
success: Final = await recorder.wait_for_async("async_log_success_event")
|
|
assert len(success) == 1
|
|
with pytest.raises(StopAsyncIteration):
|
|
await anext(stream)
|
|
|
|
|
|
def test_native_sync_messages_stream_relays_provider_events_and_logs_success_once(
|
|
messages_server: RecordingServer,
|
|
) -> None:
|
|
messages_server.enqueue(STREAM)
|
|
recorder: Final = RecordingLogger()
|
|
|
|
stream: Final = litellm.anthropic.messages.create(**arguments(messages_server, stream=True, callbacks=[recorder]))
|
|
assert isinstance(stream, Iterator)
|
|
assert get_hidden_params_dict(stream) == {"additional_headers": {"x-litellm-rust": "true"}}
|
|
|
|
assert b"".join(stream) == sse_payload()
|
|
assert_served_natively(messages_server)
|
|
assert len(recorder.wait_for("async_log_success_event")) == 1
|
|
|
|
|
|
def test_native_sync_messages_returns_the_provider_message(messages_server: RecordingServer) -> None:
|
|
recorder: Final = RecordingLogger()
|
|
|
|
response: Final = litellm.anthropic.messages.create(**arguments(messages_server, callbacks=[recorder]))
|
|
|
|
assert_served_natively(messages_server)
|
|
assert response["content"] == MESSAGES_RESPONSE["content"]
|
|
assert len(recorder.wait_for("log_success_event")) == 1
|