* refactor(rust): extract litellm-host-native as the shared Rust host driver Move service and hook dispatch out of host-http into a Driver that owns the machine and Rust handlers, returning at completion or a stream boundary and holding the demand reply until the consumer advances. Move the in-process runner onto the same driver. host-http now layers encoding, SSE, body polling and lifecycle observation over it. host-python keeps driving litellm-host directly Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(rust): interrupt the machine when the in-process stream consumer fails Restores the pre-refactor interruption path for StreamConsumer errors via Driver::fail and ports the generic run lifecycle tests into host-native. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(rust): separate the machine contract from coroutine execution * auth update * refactor(rust): use standard flow control for host requests * style(rust): keep host driver imports formatted * chores * mostly relocation * refactor(rust): separate interceptors from queued observers * refactor(rust): centralize legacy callback mappings and lifecycle * docs: define Python host boundaries and migration plan * refactor: enforce Python host and bridge boundaries * refactor(rust): separate operations from callback composition * refactor(rust): compose SDK policy through call hooks --------- Co-authored-by: Yujong Lee <yujong@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
6.5 KiB
- Target invariants; implementation and runtime validation may lag these rules
Boundary with Python consumers
This crate owns CPython execution mechanics for generic litellm-host machines and hooks. Consumers supply domain bindings, host operations, public result construction and callback policy. Neither Rust dependencies nor Python imports may require LiteLLM route modules or legacy Logging
src/native.rs belongs here: it runs a generic machine through the Python runtime and owns its pending execution and abort handle. Keep provider selection, request projection and public exception policy out of it. A rename to machine_runner.rs is optional and must not change behavior
The execution handle receives its Python lifecycle binding from its consumer through PythonLifecycle rather than import a fixed litellm.rust_bridge module. Generic suspension and execution state validation belong here; public stream wrappers and _hidden_params conventions belong to the consumer
Creating a resolved asyncio Future from an already constructed Python value belongs here, alongside runtime waiting, interpreter detachment and panic containment. Choosing which callable exceptions become a public RuntimeError belongs to the consumer; python-bridge::callable::wrap_failure owns that policy
The driver owns ordering: start, argument preparation, prepared-argument hooks, binding decode and machine start. Fallible per-call resource setup supplied by the consumer runs after all argument hooks, using the prepared argument view, and before provider work. Setup failure follows the existing terminal failure path. Creating or discarding an unstarted coroutine must not initialize clients, acquire credentials or capture execution context
Boundary tests exercise behavior with a supplied lifecycle binding without importing the LiteLLM Python package. Pin inline awaiting, awaitable final values, exception identity, cancellation, re-entry and release of retained objects, rather than module names or source layout
HookChain composes Python runtime hooks in order. Each argument, wire-request and response transformation feeds its result to the next hook. After all argument transformations, the driver calls arguments_prepared on every hook in order. Retained callback views must adopt that dictionary before later policy hooks can mutate or reject it. SDK policy is supplied by bridge composition as a hook, never a separate driver phase or parameter. Hooks implement only the stages they need; default stages preserve the supplied values
Terminal notifications share the selected response or exception. An ordinary notification error is reported as unraisable and does not skip the next hook or replace the selected outcome. Preparation, interception and transformation errors stop the chain. Cancellation stops all further hook dispatch. Suspensions stay inline in the existing driver, and the chain traverses retained event values for GC
Existing runtime invariants
- Keep this crate the CPython runtime adapter and nothing more: Serde marshalling, interpreter detachment, tokio/asyncio glue, the
Executionhandle, the call driver and thePythonBinding,PythonHostCallsandPythonOwnedtraits, and thePythonRuntimespecialization ofhost::hooks::CallHooks- No LiteLLM domain dependencies beyond
litellm-host: no route types, noLoggingpolicy, no public API registration, no cdylib build features PythonCallHooksonly constrains the shared call-stage interface toPythonRuntimeand Python ownership. It must not redeclare the stagesPythonCallEventis a specialization of the sharedCallEvent, never a separately defined lifecycle. The driver emitsSucceededorFailedexactly once and never dispatches callbacks after a cancellation; which Python objects consume those events is the legacy adapter's businessCallOptionscan publish snapshots independently of callback delivery. Terminal snapshots follow completed hook dispatch, and cancellation never calls a Python callback. For Python-driven calls, leave the machine's observation publisher unset so the driver is the sole publisher of intercepted provider-response snapshotsPythonBinding::decode_requestreceives the keyword view returned byprepare_argumentsand updated byarguments_prepared, not the caller's dict; a binding that decodes from it inherits all composed argument rewrites- A native failure, including one a host op returns as
InvokeError::Native, is classified exactly once through the binding'smap_error; a Python exception raised inside the call, and a failure inprepare_argumentsortransform_response, is raised as is - A failing
map_erroris raised with the native error's text as its__context__, never swallowed
- No LiteLLM domain dependencies beyond
- Use standard PyO3 ownership and conversion APIs
- Prefer
Bound<'py, T>for attached operations/results,Py<T>for retention; binding/unbinding does not copy payloads - Use
pythonizefor selected Serde data, never a JSON-text round trip; share conversion withPythonized<T> - Preserve
PythonizeError's standard conversion intoPyErr; do not stringify original Python exceptions into newValueErrors - Keep serializer-panic containment in
Pythonized<T>: async output conversion can run in an unjoined blocking task and otherwise strand delivery
- Prefer
- Use
Python::detachfor Rust-only work; Python operations require attachment- Keep diagnostic counters in the consumer; wrapper invocations do not measure every interpreter release
- Release exclusive class borrows/locks before Python calls or decrements that can invoke finalizers; expose retained Python edges to GC without calling Python during traversal
- Keep coroutine driving in the shared Python driver and the native handle
- Shared driver implementation:
litellm/rust_bridge/lifecycle.py; handle:src/handle.rs; call driver:src/driver.rs; native-backed behavior tests:tests/lifecycle.py. The consumer supplies the lifecycle binding - Every lifecycle suspension is awaited inline in the caller's task;
into_futurecreates a separate task and cannot satisfy this contract
- Shared driver implementation:
- References: ownership, conversions, pythonize errors