mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-09 08:18:12 +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>
153 lines
5.1 KiB
Rust
153 lines
5.1 KiB
Rust
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
|
|
use crate::{Error, panic_to_pyerr};
|
|
use pyo3::exceptions::PyBaseException;
|
|
use pyo3::gc::{PyTraverseError, PyVisit};
|
|
use pyo3::prelude::*;
|
|
|
|
pub type StepTypes = for<'py> fn(Python<'py>) -> PyResult<Bound<'py, PyModule>>;
|
|
|
|
/// What a suspended execution waits on: an awaitable the caller awaits, or a stream
|
|
/// boundary the caller's stream reads past.
|
|
pub enum Suspension {
|
|
Await(Py<PyAny>),
|
|
/// The call streams: the caller gets a stream over this execution carrying this head,
|
|
/// and the execution stays suspended until the stream asks for a chunk.
|
|
StreamOpen(Py<PyAny>),
|
|
StreamChunk(Py<PyAny>),
|
|
}
|
|
|
|
enum Resume {
|
|
Send(Py<PyAny>),
|
|
Throw(PyErr),
|
|
}
|
|
|
|
pub enum ExecutionStep {
|
|
Suspended(Suspension),
|
|
Return(Py<PyAny>),
|
|
}
|
|
|
|
pub trait ExecutionBody: Send + Sync {
|
|
fn resume(&mut self, result: Option<PyResult<Py<PyAny>>>) -> PyResult<ExecutionStep>;
|
|
fn traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError>;
|
|
}
|
|
|
|
enum ExecutionState {
|
|
Created(Box<dyn ExecutionBody>),
|
|
Running,
|
|
Suspended(Box<dyn ExecutionBody>),
|
|
Closed,
|
|
}
|
|
|
|
#[pyclass]
|
|
pub struct Execution {
|
|
state: ExecutionState,
|
|
steps: StepTypes,
|
|
}
|
|
|
|
impl Execution {
|
|
pub fn new(body: impl ExecutionBody + 'static, steps: StepTypes) -> Self {
|
|
Self {
|
|
state: ExecutionState::Created(Box::new(body)),
|
|
steps,
|
|
}
|
|
}
|
|
|
|
fn advance(slf: &Bound<'_, Self>, py: Python<'_>, input: Resume) -> PyResult<Py<PyAny>> {
|
|
let (mut body, started) = {
|
|
let mut execution = slf.borrow_mut();
|
|
let started = match (&execution.state, &input) {
|
|
(ExecutionState::Created(_), Resume::Send(value)) if value.is_none(py) => false,
|
|
(ExecutionState::Created(_), Resume::Send(_)) => {
|
|
return Err(Error::SendBeforeStart.into());
|
|
}
|
|
(ExecutionState::Suspended(_), _) => true,
|
|
(ExecutionState::Running, _) => {
|
|
return Err(Error::ExecutionRunning.into());
|
|
}
|
|
(ExecutionState::Closed, _) => {
|
|
return Err(Error::ExecutionClosed.into());
|
|
}
|
|
(ExecutionState::Created(_), Resume::Throw(_)) => {
|
|
return Err(Error::ExecutionOutOfOrder.into());
|
|
}
|
|
};
|
|
match std::mem::replace(&mut execution.state, ExecutionState::Running) {
|
|
ExecutionState::Created(body) | ExecutionState::Suspended(body) => (body, started),
|
|
_ => unreachable!(),
|
|
}
|
|
};
|
|
let result = match input {
|
|
Resume::Send(value) => started.then_some(Ok(value)),
|
|
Resume::Throw(error) => Some(Err(error)),
|
|
};
|
|
let steps = slf.borrow().steps;
|
|
let outcome = catch_unwind(AssertUnwindSafe(|| {
|
|
let step = body.resume(result)?;
|
|
let (tag, value, suspended) = match step {
|
|
ExecutionStep::Suspended(Suspension::Await(value)) => ("Await", value, true),
|
|
ExecutionStep::Suspended(Suspension::StreamOpen(head)) => {
|
|
("StreamOpen", head, true)
|
|
}
|
|
ExecutionStep::Suspended(Suspension::StreamChunk(chunk)) => {
|
|
("StreamChunk", chunk, true)
|
|
}
|
|
ExecutionStep::Return(value) => ("Complete", value, false),
|
|
};
|
|
let step = steps(py)?.getattr(tag)?.call1((value,))?.unbind();
|
|
Ok((step, suspended))
|
|
}))
|
|
.map_err(panic_to_pyerr)
|
|
.and_then(|result| result);
|
|
match outcome {
|
|
Ok((step, true)) if matches!(slf.borrow().state, ExecutionState::Running) => {
|
|
slf.borrow_mut().state = ExecutionState::Suspended(body);
|
|
Ok(step)
|
|
}
|
|
outcome => {
|
|
slf.borrow_mut().state = ExecutionState::Closed;
|
|
drop(body);
|
|
outcome.and_then(|(step, suspended)| {
|
|
if suspended {
|
|
Err(Error::ExecutionClosedWhileRunning.into())
|
|
} else {
|
|
Ok(step)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[pymethods]
|
|
impl Execution {
|
|
fn send(slf: &Bound<'_, Self>, py: Python<'_>, value: Py<PyAny>) -> PyResult<Py<PyAny>> {
|
|
Self::advance(slf, py, Resume::Send(value))
|
|
}
|
|
|
|
fn throw(
|
|
slf: &Bound<'_, Self>,
|
|
py: Python<'_>,
|
|
error: Bound<'_, PyBaseException>,
|
|
) -> PyResult<Py<PyAny>> {
|
|
Self::advance(slf, py, Resume::Throw(PyErr::from_value(error.into_any())))
|
|
}
|
|
|
|
fn close(slf: &Bound<'_, Self>) {
|
|
let state = std::mem::replace(&mut slf.borrow_mut().state, ExecutionState::Closed);
|
|
drop(state);
|
|
}
|
|
|
|
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
|
match &self.state {
|
|
ExecutionState::Created(body) | ExecutionState::Suspended(body) => {
|
|
body.traverse(&visit)
|
|
}
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
fn __clear__(slf: &Bound<'_, Self>) {
|
|
Self::close(slf);
|
|
}
|
|
}
|