litellm/tests/unit/rust_bridge/test_bindings.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

145 lines
5.2 KiB
Python

import asyncio
from collections.abc import Coroutine
from dataclasses import dataclass
from types import SimpleNamespace
from typing import Final, cast # noqa: TID251 # narrows the async binding's untyped coroutine
import pytest
from litellm.rust_bridge import bindings
from litellm.rust_bridge.chat_completions import entrypoints as chat_completions
from litellm.rust_bridge.embeddings import entrypoints as embeddings
from litellm.rust_bridge.execution import Complete, Step
from litellm.rust_bridge.messages import entrypoints as messages
from litellm.rust_bridge.ocr import entrypoints as ocr
from litellm.rust_bridge.public_call import NativeCall
from litellm.rust_bridge.responses import entrypoints as responses
from litellm.rust_bridge.transcription import native as transcription
def test_binding_distinguishes_disable_from_reset(monkeypatch) -> None:
native = SimpleNamespace(route=lambda: "native")
monkeypatch.setattr(bindings, "get_native_bridge", lambda: native)
binding: bindings.NativeBinding[object] = bindings.NativeBinding("route", validate=lambda value: value)
assert binding.load() is native.route
binding.override(None)
assert binding.load() is None
replacement = object()
binding.override(replacement)
assert binding.load() is replacement
binding.reset()
assert binding.load() is native.route
@pytest.mark.parametrize(("value", "expected"), ((3, 3), ("invalid", None), (None, None)))
def test_binding_validates_native_attribute(
monkeypatch: pytest.MonkeyPatch, value: object, expected: int | None
) -> None:
native: Final = SimpleNamespace(route=value)
monkeypatch.setattr(bindings, "get_native_bridge", lambda: native)
binding: Final = bindings.NativeBinding("route", validate=lambda item: item if isinstance(item, int) else None)
assert binding.load() == expected
EXECUTION_BINDINGS: Final = (
("completion", chat_completions.NATIVE_COMPLETION, False),
("acompletion", chat_completions.NATIVE_ACOMPLETION, True),
("embedding", embeddings.NATIVE_EMBEDDING, False),
("aembedding", embeddings.NATIVE_AEMBEDDING, True),
("messages", messages.NATIVE_MESSAGES, False),
("amessages", messages.NATIVE_AMESSAGES, True),
("responses", responses.NATIVE_RESPONSES, False),
("aresponses", responses.NATIVE_ARESPONSES, True),
("ocr", ocr.NATIVE_OCR, False),
("aocr", ocr.NATIVE_AOCR, True),
)
TRANSCRIPTION_BINDINGS: Final = (
("transcription", transcription.NATIVE_TRANSCRIPTION),
("atranscription", transcription.NATIVE_ATRANSCRIPTION),
)
ROUTE_BINDINGS: Final = tuple((attribute, binding) for attribute, binding, _ in EXECUTION_BINDINGS) + (
TRANSCRIPTION_BINDINGS
)
@pytest.mark.parametrize(
("attribute", "route_binding"), ROUTE_BINDINGS, ids=[attribute for attribute, _ in ROUTE_BINDINGS]
)
def test_route_bindings_reject_non_callable_native_attributes(
monkeypatch: pytest.MonkeyPatch, attribute: str, route_binding: bindings.NativeBinding[object]
) -> None:
monkeypatch.setattr(bindings, "get_native_bridge", lambda: SimpleNamespace(**{attribute: "not callable"}))
route_binding.reset()
assert route_binding.load() is None
route_binding.reset()
@pytest.mark.parametrize(
("attribute", "route_binding"), TRANSCRIPTION_BINDINGS, ids=[attribute for attribute, _ in TRANSCRIPTION_BINDINGS]
)
def test_transcription_bindings_expose_the_native_callable_itself(
monkeypatch: pytest.MonkeyPatch, attribute: str, route_binding: bindings.NativeBinding[object]
) -> None:
def native_route() -> None:
pass
monkeypatch.setattr(bindings, "get_native_bridge", lambda: SimpleNamespace(**{attribute: native_route}))
route_binding.reset()
assert route_binding.load() is native_route
route_binding.reset()
@dataclass(frozen=True, slots=True)
class _EchoingExecution:
"""Completes on its first resume with the value it was resumed with."""
def send(self, value: object) -> Step:
return Complete(("completed with", value))
def throw(self, error: BaseException) -> Step:
raise error
def close(self) -> None:
pass
@pytest.mark.parametrize(
("attribute", "route_binding", "asynchronous"),
EXECUTION_BINDINGS,
ids=[attribute for attribute, _, _ in EXECUTION_BINDINGS],
)
def test_execution_bindings_start_the_native_execution_and_return_its_result(
monkeypatch: pytest.MonkeyPatch,
attribute: str,
route_binding: bindings.NativeBinding[object],
asynchronous: bool,
) -> None:
call: Final = NativeCall(args=(), kwargs={}, base={})
received: list[NativeCall] = []
def native_route(incoming: NativeCall) -> _EchoingExecution:
received.append(incoming)
return _EchoingExecution()
monkeypatch.setattr(bindings, "get_native_bridge", lambda: SimpleNamespace(**{attribute: native_route}))
route_binding.reset()
bound: Final = route_binding.load()
assert callable(bound)
outcome: Final = bound(call)
result: Final = (
asyncio.run(cast("Coroutine[None, None, object]", outcome)) # cast-ok: async bindings return a coroutine
if asynchronous
else outcome
)
route_binding.reset()
assert received == [call]
assert result == ("completed with", None)