mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-02 07:13:02 +00:00
* 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>
193 lines
5.7 KiB
Python
193 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping
|
|
from contextlib import ExitStack
|
|
from dataclasses import dataclass
|
|
from typing import Final, Protocol, TypeVar, cast # noqa: TID251 # trusts the native stub at the binding boundary
|
|
|
|
_Call: Final = TypeVar("_Call")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Await:
|
|
awaitable: Awaitable[object]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Complete:
|
|
value: object
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class StreamOpen:
|
|
head: object
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class StreamChunk:
|
|
chunk: object
|
|
|
|
|
|
StreamStep = StreamOpen | StreamChunk
|
|
Settled = Complete | StreamStep
|
|
Step = Await | Settled
|
|
|
|
|
|
class Execution(Protocol):
|
|
def send(self, value: object) -> Step: ...
|
|
|
|
def throw(self, error: BaseException) -> Step: ...
|
|
|
|
def close(self) -> None: ...
|
|
|
|
|
|
_REOPENED: Final = "execution opened a stream twice"
|
|
|
|
|
|
def _headers(head: object) -> Mapping[str, object]:
|
|
return cast("Mapping[str, object]", head) # cast-ok: encode_stream_head always builds a str-keyed dict
|
|
|
|
|
|
class StreamClosed(Exception):
|
|
"""Tells a streaming execution that its caller stopped reading."""
|
|
|
|
|
|
async def _settle(execution: Execution, step: Step) -> Settled:
|
|
while isinstance(step, Await):
|
|
try:
|
|
value = await step.awaitable
|
|
except GeneratorExit:
|
|
raise
|
|
except BaseException as error:
|
|
step = execution.throw(error) # rebind-ok: advance the execution protocol
|
|
else:
|
|
step = execution.send(value) # rebind-ok: advance the execution protocol
|
|
return step
|
|
|
|
|
|
def _settled(step: Step) -> Settled:
|
|
if isinstance(step, Await):
|
|
raise RuntimeError("sync call suspended")
|
|
return step
|
|
|
|
|
|
def _hand_off(
|
|
execution: Execution, step: Settled, open_stream: Callable[[Execution, object], object], cleanup: ExitStack
|
|
) -> object:
|
|
match step:
|
|
case Complete(value):
|
|
return value
|
|
case StreamOpen(head):
|
|
stream: Final = open_stream(execution, head)
|
|
cleanup.pop_all()
|
|
return stream
|
|
case StreamChunk():
|
|
raise RuntimeError("execution yielded a chunk before opening a stream")
|
|
|
|
|
|
async def drive(execution: Execution, open_stream: Callable[[Execution, object], object]) -> object:
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(execution.close)
|
|
return _hand_off(execution, await _settle(execution, execution.send(None)), open_stream, cleanup)
|
|
|
|
|
|
def run(execution: Execution, open_stream: Callable[[Execution, object], object]) -> object:
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(execution.close)
|
|
return _hand_off(execution, _settled(execution.send(None)), open_stream, cleanup)
|
|
|
|
|
|
def synchronous(native: Callable[[_Call], object]) -> Callable[[_Call], object]:
|
|
return lambda call: run(_execution(native(call)), SyncStream)
|
|
|
|
|
|
def asynchronous(native: Callable[[_Call], object]) -> Callable[[_Call], Awaitable[object]]:
|
|
return lambda call: drive(_execution(native(call)), AsyncStream)
|
|
|
|
|
|
def _execution(value: object) -> Execution:
|
|
return cast("Execution", value) # cast-ok: native entrypoints return Execution per _native.pyi
|
|
|
|
|
|
class AsyncStream(AsyncIterator[object]):
|
|
"""A streamed native call: each read resumes the execution until its next chunk."""
|
|
|
|
def __init__(self, execution: Execution, head: object) -> None:
|
|
self._execution: Final = execution
|
|
self._done = False
|
|
self._hidden_params: dict[str, object] = dict(_headers(head)) # mutable-ok: header writers mutate it
|
|
|
|
def __aiter__(self) -> AsyncStream:
|
|
return self
|
|
|
|
async def __anext__(self) -> object:
|
|
if self._done:
|
|
raise StopAsyncIteration
|
|
try:
|
|
step: Final = await _settle(self._execution, self._execution.send(None))
|
|
except BaseException:
|
|
self._finish()
|
|
raise
|
|
match step:
|
|
case StreamChunk(chunk):
|
|
return chunk
|
|
case Complete():
|
|
self._finish()
|
|
raise StopAsyncIteration
|
|
case StreamOpen():
|
|
self._finish()
|
|
raise RuntimeError(_REOPENED)
|
|
|
|
async def aclose(self) -> None:
|
|
if self._done:
|
|
return
|
|
try:
|
|
await _settle(self._execution, self._execution.throw(StreamClosed()))
|
|
finally:
|
|
self._finish()
|
|
|
|
def _finish(self) -> None:
|
|
self._done = True
|
|
self._execution.close()
|
|
|
|
|
|
class SyncStream(Iterator[object]):
|
|
"""The sync form of `AsyncStream`; its execution never suspends on an awaitable."""
|
|
|
|
def __init__(self, execution: Execution, head: object) -> None:
|
|
self._execution: Final = execution
|
|
self._done = False
|
|
self._hidden_params: dict[str, object] = dict(_headers(head)) # mutable-ok: header writers mutate it
|
|
|
|
def __iter__(self) -> SyncStream:
|
|
return self
|
|
|
|
def __next__(self) -> object:
|
|
if self._done:
|
|
raise StopIteration
|
|
try:
|
|
step: Final = _settled(self._execution.send(None))
|
|
except BaseException:
|
|
self._finish()
|
|
raise
|
|
match step:
|
|
case StreamChunk(chunk):
|
|
return chunk
|
|
case Complete():
|
|
self._finish()
|
|
raise StopIteration
|
|
case StreamOpen():
|
|
self._finish()
|
|
raise RuntimeError(_REOPENED)
|
|
|
|
def close(self) -> None:
|
|
if self._done:
|
|
return
|
|
try:
|
|
_settled(self._execution.throw(StreamClosed()))
|
|
finally:
|
|
self._finish()
|
|
|
|
def _finish(self) -> None:
|
|
self._done = True
|
|
self._execution.close()
|