litellm/tests/unit/rust_bridge/test_execution.py
yujonglee 8302f006dd
refactor(rust-bridge): return an unstarted Execution from native entrypoints and drive it in Python (#46299)
* refactor(rust-bridge): return an unstarted Execution from native entrypoints and drive it in Python

Rust's run_call built Python streams itself and imported
litellm.rust_bridge.streams by name, so sync calls were driven in Rust
while async calls came back as a Rust-made coroutine. Every native
inference entrypoint now returns one unstarted Execution and
public_execution.py drives it with lifecycle.run or lifecycle.drive,
wrapping streams and _hidden_params on the Python side. Rust only
imports the lifecycle step classes through StepTypes, and
python-bridge/src/lifecycle.rs, PythonLifecycle and streams.py are gone.

The adapter trusts the _native.pyi contract with a single cast instead
of a per-request runtime_checkable isinstance. Binding tests assert the
adapter starts the execution and returns its completed value rather
than checking callable identity.

* refactor(rust-bridge): fold lifecycle and public_execution into one execution module

The public stream wrappers only forwarded to the lifecycle streams and added the
header dict, so the streams now carry _hidden_params themselves. lifecycle.py no
longer held any call lifecycle, just the Execution driver, so it is renamed to
execution.py and absorbs the synchronous and asynchronous adapters

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(rust-bridge): keep execution.py free of third-party imports

The host-python tests load execution.py into a bare interpreter, so the
pydantic header validation broke them. The stream head comes from the
binding's encode_stream_head, which always builds a str-keyed dict

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(host-python): preserve pre-async hook coverage after execution handoff

* style(rust-bridge): sort litellm_host_python imports

Co-Authored-By: Yujong Lee <yujong@berri.ai>

* test(rust-bridge): drive unstarted native calls through execution in the split lifecycle tests

Co-Authored-By: Yujong Lee <yujong@berri.ai>

* fix(rust-bridge): mark execution TypeVar as Final

Co-Authored-By: Yujong Lee <yujong@berri.ai>

* test(rust-bridge): cover stream close, read failure, and sync suspension in execution

Co-Authored-By: Yujong Lee <yujong@berri.ai>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 17:50:44 -07:00

192 lines
6 KiB
Python

from __future__ import annotations
import asyncio
from collections.abc import Sequence
from typing import Final
import pytest
from litellm.rust_bridge.execution import (
AsyncStream,
Await,
Complete,
Execution,
Step,
StreamChunk,
StreamClosed,
StreamOpen,
SyncStream,
drive,
run,
)
class ScriptedExecution:
"""Plays scripted steps and records how it was resumed and whether it was closed."""
def __init__(self, steps: Sequence[Step | BaseException]) -> None:
self._steps: Final = list(steps)
self.resumed: list[tuple[str, object]] = []
self.closed = False
def send(self, value: object) -> Step:
self.resumed.append(("value", value))
return self._next()
def throw(self, error: BaseException) -> Step:
self.resumed.append(("error", type(error)))
return self._next()
def _next(self) -> Step:
step: Final = self._steps.pop(0)
if isinstance(step, BaseException):
raise step
return step
def close(self) -> None:
self.closed = True
async def ready(value: object) -> object:
return value
async def failing() -> object:
raise ValueError("boom")
async def exiting() -> object:
raise GeneratorExit
def test_drive_resumes_each_await_with_its_result_or_error_and_returns_the_completed_value() -> None:
execution: Final = ScriptedExecution([Await(ready(1)), Await(failing()), Complete("done")])
assert asyncio.run(drive(execution, AsyncStream)) == "done"
assert execution.resumed == [("value", None), ("value", 1), ("error", ValueError)]
assert execution.closed
@pytest.mark.parametrize("factory_fails", (False, True))
def test_stream_handoff_preserves_head_identity_and_closes_on_construction_failure(factory_fails: bool) -> None:
head: Final = object()
stream: Final = object()
execution: Final = ScriptedExecution([StreamOpen(head)])
failure: Final = ValueError("stream construction failed")
def construct(owner: Execution, received: object) -> object:
assert owner is execution
assert received is head
if factory_fails:
raise failure
return stream
if factory_fails:
with pytest.raises(ValueError, match="stream construction failed") as caught:
asyncio.run(drive(execution, construct))
assert caught.value is failure
assert execution.closed
else:
assert asyncio.run(drive(execution, construct)) is stream
assert not execution.closed
execution.close()
def test_drive_rejects_a_chunk_before_the_stream_opens_and_closes_the_execution() -> None:
execution: Final = ScriptedExecution([StreamChunk("chunk")])
with pytest.raises(RuntimeError, match="chunk before opening a stream"):
asyncio.run(drive(execution, AsyncStream))
assert execution.closed
@pytest.mark.parametrize("asynchronous", (False, True))
def test_stream_read_rejects_a_second_open_and_closes_the_execution(asynchronous: bool) -> None:
execution: Final = ScriptedExecution([StreamChunk("first"), StreamOpen("head")])
async def read() -> tuple[object, ...]:
stream: Final = AsyncStream(execution, {})
return (await anext(stream), await anext(stream))
def read_sync() -> tuple[object, ...]:
stream: Final = SyncStream(execution, {})
return (next(stream), next(stream))
with pytest.raises(RuntimeError, match="opened a stream twice"):
asyncio.run(read()) if asynchronous else read_sync()
assert execution.closed
def test_drive_propagates_generator_exit_without_resuming_the_execution() -> None:
execution: Final = ScriptedExecution([Await(exiting())])
with pytest.raises(GeneratorExit):
asyncio.run(drive(execution, AsyncStream))
assert execution.resumed == [("value", None)]
assert execution.closed
def test_run_rejects_a_sync_call_that_suspends_and_closes_the_execution() -> None:
pending: Final = ready(1)
execution: Final = ScriptedExecution([Await(pending)])
with pytest.raises(RuntimeError, match="sync call suspended"):
run(execution, SyncStream)
pending.close()
assert execution.closed
@pytest.mark.parametrize("asynchronous", (False, True))
def test_closing_a_stream_tells_the_execution_once_and_closes_it(asynchronous: bool) -> None:
execution: Final = ScriptedExecution([StreamChunk("first"), Complete(None)])
async def read_then_close() -> object:
stream: Final = AsyncStream(execution, {})
first: Final = await anext(stream)
await stream.aclose()
await stream.aclose()
return first
def read_then_close_sync() -> object:
stream: Final = SyncStream(execution, {})
first: Final = next(stream)
stream.close()
stream.close()
return first
assert (asyncio.run(read_then_close()) if asynchronous else read_then_close_sync()) == "first"
assert execution.resumed == [("value", None), ("error", StreamClosed)]
assert execution.closed
@pytest.mark.parametrize("asynchronous", (False, True))
def test_stream_read_failure_closes_the_execution_and_ends_the_stream(asynchronous: bool) -> None:
failure: Final = ValueError("read failed")
execution: Final = ScriptedExecution([failure])
async def read_twice() -> None:
stream: Final = AsyncStream(execution, {})
with pytest.raises(ValueError, match="read failed") as caught:
await anext(stream)
assert caught.value is failure
with pytest.raises(StopAsyncIteration):
await anext(stream)
def read_twice_sync() -> None:
stream: Final = SyncStream(execution, {})
with pytest.raises(ValueError, match="read failed") as caught:
next(stream)
assert caught.value is failure
with pytest.raises(StopIteration):
next(stream)
asyncio.run(read_twice()) if asynchronous else read_twice_sync()
assert execution.resumed == [("value", None)]
assert execution.closed