mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
refactor(rust): unify public operation entrypoints
This commit is contained in:
parent
0750ef81ca
commit
5bc0591784
85 changed files with 2221 additions and 2865 deletions
|
|
@ -25,6 +25,8 @@ pub enum AdmissionDecline {
|
|||
Provider,
|
||||
#[strum(to_string = "required host operations are not supported")]
|
||||
HostOperations,
|
||||
#[strum(to_string = "request contains values that cannot be inspected without Python effects")]
|
||||
Uninspectable,
|
||||
#[strum(to_string = "{0}")]
|
||||
Feature(&'static str),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,13 @@ pub trait CompletedRoute: Send + Sync + 'static {
|
|||
fn run(request: Self::Request, hooks: Arc<dyn ProviderHooks>)
|
||||
-> WorkflowFuture<Self::Response>;
|
||||
fn context(request: &Self::Request) -> CallLifecycleContext;
|
||||
|
||||
fn operation(asynchronous: bool) -> CompletedCall<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
CompletedCall::new(CompletedWorkflow::default(), asynchronous)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompletedWorkflow<R: CompletedRoute> {
|
||||
|
|
@ -349,7 +356,15 @@ impl<Q: Send + 'static, T: Send + Sync + 'static> ProviderHooks for ExchangeHook
|
|||
pub type CompletedCall<R> = LifecycleCall<CompletedWorkflow<R>>;
|
||||
|
||||
pub async fn run_completed<R: CompletedRoute>(request: R::Request) -> Result<R::Response, Error> {
|
||||
run_completed_with_hooks::<R>(request, Arc::new(NoopProviderHooks)).await
|
||||
let mut call = R::operation(false);
|
||||
drive(
|
||||
&mut call,
|
||||
&CompletedBackend::<R> {
|
||||
request: Mutex::new(Some(request)),
|
||||
route: PhantomData,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run_completed_with_hooks<R: CompletedRoute>(
|
||||
|
|
|
|||
|
|
@ -274,6 +274,15 @@ impl ResponsesWebSocketConnection {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn admit(
|
||||
provider: Option<&str>,
|
||||
) -> Result<(), crate::call_lifecycle::admission::AdmissionDecline> {
|
||||
match provider {
|
||||
Some("openai") => Ok(()),
|
||||
_ => Err(crate::call_lifecycle::admission::AdmissionDecline::Provider),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -335,12 +344,3 @@ mod tests {
|
|||
assert!(!nested_without_flat.data.contains_key("model"));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn admit(
|
||||
provider: Option<&str>,
|
||||
) -> Result<(), crate::call_lifecycle::admission::AdmissionDecline> {
|
||||
match provider {
|
||||
Some("openai") => Ok(()),
|
||||
_ => Err(crate::call_lifecycle::admission::AdmissionDecline::Provider),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ fn cache_options<'py>(value: Option<Bound<'py, PyAny>>) -> PyResult<CacheOptions
|
|||
fn is_true(values: &Bound<'_, PyDict>, name: &str) -> PyResult<bool> {
|
||||
Ok(values
|
||||
.get_item(name)?
|
||||
.is_some_and(|value| value.is(&PyBool::new(values.py(), true))))
|
||||
.is_some_and(|value| value.is(PyBool::new(values.py(), true))))
|
||||
}
|
||||
|
||||
fn optional_text(value: Option<Bound<'_, PyAny>>) -> PyResult<Option<String>> {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,23 @@ pub(crate) fn host_callback_error(py: Python<'_>, error: PyErr) -> PyErr {
|
|||
wrapped
|
||||
}
|
||||
|
||||
pub(crate) fn terminal_pyerr(error: PyErr) -> PyErr {
|
||||
Python::attach(|py| {
|
||||
if error.is_instance_of::<RustBridgeDeclined>(py)
|
||||
|| error.is_instance_of::<RustBridgeUnavailable>(py)
|
||||
{
|
||||
return host_callback_error(py, error);
|
||||
}
|
||||
error
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn admit(
|
||||
result: Result<(), litellm_core::call_lifecycle::admission::AdmissionDecline>,
|
||||
) -> PyResult<()> {
|
||||
result.map_err(|reason| RustBridgeDeclined::new_err(reason.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -144,10 +161,21 @@ mod tests {
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn admit(
|
||||
result: Result<(), litellm_core::call_lifecycle::admission::AdmissionDecline>,
|
||||
) -> PyResult<()> {
|
||||
result.map_err(|reason| RustBridgeDeclined::new_err(reason.to_string()))
|
||||
#[test]
|
||||
fn terminal_reserved_errors_are_wrapped_with_the_original_cause() {
|
||||
Python::initialize();
|
||||
Python::attach(|py| {
|
||||
for error in [
|
||||
RustBridgeDeclined::new_err("callback decline"),
|
||||
RustBridgeUnavailable::new_err("callback unavailable"),
|
||||
] {
|
||||
let original = error.value(py).clone().unbind();
|
||||
let wrapped = terminal_pyerr(error);
|
||||
assert!(wrapped.is_instance_of::<RustHostCallbackError>(py));
|
||||
let cause = wrapped.value(py).getattr("__cause__").unwrap();
|
||||
assert!(cause.is(original.bind(py)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use serde::Serialize;
|
|||
use tokio::runtime::{Handle, Runtime};
|
||||
use tokio::time::{self, MissedTickBehavior};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn run_sync<T, E, F>(
|
||||
py: Python<'_>,
|
||||
future: F,
|
||||
|
|
@ -51,6 +52,7 @@ where
|
|||
release_gil(py, move || runtime.block_on(wait_for_sync_result(future)))?
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn run_sync_on<T, E, F>(
|
||||
py: Python<'_>,
|
||||
runtime: &Runtime,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,21 @@ mod tests {
|
|||
"amessages",
|
||||
"chat_completions",
|
||||
"achat_completions",
|
||||
"embedding",
|
||||
"aembedding",
|
||||
"image_edit",
|
||||
"aimage_edit",
|
||||
"image_generation",
|
||||
"aimage_generation",
|
||||
"moderation",
|
||||
"amoderation",
|
||||
"rerank",
|
||||
"arerank",
|
||||
"ResponsesWebSocketConnection",
|
||||
"responses",
|
||||
"aresponses",
|
||||
"speech",
|
||||
"aspeech",
|
||||
"count_input_tokens",
|
||||
"gil_stats",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ use serde::Serialize;
|
|||
|
||||
use litellm_core::call_lifecycle::host::{HostPhase, HostStep};
|
||||
use litellm_core::call_lifecycle::provider::{
|
||||
CompletedCall, CompletedOperation, CompletedReply, CompletedRoute, CompletedWorkflow,
|
||||
ProviderRequest, ProviderResponse,
|
||||
CompletedCall, CompletedOperation, CompletedReply, CompletedRoute, ProviderRequest,
|
||||
ProviderResponse,
|
||||
};
|
||||
use litellm_core::call_lifecycle::workflow::LifecycleOperation;
|
||||
use litellm_python_interop::{
|
||||
|
|
@ -251,7 +251,8 @@ where
|
|||
R::SYNC_CALL_TYPE.as_str()
|
||||
},
|
||||
&request,
|
||||
)?;
|
||||
)
|
||||
.map_err(crate::errors::terminal_pyerr)?;
|
||||
crate::errors::admit(
|
||||
litellm_core::call_lifecycle::cache::ResponseCachePlan {
|
||||
controls,
|
||||
|
|
@ -259,7 +260,7 @@ where
|
|||
}
|
||||
.admit(),
|
||||
)?;
|
||||
let call = CompletedCall::<R>::new(CompletedWorkflow::default(), asynchronous);
|
||||
let call = R::operation(asynchronous);
|
||||
let host = PythonCompletedHost::<R> {
|
||||
state: PythonCallState::new(
|
||||
py,
|
||||
|
|
@ -277,5 +278,5 @@ where
|
|||
pending: None,
|
||||
route: PhantomData,
|
||||
};
|
||||
run_call(py, call, host)
|
||||
run_call(py, call, host).map_err(crate::errors::terminal_pyerr)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,61 @@
|
|||
use litellm_core::call_lifecycle::provider::ProviderOptions;
|
||||
use litellm_python_interop::from_py_preserving_errors as from_py;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyDict;
|
||||
use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use super::contract::RequestField;
|
||||
|
||||
pub(crate) fn exact_json(value: &Bound<'_, PyAny>) -> bool {
|
||||
let py = value.py();
|
||||
let value_type = value.get_type();
|
||||
if value.is_none()
|
||||
|| value_type.is(py.get_type::<PyBool>())
|
||||
|| value_type.is(py.get_type::<PyInt>())
|
||||
|| value_type.is(py.get_type::<PyFloat>())
|
||||
|| value_type.is(py.get_type::<PyString>())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if value_type.is(py.get_type::<PyList>()) {
|
||||
let Ok(values) = value.cast::<PyList>() else {
|
||||
return false;
|
||||
};
|
||||
return values.iter().all(|item| exact_json(&item));
|
||||
}
|
||||
if value_type.is(py.get_type::<PyDict>()) {
|
||||
let Ok(values) = value.cast::<PyDict>() else {
|
||||
return false;
|
||||
};
|
||||
return values
|
||||
.iter()
|
||||
.all(|(key, item)| key.get_type().is(py.get_type::<PyString>()) && exact_json(&item));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn exact_optional_string(value: Option<&Bound<'_, PyAny>>) -> bool {
|
||||
value.is_none_or(|value| {
|
||||
value.is_none() || value.get_type().is(value.py().get_type::<PyString>())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn exact_list(value: &Bound<'_, PyAny>) -> bool {
|
||||
value.get_type().is(value.py().get_type::<PyList>()) && exact_json(value)
|
||||
}
|
||||
|
||||
pub(crate) fn exact_optional_object(value: Option<&Bound<'_, PyAny>>) -> bool {
|
||||
value.is_none_or(|value| {
|
||||
value.is_none()
|
||||
|| (value.get_type().is(value.py().get_type::<PyDict>()) && exact_json(value))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn exact_optional_bool(value: Option<&Bound<'_, PyAny>>) -> bool {
|
||||
value
|
||||
.is_none_or(|value| value.is_none() || value.get_type().is(value.py().get_type::<PyBool>()))
|
||||
}
|
||||
|
||||
pub(crate) fn required<'py>(
|
||||
request: &Bound<'py, PyDict>,
|
||||
field: RequestField,
|
||||
|
|
|
|||
|
|
@ -260,16 +260,18 @@ impl<R: PythonRoute> ExecutionBody for PythonLifecycle<R> {
|
|||
let result = Python::attach(|py| self.drive(py, result));
|
||||
match result {
|
||||
Ok(ExecutionStep::Await(value)) => Ok(ExecutionStep::Await(value)),
|
||||
result => result.map_err(|error| {
|
||||
Python::attach(|py| {
|
||||
self.route
|
||||
.state_mut()
|
||||
.error
|
||||
.take()
|
||||
.map(|value| PyErr::from_value(value.into_bound(py).into_any()))
|
||||
.unwrap_or(error)
|
||||
result => result
|
||||
.map_err(|error| {
|
||||
Python::attach(|py| {
|
||||
self.route
|
||||
.state_mut()
|
||||
.error
|
||||
.take()
|
||||
.map(|value| PyErr::from_value(value.into_bound(py).into_any()))
|
||||
.unwrap_or(error)
|
||||
})
|
||||
})
|
||||
}),
|
||||
.map_err(crate::errors::terminal_pyerr),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,68 +9,6 @@ use serde_json::{Map, Value};
|
|||
use litellm_core::auth::InputSource;
|
||||
use litellm_python_interop::from_py_preserving_errors as from_py;
|
||||
|
||||
pub(crate) struct RouteOptions {
|
||||
pub(crate) model: String,
|
||||
pub(crate) api_key: Option<String>,
|
||||
pub(crate) api_base: Option<String>,
|
||||
pub(crate) custom_llm_provider: Option<String>,
|
||||
pub(crate) extra_headers: Option<Map<String, Value>>,
|
||||
pub(crate) timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub(crate) struct RouteOptionsInputs {
|
||||
pub(crate) model: String,
|
||||
pub(crate) api_key: Option<String>,
|
||||
pub(crate) api_base: Option<String>,
|
||||
pub(crate) custom_llm_provider: Option<String>,
|
||||
pub(crate) extra_headers: Option<Value>,
|
||||
pub(crate) timeout_seconds: Option<f64>,
|
||||
}
|
||||
|
||||
impl RouteOptions {
|
||||
pub(crate) fn from_python(inputs: RouteOptionsInputs) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
model: inputs.model,
|
||||
api_key: inputs.api_key,
|
||||
api_base: inputs.api_base,
|
||||
custom_llm_provider: inputs.custom_llm_provider,
|
||||
extra_headers: optional_object("extra_headers", inputs.extra_headers)?,
|
||||
timeout: optional_timeout(inputs.timeout_seconds),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn required_array(name: &'static str, value: Value) -> PyResult<Vec<Value>> {
|
||||
match value {
|
||||
Value::Array(values) => Ok(values),
|
||||
_ => Err(PyValueError::new_err(format!("{name} must be a list"))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn required_object(name: &'static str, value: Value) -> PyResult<Map<String, Value>> {
|
||||
match value {
|
||||
Value::Object(values) => Ok(values),
|
||||
_ => Err(PyValueError::new_err(format!("{name} must be a dict"))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn object_or_empty(
|
||||
name: &'static str,
|
||||
value: Option<Value>,
|
||||
) -> PyResult<Map<String, Value>> {
|
||||
match value {
|
||||
Some(value) => required_object(name, value),
|
||||
None => Ok(Map::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_object(
|
||||
name: &'static str,
|
||||
value: Option<Value>,
|
||||
) -> PyResult<Option<Map<String, Value>>> {
|
||||
value.map(|value| required_object(name, value)).transpose()
|
||||
}
|
||||
|
||||
pub(crate) fn optional_timeout(timeout_seconds: Option<f64>) -> Option<Duration> {
|
||||
timeout_seconds.and_then(|secs| {
|
||||
if secs.is_finite() && secs > 0.0 {
|
||||
|
|
@ -110,13 +48,10 @@ struct RequestFieldSources<'py> {
|
|||
impl<'py> RequestFieldSources<'py> {
|
||||
fn extract(proxy_request: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let proxy_request = proxy_request.cast::<PyDict>()?;
|
||||
|
||||
let body = proxy_request
|
||||
.get_item("body_fields")?
|
||||
.or(proxy_request.get_item("body")?);
|
||||
|
||||
let credentials = proxy_request.get_item("credential_fields")?;
|
||||
|
||||
Ok(Self { body, credentials })
|
||||
}
|
||||
|
||||
|
|
@ -138,9 +73,7 @@ pub(crate) fn request_input_sources<'a>(
|
|||
let Some(proxy_request) = kwargs.get_item("proxy_server_request")? else {
|
||||
return Ok(BTreeMap::new());
|
||||
};
|
||||
|
||||
let sources = RequestFieldSources::extract(&proxy_request)?;
|
||||
|
||||
Ok(names
|
||||
.filter(|name| sources.contains(name))
|
||||
.map(|name| (name.to_string(), InputSource::Request))
|
||||
|
|
@ -148,10 +81,7 @@ pub(crate) fn request_input_sources<'a>(
|
|||
}
|
||||
|
||||
pub(crate) fn marshal_headers(headers: Option<Value>) -> PyResult<HashMap<String, String>> {
|
||||
let value = match headers {
|
||||
Some(headers) => headers,
|
||||
None => Value::Object(Map::new()),
|
||||
};
|
||||
let value = headers.unwrap_or_else(|| Value::Object(Map::new()));
|
||||
let Value::Object(headers) = value else {
|
||||
return Err(PyValueError::new_err("headers must be a dict"));
|
||||
};
|
||||
|
|
@ -165,199 +95,3 @@ pub(crate) fn marshal_headers(headers: Option<Value>) -> PyResult<HashMap<String
|
|||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pyo3::exceptions::PyTypeError;
|
||||
use serde_json::json;
|
||||
|
||||
fn eval<'py>(py: Python<'py>, source: &std::ffi::CStr) -> Bound<'py, PyDict> {
|
||||
let locals = PyDict::new(py);
|
||||
py.run(source, Some(&locals), Some(&locals)).unwrap();
|
||||
locals
|
||||
}
|
||||
|
||||
fn sources(
|
||||
py: Python<'_>,
|
||||
proxy: &Bound<'_, PyAny>,
|
||||
names: &[&str],
|
||||
) -> PyResult<BTreeMap<String, InputSource>> {
|
||||
let kwargs = PyDict::new(py);
|
||||
kwargs.set_item("proxy_server_request", proxy)?;
|
||||
request_input_sources(&kwargs, names.iter().copied())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_shapes_preserve_nested_values_and_existing_errors() {
|
||||
let nested = json!([{"role": "user", "content": [{"type": "text", "text": "hi"}]}]);
|
||||
assert_eq!(
|
||||
Value::Array(required_array("messages", nested.clone()).unwrap()),
|
||||
nested
|
||||
);
|
||||
|
||||
let body = json!({"model": "claude", "metadata": {"user": "1"}});
|
||||
assert_eq!(
|
||||
Value::Object(required_object("body", body.clone()).unwrap()),
|
||||
body
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
required_array("messages", json!({"role": "user"}))
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"ValueError: messages must be a list"
|
||||
);
|
||||
assert_eq!(
|
||||
required_object("body", json!([])).unwrap_err().to_string(),
|
||||
"ValueError: body must be a dict"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_parameters_treat_missing_as_empty() {
|
||||
assert_eq!(
|
||||
object_or_empty("optional_params", None).unwrap(),
|
||||
Map::new()
|
||||
);
|
||||
assert_eq!(
|
||||
object_or_empty("optional_params", Some(json!({"temperature": 0.2}))).unwrap(),
|
||||
required_object("optional_params", json!({"temperature": 0.2})).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_none_and_empty_proxy_metadata_are_distinct() {
|
||||
Python::initialize();
|
||||
Python::attach(|py| {
|
||||
let kwargs = PyDict::new(py);
|
||||
assert!(
|
||||
request_input_sources(&kwargs, ["api_key"].into_iter())
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
kwargs.set_item("proxy_server_request", py.None()).unwrap();
|
||||
assert!(
|
||||
request_input_sources(&kwargs, ["api_key"].into_iter())
|
||||
.unwrap_err()
|
||||
.is_instance_of::<PyTypeError>(py)
|
||||
);
|
||||
|
||||
kwargs
|
||||
.set_item("proxy_server_request", PyDict::new(py))
|
||||
.unwrap();
|
||||
assert!(
|
||||
request_input_sources(&kwargs, ["api_key"].into_iter())
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn body_fields_win_over_body_and_explicit_none_does_not_fall_back() {
|
||||
Python::initialize();
|
||||
Python::attach(|py| {
|
||||
let locals = eval(
|
||||
py,
|
||||
c"
|
||||
proxy = {'body_fields': ['api_key'], 'body': ['api_base']}
|
||||
none_fields = {'body_fields': None, 'body': ['api_key']}
|
||||
body_only = {'body': ['api_base']}
|
||||
",
|
||||
);
|
||||
let named = sources(
|
||||
py,
|
||||
&locals.get_item("proxy").unwrap().unwrap(),
|
||||
&["api_key", "api_base"],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(named.get("api_key").copied(), Some(InputSource::Request));
|
||||
assert!(!named.contains_key("api_base"));
|
||||
|
||||
assert!(
|
||||
sources(
|
||||
py,
|
||||
&locals.get_item("none_fields").unwrap().unwrap(),
|
||||
&["api_key"],
|
||||
)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
let body_only = sources(
|
||||
py,
|
||||
&locals.get_item("body_only").unwrap().unwrap(),
|
||||
&["api_base"],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
body_only.get("api_base").copied(),
|
||||
Some(InputSource::Request)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn body_and_credential_membership_can_mark_request_fields() {
|
||||
Python::initialize();
|
||||
Python::attach(|py| {
|
||||
let locals = eval(
|
||||
py,
|
||||
c"
|
||||
class Raising:
|
||||
def __contains__(self, item):
|
||||
raise RuntimeError('credential membership')
|
||||
proxy = {
|
||||
'body_fields': ['api_key'],
|
||||
'credential_fields': Raising(),
|
||||
}
|
||||
credentials_only = {'credential_fields': ['extra_headers']}
|
||||
erroring = {'body_fields': Raising()}
|
||||
extra = {'body_fields': ['api_key', 'unused']}
|
||||
",
|
||||
);
|
||||
let skipped = sources(
|
||||
py,
|
||||
&locals.get_item("proxy").unwrap().unwrap(),
|
||||
&["api_key"],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(skipped.get("api_key").copied(), Some(InputSource::Request));
|
||||
|
||||
let credentials = sources(
|
||||
py,
|
||||
&locals.get_item("credentials_only").unwrap().unwrap(),
|
||||
&["extra_headers"],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
credentials.get("extra_headers").copied(),
|
||||
Some(InputSource::Request)
|
||||
);
|
||||
|
||||
assert!(
|
||||
sources(
|
||||
py,
|
||||
&locals.get_item("erroring").unwrap().unwrap(),
|
||||
&["api_key"],
|
||||
)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
let requested = sources(
|
||||
py,
|
||||
&locals.get_item("extra").unwrap().unwrap(),
|
||||
&["api_key"],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(requested.len(), 1);
|
||||
assert_eq!(
|
||||
requested.get("api_key").copied(),
|
||||
Some(InputSource::Request)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,21 +7,44 @@ use pyo3::types::{PyDict, PyTuple};
|
|||
|
||||
use crate::lifecycle::completed::{self, PythonCompletedRoute};
|
||||
use crate::lifecycle::contract::{PythonCallType, RequestField};
|
||||
use crate::lifecycle::request::{object, optional_string, options, required};
|
||||
use crate::lifecycle::request::{
|
||||
exact_list, exact_optional_object, exact_optional_string, object, options, required,
|
||||
};
|
||||
|
||||
impl PythonCompletedRoute for ChatCompletionsRoute {
|
||||
const SYNC_CALL_TYPE: PythonCallType = PythonCallType::Completion;
|
||||
const ASYNC_CALL_TYPE: PythonCallType = PythonCallType::AsyncCompletion;
|
||||
|
||||
fn admit(request: &Bound<'_, PyDict>) -> PyResult<()> {
|
||||
let model = required(request, RequestField::Model)?;
|
||||
let provider = request.get_item(RequestField::CustomLlmProvider.key(request.py()))?;
|
||||
let messages = required(request, RequestField::Messages)?;
|
||||
let params = request.get_item(RequestField::OptionalParams.key(request.py()))?;
|
||||
let headers = request.get_item(RequestField::ExtraHeaders.key(request.py()))?;
|
||||
let facts = request.get_item(RequestField::HostFacts.key(request.py()))?;
|
||||
if !exact_optional_string(Some(&model))
|
||||
|| !exact_optional_string(provider.as_ref())
|
||||
|| !exact_list(&messages)
|
||||
|| !exact_optional_object(params.as_ref())
|
||||
|| !exact_optional_object(headers.as_ref())
|
||||
|| !exact_optional_object(facts.as_ref())
|
||||
{
|
||||
return crate::errors::admit(Err(
|
||||
litellm_core::call_lifecycle::admission::AdmissionDecline::Uninspectable,
|
||||
));
|
||||
}
|
||||
let provider: Option<String> = provider
|
||||
.as_ref()
|
||||
.map(|value| value.extract::<Option<String>>())
|
||||
.transpose()?
|
||||
.flatten();
|
||||
crate::errors::admit(litellm_core::chat_completions::admit(
|
||||
&required(request, RequestField::Model)?.extract::<String>()?,
|
||||
optional_string(request, RequestField::CustomLlmProvider)?.as_deref(),
|
||||
from_py(&required(request, RequestField::Messages)?)?,
|
||||
&model.extract::<String>()?,
|
||||
provider.as_deref(),
|
||||
from_py(&messages)?,
|
||||
&object(request, RequestField::OptionalParams)?,
|
||||
Some(&object(request, RequestField::ExtraHeaders)?),
|
||||
request
|
||||
.get_item(RequestField::HostFacts.key(request.py()))?
|
||||
facts
|
||||
.map(|value| from_py(&value))
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
|
|
@ -38,20 +61,28 @@ impl PythonCompletedRoute for ChatCompletionsRoute {
|
|||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _chat_completions_lifecycle(
|
||||
fn chat_completions(
|
||||
py: Python<'_>,
|
||||
request: Bound<'_, PyDict>,
|
||||
args: Bound<'_, PyTuple>,
|
||||
kwargs: Bound<'_, PyDict>,
|
||||
asynchronous: bool,
|
||||
host: Bound<'_, PyAny>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
completed::run::<ChatCompletionsRoute>(py, request, args, kwargs, asynchronous, host)
|
||||
completed::run::<ChatCompletionsRoute>(py, request, args, kwargs, false, host)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn achat_completions(
|
||||
py: Python<'_>,
|
||||
request: Bound<'_, PyDict>,
|
||||
args: Bound<'_, PyTuple>,
|
||||
kwargs: Bound<'_, PyDict>,
|
||||
host: Bound<'_, PyAny>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
completed::run::<ChatCompletionsRoute>(py, request, args, kwargs, true, host)
|
||||
}
|
||||
|
||||
pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
crate::routes::definition::add_function(
|
||||
module,
|
||||
wrap_pyfunction!(_chat_completions_lifecycle, module)?,
|
||||
)
|
||||
crate::routes::definition::add_function(module, wrap_pyfunction!(chat_completions, module)?)?;
|
||||
crate::routes::definition::add_function(module, wrap_pyfunction!(achat_completions, module)?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
mod lifecycle;
|
||||
mod value;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
lifecycle::register(module)?;
|
||||
value::register(module)
|
||||
lifecycle::register(module)
|
||||
}
|
||||
|
||||
#[cfg(feature = "trace-parity")]
|
||||
pub(super) fn register_trace(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
value::register_trace(module)
|
||||
lifecycle::register(module)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
use litellm_core::Error;
|
||||
use std::future::Future;
|
||||
|
||||
use litellm_core::chat_completions::types::{ChatCompletionsRequest, ChatCompletionsResponse};
|
||||
use litellm_core::chat_completions::{AdmissionContext, chat_completions as run_chat_completions};
|
||||
use pyo3::prelude::*;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::errors::execution_error_to_pyerr;
|
||||
use crate::marshal::{RouteOptions, RouteOptionsInputs, object_or_empty, required_array};
|
||||
|
||||
fn prepare_chat_completions(
|
||||
inputs: ChatCompletionsInputs,
|
||||
) -> PyResult<impl Future<Output = Result<ChatCompletionsResponse, Error>> + Send + 'static> {
|
||||
let messages = required_array("messages", inputs.messages)?;
|
||||
let optional_params = object_or_empty("optional_params", inputs.optional_params)?;
|
||||
let context: AdmissionContext = inputs
|
||||
.host_facts
|
||||
.map(serde_json::from_value)
|
||||
.transpose()
|
||||
.map_err(|error| pyo3::exceptions::PyValueError::new_err(error.to_string()))?
|
||||
.unwrap_or_default();
|
||||
let options = RouteOptions::from_python(RouteOptionsInputs {
|
||||
model: inputs.model,
|
||||
api_key: inputs.api_key,
|
||||
api_base: inputs.api_base,
|
||||
custom_llm_provider: inputs.custom_llm_provider,
|
||||
extra_headers: inputs.extra_headers,
|
||||
timeout_seconds: inputs.timeout_seconds,
|
||||
})?;
|
||||
|
||||
crate::errors::admit(litellm_core::chat_completions::admit(
|
||||
&options.model,
|
||||
options.custom_llm_provider.as_deref(),
|
||||
Value::Array(messages.clone()),
|
||||
&optional_params,
|
||||
options.extra_headers.as_ref(),
|
||||
context,
|
||||
))?;
|
||||
if let Some(on_request) = inputs.on_request {
|
||||
Python::attach(|py| {
|
||||
on_request
|
||||
.call0(py)
|
||||
.map(|_| ())
|
||||
.map_err(|error| crate::errors::host_callback_error(py, error))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(async move {
|
||||
let RouteOptions {
|
||||
model,
|
||||
api_key,
|
||||
api_base,
|
||||
custom_llm_provider,
|
||||
extra_headers,
|
||||
timeout,
|
||||
} = options;
|
||||
run_chat_completions(ChatCompletionsRequest {
|
||||
model: &model,
|
||||
messages: Value::Array(messages),
|
||||
optional_params,
|
||||
api_key: api_key.as_deref(),
|
||||
api_base: api_base.as_deref(),
|
||||
custom_llm_provider: custom_llm_provider.as_deref(),
|
||||
extra_headers,
|
||||
timeout,
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
bridge_route! {
|
||||
sync = chat_completions,
|
||||
asynchronous = achat_completions,
|
||||
inputs = ChatCompletionsInputs,
|
||||
required = {
|
||||
model: String,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
messages: serde_json::Value,
|
||||
},
|
||||
optional = {
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
optional_params: Option<serde_json::Value>,
|
||||
api_key: Option<String>,
|
||||
api_base: Option<String>,
|
||||
custom_llm_provider: Option<String>,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
extra_headers: Option<serde_json::Value>,
|
||||
timeout_seconds: Option<f64>,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
host_facts: Option<serde_json::Value>,
|
||||
on_request: Option<Py<PyAny>>,
|
||||
},
|
||||
prepare = prepare_chat_completions,
|
||||
errors = execution_error_to_pyerr,
|
||||
}
|
||||
|
|
@ -3,20 +3,11 @@ use pyo3::prelude::*;
|
|||
use pyo3::types::PyCFunction;
|
||||
|
||||
macro_rules! unimplemented_lifecycle_route {
|
||||
($route:ident, $entrypoint:ident) => {
|
||||
#[pyo3::pyfunction]
|
||||
#[pyo3(signature = (request, args, kwargs, asynchronous, host))]
|
||||
fn $entrypoint(
|
||||
request: pyo3::Bound<'_, pyo3::PyAny>,
|
||||
args: pyo3::Bound<'_, pyo3::types::PyTuple>,
|
||||
kwargs: pyo3::Bound<'_, pyo3::types::PyDict>,
|
||||
asynchronous: bool,
|
||||
host: pyo3::Bound<'_, pyo3::PyAny>,
|
||||
) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
|
||||
($route:ident, $sync:ident, $asynchronous:ident) => {
|
||||
fn decline() -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
|
||||
use litellm_core::call_lifecycle::admission::{
|
||||
UnimplementedRoute, admit_unimplemented,
|
||||
};
|
||||
let _ = (request, args, kwargs, asynchronous, host);
|
||||
match admit_unimplemented(UnimplementedRoute::$route) {
|
||||
Ok(never) => match never {},
|
||||
Err(route) => Err($crate::errors::RustBridgeDeclined::new_err(format!(
|
||||
|
|
@ -25,17 +16,44 @@ macro_rules! unimplemented_lifecycle_route {
|
|||
}
|
||||
}
|
||||
|
||||
#[pyo3::pyfunction]
|
||||
fn $sync(
|
||||
request: pyo3::Bound<'_, pyo3::PyAny>,
|
||||
args: pyo3::Bound<'_, pyo3::types::PyTuple>,
|
||||
kwargs: pyo3::Bound<'_, pyo3::types::PyDict>,
|
||||
host: pyo3::Bound<'_, pyo3::PyAny>,
|
||||
) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
|
||||
let _ = (request, args, kwargs, host);
|
||||
decline()
|
||||
}
|
||||
|
||||
#[pyo3::pyfunction]
|
||||
fn $asynchronous(
|
||||
request: pyo3::Bound<'_, pyo3::PyAny>,
|
||||
args: pyo3::Bound<'_, pyo3::types::PyTuple>,
|
||||
kwargs: pyo3::Bound<'_, pyo3::types::PyDict>,
|
||||
host: pyo3::Bound<'_, pyo3::PyAny>,
|
||||
) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
|
||||
let _ = (request, args, kwargs, host);
|
||||
decline()
|
||||
}
|
||||
|
||||
pub(super) fn register(
|
||||
module: &pyo3::Bound<'_, pyo3::types::PyModule>,
|
||||
) -> pyo3::PyResult<()> {
|
||||
$crate::routes::definition::add_function(
|
||||
module,
|
||||
pyo3::wrap_pyfunction!($entrypoint, module)?,
|
||||
pyo3::wrap_pyfunction!($sync, module)?,
|
||||
)?;
|
||||
$crate::routes::definition::add_function(
|
||||
module,
|
||||
pyo3::wrap_pyfunction!($asynchronous, module)?,
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
macro_rules! bridge_route {
|
||||
(
|
||||
sync = $sync_name:ident,
|
||||
|
|
@ -256,25 +274,17 @@ mod tests {
|
|||
let module = PyModule::new(py, "routes").expect("module should be created");
|
||||
crate::routes::register(&module).expect("routes should register");
|
||||
let routes = [
|
||||
(
|
||||
"ocr",
|
||||
"aocr",
|
||||
"(model, document, api_key=None, api_base=None, custom_llm_provider=None, extra_headers=None, optional_params=None, input_sources=None, timeout_seconds=None)",
|
||||
),
|
||||
("ocr", "aocr", "(request, args, kwargs, host)"),
|
||||
(
|
||||
"transcription",
|
||||
"atranscription",
|
||||
"(model, audio, api_key=None, api_base=None, custom_llm_provider=None, extra_headers=None, optional_params=None, timeout_seconds=None)",
|
||||
),
|
||||
(
|
||||
"messages",
|
||||
"amessages",
|
||||
"(model, body, api_key=None, api_base=None, custom_llm_provider=None, extra_headers=None, timeout_seconds=None, has_agentic_hook=None, on_request=None)",
|
||||
"(request, args, kwargs, host)",
|
||||
),
|
||||
("messages", "amessages", "(request, args, kwargs, host)"),
|
||||
(
|
||||
"chat_completions",
|
||||
"achat_completions",
|
||||
"(model, messages, optional_params=None, api_key=None, api_base=None, custom_llm_provider=None, extra_headers=None, timeout_seconds=None, host_facts=None, on_request=None)",
|
||||
"(request, args, kwargs, host)",
|
||||
),
|
||||
];
|
||||
|
||||
|
|
@ -304,122 +314,39 @@ mod tests {
|
|||
crate::routes::register(&module).expect("routes should register");
|
||||
|
||||
let invalid_messages = PyDict::new(py);
|
||||
let request = PyDict::new(py);
|
||||
request.set_item("model", "anthropic/model").unwrap();
|
||||
request.set_item("messages", &invalid_messages).unwrap();
|
||||
let sync_chat_error = module
|
||||
.getattr("chat_completions")
|
||||
.and_then(|function| function.call1(("model", &invalid_messages)))
|
||||
.and_then(|function| function.call1((&request, (), PyDict::new(py), py.None())))
|
||||
.expect_err("sync chat should reject a non-list messages value");
|
||||
let async_chat_error = module
|
||||
.getattr("achat_completions")
|
||||
.and_then(|function| function.call1(("model", &invalid_messages)))
|
||||
.and_then(|function| function.call1((&request, (), PyDict::new(py), py.None())))
|
||||
.expect_err("async chat should reject a non-list messages value");
|
||||
|
||||
assert_eq!(
|
||||
sync_chat_error.to_string(),
|
||||
"ValueError: messages must be a list"
|
||||
);
|
||||
assert!(sync_chat_error.is_instance_of::<crate::errors::RustBridgeDeclined>(py));
|
||||
assert_eq!(async_chat_error.to_string(), sync_chat_error.to_string());
|
||||
|
||||
let invalid_body = PyList::empty(py);
|
||||
let request = PyDict::new(py);
|
||||
request.set_item("model", "anthropic/model").unwrap();
|
||||
request.set_item("body", &invalid_body).unwrap();
|
||||
let sync_messages_error = module
|
||||
.getattr("messages")
|
||||
.and_then(|function| function.call1(("model", &invalid_body)))
|
||||
.and_then(|function| function.call1((&request, (), PyDict::new(py), py.None())))
|
||||
.expect_err("sync Messages should reject a non-dict body");
|
||||
let async_messages_error = module
|
||||
.getattr("amessages")
|
||||
.and_then(|function| function.call1(("model", &invalid_body)))
|
||||
.and_then(|function| function.call1((&request, (), PyDict::new(py), py.None())))
|
||||
.expect_err("async Messages should reject a non-dict body");
|
||||
|
||||
assert_eq!(
|
||||
sync_messages_error.to_string(),
|
||||
"ValueError: body must be a dict"
|
||||
);
|
||||
assert!(sync_messages_error.is_instance_of::<crate::errors::RustBridgeDeclined>(py));
|
||||
assert_eq!(
|
||||
async_messages_error.to_string(),
|
||||
sync_messages_error.to_string()
|
||||
);
|
||||
|
||||
let invalid_headers = PyList::empty(py);
|
||||
let kwargs = PyDict::new(py);
|
||||
kwargs
|
||||
.set_item("extra_headers", &invalid_headers)
|
||||
.expect("kwargs should accept extra_headers");
|
||||
let document = PyDict::new(py);
|
||||
|
||||
for (sync_name, async_name) in [("ocr", "aocr"), ("transcription", "atranscription")] {
|
||||
let sync_error = module
|
||||
.getattr(sync_name)
|
||||
.and_then(|function| function.call(("model", &document), Some(&kwargs)))
|
||||
.expect_err("sync route should reject non-dict extra_headers");
|
||||
let async_error = module
|
||||
.getattr(async_name)
|
||||
.and_then(|function| function.call(("model", &document), Some(&kwargs)))
|
||||
.expect_err("async route should reject non-dict extra_headers");
|
||||
|
||||
assert_eq!(
|
||||
sync_error.to_string(),
|
||||
"ValueError: extra_headers must be a dict"
|
||||
);
|
||||
assert_eq!(async_error.to_string(), sync_error.to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_input_validation_preserves_left_to_right_order() {
|
||||
Python::initialize();
|
||||
Python::attach(|py| {
|
||||
let module = PyModule::new(py, "routes").expect("module should be created");
|
||||
crate::routes::register(&module).expect("routes should register");
|
||||
let invalid = PyList::empty(py);
|
||||
|
||||
let chat_kwargs = PyDict::new(py);
|
||||
chat_kwargs
|
||||
.set_item("optional_params", &invalid)
|
||||
.expect("kwargs should accept optional_params");
|
||||
chat_kwargs
|
||||
.set_item("extra_headers", &invalid)
|
||||
.expect("kwargs should accept extra_headers");
|
||||
let invalid_messages = PyDict::new(py);
|
||||
let error = module
|
||||
.getattr("chat_completions")
|
||||
.and_then(|function| {
|
||||
function.call(("model", &invalid_messages), Some(&chat_kwargs))
|
||||
})
|
||||
.expect_err("messages should be validated first");
|
||||
assert_eq!(error.to_string(), "ValueError: messages must be a list");
|
||||
|
||||
let valid_messages = PyList::empty(py);
|
||||
let error = module
|
||||
.getattr("chat_completions")
|
||||
.and_then(|function| function.call(("model", &valid_messages), Some(&chat_kwargs)))
|
||||
.expect_err("optional_params should be validated before headers");
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"ValueError: optional_params must be a dict"
|
||||
);
|
||||
|
||||
let headers_kwargs = PyDict::new(py);
|
||||
headers_kwargs
|
||||
.set_item("extra_headers", &invalid)
|
||||
.expect("kwargs should accept extra_headers");
|
||||
let invalid_body = PyList::empty(py);
|
||||
let error = module
|
||||
.getattr("messages")
|
||||
.and_then(|function| function.call(("model", &invalid_body), Some(&headers_kwargs)))
|
||||
.expect_err("body should be validated before headers");
|
||||
assert_eq!(error.to_string(), "ValueError: body must be a dict");
|
||||
|
||||
let invalid_payload =
|
||||
PyModule::new(py, "invalid_payload").expect("invalid payload should be created");
|
||||
for name in ["ocr", "transcription"] {
|
||||
let error = module
|
||||
.getattr(name)
|
||||
.and_then(|function| {
|
||||
function.call(("model", &invalid_payload), Some(&headers_kwargs))
|
||||
})
|
||||
.expect_err("payload should be validated before headers");
|
||||
assert!(!error.to_string().contains("extra_headers"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -429,32 +356,24 @@ mod tests {
|
|||
Python::attach(|py| {
|
||||
let module = PyModule::new(py, "routes").expect("module should be created");
|
||||
crate::routes::register(&module).expect("routes should register");
|
||||
let messages = PyList::empty(py);
|
||||
let messages = PyList::new(py, [PyDict::new(py)]).unwrap();
|
||||
let headers = PyList::empty(py);
|
||||
let omitted = PyDict::new(py);
|
||||
omitted
|
||||
.set_item("extra_headers", &headers)
|
||||
.expect("kwargs should accept extra_headers");
|
||||
let explicit = PyDict::new(py);
|
||||
explicit
|
||||
.set_item("optional_params", py.None())
|
||||
.expect("kwargs should accept optional_params");
|
||||
explicit
|
||||
.set_item("extra_headers", &headers)
|
||||
.expect("kwargs should accept extra_headers");
|
||||
omitted.set_item("model", "anthropic/model").unwrap();
|
||||
omitted.set_item("messages", &messages).unwrap();
|
||||
omitted.set_item("extra_headers", &headers).unwrap();
|
||||
let explicit = omitted.copy().unwrap();
|
||||
explicit.set_item("optional_params", py.None()).unwrap();
|
||||
|
||||
let omitted_error = module
|
||||
.getattr("chat_completions")
|
||||
.and_then(|function| function.call(("model", &messages), Some(&omitted)))
|
||||
.and_then(|function| function.call1((&omitted, (), PyDict::new(py), py.None())))
|
||||
.expect_err("omitted optional_params should reach header validation");
|
||||
let explicit_error = module
|
||||
.getattr("chat_completions")
|
||||
.and_then(|function| function.call(("model", &messages), Some(&explicit)))
|
||||
.and_then(|function| function.call1((&explicit, (), PyDict::new(py), py.None())))
|
||||
.expect_err("None optional_params should reach header validation");
|
||||
assert_eq!(
|
||||
omitted_error.to_string(),
|
||||
"ValueError: extra_headers must be a dict"
|
||||
);
|
||||
assert!(omitted_error.is_instance_of::<crate::errors::RustBridgeDeclined>(py));
|
||||
assert_eq!(explicit_error.to_string(), omitted_error.to_string());
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
// TODO: implement embeddings lifecycle checkpoints before replacing the Python lifecycle
|
||||
unimplemented_lifecycle_route!(Embeddings, _embeddings_lifecycle);
|
||||
unimplemented_lifecycle_route!(Embeddings, embedding, aembedding);
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
// TODO: implement image_edit lifecycle checkpoints before replacing the Python lifecycle
|
||||
unimplemented_lifecycle_route!(ImageEdit, _image_edit_lifecycle);
|
||||
unimplemented_lifecycle_route!(ImageEdit, image_edit, aimage_edit);
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
// TODO: implement image_generation lifecycle checkpoints before replacing the Python lifecycle
|
||||
unimplemented_lifecycle_route!(ImageGeneration, _image_generation_lifecycle);
|
||||
unimplemented_lifecycle_route!(ImageGeneration, image_generation, aimage_generation);
|
||||
|
|
|
|||
|
|
@ -6,18 +6,37 @@ use litellm_python_interop::from_py_preserving_errors as from_py;
|
|||
|
||||
use crate::lifecycle::completed::{self, PythonCompletedRoute};
|
||||
use crate::lifecycle::contract::{PythonCallType, RequestField};
|
||||
use crate::lifecycle::request::{optional_string, options, required};
|
||||
use crate::lifecycle::request::{
|
||||
exact_optional_bool, exact_optional_object, exact_optional_string, options, required,
|
||||
};
|
||||
|
||||
impl PythonCompletedRoute for MessagesRoute {
|
||||
const SYNC_CALL_TYPE: PythonCallType = PythonCallType::AnthropicMessages;
|
||||
const ASYNC_CALL_TYPE: PythonCallType = PythonCallType::AnthropicMessages;
|
||||
|
||||
fn admit(request: &Bound<'_, PyDict>) -> PyResult<()> {
|
||||
let model = required(request, RequestField::Model)?;
|
||||
let provider = request.get_item(RequestField::CustomLlmProvider.key(request.py()))?;
|
||||
let body = request.get_item(RequestField::Body.key(request.py()))?;
|
||||
let host_hook = request.get_item(RequestField::HasAgenticHook.key(request.py()))?;
|
||||
if !exact_optional_string(Some(&model))
|
||||
|| !exact_optional_string(provider.as_ref())
|
||||
|| !exact_optional_object(body.as_ref())
|
||||
|| !exact_optional_bool(host_hook.as_ref())
|
||||
{
|
||||
return crate::errors::admit(Err(
|
||||
litellm_core::call_lifecycle::admission::AdmissionDecline::Uninspectable,
|
||||
));
|
||||
}
|
||||
let provider: Option<String> = provider
|
||||
.as_ref()
|
||||
.map(|value| value.extract::<Option<String>>())
|
||||
.transpose()?
|
||||
.flatten();
|
||||
crate::errors::admit(litellm_core::messages::admit(
|
||||
&required(request, RequestField::Model)?.extract::<String>()?,
|
||||
optional_string(request, RequestField::CustomLlmProvider)?.as_deref(),
|
||||
request
|
||||
.get_item(RequestField::HasAgenticHook.key(request.py()))?
|
||||
&model.extract::<String>()?,
|
||||
provider.as_deref(),
|
||||
host_hook
|
||||
.map(|value| value.extract())
|
||||
.transpose()?
|
||||
.unwrap_or(false),
|
||||
|
|
@ -33,17 +52,28 @@ impl PythonCompletedRoute for MessagesRoute {
|
|||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _messages_lifecycle(
|
||||
fn messages(
|
||||
py: Python<'_>,
|
||||
request: Bound<'_, PyDict>,
|
||||
args: Bound<'_, PyTuple>,
|
||||
kwargs: Bound<'_, PyDict>,
|
||||
asynchronous: bool,
|
||||
host: Bound<'_, PyAny>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
completed::run::<MessagesRoute>(py, request, args, kwargs, asynchronous, host)
|
||||
completed::run::<MessagesRoute>(py, request, args, kwargs, false, host)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn amessages(
|
||||
py: Python<'_>,
|
||||
request: Bound<'_, PyDict>,
|
||||
args: Bound<'_, PyTuple>,
|
||||
kwargs: Bound<'_, PyDict>,
|
||||
host: Bound<'_, PyAny>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
completed::run::<MessagesRoute>(py, request, args, kwargs, true, host)
|
||||
}
|
||||
|
||||
pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
crate::routes::definition::add_function(module, wrap_pyfunction!(_messages_lifecycle, module)?)
|
||||
crate::routes::definition::add_function(module, wrap_pyfunction!(messages, module)?)?;
|
||||
crate::routes::definition::add_function(module, wrap_pyfunction!(amessages, module)?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
mod lifecycle;
|
||||
mod value;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
lifecycle::register(module)?;
|
||||
value::register(module)
|
||||
lifecycle::register(module)
|
||||
}
|
||||
|
||||
#[cfg(feature = "trace-parity")]
|
||||
pub(super) fn register_trace(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
value::register_trace(module)
|
||||
lifecycle::register(module)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
use litellm_core::Error;
|
||||
use litellm_core::messages::messages as run_messages;
|
||||
use litellm_core::messages::types::{AnthropicMessagesResponse, MessagesRequest};
|
||||
use pyo3::prelude::*;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
|
||||
use crate::errors::{admit, execution_error_to_pyerr};
|
||||
use crate::marshal::{RouteOptions, RouteOptionsInputs, required_object};
|
||||
|
||||
fn prepare_messages(
|
||||
inputs: MessagesInputs,
|
||||
) -> PyResult<impl Future<Output = Result<AnthropicMessagesResponse, Error>> + Send + 'static> {
|
||||
let body = required_object("body", inputs.body)?;
|
||||
let options = RouteOptions::from_python(RouteOptionsInputs {
|
||||
model: inputs.model,
|
||||
api_key: inputs.api_key,
|
||||
api_base: inputs.api_base,
|
||||
custom_llm_provider: inputs.custom_llm_provider,
|
||||
extra_headers: inputs.extra_headers,
|
||||
timeout_seconds: inputs.timeout_seconds,
|
||||
})?;
|
||||
|
||||
admit(litellm_core::messages::admit(
|
||||
&options.model,
|
||||
options.custom_llm_provider.as_deref(),
|
||||
inputs.has_agentic_hook.unwrap_or(false),
|
||||
))?;
|
||||
if let Some(on_request) = inputs.on_request {
|
||||
Python::attach(|py| {
|
||||
on_request
|
||||
.call0(py)
|
||||
.map(|_| ())
|
||||
.map_err(|error| crate::errors::host_callback_error(py, error))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(async move {
|
||||
let RouteOptions {
|
||||
model,
|
||||
api_key,
|
||||
api_base,
|
||||
custom_llm_provider,
|
||||
extra_headers,
|
||||
timeout,
|
||||
} = options;
|
||||
run_messages(MessagesRequest {
|
||||
model: &model,
|
||||
body: Value::Object(body),
|
||||
api_key: api_key.as_deref(),
|
||||
api_base: api_base.as_deref(),
|
||||
custom_llm_provider: custom_llm_provider.as_deref(),
|
||||
extra_headers,
|
||||
timeout,
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
bridge_route! {
|
||||
sync = messages,
|
||||
asynchronous = amessages,
|
||||
inputs = MessagesInputs,
|
||||
required = {
|
||||
model: String,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
body: serde_json::Value,
|
||||
},
|
||||
optional = {
|
||||
api_key: Option<String>,
|
||||
api_base: Option<String>,
|
||||
custom_llm_provider: Option<String>,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
extra_headers: Option<serde_json::Value>,
|
||||
timeout_seconds: Option<f64>,
|
||||
has_agentic_hook: Option<bool>,
|
||||
on_request: Option<Py<PyAny>>,
|
||||
},
|
||||
prepare = prepare_messages,
|
||||
errors = execution_error_to_pyerr,
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
// TODO: implement moderation lifecycle checkpoints before replacing the Python lifecycle
|
||||
unimplemented_lifecycle_route!(Moderation, _moderation_lifecycle);
|
||||
unimplemented_lifecycle_route!(Moderation, moderation, amoderation);
|
||||
|
|
|
|||
|
|
@ -284,8 +284,7 @@ impl litellm_core::ocr::hooks::OcrHooks for BridgeOcrHooks {
|
|||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _ocr_lifecycle(
|
||||
fn run(
|
||||
py: Python<'_>,
|
||||
request: Bound<'_, PyAny>,
|
||||
args: Bound<'_, PyTuple>,
|
||||
|
|
@ -321,6 +320,29 @@ fn _ocr_lifecycle(
|
|||
run_call(py, call, host)
|
||||
}
|
||||
|
||||
pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
crate::routes::definition::add_function(module, wrap_pyfunction!(_ocr_lifecycle, module)?)
|
||||
#[pyfunction]
|
||||
fn ocr(
|
||||
py: Python<'_>,
|
||||
request: Bound<'_, PyAny>,
|
||||
args: Bound<'_, PyTuple>,
|
||||
kwargs: Bound<'_, PyDict>,
|
||||
host: Bound<'_, PyAny>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
run(py, request, args, kwargs, false, host)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn aocr(
|
||||
py: Python<'_>,
|
||||
request: Bound<'_, PyAny>,
|
||||
args: Bound<'_, PyTuple>,
|
||||
kwargs: Bound<'_, PyDict>,
|
||||
host: Bound<'_, PyAny>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
run(py, request, args, kwargs, true, host)
|
||||
}
|
||||
|
||||
pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
crate::routes::definition::add_function(module, wrap_pyfunction!(ocr, module)?)?;
|
||||
crate::routes::definition::add_function(module, wrap_pyfunction!(aocr, module)?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,18 @@ mod document;
|
|||
mod errors;
|
||||
mod lifecycle;
|
||||
mod project;
|
||||
mod value;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
value::register(module)?;
|
||||
document::register(module)?;
|
||||
lifecycle::register(module)
|
||||
}
|
||||
|
||||
#[cfg(feature = "trace-parity")]
|
||||
pub(super) fn register_trace(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
value::register_trace(module)
|
||||
document::register(module)?;
|
||||
lifecycle::register(module)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -27,7 +26,8 @@ mod tests {
|
|||
Python::initialize();
|
||||
Python::attach(|py| {
|
||||
for name in [
|
||||
"_ocr_lifecycle",
|
||||
"ocr",
|
||||
"aocr",
|
||||
"_ocr_upload_document",
|
||||
"_ocr_file_document",
|
||||
"_ocr_mime_type",
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
use std::future::Future;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use serde_json::Value;
|
||||
|
||||
use litellm_core::Error;
|
||||
use litellm_core::ocr::wire::{OcrWireRequest, decode_request, validate_document_url};
|
||||
|
||||
use super::errors::to_pyerr as ocr_error_to_pyerr;
|
||||
use crate::marshal::{RouteOptions, RouteOptionsInputs, object_or_empty};
|
||||
|
||||
fn prepare_ocr(
|
||||
inputs: OcrInputs,
|
||||
) -> PyResult<impl Future<Output = Result<Value, Error>> + Send + 'static> {
|
||||
let document = inputs.document;
|
||||
let options = RouteOptions::from_python(RouteOptionsInputs {
|
||||
model: inputs.model,
|
||||
api_key: inputs.api_key,
|
||||
api_base: inputs.api_base,
|
||||
custom_llm_provider: inputs.custom_llm_provider,
|
||||
extra_headers: inputs.extra_headers,
|
||||
timeout_seconds: inputs.timeout_seconds,
|
||||
})?;
|
||||
let optional_params = object_or_empty("optional_params", inputs.optional_params)?;
|
||||
let input_sources = inputs
|
||||
.input_sources
|
||||
.map(serde_json::from_value)
|
||||
.transpose()
|
||||
.map_err(|error| pyo3::exceptions::PyValueError::new_err(error.to_string()))?
|
||||
.unwrap_or_default();
|
||||
|
||||
validate_document_url(&document)
|
||||
.map_err(Error::from)
|
||||
.map_err(ocr_error_to_pyerr)?;
|
||||
crate::errors::admit(litellm_core::ocr::admit_value(
|
||||
&options.model,
|
||||
options.custom_llm_provider.as_deref(),
|
||||
))?;
|
||||
|
||||
Ok(async move {
|
||||
let RouteOptions {
|
||||
model,
|
||||
api_key,
|
||||
api_base,
|
||||
custom_llm_provider,
|
||||
extra_headers,
|
||||
timeout,
|
||||
} = options;
|
||||
let request = decode_request(OcrWireRequest {
|
||||
model,
|
||||
document,
|
||||
api_key,
|
||||
api_base,
|
||||
custom_llm_provider,
|
||||
extra_headers,
|
||||
optional_params,
|
||||
input_sources,
|
||||
timeout_seconds: timeout.map(|value| value.as_secs_f64()),
|
||||
})?;
|
||||
litellm_core::ocr::ocr(request)
|
||||
.await
|
||||
.map(|response| response.into_json())
|
||||
})
|
||||
}
|
||||
|
||||
bridge_route! {
|
||||
sync = ocr,
|
||||
asynchronous = aocr,
|
||||
inputs = OcrInputs,
|
||||
required = {
|
||||
model: String,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
document: serde_json::Value,
|
||||
},
|
||||
optional = {
|
||||
api_key: Option<String>,
|
||||
api_base: Option<String>,
|
||||
custom_llm_provider: Option<String>,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
extra_headers: Option<serde_json::Value>,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
optional_params: Option<serde_json::Value>,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
input_sources: Option<serde_json::Value>,
|
||||
timeout_seconds: Option<f64>,
|
||||
},
|
||||
prepare = prepare_ocr,
|
||||
errors = ocr_error_to_pyerr,
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
// TODO: implement rerank lifecycle checkpoints before replacing the Python lifecycle
|
||||
unimplemented_lifecycle_route!(Rerank, _rerank_lifecycle);
|
||||
unimplemented_lifecycle_route!(Rerank, rerank, arerank);
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
// TODO: implement responses lifecycle checkpoints before replacing the Python lifecycle
|
||||
unimplemented_lifecycle_route!(Responses, _responses_lifecycle);
|
||||
unimplemented_lifecycle_route!(Responses, responses, aresponses);
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
// TODO: implement speech lifecycle checkpoints before replacing the Python lifecycle
|
||||
unimplemented_lifecycle_route!(Speech, _speech_lifecycle);
|
||||
unimplemented_lifecycle_route!(Speech, speech, aspeech);
|
||||
|
|
|
|||
|
|
@ -8,16 +8,31 @@ use litellm_python_interop::from_py_preserving_errors as from_py;
|
|||
|
||||
use crate::lifecycle::completed::{self, PythonCompletedRoute};
|
||||
use crate::lifecycle::contract::{PythonCallType, RequestField};
|
||||
use crate::lifecycle::request::{object, optional_string, options, required};
|
||||
use crate::lifecycle::request::{
|
||||
exact_optional_object, exact_optional_string, object, optional_string, options, required,
|
||||
};
|
||||
|
||||
impl PythonCompletedRoute for AudioTranscriptionRoute {
|
||||
const SYNC_CALL_TYPE: PythonCallType = PythonCallType::Transcription;
|
||||
const ASYNC_CALL_TYPE: PythonCallType = PythonCallType::AsyncTranscription;
|
||||
|
||||
fn admit(request: &Bound<'_, PyDict>) -> PyResult<()> {
|
||||
let audio = from_py(&required(request, RequestField::Audio)?)?;
|
||||
let model = required(request, RequestField::Model)?;
|
||||
let provider = request.get_item(RequestField::CustomLlmProvider.key(request.py()))?;
|
||||
let audio_value = required(request, RequestField::Audio)?;
|
||||
let optional_params = request.get_item(RequestField::OptionalParams.key(request.py()))?;
|
||||
if !exact_optional_string(Some(&model))
|
||||
|| !exact_optional_string(provider.as_ref())
|
||||
|| !exact_optional_object(Some(&audio_value))
|
||||
|| !exact_optional_object(optional_params.as_ref())
|
||||
{
|
||||
return crate::errors::admit(Err(
|
||||
litellm_core::call_lifecycle::admission::AdmissionDecline::Uninspectable,
|
||||
));
|
||||
}
|
||||
let audio = from_py(&audio_value)?;
|
||||
crate::errors::admit(litellm_core::audio_transcription::admit(
|
||||
&required(request, RequestField::Model)?.extract::<String>()?,
|
||||
&model.extract::<String>()?,
|
||||
optional_string(request, RequestField::CustomLlmProvider)?.as_deref(),
|
||||
&audio,
|
||||
))
|
||||
|
|
@ -33,20 +48,28 @@ impl PythonCompletedRoute for AudioTranscriptionRoute {
|
|||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _transcription_lifecycle(
|
||||
fn transcription(
|
||||
py: Python<'_>,
|
||||
request: Bound<'_, PyDict>,
|
||||
args: Bound<'_, PyTuple>,
|
||||
kwargs: Bound<'_, PyDict>,
|
||||
asynchronous: bool,
|
||||
host: Bound<'_, PyAny>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
completed::run::<AudioTranscriptionRoute>(py, request, args, kwargs, asynchronous, host)
|
||||
completed::run::<AudioTranscriptionRoute>(py, request, args, kwargs, false, host)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn atranscription(
|
||||
py: Python<'_>,
|
||||
request: Bound<'_, PyDict>,
|
||||
args: Bound<'_, PyTuple>,
|
||||
kwargs: Bound<'_, PyDict>,
|
||||
host: Bound<'_, PyAny>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
completed::run::<AudioTranscriptionRoute>(py, request, args, kwargs, true, host)
|
||||
}
|
||||
|
||||
pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
crate::routes::definition::add_function(
|
||||
module,
|
||||
wrap_pyfunction!(_transcription_lifecycle, module)?,
|
||||
)
|
||||
crate::routes::definition::add_function(module, wrap_pyfunction!(transcription, module)?)?;
|
||||
crate::routes::definition::add_function(module, wrap_pyfunction!(atranscription, module)?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
mod lifecycle;
|
||||
mod value;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
pub(super) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
lifecycle::register(module)?;
|
||||
value::register(module)
|
||||
lifecycle::register(module)
|
||||
}
|
||||
|
||||
#[cfg(feature = "trace-parity")]
|
||||
pub(super) fn register_trace(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
value::register_trace(module)
|
||||
lifecycle::register(module)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
use litellm_core::Error;
|
||||
use std::future::Future;
|
||||
|
||||
use litellm_core::audio_transcription::{
|
||||
AudioTranscriptionRequest, audio_transcription as run_audio_transcription,
|
||||
};
|
||||
use pyo3::prelude::*;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::errors::{admit, execution_error_to_pyerr};
|
||||
use crate::marshal::{RouteOptions, RouteOptionsInputs, object_or_empty};
|
||||
|
||||
fn prepare_transcription(
|
||||
inputs: AudioTranscriptionInputs,
|
||||
) -> PyResult<impl Future<Output = Result<Value, Error>> + Send + 'static> {
|
||||
let audio = inputs.audio;
|
||||
let options = RouteOptions::from_python(RouteOptionsInputs {
|
||||
model: inputs.model,
|
||||
api_key: inputs.api_key,
|
||||
api_base: inputs.api_base,
|
||||
custom_llm_provider: inputs.custom_llm_provider,
|
||||
extra_headers: inputs.extra_headers,
|
||||
timeout_seconds: inputs.timeout_seconds,
|
||||
})?;
|
||||
let optional_params = object_or_empty("optional_params", inputs.optional_params)?;
|
||||
|
||||
admit(litellm_core::audio_transcription::admit(
|
||||
&options.model,
|
||||
options.custom_llm_provider.as_deref(),
|
||||
&audio,
|
||||
))?;
|
||||
|
||||
Ok(async move {
|
||||
let RouteOptions {
|
||||
model,
|
||||
api_key,
|
||||
api_base,
|
||||
custom_llm_provider,
|
||||
extra_headers,
|
||||
timeout,
|
||||
} = options;
|
||||
run_audio_transcription(AudioTranscriptionRequest {
|
||||
model: &model,
|
||||
audio,
|
||||
api_key: api_key.as_deref(),
|
||||
api_base: api_base.as_deref(),
|
||||
custom_llm_provider: custom_llm_provider.as_deref(),
|
||||
extra_headers,
|
||||
optional_params,
|
||||
timeout,
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
bridge_route! {
|
||||
sync = transcription,
|
||||
asynchronous = atranscription,
|
||||
inputs = AudioTranscriptionInputs,
|
||||
required = {
|
||||
model: String,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
audio: serde_json::Value,
|
||||
},
|
||||
optional = {
|
||||
api_key: Option<String>,
|
||||
api_base: Option<String>,
|
||||
custom_llm_provider: Option<String>,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
extra_headers: Option<serde_json::Value>,
|
||||
#[pyo3(from_py_with = litellm_python_interop::from_py)]
|
||||
optional_params: Option<serde_json::Value>,
|
||||
timeout_seconds: Option<f64>,
|
||||
},
|
||||
prepare = prepare_transcription,
|
||||
errors = execution_error_to_pyerr,
|
||||
}
|
||||
|
|
@ -911,7 +911,7 @@ openai_compatible_endpoints: Final[list] = [
|
|||
]
|
||||
|
||||
|
||||
openai_compatible_providers: Final[list[str]] = [
|
||||
openai_compatible_providers: Final[list[str]] = [ # mutable-ok: module registry is initialized once
|
||||
"anyscale",
|
||||
"groq",
|
||||
"nvidia_nim",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ from litellm.llms.custom_httpx.http_handler import (
|
|||
_get_httpx_client,
|
||||
get_async_httpx_client,
|
||||
)
|
||||
from litellm.rust_bridge import chat_completions as rust_chat_completions_bridge
|
||||
from litellm.types.llms.anthropic import (
|
||||
ContentBlockDelta,
|
||||
ContentBlockStart,
|
||||
|
|
@ -380,7 +379,7 @@ class AnthropicChatCompletion(BaseLLM):
|
|||
logging_obj.pre_call(
|
||||
input=messages,
|
||||
api_key=api_key,
|
||||
additional_args={
|
||||
additional_args={ # mutable-ok: logging owns this request snapshot
|
||||
"complete_input_dict": data,
|
||||
"api_base": api_base,
|
||||
"headers": request_headers,
|
||||
|
|
@ -487,7 +486,9 @@ class AnthropicChatCompletion(BaseLLM):
|
|||
)
|
||||
|
||||
sync_client: Final = (
|
||||
client if isinstance(client, HTTPHandler) else _get_httpx_client(params={"timeout": timeout})
|
||||
client
|
||||
if isinstance(client, HTTPHandler)
|
||||
else _get_httpx_client(params={"timeout": timeout}) # mutable-ok: client factory owns parameters
|
||||
)
|
||||
try:
|
||||
response: Final = sync_client.post(
|
||||
|
|
@ -526,62 +527,9 @@ class AnthropicChatCompletion(BaseLLM):
|
|||
json_mode=json_mode,
|
||||
)
|
||||
|
||||
rust_optional_params: Final = { # mutable-ok: json.dumps in the bridge rejects a mappingproxy
|
||||
**AnthropicConfig.get_config(model=model),
|
||||
**optional_params,
|
||||
}
|
||||
rust_logging_args: Final = { # mutable-ok: logging callbacks read additional_args as a plain dict
|
||||
"complete_input_dict": { # mutable-ok: same, and it is serialized alongside its parent
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
**rust_optional_params,
|
||||
},
|
||||
"api_base": api_base,
|
||||
"headers": headers,
|
||||
}
|
||||
|
||||
def log_rust_pre_call() -> None:
|
||||
logging_obj.pre_call(input=messages, api_key=api_key, additional_args=rust_logging_args)
|
||||
|
||||
log_rust_post_call: Final = rust_chat_completions_bridge.response_logger(
|
||||
logging_obj=logging_obj,
|
||||
messages=messages,
|
||||
api_key=api_key,
|
||||
additional_args=rust_logging_args,
|
||||
)
|
||||
if acompletion is True:
|
||||
return rust_chat_completions_bridge.achat_completions(
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params=rust_optional_params,
|
||||
model_response=model_response,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=headers,
|
||||
timeout=timeout,
|
||||
stream=stream,
|
||||
litellm_params=litellm_params,
|
||||
on_request=log_rust_pre_call,
|
||||
on_response=log_rust_post_call,
|
||||
python_fallback=acompletion_dispatch,
|
||||
)
|
||||
return rust_chat_completions_bridge.chat_completions(
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params=rust_optional_params,
|
||||
model_response=model_response,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=headers,
|
||||
timeout=timeout,
|
||||
stream=stream,
|
||||
litellm_params=litellm_params,
|
||||
on_request=log_rust_pre_call,
|
||||
on_response=log_rust_post_call,
|
||||
python_fallback=completion_dispatch,
|
||||
)
|
||||
return acompletion_dispatch()
|
||||
return completion_dispatch()
|
||||
|
||||
def embedding(self):
|
||||
# logic for parsing in - calling - parsing out model embedding calls
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ async def anthropic_messages(
|
|||
kwargs["is_async"] = True
|
||||
|
||||
func: Final = partial(
|
||||
anthropic_messages_handler,
|
||||
_python_anthropic_messages_handler,
|
||||
max_tokens=max_tokens,
|
||||
messages=messages,
|
||||
model=model,
|
||||
|
|
@ -664,3 +664,16 @@ def anthropic_messages_handler(
|
|||
stream=stream,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
|
||||
|
||||
from litellm.rust_bridge.messages.lifecycle import wrap_async as _wrap_messages_async
|
||||
from litellm.rust_bridge.messages.lifecycle import wrap_sync as _wrap_messages_sync
|
||||
|
||||
_python_anthropic_messages: Final = anthropic_messages
|
||||
_python_anthropic_messages_handler: Final = anthropic_messages_handler
|
||||
anthropic_messages = _wrap_messages_async( # rebind-ok: public selector wraps the captured Python lifecycle
|
||||
_python_anthropic_messages
|
||||
)
|
||||
anthropic_messages_handler = _wrap_messages_sync( # rebind-ok: public selector wraps the captured Python lifecycle
|
||||
_python_anthropic_messages_handler
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,34 +1,9 @@
|
|||
import base64
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
|
||||
from litellm.litellm_core_utils.audio_utils.utils import process_audio_file
|
||||
from litellm.rust_bridge import transcription as rust_transcription_bridge
|
||||
from litellm.types.utils import FileTypes, TranscriptionResponse
|
||||
|
||||
|
||||
class BedrockAudioTranscriptionRustDispatch:
|
||||
@staticmethod
|
||||
def _audio_payload(audio_file: FileTypes) -> dict[str, object]:
|
||||
processed_audio: Final = process_audio_file(audio_file)
|
||||
formats: Final = {
|
||||
"audio/flac": "flac",
|
||||
"audio/mpeg": "mp3",
|
||||
"audio/mp3": "mp3",
|
||||
"audio/ogg": "ogg",
|
||||
"audio/wav": "wav",
|
||||
"audio/x-wav": "wav",
|
||||
}
|
||||
audio_format: Final = formats.get(processed_audio.content_type) or (
|
||||
processed_audio.filename.rsplit(".", 1)[-1].lower() if "." in processed_audio.filename else ""
|
||||
)
|
||||
return {
|
||||
"data": base64.b64encode(processed_audio.file_content).decode("ascii"),
|
||||
"format": audio_format,
|
||||
"filename": processed_audio.filename,
|
||||
}
|
||||
|
||||
def audio_transcriptions(
|
||||
self,
|
||||
*,
|
||||
|
|
@ -41,18 +16,7 @@ class BedrockAudioTranscriptionRustDispatch:
|
|||
optional_params: dict[str, object],
|
||||
timeout: float | httpx.Timeout | None,
|
||||
) -> TranscriptionResponse:
|
||||
rust_response: Final = rust_transcription_bridge.transcription(
|
||||
model=model,
|
||||
audio=self._audio_payload(audio_file),
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
optional_params=optional_params,
|
||||
timeout=timeout,
|
||||
python_fallback=None,
|
||||
)
|
||||
return TranscriptionResponse(**rust_response)
|
||||
raise RuntimeError("Bedrock audio transcription must be selected at the public boundary")
|
||||
|
||||
async def async_audio_transcriptions(
|
||||
self,
|
||||
|
|
@ -66,15 +30,4 @@ class BedrockAudioTranscriptionRustDispatch:
|
|||
optional_params: dict[str, object],
|
||||
timeout: float | httpx.Timeout | None,
|
||||
) -> TranscriptionResponse:
|
||||
rust_response: Final = await rust_transcription_bridge.atranscription(
|
||||
model=model,
|
||||
audio=self._audio_payload(audio_file),
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
optional_params=optional_params,
|
||||
timeout=timeout,
|
||||
python_fallback=None,
|
||||
)
|
||||
return TranscriptionResponse(**rust_response)
|
||||
raise RuntimeError("Bedrock audio transcription must be selected at the public boundary")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import json
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -16,7 +14,6 @@ from litellm.llms.custom_httpx.http_handler import (
|
|||
_get_httpx_client,
|
||||
get_async_httpx_client,
|
||||
)
|
||||
from litellm.rust_bridge import chat_completions as rust_chat_completions_bridge
|
||||
from litellm.types.utils import ModelResponse
|
||||
from litellm.utils import CustomStreamWrapper
|
||||
|
||||
|
|
@ -25,22 +22,6 @@ from ..common_utils import BedrockError, _get_all_bedrock_regions, error_respons
|
|||
from .invoke_handler import AWSEventStreamDecoder, MockResponseIterator, make_call
|
||||
|
||||
|
||||
def _sigv4_principal(credentials: Credentials | None) -> Mapping[str, str]:
|
||||
if credentials is None:
|
||||
return MappingProxyType({})
|
||||
return MappingProxyType(
|
||||
{
|
||||
key: value
|
||||
for key, value in (
|
||||
("aws_access_key_id", credentials.access_key),
|
||||
("aws_secret_access_key", credentials.secret_key),
|
||||
("aws_session_token", credentials.token),
|
||||
)
|
||||
if value is not None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def make_sync_call(
|
||||
client: HTTPHandler | None,
|
||||
api_base: str,
|
||||
|
|
@ -150,7 +131,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
logging_obj.pre_call(
|
||||
input=messages,
|
||||
api_key="",
|
||||
additional_args={
|
||||
additional_args={ # mutable-ok: logging owns this request snapshot
|
||||
"complete_input_dict": data,
|
||||
"api_base": api_base,
|
||||
"headers": dict(prepped.headers),
|
||||
|
|
@ -232,7 +213,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
|
||||
headers = dict(prepped.headers)
|
||||
if client is None or not isinstance(client, AsyncHTTPHandler):
|
||||
_params: Final = {}
|
||||
_params: Final = {} # mutable-ok: timeout is conditionally added before client construction
|
||||
if timeout is not None:
|
||||
if isinstance(timeout, float) or isinstance(timeout, int):
|
||||
timeout = httpx.Timeout(timeout)
|
||||
|
|
@ -398,30 +379,6 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
# Filter beta headers in HTTP headers before making the request
|
||||
headers = update_headers_with_filtered_beta(headers=headers, provider="bedrock_converse")
|
||||
|
||||
rust_optional_params: Final = { # mutable-ok: json.dumps in the bridge rejects a mappingproxy
|
||||
**optional_params,
|
||||
**_sigv4_principal(credentials),
|
||||
"aws_region_name": aws_region_name,
|
||||
}
|
||||
rust_logging_args: Final = { # mutable-ok: logging callbacks read additional_args as a plain dict
|
||||
"complete_input_dict": { # mutable-ok: same, and it is serialized alongside its parent
|
||||
"messages": messages,
|
||||
**optional_params,
|
||||
},
|
||||
"api_base": proxy_endpoint_url,
|
||||
"headers": headers,
|
||||
}
|
||||
|
||||
def log_rust_pre_call() -> None:
|
||||
logging_obj.pre_call(input=messages, api_key="", additional_args=rust_logging_args)
|
||||
|
||||
log_rust_post_call: Final = rust_chat_completions_bridge.response_logger(
|
||||
logging_obj=logging_obj,
|
||||
messages=messages,
|
||||
api_key="",
|
||||
additional_args=rust_logging_args,
|
||||
)
|
||||
|
||||
def completion_dispatch() -> ModelResponse | CustomStreamWrapper:
|
||||
request_data: Final = litellm.AmazonConverseConfig()._transform_request(
|
||||
model=model,
|
||||
|
|
@ -443,7 +400,7 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
logging_obj.pre_call(
|
||||
input=messages,
|
||||
api_key="",
|
||||
additional_args={
|
||||
additional_args={ # mutable-ok: logging owns this request snapshot
|
||||
"complete_input_dict": data,
|
||||
"api_base": proxy_endpoint_url,
|
||||
"headers": prepped.headers,
|
||||
|
|
@ -453,7 +410,11 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
sync_client: Final = (
|
||||
client
|
||||
if isinstance(client, HTTPHandler)
|
||||
else _get_httpx_client({} if client_timeout is None else {"timeout": client_timeout})
|
||||
else _get_httpx_client(
|
||||
{} # mutable-ok: client factory owns parameters
|
||||
if client_timeout is None
|
||||
else {"timeout": client_timeout}
|
||||
)
|
||||
)
|
||||
if stream is True:
|
||||
completion_stream, response_headers = make_sync_call(
|
||||
|
|
@ -511,51 +472,21 @@ class BedrockConverseLLM(BaseAWSLLM):
|
|||
return transformed_response
|
||||
|
||||
if acompletion:
|
||||
return rust_chat_completions_bridge.achat_completions(
|
||||
return self.async_completion(
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params=rust_optional_params,
|
||||
model_response=model_response,
|
||||
api_key=api_key,
|
||||
api_base=proxy_endpoint_url,
|
||||
custom_llm_provider="bedrock",
|
||||
extra_headers=headers,
|
||||
timeout=timeout,
|
||||
model_response=model_response,
|
||||
encoding=encoding,
|
||||
logging_obj=logging_obj,
|
||||
optional_params=optional_params,
|
||||
stream=stream,
|
||||
litellm_params=litellm_params,
|
||||
on_request=log_rust_pre_call,
|
||||
on_response=log_rust_post_call,
|
||||
python_fallback=lambda: self.async_completion(
|
||||
model=model,
|
||||
messages=messages,
|
||||
api_base=proxy_endpoint_url,
|
||||
model_response=model_response,
|
||||
encoding=encoding,
|
||||
logging_obj=logging_obj,
|
||||
optional_params=optional_params,
|
||||
stream=stream,
|
||||
litellm_params=litellm_params,
|
||||
logger_fn=logger_fn,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
client=client,
|
||||
credentials=credentials,
|
||||
api_key=api_key,
|
||||
),
|
||||
logger_fn=logger_fn,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
client=client,
|
||||
credentials=credentials,
|
||||
api_key=api_key,
|
||||
)
|
||||
return rust_chat_completions_bridge.chat_completions(
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params=rust_optional_params,
|
||||
model_response=model_response,
|
||||
api_key=api_key,
|
||||
api_base=proxy_endpoint_url,
|
||||
custom_llm_provider="bedrock",
|
||||
extra_headers=headers,
|
||||
timeout=timeout,
|
||||
stream=stream,
|
||||
litellm_params=litellm_params,
|
||||
on_request=log_rust_pre_call,
|
||||
on_response=log_rust_post_call,
|
||||
python_fallback=completion_dispatch,
|
||||
)
|
||||
return completion_dispatch()
|
||||
|
|
|
|||
|
|
@ -174,9 +174,6 @@ if TYPE_CHECKING:
|
|||
from litellm.llms.anthropic.experimental_pass_through.messages.fake_stream_iterator import (
|
||||
FakeAnthropicMessagesStreamIterator,
|
||||
)
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.streaming_iterator import (
|
||||
AnthropicMessagesStreamingResponse,
|
||||
)
|
||||
from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig
|
||||
from litellm.types.llms.openai_evals import (
|
||||
CancelEvalResponse,
|
||||
|
|
@ -2266,9 +2263,11 @@ class BaseLLMHTTPHandler:
|
|||
|
||||
def log_pre_call() -> None:
|
||||
logging_obj.pre_call(
|
||||
input=[{"role": "user", "content": request_body_json}],
|
||||
input=[ # mutable-ok: logging owns this synthesized message snapshot
|
||||
{"role": "user", "content": request_body_json}
|
||||
],
|
||||
api_key="",
|
||||
additional_args={
|
||||
additional_args={ # mutable-ok: logging owns this request snapshot
|
||||
"complete_input_dict": request_body,
|
||||
"api_base": str(request_url),
|
||||
"headers": headers,
|
||||
|
|
@ -2332,7 +2331,9 @@ class BaseLLMHTTPHandler:
|
|||
anthropic_messages_optional_request_params=anthropic_messages_optional_request_params,
|
||||
logging_obj=logging_obj,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
kwargs={**kwargs, "api_key": api_key} if api_key else kwargs,
|
||||
kwargs={**kwargs, "api_key": api_key} # mutable-ok: iterator owns enriched kwargs
|
||||
if api_key
|
||||
else kwargs,
|
||||
hold_back=bool(held_back_tool_names),
|
||||
server_fulfilled_tool_names=held_back_tool_names,
|
||||
)
|
||||
|
|
@ -2358,43 +2359,7 @@ class BaseLLMHTTPHandler:
|
|||
kwargs=kwargs,
|
||||
)
|
||||
|
||||
async def adapt_rust_response(response: dict[str, object]) -> AnthropicMessagesResponse | AsyncIterator:
|
||||
response_obj: Final = cast(AnthropicMessagesResponse, dict(response))
|
||||
response_obj["_hidden_params"] = {"additional_headers": {"x-litellm-rust": "true"}}
|
||||
if stream:
|
||||
return self._rust_anthropic_messages_fake_stream(response_obj)
|
||||
return await self._finalize_anthropic_messages_response(
|
||||
initial_response=response_obj,
|
||||
model=model,
|
||||
messages=messages,
|
||||
anthropic_messages_provider_config=anthropic_messages_provider_config,
|
||||
anthropic_messages_optional_request_params=anthropic_messages_optional_request_params,
|
||||
logging_obj=logging_obj,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
api_key=api_key,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
|
||||
from litellm.rust_bridge import messages as rust_messages_bridge
|
||||
|
||||
upstream_body: Final = {key: value for key, value in request_body.items() if key != "stream"}
|
||||
return await rust_messages_bridge.amessages(
|
||||
model=model,
|
||||
body=upstream_body,
|
||||
has_agentic_hook=self._has_agentic_completion_hook(logging_obj),
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=headers,
|
||||
timeout=self._resolve_anthropic_messages_timeout(
|
||||
litellm_params=litellm_params,
|
||||
stream=stream or False,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
),
|
||||
on_request=log_pre_call,
|
||||
python_fallback=python_fallback,
|
||||
adapt=adapt_rust_response,
|
||||
)
|
||||
return await python_fallback()
|
||||
|
||||
async def _finalize_anthropic_messages_response(
|
||||
self,
|
||||
|
|
@ -2432,25 +2397,6 @@ class BaseLLMHTTPHandler:
|
|||
"anthropic_messages",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _rust_anthropic_messages_fake_stream(
|
||||
rust_response: AnthropicMessagesResponse,
|
||||
) -> "AnthropicMessagesStreamingResponse":
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.fake_stream_iterator import (
|
||||
FakeAnthropicMessagesStreamIterator,
|
||||
)
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.streaming_iterator import (
|
||||
AnthropicMessagesStreamHiddenParams,
|
||||
AnthropicMessagesStreamingResponse,
|
||||
)
|
||||
|
||||
completion_stream = cast(AsyncIterator[bytes], FakeAnthropicMessagesStreamIterator(response=rust_response))
|
||||
hidden_params: Final = AnthropicMessagesStreamHiddenParams(additional_headers={"x-litellm-rust": "true"})
|
||||
return AnthropicMessagesStreamingResponse(
|
||||
completion_stream=completion_stream,
|
||||
hidden_params=hidden_params,
|
||||
)
|
||||
|
||||
def anthropic_messages_handler(
|
||||
self,
|
||||
model: str,
|
||||
|
|
@ -6612,7 +6558,9 @@ class BaseLLMHTTPHandler:
|
|||
|
||||
backend: Final = await rust_responses_websocket.connect(
|
||||
url=ws_url,
|
||||
headers={str(key): str(value) for key, value in headers.items()},
|
||||
headers={ # mutable-ok: WebSocket bridge owns normalized headers
|
||||
str(key): str(value) for key, value in headers.items()
|
||||
},
|
||||
timeout=timeout,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -633,7 +633,7 @@ async def acompletion(
|
|||
try:
|
||||
# Use a partial function to pass your keyword arguments
|
||||
kwargs.pop("acompletion", None)
|
||||
func: Final = partial(completion, **completion_kwargs, **kwargs)
|
||||
func: Final = partial(_python_completion, **completion_kwargs, **kwargs)
|
||||
|
||||
# Add the context to the function
|
||||
ctx: Final = contextvars.copy_context()
|
||||
|
|
@ -7685,7 +7685,7 @@ async def atranscription(*args, **kwargs) -> TranscriptionResponse:
|
|||
custom_llm_provider = None
|
||||
try:
|
||||
# Use a partial function to pass your keyword arguments
|
||||
func: Final = partial(transcription, *args, **kwargs)
|
||||
func: Final = partial(_python_transcription, *args, **kwargs)
|
||||
|
||||
# Add the context to the function
|
||||
ctx: Final = contextvars.copy_context()
|
||||
|
|
@ -9181,3 +9181,24 @@ def __getattr__(name: str) -> tiktoken.Encoding:
|
|||
_encoding_cache = _encoding
|
||||
return _encoding
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
from litellm.rust_bridge.chat_completions.lifecycle import wrap_async as _wrap_chat_async
|
||||
from litellm.rust_bridge.chat_completions.lifecycle import wrap_sync as _wrap_chat_sync
|
||||
from litellm.rust_bridge.transcription.lifecycle import wrap_async as _wrap_transcription_async
|
||||
from litellm.rust_bridge.transcription.lifecycle import wrap_sync as _wrap_transcription_sync
|
||||
|
||||
_python_completion: Final = completion
|
||||
_python_acompletion: Final = acompletion
|
||||
_python_transcription: Final = transcription
|
||||
_python_atranscription: Final = atranscription
|
||||
completion = _wrap_chat_sync(_python_completion) # rebind-ok: the public selector wraps the captured Python lifecycle
|
||||
acompletion = _wrap_chat_async(
|
||||
_python_acompletion
|
||||
) # rebind-ok: the public selector wraps the captured Python lifecycle
|
||||
transcription = _wrap_transcription_sync(
|
||||
_python_transcription
|
||||
) # rebind-ok: the public selector wraps the captured Python lifecycle
|
||||
atranscription = _wrap_transcription_async(
|
||||
_python_atranscription
|
||||
) # rebind-ok: the public selector wraps the captured Python lifecycle
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ from litellm.ocr.input import convert_file_document_to_url_document, get_mime_ty
|
|||
from litellm.rust_bridge.ocr import LiteLLMOcrRequest
|
||||
from litellm.rust_bridge.ocr.definition import COMPONENT
|
||||
from litellm.rust_bridge.ocr.host import HOST
|
||||
from litellm.rust_bridge.ocr.lifecycle import select
|
||||
from litellm.rust_bridge.runtime import BridgeErrorContext, ainvoke, invoke
|
||||
from litellm.rust_bridge.ocr.lifecycle import select_aocr, select_ocr
|
||||
from litellm.rust_bridge.runtime import ainvoke_lifecycle, invoke_lifecycle
|
||||
|
||||
__all__ = ("aocr", "convert_file_document_to_url_document", "get_mime_type", "ocr")
|
||||
|
||||
|
|
@ -50,51 +50,36 @@ def ocr(
|
|||
) -> OCRResponse | Coroutine[object, object, OCRResponse]:
|
||||
request: Final = _public_request("ocr", args, kwargs)
|
||||
execution: Final = COMPONENT.resolve()
|
||||
native: Final = select(request, execution)
|
||||
native: Final = select_ocr(request, execution)
|
||||
fallback: Final = cast( # cast-ok: forward the original call shape through the legacy @client decorator
|
||||
Callable[..., OCRResponse | Coroutine[object, object, OCRResponse]], legacy.ocr
|
||||
)
|
||||
native_call: Final[Callable[[], OCRResponse] | None] = (
|
||||
(lambda: native(request, args, kwargs, False, HOST)) if native is not None else None
|
||||
(lambda: native(request, args, kwargs, HOST)) if native is not None else None
|
||||
)
|
||||
return invoke(
|
||||
return invoke_lifecycle(
|
||||
execution=execution,
|
||||
native_call=native_call,
|
||||
python_fallback=lambda: fallback(*args, **kwargs),
|
||||
adapt=lambda value: value,
|
||||
context=BridgeErrorContext(
|
||||
route=COMPONENT.name.value,
|
||||
provider=request.custom_llm_provider or "",
|
||||
model=request.model,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def aocr(*args: object, **kwargs: object) -> OCRResponse: # kwargs-ok: preserve the public OCR call shape
|
||||
request: Final = _public_request("aocr", args, kwargs)
|
||||
execution: Final = COMPONENT.resolve()
|
||||
native: Final = select(request, execution)
|
||||
native: Final = select_aocr(request, execution)
|
||||
fallback: Final = cast( # cast-ok: forward the original call shape through the legacy @client decorator
|
||||
Callable[..., Awaitable[OCRResponse]], legacy.aocr
|
||||
)
|
||||
native_call: Final[Callable[[], Awaitable[OCRResponse]] | None] = (
|
||||
(lambda: native(request, args, kwargs, True, HOST)) if native is not None else None
|
||||
(lambda: native(request, args, kwargs, HOST)) if native is not None else None
|
||||
)
|
||||
|
||||
async def python_fallback() -> OCRResponse:
|
||||
return await fallback(*args, **kwargs)
|
||||
|
||||
async def adapt(value: OCRResponse) -> OCRResponse:
|
||||
return value
|
||||
|
||||
return await ainvoke(
|
||||
return await ainvoke_lifecycle(
|
||||
execution=execution,
|
||||
native_call=native_call,
|
||||
python_fallback=python_fallback,
|
||||
adapt=adapt,
|
||||
context=BridgeErrorContext(
|
||||
route=COMPONENT.name.value,
|
||||
provider=request.custom_llm_provider or "",
|
||||
model=request.model,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1418,7 +1418,10 @@ async def count_request_input_tokens(
|
|||
tokenizers=tokenizers,
|
||||
python_fallback=python_fallback,
|
||||
)
|
||||
verbose_proxy_logger.debug("input token counts: %s", dict(counts))
|
||||
verbose_proxy_logger.debug(
|
||||
"input token counts: %s",
|
||||
dict(counts), # mutable-ok: temporary logging projection is not retained
|
||||
)
|
||||
return counts
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Every SDK API has one `NativeComponent` in the immutable `COMPONENTS` catalog. A
|
|||
|
||||
`RustImplementationState` records whether Rust is unimplemented, experimental, or ready. `RolloutPolicy` independently selects unsupported, Python-only, Rust opt-in, Rust opt-out, or Rust-required execution. Optional Rust execution can fall back to Python. Rust-required execution cannot
|
||||
|
||||
OCR completed delivery is ready and default-on. Messages, chat completions, and Responses WebSocket transport are experimental and opt-in. The public `litellm.token_counter()` and other completed APIs remain Python-only. Bedrock transcription requires Rust because it has no Python implementation; Python-backed transcription providers remain on Python
|
||||
OCR completed delivery is ready and default-on. Messages and chat completions are experimental and opt-in. Responses WebSocket transport remains a separate experimental surface. The public `litellm.token_counter()` and other completed APIs remain Python-only. Bedrock transcription requires Rust because it has no Python implementation; Python-backed transcription providers remain on Python
|
||||
|
||||
```python
|
||||
execution = COMPONENT.resolve(
|
||||
|
|
@ -26,7 +26,7 @@ Each API calls its native entrypoint at most once. Rust performs request admissi
|
|||
|
||||
Provider failures, host callback failures, cancellation, conversion failures, and response adaptation failures propagate without replay. Adaptation runs outside the decline-catching boundary
|
||||
|
||||
`invoke` and `ainvoke` return the native result or execute the supplied fallback directly. There is no public admission, prepare, accepts, or can-handle API
|
||||
Lifecycle boundaries use `invoke_lifecycle` and `ainvoke_lifecycle`. They catch a decline only while entering the native operation; once execution starts, reserved decline or unavailable errors are terminal. The generic `invoke` helpers retain the contracts required by token counting and WebSocket transport. There is no public admission, prepare, accepts, or can-handle API
|
||||
|
||||
`ComponentName` identifies every API in the catalog, including token counting. The public `litellm.token_counter()` resolves `ComponentName.TOKEN_COUNTER` and executes through `invoke`. Its policy is `PYTHON_ONLY`, so environment and process overrides keep public calls on the Python implementation
|
||||
|
||||
|
|
@ -36,4 +36,6 @@ The raw-body binding remains experimental. It does not implement the synchronous
|
|||
|
||||
## Package layout
|
||||
|
||||
Python component packages keep their descriptor in `definition.py`, dynamic call protocols in `types.py`, and entrypoint adapters in `value.py`, `lifecycle.py`, or transport modules. Rust mirrors those APIs below `crates/python-bridge/src/routes/`
|
||||
Python component packages keep their descriptor in `definition.py`, dynamic call protocols in `types.py`, public selectors in `lifecycle.py`, and core-requested Python work in `host.py`. Implemented completed operations expose exactly one sync/async pair: `chat_completions`/`achat_completions`, `messages`/`amessages`, `ocr`/`aocr`, and `transcription`/`atranscription`. Their selection occurs at the public Python boundary, before the captured legacy lifecycle
|
||||
|
||||
Python-only completed operations expose uniform placeholder pairs that decline without inspecting arguments. Responses WebSocket and token counting keep their delivery-specific bindings. Rust mirrors these APIs below `crates/python-bridge/src/routes/`
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from asyncio import Future
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Literal, Protocol, TypedDict, final, overload
|
||||
from typing import Literal, Protocol, TypedDict, final
|
||||
|
||||
from typing_extensions import Never, NotRequired, Required
|
||||
|
||||
|
|
@ -53,179 +53,68 @@ class RustBridgeUnavailable(Exception): ...
|
|||
class RustHostCallbackError(Exception): ...
|
||||
class RustUpstreamError(Exception): ...
|
||||
|
||||
@overload
|
||||
def _ocr_lifecycle(
|
||||
def ocr(
|
||||
request: LiteLLMOcrRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
asynchronous: Literal[False],
|
||||
host: object,
|
||||
) -> OCRResponse: ...
|
||||
@overload
|
||||
def _ocr_lifecycle(
|
||||
def aocr(
|
||||
request: LiteLLMOcrRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
asynchronous: Literal[True],
|
||||
host: object,
|
||||
) -> Coroutine[object, object, OCRResponse]: ...
|
||||
@overload
|
||||
def _messages_lifecycle(
|
||||
request: _MessagesLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
asynchronous: Literal[False],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> object: ...
|
||||
@overload
|
||||
def _messages_lifecycle(
|
||||
request: _MessagesLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
asynchronous: Literal[True],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> Coroutine[object, object, object]: ...
|
||||
@overload
|
||||
def _chat_completions_lifecycle(
|
||||
request: _ChatCompletionsLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
asynchronous: Literal[False],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> object: ...
|
||||
@overload
|
||||
def _chat_completions_lifecycle(
|
||||
request: _ChatCompletionsLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
asynchronous: Literal[True],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> Coroutine[object, object, object]: ...
|
||||
@overload
|
||||
def _transcription_lifecycle(
|
||||
request: _TranscriptionLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
asynchronous: Literal[False],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> object: ...
|
||||
@overload
|
||||
def _transcription_lifecycle(
|
||||
request: _TranscriptionLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
asynchronous: Literal[True],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> Coroutine[object, object, object]: ...
|
||||
def _embeddings_lifecycle(
|
||||
request: object, args: tuple[object, ...], kwargs: dict[str, object], asynchronous: bool, host: object
|
||||
) -> Never: ...
|
||||
def _rerank_lifecycle(
|
||||
request: object, args: tuple[object, ...], kwargs: dict[str, object], asynchronous: bool, host: object
|
||||
) -> Never: ...
|
||||
def _image_generation_lifecycle(
|
||||
request: object, args: tuple[object, ...], kwargs: dict[str, object], asynchronous: bool, host: object
|
||||
) -> Never: ...
|
||||
def _image_edit_lifecycle(
|
||||
request: object, args: tuple[object, ...], kwargs: dict[str, object], asynchronous: bool, host: object
|
||||
) -> Never: ...
|
||||
def _speech_lifecycle(
|
||||
request: object, args: tuple[object, ...], kwargs: dict[str, object], asynchronous: bool, host: object
|
||||
) -> Never: ...
|
||||
def _moderation_lifecycle(
|
||||
request: object, args: tuple[object, ...], kwargs: dict[str, object], asynchronous: bool, host: object
|
||||
) -> Never: ...
|
||||
def _responses_lifecycle(
|
||||
request: object, args: tuple[object, ...], kwargs: dict[str, object], asynchronous: bool, host: object
|
||||
) -> Never: ...
|
||||
def ocr(
|
||||
model: str,
|
||||
document: object,
|
||||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
custom_llm_provider: str | None = None,
|
||||
extra_headers: object = None,
|
||||
optional_params: object = None,
|
||||
input_sources: object = None,
|
||||
timeout_seconds: float | None = None,
|
||||
) -> dict[str, object]: ...
|
||||
def aocr(
|
||||
model: str,
|
||||
document: object,
|
||||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
custom_llm_provider: str | None = None,
|
||||
extra_headers: object = None,
|
||||
optional_params: object = None,
|
||||
input_sources: object = None,
|
||||
timeout_seconds: float | None = None,
|
||||
) -> Future[dict[str, object]]: ...
|
||||
def transcription(
|
||||
model: str,
|
||||
audio: object,
|
||||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
custom_llm_provider: str | None = None,
|
||||
extra_headers: object = None,
|
||||
optional_params: object = None,
|
||||
timeout_seconds: float | None = None,
|
||||
) -> dict[str, object]: ...
|
||||
request: _TranscriptionLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> object: ...
|
||||
def atranscription(
|
||||
model: str,
|
||||
audio: object,
|
||||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
custom_llm_provider: str | None = None,
|
||||
extra_headers: object = None,
|
||||
optional_params: object = None,
|
||||
timeout_seconds: float | None = None,
|
||||
) -> Future[dict[str, object]]: ...
|
||||
request: _TranscriptionLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> Coroutine[object, object, object]: ...
|
||||
def embedding(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def aembedding(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def rerank(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def arerank(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def image_generation(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def aimage_generation(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def image_edit(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def aimage_edit(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def speech(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def aspeech(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def moderation(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def amoderation(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def responses(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def aresponses(request: object, args: tuple[object, ...], kwargs: dict[str, object], host: object) -> Never: ...
|
||||
def messages(
|
||||
model: str,
|
||||
body: object,
|
||||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
custom_llm_provider: str | None = None,
|
||||
extra_headers: object = None,
|
||||
timeout_seconds: float | None = None,
|
||||
has_agentic_hook: bool | None = None,
|
||||
on_request: Callable[[], None] | None = None,
|
||||
) -> dict[str, object]: ...
|
||||
request: _MessagesLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> object: ...
|
||||
def amessages(
|
||||
model: str,
|
||||
body: object,
|
||||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
custom_llm_provider: str | None = None,
|
||||
extra_headers: object = None,
|
||||
timeout_seconds: float | None = None,
|
||||
has_agentic_hook: bool | None = None,
|
||||
on_request: Callable[[], None] | None = None,
|
||||
) -> Future[dict[str, object]]: ...
|
||||
request: _MessagesLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> Coroutine[object, object, object]: ...
|
||||
def chat_completions(
|
||||
model: str,
|
||||
messages: object,
|
||||
optional_params: object = None,
|
||||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
custom_llm_provider: str | None = None,
|
||||
extra_headers: object = None,
|
||||
timeout_seconds: float | None = None,
|
||||
host_facts: object = None,
|
||||
on_request: Callable[[], None] | None = None,
|
||||
) -> dict[str, object]: ...
|
||||
request: _ChatCompletionsLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> object: ...
|
||||
def achat_completions(
|
||||
model: str,
|
||||
messages: object,
|
||||
optional_params: object = None,
|
||||
api_key: str | None = None,
|
||||
api_base: str | None = None,
|
||||
custom_llm_provider: str | None = None,
|
||||
extra_headers: object = None,
|
||||
timeout_seconds: float | None = None,
|
||||
host_facts: object = None,
|
||||
on_request: Callable[[], None] | None = None,
|
||||
) -> Future[dict[str, object]]: ...
|
||||
request: _ChatCompletionsLifecycleRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: _CompletedLifecycleHost,
|
||||
) -> Coroutine[object, object, object]: ...
|
||||
|
||||
_OCR_MAX_FILE_BYTES: int
|
||||
|
||||
|
|
@ -234,7 +123,6 @@ def _ocr_mime_type(file_name: str) -> str: ...
|
|||
def _ocr_upload_document(
|
||||
file_content: bytes, file_name: str | None = None, content_type: str | None = None
|
||||
) -> dict[str, object]: ...
|
||||
|
||||
@final
|
||||
class ResponsesWebSocketConnection:
|
||||
@classmethod
|
||||
|
|
@ -261,33 +149,36 @@ def gil_stats() -> dict[str, int]: ...
|
|||
|
||||
__all__ = [
|
||||
"_OCR_MAX_FILE_BYTES",
|
||||
"_chat_completions_lifecycle",
|
||||
"_embeddings_lifecycle",
|
||||
"_image_edit_lifecycle",
|
||||
"_image_generation_lifecycle",
|
||||
"_messages_lifecycle",
|
||||
"_moderation_lifecycle",
|
||||
"_ocr_file_document",
|
||||
"_ocr_lifecycle",
|
||||
"_ocr_mime_type",
|
||||
"_ocr_upload_document",
|
||||
"_rerank_lifecycle",
|
||||
"_responses_lifecycle",
|
||||
"_speech_lifecycle",
|
||||
"_transcription_lifecycle",
|
||||
"ResponsesWebSocketConnection",
|
||||
"RustBridgeDeclined",
|
||||
"RustBridgeUnavailable",
|
||||
"RustHostCallbackError",
|
||||
"RustUpstreamError",
|
||||
"_ocr_file_document",
|
||||
"_ocr_mime_type",
|
||||
"_ocr_upload_document",
|
||||
"achat_completions",
|
||||
"aembedding",
|
||||
"aimage_edit",
|
||||
"aimage_generation",
|
||||
"amessages",
|
||||
"amoderation",
|
||||
"aocr",
|
||||
"arerank",
|
||||
"aresponses",
|
||||
"aspeech",
|
||||
"atranscription",
|
||||
"chat_completions",
|
||||
"count_input_tokens",
|
||||
"embedding",
|
||||
"gil_stats",
|
||||
"image_edit",
|
||||
"image_generation",
|
||||
"messages",
|
||||
"moderation",
|
||||
"ocr",
|
||||
"rerank",
|
||||
"responses",
|
||||
"speech",
|
||||
"transcription",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -103,36 +103,39 @@ COMPONENTS: Final[Mapping[ComponentName, NativeComponent]] = MappingProxyType(
|
|||
"_ocr_upload_document",
|
||||
"_OCR_MAX_FILE_BYTES",
|
||||
"_ocr_mime_type",
|
||||
"_ocr_lifecycle",
|
||||
),
|
||||
),
|
||||
ComponentName.MESSAGES: _component(
|
||||
ComponentName.MESSAGES,
|
||||
_experimental_completed,
|
||||
("messages", "amessages", "_messages_lifecycle"),
|
||||
("messages", "amessages"),
|
||||
),
|
||||
ComponentName.CHAT_COMPLETIONS: _component(
|
||||
ComponentName.CHAT_COMPLETIONS,
|
||||
_experimental(),
|
||||
("chat_completions", "achat_completions", "_chat_completions_lifecycle"),
|
||||
("chat_completions", "achat_completions"),
|
||||
),
|
||||
ComponentName.TRANSCRIPTION: _component(
|
||||
ComponentName.TRANSCRIPTION,
|
||||
_transcription_capability,
|
||||
("transcription", "atranscription", "_transcription_lifecycle"),
|
||||
("transcription", "atranscription"),
|
||||
),
|
||||
ComponentName.EMBEDDINGS: _component(ComponentName.EMBEDDINGS, _python_completed, ("_embeddings_lifecycle",)),
|
||||
ComponentName.RERANK: _component(ComponentName.RERANK, _python_completed, ("_rerank_lifecycle",)),
|
||||
ComponentName.EMBEDDINGS: _component(ComponentName.EMBEDDINGS, _python_completed, ("embedding", "aembedding")),
|
||||
ComponentName.RERANK: _component(ComponentName.RERANK, _python_completed, ("rerank", "arerank")),
|
||||
ComponentName.IMAGE_GENERATION: _component(
|
||||
ComponentName.IMAGE_GENERATION, _python_completed, ("_image_generation_lifecycle",)
|
||||
ComponentName.IMAGE_GENERATION, _python_completed, ("image_generation", "aimage_generation")
|
||||
),
|
||||
ComponentName.IMAGE_EDIT: _component(
|
||||
ComponentName.IMAGE_EDIT, _python_completed, ("image_edit", "aimage_edit")
|
||||
),
|
||||
ComponentName.SPEECH: _component(ComponentName.SPEECH, _python_completed, ("speech", "aspeech")),
|
||||
ComponentName.MODERATION: _component(
|
||||
ComponentName.MODERATION, _python_completed, ("moderation", "amoderation")
|
||||
),
|
||||
ComponentName.IMAGE_EDIT: _component(ComponentName.IMAGE_EDIT, _python_completed, ("_image_edit_lifecycle",)),
|
||||
ComponentName.SPEECH: _component(ComponentName.SPEECH, _python_completed, ("_speech_lifecycle",)),
|
||||
ComponentName.MODERATION: _component(ComponentName.MODERATION, _python_completed, ("_moderation_lifecycle",)),
|
||||
ComponentName.RESPONSES: _component(
|
||||
ComponentName.RESPONSES,
|
||||
_responses_capability,
|
||||
("ResponsesWebSocketConnection", "_responses_lifecycle"),
|
||||
("ResponsesWebSocketConnection", "responses", "aresponses"),
|
||||
),
|
||||
ComponentName.TOKEN_COUNTER: _component(
|
||||
ComponentName.TOKEN_COUNTER,
|
||||
|
|
|
|||
|
|
@ -1,31 +1,23 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.rust_bridge.chat_completions.callbacks import response_logger
|
||||
from litellm.rust_bridge.chat_completions.definition import COMPONENT
|
||||
from litellm.rust_bridge.chat_completions.host import RUST_RESPONSE_HEADER
|
||||
from litellm.rust_bridge.chat_completions.lifecycle import (
|
||||
set_rust_chat_completions,
|
||||
wrap_async,
|
||||
wrap_sync,
|
||||
)
|
||||
from litellm.rust_bridge.chat_completions.types import (
|
||||
ResponseObserver,
|
||||
RustAchatCompletions,
|
||||
RustChatCompletions,
|
||||
)
|
||||
from litellm.rust_bridge.chat_completions.value import (
|
||||
RUST_RESPONSE_HEADER,
|
||||
achat_completions,
|
||||
chat_completions,
|
||||
load_rust_achat_completions,
|
||||
load_rust_chat_completions,
|
||||
set_rust_chat_completions,
|
||||
)
|
||||
|
||||
__all__: Final = (
|
||||
"COMPONENT",
|
||||
"RUST_RESPONSE_HEADER",
|
||||
"ResponseObserver",
|
||||
"RustAchatCompletions",
|
||||
"RustChatCompletions",
|
||||
"achat_completions",
|
||||
"chat_completions",
|
||||
"load_rust_achat_completions",
|
||||
"load_rust_chat_completions",
|
||||
"response_logger",
|
||||
"set_rust_chat_completions",
|
||||
"wrap_async",
|
||||
"wrap_sync",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,38 +1 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from litellm.rust_bridge.chat_completions.types import ResponseObserver
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
|
||||
|
||||
def response_logger(
|
||||
*,
|
||||
logging_obj: LiteLLMLoggingObj,
|
||||
messages: Sequence[object],
|
||||
api_key: str,
|
||||
additional_args: Mapping[str, object],
|
||||
) -> ResponseObserver:
|
||||
"""A `ResponseObserver` that emits the caller's `post_call` for a Rust-served
|
||||
request.
|
||||
|
||||
The core owns the provider call, so the Python transform that normally
|
||||
raises this event never runs; without it every `post_call` callback goes
|
||||
silent on a Rust-served request and `original_response` stays unset. The
|
||||
payload is the core's normalized response rather than the provider's wire
|
||||
body, which is the closest thing that crosses the bridge.
|
||||
"""
|
||||
|
||||
def log(rust_response: Mapping[str, object], /) -> None:
|
||||
logging_obj.post_call(
|
||||
input=messages,
|
||||
api_key=api_key,
|
||||
original_response=json.dumps(rust_response),
|
||||
additional_args=additional_args,
|
||||
)
|
||||
|
||||
return log
|
||||
"""Removed legacy chat-completions callback adapter."""
|
||||
|
|
|
|||
122
litellm/rust_bridge/chat_completions/host.py
Normal file
122
litellm/rust_bridge/chat_completions/host.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Final, Protocol, cast # noqa: TID251 # legacy callables are validated at the boundary
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
|
||||
convert_to_model_response_object, # pyright: ignore[reportUnknownVariableType] # legacy converter lacks complete annotations
|
||||
)
|
||||
from litellm.rust_bridge.lifecycle import Complete
|
||||
from litellm.types.utils import ModelResponse
|
||||
|
||||
RUST_RESPONSE_HEADER: Final = "x-litellm-rust"
|
||||
_OBJECT_MAPPING: Final = TypeAdapter(dict[str, object])
|
||||
_HOST_ONLY_FIELDS: Final = frozenset(
|
||||
{
|
||||
"acompletion",
|
||||
"api_base",
|
||||
"api_key",
|
||||
"base_url",
|
||||
"client",
|
||||
"custom_llm_provider",
|
||||
"host_facts",
|
||||
"litellm_call_id",
|
||||
"litellm_logging_obj",
|
||||
"logger_fn",
|
||||
"model",
|
||||
"model_list",
|
||||
"optional_params",
|
||||
"shared_session",
|
||||
"timeout",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class OptionalParamsMapper(Protocol):
|
||||
def __call__(self, *, model: str, custom_llm_provider: str, **kwargs: object) -> object: ...
|
||||
|
||||
|
||||
class ModelDumper(Protocol):
|
||||
def model_dump(self) -> object: ...
|
||||
|
||||
|
||||
def _mapping(value: object) -> dict[str, object]:
|
||||
return _OBJECT_MAPPING.validate_python(value)
|
||||
|
||||
|
||||
def _response(value: object) -> ModelResponse:
|
||||
built: Final = convert_to_model_response_object(
|
||||
response_object=dict(_mapping(value)), # mutable-ok: the converter takes a real dict and rewrites it
|
||||
model_response_object=ModelResponse(),
|
||||
hidden_params={"additional_headers": {RUST_RESPONSE_HEADER: "true"}}, # mutable-ok: converter rewrites it
|
||||
)
|
||||
if not isinstance(built, ModelResponse):
|
||||
raise TypeError(f"expected a ModelResponse from the rust path, got {type(built).__name__}")
|
||||
return built
|
||||
|
||||
|
||||
def _provider(model: object, explicit: object) -> tuple[str, str | None]:
|
||||
if not isinstance(model, str):
|
||||
raise TypeError("model must be a string")
|
||||
if isinstance(explicit, str) and explicit:
|
||||
return model.removeprefix(f"{explicit}/"), explicit
|
||||
prefix, separator, suffix = model.partition("/")
|
||||
if separator and prefix in ("anthropic", "bedrock"):
|
||||
return suffix, prefix
|
||||
return model, None
|
||||
|
||||
|
||||
def _project(request: Mapping[str, object], kwargs: Mapping[str, object]) -> dict[str, object]:
|
||||
from litellm.utils import (
|
||||
get_optional_params, # pyright: ignore[reportUnknownVariableType] # legacy mapper is untyped
|
||||
)
|
||||
|
||||
merged: Final = {**request, **kwargs}
|
||||
model, provider = _provider(merged.get("model"), merged.get("custom_llm_provider"))
|
||||
mapping_args: Final = {key: value for key, value in merged.items() if key not in _HOST_ONLY_FIELDS}
|
||||
mapper: Final = cast(OptionalParamsMapper, get_optional_params)
|
||||
optional_params: Final = _OBJECT_MAPPING.validate_python(
|
||||
mapper(model=model, custom_llm_provider=provider or "", **mapping_args)
|
||||
)
|
||||
return {
|
||||
"model": model,
|
||||
"messages": merged.get("messages", []),
|
||||
"optional_params": optional_params,
|
||||
"api_key": merged.get("api_key"),
|
||||
"api_base": merged.get("api_base") or merged.get("base_url"),
|
||||
"custom_llm_provider": provider,
|
||||
"extra_headers": merged.get("extra_headers"),
|
||||
"timeout": merged.get("timeout"),
|
||||
"litellm_call_id": merged.get("litellm_call_id"),
|
||||
}
|
||||
|
||||
|
||||
class ChatLifecycleHost:
|
||||
def invoke(
|
||||
self,
|
||||
operation: str,
|
||||
payload: object,
|
||||
request: object,
|
||||
kwargs: dict[str, object],
|
||||
logger: object,
|
||||
) -> Complete:
|
||||
if operation == "project":
|
||||
return Complete(_project(_mapping(request), kwargs))
|
||||
if operation in ("response", "cached_response"):
|
||||
return Complete(_response(payload))
|
||||
if operation == "cache_response":
|
||||
dumper: Final = cast(ModelDumper, payload)
|
||||
value: Final = dumper.model_dump() if isinstance(payload, ModelResponse) else payload
|
||||
return Complete(value)
|
||||
if operation in ("before_request", "after_response"):
|
||||
return Complete(payload)
|
||||
if operation == "map_failure":
|
||||
return Complete(payload)
|
||||
if operation == "post_process":
|
||||
return Complete(None)
|
||||
raise ValueError(f"unknown chat lifecycle operation: {operation}")
|
||||
|
||||
|
||||
HOST: Final = ChatLifecycleHost()
|
||||
|
|
@ -1,5 +1,102 @@
|
|||
from typing import Final
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from functools import wraps
|
||||
from inspect import signature
|
||||
from typing import Final, ParamSpec, TypeVar, cast # noqa: TID251 # native bindings are validated when loaded
|
||||
|
||||
from litellm.rust_bridge.bindings import BINDING_UNSET, BindingUnset
|
||||
from litellm.rust_bridge.chat_completions.definition import COMPONENT
|
||||
from litellm.rust_bridge.chat_completions.host import HOST
|
||||
from litellm.rust_bridge.chat_completions.types import RustAchatCompletions, RustChatCompletions
|
||||
from litellm.rust_bridge.configuration import CapabilityContext, DeliveryMode
|
||||
from litellm.rust_bridge.runtime import ainvoke_lifecycle, invoke_lifecycle
|
||||
|
||||
LIFECYCLE: Final = COMPONENT.lifecycle()
|
||||
Params = ParamSpec("Params")
|
||||
ResultT = TypeVar("ResultT")
|
||||
|
||||
|
||||
def _as_chat(value: object) -> RustChatCompletions | None:
|
||||
return cast(RustChatCompletions, value) if callable(value) else None # cast-ok: callable checked at binding
|
||||
|
||||
|
||||
def _as_achat(value: object) -> RustAchatCompletions | None:
|
||||
return cast(RustAchatCompletions, value) if callable(value) else None # cast-ok: callable checked at binding
|
||||
|
||||
|
||||
_CHAT: Final = COMPONENT.bind("chat_completions", validate=_as_chat)
|
||||
_ACHAT: Final = COMPONENT.bind("achat_completions", validate=_as_achat)
|
||||
|
||||
|
||||
def set_rust_chat_completions(
|
||||
*,
|
||||
chat_completions: RustChatCompletions | None | BindingUnset = BINDING_UNSET,
|
||||
achat_completions: RustAchatCompletions | None | BindingUnset = BINDING_UNSET,
|
||||
) -> None:
|
||||
_CHAT.configure(chat_completions)
|
||||
_ACHAT.configure(achat_completions)
|
||||
|
||||
|
||||
def _request(args: tuple[object, ...], kwargs: dict[str, object]) -> dict[str, object]:
|
||||
model: Final = args[0] if args else kwargs.get("model")
|
||||
messages: Final = args[1] if len(args) > 1 else kwargs.get("messages")
|
||||
stream: Final = kwargs.get("stream") is True
|
||||
return { # mutable-ok: PyO3 requires an owned exact dict at admission
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
**kwargs,
|
||||
"optional_params": {}, # mutable-ok: native projection fills parameters after admission
|
||||
"host_facts": {"stream": stream}, # mutable-ok: exact admission facts are passed by value
|
||||
}
|
||||
|
||||
|
||||
def _context(request: dict[str, object]) -> CapabilityContext:
|
||||
model: Final = request.get("model")
|
||||
provider: Final = request.get("custom_llm_provider")
|
||||
return CapabilityContext(
|
||||
provider=provider if isinstance(provider, str) else "",
|
||||
model=model if isinstance(model, str) else "",
|
||||
delivery=DeliveryMode.STREAMING if request.get("stream") is True else DeliveryMode.COMPLETED,
|
||||
)
|
||||
|
||||
|
||||
def wrap_sync(function: Callable[Params, ResultT]) -> Callable[Params, ResultT | object]:
|
||||
@wraps(function)
|
||||
def wrapped(
|
||||
*args: Params.args,
|
||||
**kwargs: Params.kwargs, # kwargs-ok: preserves public SDK call shape
|
||||
) -> ResultT | object:
|
||||
signature(function).bind(*args, **kwargs)
|
||||
call_args: Final = tuple(args)
|
||||
call_kwargs: Final = dict(kwargs) # mutable-ok: PyO3 requires the original concrete kwargs dict
|
||||
request: Final = _request(call_args, call_kwargs)
|
||||
execution: Final = COMPONENT.resolve(_context(request))
|
||||
native: Final = execution.select(_CHAT)
|
||||
return invoke_lifecycle(
|
||||
execution=execution,
|
||||
native_call=(lambda: native(request, call_args, call_kwargs, HOST)) if native is not None else None,
|
||||
python_fallback=lambda: function(*args, **kwargs),
|
||||
)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def wrap_async(function: Callable[Params, Awaitable[ResultT]]) -> Callable[Params, Awaitable[ResultT | object]]:
|
||||
@wraps(function)
|
||||
async def wrapped(
|
||||
*args: Params.args,
|
||||
**kwargs: Params.kwargs, # kwargs-ok: preserves public SDK call shape
|
||||
) -> ResultT | object:
|
||||
signature(function).bind(*args, **kwargs)
|
||||
call_args: Final = tuple(args)
|
||||
call_kwargs: Final = dict(kwargs) # mutable-ok: PyO3 requires the original concrete kwargs dict
|
||||
request: Final = _request(call_args, call_kwargs)
|
||||
execution: Final = COMPONENT.resolve(_context(request))
|
||||
native: Final = execution.select(_ACHAT)
|
||||
return await ainvoke_lifecycle(
|
||||
execution=execution,
|
||||
native_call=(lambda: native(request, call_args, call_kwargs, HOST)) if native is not None else None,
|
||||
python_fallback=lambda: function(*args, **kwargs),
|
||||
)
|
||||
|
||||
return wrapped
|
||||
|
|
|
|||
|
|
@ -1,50 +1,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
||||
from collections.abc import Awaitable
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class RustChatCompletions(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
model: str,
|
||||
messages: Sequence[object],
|
||||
optional_params: Mapping[str, object] | None,
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: Mapping[str, object] | None,
|
||||
timeout_seconds: float | None,
|
||||
host_facts: Mapping[str, bool] | None = None,
|
||||
on_request: Callable[[], None] | None = None,
|
||||
) -> Mapping[str, object]:
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> object:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class RustAchatCompletions(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
model: str,
|
||||
messages: Sequence[object],
|
||||
optional_params: Mapping[str, object] | None,
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: Mapping[str, object] | None,
|
||||
timeout_seconds: float | None,
|
||||
host_facts: Mapping[str, bool] | None = None,
|
||||
on_request: Callable[[], None] | None = None,
|
||||
) -> Awaitable[Mapping[str, object]]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ResponseObserver(Protocol):
|
||||
"""Invoked with the payload the core returned, on success only.
|
||||
|
||||
Lets the caller emit its own `post_call` on whichever path served the
|
||||
request. Both entry points call it, so the synchronous and asynchronous
|
||||
paths cannot drift apart the way the pre_call suppression once did.
|
||||
"""
|
||||
|
||||
def __call__(self, rust_response: Mapping[str, object], /) -> None:
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> Awaitable[object]:
|
||||
raise NotImplementedError
|
||||
|
|
|
|||
|
|
@ -1,190 +1 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import Final, cast # noqa: TID251 # native callables are validated at load time
|
||||
|
||||
import httpx
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
|
||||
convert_to_model_response_object, # pyright: ignore[reportUnknownVariableType] # legacy converter lacks complete annotations
|
||||
)
|
||||
from litellm.llms.bedrock.request_metadata import bedrock_request_metadata_is_owned
|
||||
from litellm.rust_bridge.bindings import BINDING_UNSET, BindingUnset
|
||||
from litellm.rust_bridge.chat_completions.definition import COMPONENT
|
||||
from litellm.rust_bridge.chat_completions.types import ResponseObserver, RustAchatCompletions, RustChatCompletions
|
||||
from litellm.rust_bridge.configuration import CapabilityContext, DeliveryMode
|
||||
from litellm.rust_bridge.runtime import BridgeErrorContext, ainvoke, invoke
|
||||
from litellm.rust_bridge.timeouts import timeout_to_seconds
|
||||
from litellm.types.utils import ModelResponse
|
||||
|
||||
_LITELLM_METADATA_ADAPTER: Final = TypeAdapter(Mapping[str, object])
|
||||
RUST_RESPONSE_HEADER: Final = "x-litellm-rust"
|
||||
|
||||
|
||||
def _as_chat(value: object) -> RustChatCompletions | None:
|
||||
return cast(RustChatCompletions, value) if callable(value) else None
|
||||
|
||||
|
||||
def _as_achat(value: object) -> RustAchatCompletions | None:
|
||||
return cast(RustAchatCompletions, value) if callable(value) else None
|
||||
|
||||
|
||||
_CHAT: Final = COMPONENT.bind("chat_completions", validate=_as_chat)
|
||||
_ACHAT: Final = COMPONENT.bind("achat_completions", validate=_as_achat)
|
||||
|
||||
|
||||
def set_rust_chat_completions(
|
||||
*,
|
||||
chat_completions: RustChatCompletions | None | BindingUnset = BINDING_UNSET,
|
||||
achat_completions: RustAchatCompletions | None | BindingUnset = BINDING_UNSET,
|
||||
) -> None:
|
||||
_CHAT.configure(chat_completions)
|
||||
_ACHAT.configure(achat_completions)
|
||||
|
||||
|
||||
def load_rust_chat_completions() -> RustChatCompletions | None:
|
||||
return COMPONENT.resolve().select(_CHAT)
|
||||
|
||||
|
||||
def load_rust_achat_completions() -> RustAchatCompletions | None:
|
||||
return COMPONENT.resolve().select(_ACHAT)
|
||||
|
||||
|
||||
def _anthropic_user_id_reaches_the_body(litellm_params: Mapping[str, object] | None) -> bool:
|
||||
metadata: Final = litellm_params.get("metadata") if litellm_params is not None else None
|
||||
try:
|
||||
entries: Final = _LITELLM_METADATA_ADAPTER.validate_python(metadata)
|
||||
except ValidationError:
|
||||
return False
|
||||
return entries.get("user_id") is not None
|
||||
|
||||
|
||||
def _host_facts(stream: object, litellm_params: Mapping[str, object] | None) -> Mapping[str, bool]:
|
||||
return MappingProxyType(
|
||||
{
|
||||
"stream": bool(stream),
|
||||
"anthropic_user_id": _anthropic_user_id_reaches_the_body(litellm_params),
|
||||
"bedrock_metadata_owned": bedrock_request_metadata_is_owned(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _build_model_response(rust_response: Mapping[str, object], model_response: ModelResponse) -> ModelResponse:
|
||||
built: Final = convert_to_model_response_object(
|
||||
response_object=dict(rust_response), # mutable-ok: the converter takes a real dict and rewrites it
|
||||
model_response_object=model_response,
|
||||
hidden_params={"additional_headers": {RUST_RESPONSE_HEADER: "true"}}, # mutable-ok: converter rewrites it
|
||||
)
|
||||
if not isinstance(built, ModelResponse):
|
||||
raise TypeError(f"expected a ModelResponse from the rust path, got {type(built).__name__}")
|
||||
return built
|
||||
|
||||
|
||||
def chat_completions(
|
||||
*,
|
||||
model: str,
|
||||
messages: Sequence[object],
|
||||
optional_params: Mapping[str, object],
|
||||
model_response: ModelResponse,
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: Mapping[str, object] | None,
|
||||
timeout: float | httpx.Timeout | None,
|
||||
python_fallback: Callable[[], object],
|
||||
stream: object = False,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
on_request: Callable[[], None] = lambda: None,
|
||||
on_response: ResponseObserver = lambda _response: None,
|
||||
) -> object:
|
||||
execution: Final = COMPONENT.resolve(
|
||||
CapabilityContext(
|
||||
provider=custom_llm_provider or "",
|
||||
model=model,
|
||||
delivery=DeliveryMode.STREAMING if bool(stream) else DeliveryMode.COMPLETED,
|
||||
)
|
||||
)
|
||||
rust_chat_completions: Final = execution.select(_CHAT)
|
||||
|
||||
def adapt(rust_response: Mapping[str, object]) -> ModelResponse:
|
||||
on_response(rust_response)
|
||||
return _build_model_response(rust_response, model_response)
|
||||
|
||||
def native_call() -> Mapping[str, object]:
|
||||
assert rust_chat_completions is not None
|
||||
return rust_chat_completions(
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params=optional_params,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
timeout_seconds=timeout_to_seconds(timeout),
|
||||
host_facts=_host_facts(stream, litellm_params),
|
||||
on_request=on_request,
|
||||
)
|
||||
|
||||
return invoke(
|
||||
execution=execution,
|
||||
native_call=native_call if rust_chat_completions is not None else None,
|
||||
python_fallback=python_fallback,
|
||||
adapt=adapt,
|
||||
context=BridgeErrorContext(route=COMPONENT.name.value, provider=custom_llm_provider or "", model=model),
|
||||
)
|
||||
|
||||
|
||||
async def achat_completions(
|
||||
*,
|
||||
model: str,
|
||||
messages: Sequence[object],
|
||||
optional_params: Mapping[str, object],
|
||||
model_response: ModelResponse,
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: Mapping[str, object] | None,
|
||||
timeout: float | httpx.Timeout | None,
|
||||
python_fallback: Callable[[], Awaitable[object]],
|
||||
stream: object = False,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
on_request: Callable[[], None] = lambda: None,
|
||||
on_response: ResponseObserver = lambda _response: None,
|
||||
) -> object:
|
||||
execution: Final = COMPONENT.resolve(
|
||||
CapabilityContext(
|
||||
provider=custom_llm_provider or "",
|
||||
model=model,
|
||||
delivery=DeliveryMode.STREAMING if bool(stream) else DeliveryMode.COMPLETED,
|
||||
)
|
||||
)
|
||||
rust_achat_completions: Final = execution.select(_ACHAT)
|
||||
|
||||
async def adapt(rust_response: Mapping[str, object]) -> ModelResponse:
|
||||
on_response(rust_response)
|
||||
return _build_model_response(rust_response, model_response)
|
||||
|
||||
def native_call() -> Awaitable[Mapping[str, object]]:
|
||||
assert rust_achat_completions is not None
|
||||
return rust_achat_completions(
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params=optional_params,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
timeout_seconds=timeout_to_seconds(timeout),
|
||||
host_facts=_host_facts(stream, litellm_params),
|
||||
on_request=on_request,
|
||||
)
|
||||
|
||||
return await ainvoke(
|
||||
execution=execution,
|
||||
native_call=native_call if rust_achat_completions is not None else None,
|
||||
python_fallback=python_fallback,
|
||||
adapt=adapt,
|
||||
context=BridgeErrorContext(route=COMPONENT.name.value, provider=custom_llm_provider or "", model=model),
|
||||
)
|
||||
"""Removed legacy chat-completions value adapter."""
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.rust_bridge.embeddings.definition import COMPONENT
|
||||
|
||||
LIFECYCLE: Final = COMPONENT.lifecycle()
|
||||
"""Embeddings remains Python-only; no native lifecycle binding is selected."""
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.rust_bridge.image_edit.definition import COMPONENT
|
||||
|
||||
LIFECYCLE: Final = COMPONENT.lifecycle()
|
||||
"""Image editing remains Python-only; no native lifecycle binding is selected."""
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.rust_bridge.image_generation.definition import COMPONENT
|
||||
|
||||
LIFECYCLE: Final = COMPONENT.lifecycle()
|
||||
"""Image generation remains Python-only; no native lifecycle binding is selected."""
|
||||
|
|
|
|||
|
|
@ -1,22 +1,14 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.rust_bridge.messages.definition import COMPONENT
|
||||
from litellm.rust_bridge.messages.lifecycle import set_rust_messages, wrap_async, wrap_sync
|
||||
from litellm.rust_bridge.messages.types import RustAmessages, RustMessages
|
||||
from litellm.rust_bridge.messages.value import (
|
||||
amessages,
|
||||
load_rust_amessages,
|
||||
load_rust_messages,
|
||||
messages,
|
||||
set_rust_messages,
|
||||
)
|
||||
|
||||
__all__: Final = (
|
||||
"COMPONENT",
|
||||
"RustAmessages",
|
||||
"RustMessages",
|
||||
"amessages",
|
||||
"load_rust_amessages",
|
||||
"load_rust_messages",
|
||||
"messages",
|
||||
"set_rust_messages",
|
||||
"wrap_async",
|
||||
"wrap_sync",
|
||||
)
|
||||
|
|
|
|||
65
litellm/rust_bridge/messages/host.py
Normal file
65
litellm/rust_bridge/messages/host.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from litellm.rust_bridge.lifecycle import Complete
|
||||
|
||||
_OBJECT_MAPPING: Final = TypeAdapter(dict[str, object])
|
||||
|
||||
|
||||
def _mapping(value: object) -> dict[str, object]:
|
||||
return _OBJECT_MAPPING.validate_python(value)
|
||||
|
||||
|
||||
def _provider(model: object, explicit: object) -> tuple[str, str | None]:
|
||||
if not isinstance(model, str):
|
||||
raise TypeError("model must be a string")
|
||||
if isinstance(explicit, str) and explicit:
|
||||
return model.removeprefix(f"{explicit}/"), explicit
|
||||
prefix, separator, suffix = model.partition("/")
|
||||
if separator and prefix in ("anthropic", "bedrock"):
|
||||
return suffix, prefix
|
||||
return model, None
|
||||
|
||||
|
||||
class MessagesLifecycleHost:
|
||||
def invoke(
|
||||
self,
|
||||
operation: str,
|
||||
payload: object,
|
||||
request: object,
|
||||
kwargs: dict[str, object],
|
||||
logger: object,
|
||||
) -> Complete:
|
||||
if operation == "project":
|
||||
initial: Final = _mapping(request)
|
||||
merged: Final = {**initial, **kwargs}
|
||||
model, provider = _provider(merged.get("model"), merged.get("custom_llm_provider"))
|
||||
body: Final = _mapping(initial.get("body", {}))
|
||||
return Complete(
|
||||
{
|
||||
"model": model,
|
||||
"body": {**body, "model": model},
|
||||
"api_key": merged.get("api_key"),
|
||||
"api_base": merged.get("api_base"),
|
||||
"custom_llm_provider": provider,
|
||||
"extra_headers": merged.get("extra_headers"),
|
||||
"timeout": merged.get("timeout"),
|
||||
"litellm_call_id": merged.get("litellm_call_id"),
|
||||
}
|
||||
)
|
||||
if operation in ("response", "cached_response"):
|
||||
response: Final = _mapping(payload)
|
||||
return Complete({**response, "_hidden_params": {"additional_headers": {"x-litellm-rust": "true"}}})
|
||||
if operation == "cache_response":
|
||||
return Complete(payload)
|
||||
if operation in ("before_request", "after_response", "map_failure"):
|
||||
return Complete(payload)
|
||||
if operation == "post_process":
|
||||
return Complete(None)
|
||||
raise ValueError(f"unknown messages lifecycle operation: {operation}")
|
||||
|
||||
|
||||
HOST: Final = MessagesLifecycleHost()
|
||||
|
|
@ -1,5 +1,83 @@
|
|||
from typing import Final
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from functools import wraps
|
||||
from inspect import signature
|
||||
from typing import Final, ParamSpec, TypeVar, cast # noqa: TID251 # native bindings are validated when loaded
|
||||
|
||||
from litellm.rust_bridge.bindings import BINDING_UNSET, BindingUnset
|
||||
from litellm.rust_bridge.messages.definition import COMPONENT
|
||||
from litellm.rust_bridge.messages.host import HOST
|
||||
from litellm.rust_bridge.messages.request import context, request
|
||||
from litellm.rust_bridge.messages.types import RustAmessages, RustMessages
|
||||
from litellm.rust_bridge.runtime import ainvoke_lifecycle, invoke_lifecycle
|
||||
|
||||
LIFECYCLE: Final = COMPONENT.lifecycle()
|
||||
Params = ParamSpec("Params")
|
||||
ResultT = TypeVar("ResultT")
|
||||
|
||||
|
||||
def _as_messages(value: object) -> RustMessages | None:
|
||||
return cast(RustMessages, value) if callable(value) else None # cast-ok: callable checked at binding
|
||||
|
||||
|
||||
def _as_amessages(value: object) -> RustAmessages | None:
|
||||
return cast(RustAmessages, value) if callable(value) else None # cast-ok: callable checked at binding
|
||||
|
||||
|
||||
_MESSAGES: Final = COMPONENT.bind("messages", validate=_as_messages)
|
||||
_AMESSAGES: Final = COMPONENT.bind("amessages", validate=_as_amessages)
|
||||
|
||||
|
||||
def set_rust_messages(
|
||||
*,
|
||||
messages: RustMessages | None | BindingUnset = BINDING_UNSET,
|
||||
amessages: RustAmessages | None | BindingUnset = BINDING_UNSET,
|
||||
) -> None:
|
||||
_MESSAGES.configure(messages)
|
||||
_AMESSAGES.configure(amessages)
|
||||
|
||||
|
||||
def wrap_sync(function: Callable[Params, ResultT]) -> Callable[Params, ResultT | object]:
|
||||
@wraps(function)
|
||||
def wrapped(
|
||||
*args: Params.args,
|
||||
**kwargs: Params.kwargs, # kwargs-ok: preserves public SDK call shape
|
||||
) -> ResultT | object:
|
||||
signature(function).bind(*args, **kwargs)
|
||||
call_args: Final = tuple(args)
|
||||
call_kwargs: Final = dict(kwargs) # mutable-ok: PyO3 requires the original concrete kwargs dict
|
||||
boundary_request: Final = request(call_args, call_kwargs)
|
||||
execution: Final = COMPONENT.resolve(context(boundary_request))
|
||||
native: Final = execution.select(_MESSAGES)
|
||||
return invoke_lifecycle(
|
||||
execution=execution,
|
||||
native_call=(lambda: native(boundary_request, call_args, call_kwargs, HOST))
|
||||
if native is not None
|
||||
else None,
|
||||
python_fallback=lambda: function(*args, **kwargs),
|
||||
)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def wrap_async(function: Callable[Params, Awaitable[ResultT]]) -> Callable[Params, Awaitable[ResultT | object]]:
|
||||
@wraps(function)
|
||||
async def wrapped(
|
||||
*args: Params.args,
|
||||
**kwargs: Params.kwargs, # kwargs-ok: preserves public SDK call shape
|
||||
) -> ResultT | object:
|
||||
signature(function).bind(*args, **kwargs)
|
||||
call_args: Final = tuple(args)
|
||||
call_kwargs: Final = dict(kwargs) # mutable-ok: PyO3 requires the original concrete kwargs dict
|
||||
boundary_request: Final = request(call_args, call_kwargs)
|
||||
execution: Final = COMPONENT.resolve(context(boundary_request))
|
||||
native: Final = execution.select(_AMESSAGES)
|
||||
return await ainvoke_lifecycle(
|
||||
execution=execution,
|
||||
native_call=(lambda: native(boundary_request, call_args, call_kwargs, HOST))
|
||||
if native is not None
|
||||
else None,
|
||||
python_fallback=lambda: function(*args, **kwargs),
|
||||
)
|
||||
|
||||
return wrapped
|
||||
|
|
|
|||
94
litellm/rust_bridge/messages/request.py
Normal file
94
litellm/rust_bridge/messages/request.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Final, cast # noqa: TID251 # global callback registry is dynamically typed
|
||||
|
||||
from litellm.rust_bridge.configuration import CapabilityContext, DeliveryMode
|
||||
from litellm.types.llms.anthropic import ANTHROPIC_ADVISOR_TOOL_TYPE
|
||||
|
||||
_PARAMETERS: Final = (
|
||||
"max_tokens",
|
||||
"messages",
|
||||
"model",
|
||||
"metadata",
|
||||
"stop_sequences",
|
||||
"stream",
|
||||
"system",
|
||||
"temperature",
|
||||
"thinking",
|
||||
"tool_choice",
|
||||
"tools",
|
||||
"top_k",
|
||||
"top_p",
|
||||
"container",
|
||||
"api_key",
|
||||
"api_base",
|
||||
"client",
|
||||
"custom_llm_provider",
|
||||
)
|
||||
_BODY_FIELDS: Final = frozenset(
|
||||
{
|
||||
"container",
|
||||
"max_tokens",
|
||||
"messages",
|
||||
"metadata",
|
||||
"stop_sequences",
|
||||
"stream",
|
||||
"system",
|
||||
"temperature",
|
||||
"thinking",
|
||||
"tool_choice",
|
||||
"tools",
|
||||
"top_k",
|
||||
"top_p",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def request(args: tuple[object, ...], kwargs: dict[str, object]) -> dict[str, object]:
|
||||
positional: Final = { # mutable-ok: positional values are merged into the owned boundary request
|
||||
name: args[index] for index, name in enumerate(_PARAMETERS) if index < len(args)
|
||||
}
|
||||
supplied: Final = {**positional, **kwargs} # mutable-ok: exact public arguments are snapshotted
|
||||
body: Final = { # mutable-ok: the native route consumes an owned JSON body
|
||||
key: value for key, value in supplied.items() if key in _BODY_FIELDS and value is not None
|
||||
}
|
||||
return { # mutable-ok: PyO3 requires an owned exact dict at admission
|
||||
**supplied,
|
||||
"model": supplied.get("model"),
|
||||
"body": body,
|
||||
"has_agentic_hook": _host_operations_needed(supplied),
|
||||
}
|
||||
|
||||
|
||||
def _host_operations_needed(supplied: dict[str, object]) -> bool:
|
||||
import litellm
|
||||
|
||||
callbacks: Final = cast(list[object], litellm.callbacks) # cast-ok: global callback registry is list-backed
|
||||
if callbacks:
|
||||
return True
|
||||
tools: Final = supplied.get("tools")
|
||||
if type(tools) is not list:
|
||||
return False
|
||||
return any(
|
||||
type(tool) is dict
|
||||
and type(cast(dict[object, object], tool).get("type")) is str
|
||||
and cast(dict[object, object], tool).get("type") == ANTHROPIC_ADVISOR_TOOL_TYPE
|
||||
for tool in cast(list[object], tools) # cast-ok: exact list checked before safe element inspection
|
||||
)
|
||||
|
||||
|
||||
def context(boundary_request: dict[str, object]) -> CapabilityContext:
|
||||
model: Final = boundary_request.get("model")
|
||||
provider: Final = boundary_request.get("custom_llm_provider")
|
||||
body: Final = boundary_request.get("body")
|
||||
body_mapping: Final = (
|
||||
cast(dict[str, object], body) # cast-ok: exact dict type checked immediately before narrowing
|
||||
if type(body) is dict
|
||||
else {} # mutable-ok: empty local view is never exposed
|
||||
)
|
||||
streaming: Final = body_mapping.get("stream") is True
|
||||
return CapabilityContext(
|
||||
provider=provider if isinstance(provider, str) else "",
|
||||
model=model if isinstance(model, str) else "",
|
||||
delivery=DeliveryMode.STREAMING if streaming else DeliveryMode.COMPLETED,
|
||||
)
|
||||
|
|
@ -1,36 +1,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from collections.abc import Awaitable
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class RustMessages(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
model: str,
|
||||
body: Mapping[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: Mapping[str, object] | None,
|
||||
timeout_seconds: float | None,
|
||||
has_agentic_hook: bool = False,
|
||||
on_request: Callable[[], None] | None = None,
|
||||
) -> dict[str, object]:
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> object:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class RustAmessages(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
model: str,
|
||||
body: Mapping[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: Mapping[str, object] | None,
|
||||
timeout_seconds: float | None,
|
||||
has_agentic_hook: bool = False,
|
||||
on_request: Callable[[], None] | None = None,
|
||||
) -> Awaitable[dict[str, object]]:
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> Awaitable[object]:
|
||||
raise NotImplementedError
|
||||
|
|
|
|||
|
|
@ -1,154 +1 @@
|
|||
"""Native Messages bindings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from typing import (
|
||||
Final,
|
||||
TypeVar,
|
||||
cast, # noqa: TID251 # native callable signatures are checked by bridge contract tests
|
||||
)
|
||||
|
||||
import httpx
|
||||
|
||||
from litellm.rust_bridge.bindings import BINDING_UNSET, BindingUnset
|
||||
from litellm.rust_bridge.configuration import CapabilityContext, DeliveryMode
|
||||
from litellm.rust_bridge.messages.definition import COMPONENT
|
||||
from litellm.rust_bridge.messages.types import RustAmessages, RustMessages
|
||||
from litellm.rust_bridge.runtime import BridgeErrorContext, ainvoke, invoke
|
||||
from litellm.rust_bridge.timeouts import timeout_to_seconds
|
||||
|
||||
|
||||
def _as_messages(value: object) -> RustMessages | None:
|
||||
return cast(RustMessages, value) if callable(value) else None # cast-ok: validated callable native binding
|
||||
|
||||
|
||||
def _as_amessages(value: object) -> RustAmessages | None:
|
||||
return cast(RustAmessages, value) if callable(value) else None # cast-ok: validated callable native binding
|
||||
|
||||
|
||||
_MESSAGES: Final = COMPONENT.bind("messages", validate=_as_messages)
|
||||
_AMESSAGES: Final = COMPONENT.bind("amessages", validate=_as_amessages)
|
||||
ResultT = TypeVar("ResultT")
|
||||
|
||||
|
||||
def set_rust_messages(
|
||||
*,
|
||||
messages: RustMessages | None | BindingUnset = BINDING_UNSET,
|
||||
amessages: RustAmessages | None | BindingUnset = BINDING_UNSET,
|
||||
) -> None:
|
||||
_MESSAGES.configure(messages)
|
||||
_AMESSAGES.configure(amessages)
|
||||
|
||||
|
||||
def load_rust_messages() -> RustMessages | None:
|
||||
return COMPONENT.resolve().select(_MESSAGES)
|
||||
|
||||
|
||||
def load_rust_amessages() -> RustAmessages | None:
|
||||
return COMPONENT.resolve().select(_AMESSAGES)
|
||||
|
||||
|
||||
def messages(
|
||||
*,
|
||||
model: str,
|
||||
body: Mapping[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: Mapping[str, object] | None,
|
||||
timeout: float | httpx.Timeout | None,
|
||||
has_agentic_hook: bool = False,
|
||||
on_request: Callable[[], None] = lambda: None,
|
||||
python_fallback: Callable[[], ResultT],
|
||||
adapt: Callable[[dict[str, object]], ResultT],
|
||||
) -> ResultT:
|
||||
execution: Final = COMPONENT.resolve(
|
||||
CapabilityContext(
|
||||
provider=custom_llm_provider or "",
|
||||
model=model,
|
||||
delivery=DeliveryMode.STREAMING if body.get("stream") is True else DeliveryMode.COMPLETED,
|
||||
)
|
||||
)
|
||||
rust_messages: Final = execution.select(_MESSAGES)
|
||||
native_call: Final[Callable[[], dict[str, object]] | None] = (
|
||||
(
|
||||
lambda: rust_messages(
|
||||
model=model,
|
||||
body=body,
|
||||
has_agentic_hook=has_agentic_hook,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
timeout_seconds=timeout_to_seconds(timeout),
|
||||
on_request=on_request,
|
||||
)
|
||||
)
|
||||
if rust_messages is not None
|
||||
else None
|
||||
)
|
||||
return invoke(
|
||||
execution=execution,
|
||||
native_call=native_call,
|
||||
python_fallback=python_fallback,
|
||||
adapt=adapt,
|
||||
context=BridgeErrorContext(
|
||||
route=COMPONENT.name.value,
|
||||
provider=custom_llm_provider or "",
|
||||
model=model,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def amessages(
|
||||
*,
|
||||
model: str,
|
||||
body: Mapping[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: Mapping[str, object] | None,
|
||||
timeout: float | httpx.Timeout | None,
|
||||
has_agentic_hook: bool = False,
|
||||
on_request: Callable[[], None] = lambda: None,
|
||||
python_fallback: Callable[[], Awaitable[ResultT]],
|
||||
adapt: Callable[[dict[str, object]], Awaitable[ResultT]],
|
||||
) -> ResultT:
|
||||
execution: Final = COMPONENT.resolve(
|
||||
CapabilityContext(
|
||||
provider=custom_llm_provider or "",
|
||||
model=model,
|
||||
delivery=DeliveryMode.STREAMING if body.get("stream") is True else DeliveryMode.COMPLETED,
|
||||
)
|
||||
)
|
||||
rust_amessages: Final = execution.select(_AMESSAGES)
|
||||
native_call: Final[Callable[[], Awaitable[dict[str, object]]] | None] = (
|
||||
(
|
||||
lambda: rust_amessages(
|
||||
model=model,
|
||||
body=body,
|
||||
has_agentic_hook=has_agentic_hook,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
timeout_seconds=timeout_to_seconds(timeout),
|
||||
on_request=on_request,
|
||||
)
|
||||
)
|
||||
if rust_amessages is not None
|
||||
else None
|
||||
)
|
||||
|
||||
return await ainvoke(
|
||||
execution=execution,
|
||||
native_call=native_call,
|
||||
python_fallback=python_fallback,
|
||||
adapt=adapt,
|
||||
context=BridgeErrorContext(
|
||||
route=COMPONENT.name.value,
|
||||
provider=custom_llm_provider or "",
|
||||
model=model,
|
||||
),
|
||||
)
|
||||
"""Removed legacy Anthropic Messages value adapter."""
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.rust_bridge.moderation.definition import COMPONENT
|
||||
|
||||
LIFECYCLE: Final = COMPONENT.lifecycle()
|
||||
"""Moderation remains Python-only; no native lifecycle binding is selected."""
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.rust_bridge.configuration import rust
|
||||
from litellm.rust_bridge.ocr.definition import COMPONENT
|
||||
from litellm.rust_bridge.ocr.lifecycle import set_rust_ocr
|
||||
from litellm.rust_bridge.ocr.types import LiteLLMOcrRequest
|
||||
|
||||
__all__: Final = (
|
||||
"COMPONENT",
|
||||
"LiteLLMOcrRequest",
|
||||
"rust",
|
||||
"set_rust_ocr",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Final, Protocol
|
||||
from types import MappingProxyType
|
||||
from typing import Final, Protocol, cast # noqa: TID251 # public exception mapper is dynamically typed
|
||||
|
||||
import litellm
|
||||
from litellm.llms.base_llm.ocr.transformation import OCRResponse
|
||||
from litellm.llms.base_llm.ocr.transformation import PROVIDER_NATIVE_RESPONSE_KEY, OCRResponse
|
||||
from litellm.rust_bridge.ocr.types import LiteLLMOcrRequest
|
||||
from litellm.rust_bridge.ocr.value import adapt_response
|
||||
from litellm.types.utils import CustomPricingLiteLLMParams
|
||||
|
||||
|
||||
|
|
@ -24,7 +24,22 @@ class ExceptionMapper(Protocol):
|
|||
|
||||
class OcrLifecycleHost:
|
||||
def response(self, response: Mapping[str, object]) -> OCRResponse:
|
||||
return adapt_response(response)
|
||||
provider_native_response: Final = response.get(PROVIDER_NATIVE_RESPONSE_KEY)
|
||||
normalized: Final = OCRResponse.model_validate(
|
||||
MappingProxyType(
|
||||
{ # mutable-ok: immediately frozen before model validation
|
||||
key: value for key, value in response.items() if key != PROVIDER_NATIVE_RESPONSE_KEY
|
||||
}
|
||||
)
|
||||
)
|
||||
if isinstance(provider_native_response, Mapping):
|
||||
native_mapping: Final = cast( # cast-ok: Mapping runtime check precedes narrowing
|
||||
Mapping[str, object], provider_native_response
|
||||
)
|
||||
normalized.set_provider_native_response(
|
||||
dict(native_mapping) # mutable-ok: response API retains an owned provider payload
|
||||
)
|
||||
return normalized
|
||||
|
||||
def custom_pricing_fields(self) -> tuple[str, ...]:
|
||||
return tuple(CustomPricingLiteLLMParams.model_fields)
|
||||
|
|
@ -35,7 +50,10 @@ class OcrLifecycleHost:
|
|||
request: LiteLLMOcrRequest,
|
||||
request_provider: str,
|
||||
) -> Exception:
|
||||
mapper: Final[ExceptionMapper] = litellm.exception_type # pyright: ignore[reportAssignmentType] # legacy public mapper is callable
|
||||
mapper: Final = cast( # cast-ok: legacy public mapper is callable
|
||||
ExceptionMapper,
|
||||
litellm.exception_type, # pyright: ignore[reportUnknownMemberType] # dynamically exported mapper
|
||||
)
|
||||
try:
|
||||
return mapper(
|
||||
model=request.model.removeprefix(f"{request_provider}/"),
|
||||
|
|
|
|||
|
|
@ -1,30 +1,80 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Final, cast # noqa: TID251 # validates dynamically loaded native callables
|
||||
from collections.abc import Awaitable
|
||||
from typing import Final, Protocol, cast # noqa: TID251 # validates dynamically loaded native callables
|
||||
|
||||
from litellm.llms.base_llm.ocr.transformation import OCRResponse
|
||||
from litellm.rust_bridge.bindings import BINDING_UNSET, BindingUnset
|
||||
from litellm.rust_bridge.ocr.definition import COMPONENT
|
||||
from litellm.rust_bridge.ocr.host import HOST
|
||||
from litellm.rust_bridge.ocr.types import LiteLLMOcrRequest
|
||||
from litellm.rust_bridge.route import ComponentExecution, NativeLifecycle
|
||||
|
||||
NativeOcrLifecycle = NativeLifecycle[LiteLLMOcrRequest, OCRResponse]
|
||||
from litellm.rust_bridge.route import ComponentExecution
|
||||
|
||||
|
||||
def _binding(value: object) -> NativeOcrLifecycle | None:
|
||||
class NativeOcr(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
request: LiteLLMOcrRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> OCRResponse: ...
|
||||
|
||||
|
||||
class NativeAocr(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
request: LiteLLMOcrRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> Awaitable[OCRResponse]: ...
|
||||
|
||||
|
||||
def _ocr_binding(value: object) -> NativeOcr | None:
|
||||
if not callable(value):
|
||||
return None
|
||||
return cast("NativeOcrLifecycle", value) # cast-ok: callable validated at the native binding boundary
|
||||
return cast("NativeOcr", value) # cast-ok: callable validated at the native binding boundary
|
||||
|
||||
|
||||
LIFECYCLE: Final = COMPONENT.bind("_ocr_lifecycle", validate=_binding)
|
||||
NATIVE_OCR_LIFECYCLE: Final = LIFECYCLE
|
||||
|
||||
|
||||
def select(request: LiteLLMOcrRequest, execution: ComponentExecution) -> NativeOcrLifecycle | None:
|
||||
if request.kwargs.get("aocr"):
|
||||
def _aocr_binding(value: object) -> NativeAocr | None:
|
||||
if not callable(value):
|
||||
return None
|
||||
return execution.select(NATIVE_OCR_LIFECYCLE)
|
||||
return cast("NativeAocr", value) # cast-ok: callable validated at the native binding boundary
|
||||
|
||||
|
||||
OCR: Final = COMPONENT.bind("ocr", validate=_ocr_binding)
|
||||
AOCR: Final = COMPONENT.bind("aocr", validate=_aocr_binding)
|
||||
|
||||
|
||||
class OcrLifecycleBindings:
|
||||
def override(self, value: object) -> None:
|
||||
OCR.override(_ocr_binding(value))
|
||||
AOCR.override(_aocr_binding(value))
|
||||
|
||||
def reset(self) -> None:
|
||||
OCR.reset()
|
||||
AOCR.reset()
|
||||
|
||||
|
||||
NATIVE_OCR_LIFECYCLE: Final = OcrLifecycleBindings()
|
||||
|
||||
|
||||
def set_rust_ocr(
|
||||
*,
|
||||
ocr: NativeOcr | None | BindingUnset = BINDING_UNSET,
|
||||
aocr: NativeAocr | None | BindingUnset = BINDING_UNSET,
|
||||
) -> None:
|
||||
OCR.configure(ocr)
|
||||
AOCR.configure(aocr)
|
||||
|
||||
|
||||
def select_ocr(request: LiteLLMOcrRequest, execution: ComponentExecution) -> NativeOcr | None:
|
||||
return execution.select(OCR)
|
||||
|
||||
|
||||
def select_aocr(request: LiteLLMOcrRequest, execution: ComponentExecution) -> NativeAocr | None:
|
||||
return execution.select(AOCR)
|
||||
|
||||
|
||||
def map_failure(error: Exception, request: LiteLLMOcrRequest, request_provider: str) -> Exception:
|
||||
|
|
|
|||
|
|
@ -1,19 +1 @@
|
|||
"""Native OCR bindings and response adaptation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
from litellm.llms.base_llm.ocr.transformation import PROVIDER_NATIVE_RESPONSE_KEY, OCRResponse
|
||||
|
||||
|
||||
def adapt_response(response: Mapping[str, object]) -> OCRResponse:
|
||||
provider_native_response: Final = response.get(PROVIDER_NATIVE_RESPONSE_KEY)
|
||||
normalized: Final = OCRResponse.model_validate(
|
||||
MappingProxyType({key: value for key, value in response.items() if key != PROVIDER_NATIVE_RESPONSE_KEY})
|
||||
)
|
||||
if isinstance(provider_native_response, Mapping):
|
||||
normalized.set_provider_native_response(provider_native_response)
|
||||
return normalized
|
||||
"""Removed legacy OCR value adapter."""
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.rust_bridge.rerank.definition import COMPONENT
|
||||
|
||||
LIFECYCLE: Final = COMPONENT.lifecycle()
|
||||
"""Reranking remains Python-only; no native lifecycle binding is selected."""
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.rust_bridge.responses.definition import COMPONENT
|
||||
|
||||
LIFECYCLE: Final = COMPONENT.lifecycle()
|
||||
"""Completed Responses remains Python-only; WebSocket transport is separate."""
|
||||
|
|
|
|||
|
|
@ -95,6 +95,64 @@ async def ainvoke(
|
|||
return await adapt(value)
|
||||
|
||||
|
||||
def invoke_lifecycle(
|
||||
*,
|
||||
native_call: Callable[[], NativeT] | None,
|
||||
python_fallback: Callable[[], ResultT] | None,
|
||||
execution: ComponentExecution,
|
||||
) -> NativeT | ResultT:
|
||||
execution.require_supported()
|
||||
_validate_fallback(execution, python_fallback)
|
||||
if execution.decision is ExecutionDecision.PYTHON:
|
||||
assert python_fallback is not None
|
||||
return python_fallback()
|
||||
if native_call is None:
|
||||
return _unavailable_or_fallback(execution, python_fallback)
|
||||
|
||||
exceptions: Final = native_exception_types()
|
||||
if exceptions is None:
|
||||
return native_call()
|
||||
declined, _ = exceptions
|
||||
host_callback: Final = native_host_callback_exception()
|
||||
try:
|
||||
return native_call()
|
||||
except host_callback as error:
|
||||
_raise_host_callback(error)
|
||||
except declined as error:
|
||||
return _declined_or_fallback(execution, python_fallback, error)
|
||||
|
||||
|
||||
async def ainvoke_lifecycle(
|
||||
*,
|
||||
native_call: Callable[[], Awaitable[NativeT]] | None,
|
||||
python_fallback: Callable[[], Awaitable[ResultT]] | None,
|
||||
execution: ComponentExecution,
|
||||
) -> NativeT | ResultT:
|
||||
execution.require_supported()
|
||||
_validate_fallback(execution, python_fallback)
|
||||
if execution.decision is ExecutionDecision.PYTHON:
|
||||
assert python_fallback is not None
|
||||
return await python_fallback()
|
||||
if native_call is None:
|
||||
return await _aunavailable_or_fallback(execution, python_fallback)
|
||||
|
||||
exceptions: Final = native_exception_types()
|
||||
if exceptions is None:
|
||||
return await native_call()
|
||||
declined, _ = exceptions
|
||||
host_callback: Final = native_host_callback_exception()
|
||||
try:
|
||||
pending: Final = native_call()
|
||||
except host_callback as error:
|
||||
_raise_host_callback(error)
|
||||
except declined as error:
|
||||
return await _adeclined_or_fallback(execution, python_fallback, error)
|
||||
try:
|
||||
return await pending
|
||||
except host_callback as error:
|
||||
_raise_host_callback(error)
|
||||
|
||||
|
||||
def _unavailable_or_fallback(
|
||||
execution: ComponentExecution,
|
||||
python_fallback: Callable[[], ResultT] | None,
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.rust_bridge.speech.definition import COMPONENT
|
||||
|
||||
LIFECYCLE: Final = COMPONENT.lifecycle()
|
||||
"""Speech remains Python-only; no native lifecycle binding is selected."""
|
||||
|
|
|
|||
|
|
@ -1,22 +1,14 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.rust_bridge.transcription.definition import COMPONENT
|
||||
from litellm.rust_bridge.transcription.lifecycle import configure_rust_transcription, wrap_async, wrap_sync
|
||||
from litellm.rust_bridge.transcription.types import RustAtranscription, RustTranscription
|
||||
from litellm.rust_bridge.transcription.value import (
|
||||
atranscription,
|
||||
configure_rust_transcription,
|
||||
load_rust_atranscription,
|
||||
load_rust_transcription,
|
||||
transcription,
|
||||
)
|
||||
|
||||
__all__: Final = (
|
||||
"COMPONENT",
|
||||
"RustAtranscription",
|
||||
"RustTranscription",
|
||||
"atranscription",
|
||||
"configure_rust_transcription",
|
||||
"load_rust_atranscription",
|
||||
"load_rust_transcription",
|
||||
"transcription",
|
||||
"wrap_async",
|
||||
"wrap_sync",
|
||||
)
|
||||
|
|
|
|||
128
litellm/rust_bridge/transcription/host.py
Normal file
128
litellm/rust_bridge/transcription/host.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from collections.abc import Mapping
|
||||
from typing import Final, Protocol, cast # noqa: TID251 # legacy mapper is dynamically typed
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from litellm.litellm_core_utils.audio_utils.utils import process_audio_file
|
||||
from litellm.rust_bridge.lifecycle import Complete
|
||||
from litellm.types.utils import TranscriptionResponse
|
||||
from litellm.utils import (
|
||||
get_optional_params_transcription, # pyright: ignore[reportUnknownVariableType] # legacy mapper is untyped
|
||||
)
|
||||
|
||||
_OBJECT_MAPPING: Final = TypeAdapter(dict[str, object])
|
||||
_OPTIONAL_FIELDS: Final = (
|
||||
"language",
|
||||
"prompt",
|
||||
"response_format",
|
||||
"temperature",
|
||||
"timestamp_granularities",
|
||||
)
|
||||
_HOST_ONLY_FIELDS: Final = frozenset(
|
||||
{
|
||||
"api_base",
|
||||
"api_key",
|
||||
"atranscription",
|
||||
"client",
|
||||
"custom_llm_provider",
|
||||
"extra_headers",
|
||||
"file",
|
||||
"litellm_call_id",
|
||||
"litellm_logging_obj",
|
||||
"model",
|
||||
"timeout",
|
||||
"user",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class OptionalParamsMapper(Protocol):
|
||||
def __call__(self, *, model: str, custom_llm_provider: str, **kwargs: object) -> object: ...
|
||||
|
||||
|
||||
class ModelDumper(Protocol):
|
||||
def model_dump(self) -> object: ...
|
||||
|
||||
|
||||
def _mapping(value: object) -> dict[str, object]:
|
||||
return _OBJECT_MAPPING.validate_python(value)
|
||||
|
||||
|
||||
def _provider(model: object, explicit: object) -> tuple[str, str]:
|
||||
if type(model) is not str:
|
||||
raise TypeError("model must be a string")
|
||||
if type(explicit) is str and explicit:
|
||||
return model.removeprefix(f"{explicit}/"), explicit
|
||||
prefix, separator, suffix = model.partition("/")
|
||||
return (suffix, prefix) if separator else (model, "")
|
||||
|
||||
|
||||
def _audio(file: object) -> dict[str, object]:
|
||||
processed: Final = process_audio_file(file) # pyright: ignore[reportArgumentType] # public binding validates FileTypes
|
||||
formats: Final = {
|
||||
"audio/flac": "flac",
|
||||
"audio/mpeg": "mp3",
|
||||
"audio/mp3": "mp3",
|
||||
"audio/ogg": "ogg",
|
||||
"audio/wav": "wav",
|
||||
"audio/x-wav": "wav",
|
||||
}
|
||||
suffix: Final = processed.filename.rsplit(".", 1)[-1].lower() if "." in processed.filename else ""
|
||||
return {
|
||||
"data": base64.b64encode(processed.file_content).decode("ascii"),
|
||||
"format": formats.get(processed.content_type, suffix),
|
||||
"filename": processed.filename,
|
||||
}
|
||||
|
||||
|
||||
def _project(request: Mapping[str, object], kwargs: Mapping[str, object]) -> dict[str, object]:
|
||||
merged: Final = {**request, **kwargs}
|
||||
model, provider = _provider(merged.get("model"), merged.get("custom_llm_provider"))
|
||||
mapper: Final = cast(OptionalParamsMapper, get_optional_params_transcription)
|
||||
optional_inputs: Final = {
|
||||
**{key: value for key, value in merged.items() if key not in _HOST_ONLY_FIELDS},
|
||||
**{name: merged.get(name) for name in _OPTIONAL_FIELDS},
|
||||
}
|
||||
optional_params: Final = _OBJECT_MAPPING.validate_python(
|
||||
mapper(model=model, custom_llm_provider=provider, **optional_inputs)
|
||||
)
|
||||
return {
|
||||
"model": model,
|
||||
"audio": _audio(merged.get("file")),
|
||||
"optional_params": optional_params,
|
||||
"api_key": merged.get("api_key"),
|
||||
"api_base": merged.get("api_base"),
|
||||
"custom_llm_provider": provider,
|
||||
"extra_headers": merged.get("extra_headers"),
|
||||
"timeout": merged.get("timeout"),
|
||||
"litellm_call_id": merged.get("litellm_call_id"),
|
||||
}
|
||||
|
||||
|
||||
class TranscriptionLifecycleHost:
|
||||
def invoke(
|
||||
self,
|
||||
operation: str,
|
||||
payload: object,
|
||||
request: object,
|
||||
kwargs: dict[str, object],
|
||||
logger: object,
|
||||
) -> Complete:
|
||||
if operation == "project":
|
||||
return Complete(_project(_mapping(request), kwargs))
|
||||
if operation in ("response", "cached_response"):
|
||||
return Complete(TranscriptionResponse(**_mapping(payload)))
|
||||
if operation == "cache_response":
|
||||
dumper: Final = cast(ModelDumper, payload)
|
||||
return Complete(dumper.model_dump() if isinstance(payload, TranscriptionResponse) else payload)
|
||||
if operation in ("before_request", "after_response", "map_failure"):
|
||||
return Complete(payload)
|
||||
if operation == "post_process":
|
||||
return Complete(None)
|
||||
raise ValueError(f"unknown transcription lifecycle operation: {operation}")
|
||||
|
||||
|
||||
HOST: Final = TranscriptionLifecycleHost()
|
||||
|
|
@ -1,5 +1,94 @@
|
|||
from typing import Final
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from functools import wraps
|
||||
from inspect import signature
|
||||
from typing import Final, ParamSpec, TypeVar, cast # noqa: TID251 # native bindings are validated when loaded
|
||||
|
||||
from litellm.rust_bridge.bindings import BINDING_UNSET, BindingUnset
|
||||
from litellm.rust_bridge.configuration import ExecutionDecision
|
||||
from litellm.rust_bridge.runtime import ainvoke_lifecycle, invoke_lifecycle
|
||||
from litellm.rust_bridge.transcription.definition import COMPONENT
|
||||
from litellm.rust_bridge.transcription.host import HOST
|
||||
from litellm.rust_bridge.transcription.request import context, request
|
||||
from litellm.rust_bridge.transcription.types import RustAtranscription, RustTranscription
|
||||
|
||||
LIFECYCLE: Final = COMPONENT.lifecycle()
|
||||
Params = ParamSpec("Params")
|
||||
ResultT = TypeVar("ResultT")
|
||||
|
||||
|
||||
def _as_transcription(value: object) -> RustTranscription | None:
|
||||
return cast(RustTranscription, value) if callable(value) else None # cast-ok: callable checked at binding
|
||||
|
||||
|
||||
def _as_atranscription(value: object) -> RustAtranscription | None:
|
||||
return cast(RustAtranscription, value) if callable(value) else None # cast-ok: callable checked at binding
|
||||
|
||||
|
||||
TRANSCRIPTION: Final = COMPONENT.bind("transcription", validate=_as_transcription)
|
||||
ATRANSCRIPTION: Final = COMPONENT.bind("atranscription", validate=_as_atranscription)
|
||||
|
||||
|
||||
def configure_rust_transcription(
|
||||
*,
|
||||
transcription: RustTranscription | None | BindingUnset = BINDING_UNSET,
|
||||
atranscription: RustAtranscription | None | BindingUnset = BINDING_UNSET,
|
||||
) -> None:
|
||||
TRANSCRIPTION.configure(transcription)
|
||||
ATRANSCRIPTION.configure(atranscription)
|
||||
|
||||
|
||||
def wrap_sync(function: Callable[Params, ResultT]) -> Callable[Params, ResultT | object]:
|
||||
@wraps(function)
|
||||
def wrapped(
|
||||
*args: Params.args,
|
||||
**kwargs: Params.kwargs, # kwargs-ok: preserves public SDK call shape
|
||||
) -> ResultT | object:
|
||||
signature(function).bind(*args, **kwargs)
|
||||
call_args: Final = tuple(args)
|
||||
call_kwargs: Final = dict(kwargs) # mutable-ok: PyO3 requires the original concrete kwargs dict
|
||||
boundary_request: Final = request(call_args, call_kwargs)
|
||||
execution: Final = COMPONENT.resolve(context(boundary_request))
|
||||
native: Final = execution.select(TRANSCRIPTION)
|
||||
fallback: Final = (
|
||||
(lambda: function(*args, **kwargs))
|
||||
if execution.decision in (ExecutionDecision.PYTHON, ExecutionDecision.RUST_WITH_FALLBACK)
|
||||
else None
|
||||
)
|
||||
return invoke_lifecycle(
|
||||
execution=execution,
|
||||
native_call=(lambda: native(boundary_request, call_args, call_kwargs, HOST))
|
||||
if native is not None
|
||||
else None,
|
||||
python_fallback=fallback,
|
||||
)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def wrap_async(function: Callable[Params, Awaitable[ResultT]]) -> Callable[Params, Awaitable[ResultT | object]]:
|
||||
@wraps(function)
|
||||
async def wrapped(
|
||||
*args: Params.args,
|
||||
**kwargs: Params.kwargs, # kwargs-ok: preserves public SDK call shape
|
||||
) -> ResultT | object:
|
||||
signature(function).bind(*args, **kwargs)
|
||||
call_args: Final = tuple(args)
|
||||
call_kwargs: Final = dict(kwargs) # mutable-ok: PyO3 requires the original concrete kwargs dict
|
||||
boundary_request: Final = request(call_args, call_kwargs)
|
||||
execution: Final = COMPONENT.resolve(context(boundary_request))
|
||||
native: Final = execution.select(ATRANSCRIPTION)
|
||||
fallback: Final = (
|
||||
(lambda: function(*args, **kwargs))
|
||||
if execution.decision in (ExecutionDecision.PYTHON, ExecutionDecision.RUST_WITH_FALLBACK)
|
||||
else None
|
||||
)
|
||||
return await ainvoke_lifecycle(
|
||||
execution=execution,
|
||||
native_call=(lambda: native(boundary_request, call_args, call_kwargs, HOST))
|
||||
if native is not None
|
||||
else None,
|
||||
python_fallback=fallback,
|
||||
)
|
||||
|
||||
return wrapped
|
||||
|
|
|
|||
59
litellm/rust_bridge/transcription/request.py
Normal file
59
litellm/rust_bridge/transcription/request.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
from typing import Final, cast # noqa: TID251 # exact built-ins are narrowed at admission
|
||||
|
||||
from litellm.rust_bridge.configuration import CapabilityContext
|
||||
|
||||
_PARAMETERS: Final = (
|
||||
"model",
|
||||
"file",
|
||||
"language",
|
||||
"prompt",
|
||||
"response_format",
|
||||
"timestamp_granularities",
|
||||
"temperature",
|
||||
"user",
|
||||
"timeout",
|
||||
"api_key",
|
||||
"api_base",
|
||||
"api_version",
|
||||
"max_retries",
|
||||
"custom_llm_provider",
|
||||
)
|
||||
|
||||
|
||||
def _format(file: object) -> str:
|
||||
if type(file) in (bytes, bytearray, BytesIO):
|
||||
return "wav"
|
||||
if type(file) is tuple and len(cast(tuple[object, ...], file)) >= 2: # cast-ok: exact tuple checked first
|
||||
name: Final = cast(tuple[object, ...], file)[0] # cast-ok: exact tuple checked first
|
||||
return name.rsplit(".", 1)[-1].lower() if type(name) is str and "." in name else "wav"
|
||||
return ""
|
||||
|
||||
|
||||
def request(args: tuple[object, ...], kwargs: dict[str, object]) -> dict[str, object]:
|
||||
positional: Final = { # mutable-ok: positional values are merged into the owned boundary request
|
||||
name: args[index] for index, name in enumerate(_PARAMETERS) if index < len(args)
|
||||
}
|
||||
supplied: Final = {**positional, **kwargs} # mutable-ok: exact public arguments are snapshotted
|
||||
model: Final = supplied.get("model")
|
||||
explicit: Final = supplied.get("custom_llm_provider")
|
||||
prefix: Final = model.partition("/")[0] if type(model) is str else ""
|
||||
provider: Final = explicit if type(explicit) is str else prefix
|
||||
return { # mutable-ok: PyO3 requires an owned exact dict at admission
|
||||
**supplied,
|
||||
"model": model,
|
||||
"custom_llm_provider": provider,
|
||||
"audio": {"format": _format(supplied.get("file"))}, # mutable-ok: early admission fact is owned
|
||||
"optional_params": {}, # mutable-ok: host projection fills parameters after admission
|
||||
}
|
||||
|
||||
|
||||
def context(boundary_request: dict[str, object]) -> CapabilityContext:
|
||||
model: Final = boundary_request.get("model")
|
||||
provider: Final = boundary_request.get("custom_llm_provider")
|
||||
return CapabilityContext(
|
||||
provider=provider if type(provider) is str else "",
|
||||
model=model if type(model) is str else "",
|
||||
)
|
||||
|
|
@ -7,28 +7,18 @@ from typing import Protocol
|
|||
class RustTranscription(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
model: str,
|
||||
audio: dict[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: dict[str, object] | None,
|
||||
optional_params: dict[str, object],
|
||||
timeout_seconds: float | None,
|
||||
) -> dict[str, object]:
|
||||
raise NotImplementedError
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> object: ...
|
||||
|
||||
|
||||
class RustAtranscription(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
model: str,
|
||||
audio: dict[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: dict[str, object] | None,
|
||||
optional_params: dict[str, object],
|
||||
timeout_seconds: float | None,
|
||||
) -> Awaitable[dict[str, object]]:
|
||||
raise NotImplementedError
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> Awaitable[object]: ...
|
||||
|
|
|
|||
|
|
@ -1,121 +1 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import (
|
||||
Final,
|
||||
cast, # noqa: TID251 # native callable signatures are checked by bridge contract tests
|
||||
)
|
||||
|
||||
import httpx
|
||||
|
||||
from litellm.rust_bridge.bindings import BINDING_UNSET, BindingUnset
|
||||
from litellm.rust_bridge.configuration import CapabilityContext
|
||||
from litellm.rust_bridge.runtime import BridgeErrorContext, ainvoke, invoke
|
||||
from litellm.rust_bridge.timeouts import timeout_to_seconds
|
||||
from litellm.rust_bridge.transcription.definition import COMPONENT
|
||||
from litellm.rust_bridge.transcription.types import RustAtranscription, RustTranscription
|
||||
|
||||
|
||||
def _as_transcription(value: object) -> RustTranscription | None:
|
||||
return cast(RustTranscription, value) if callable(value) else None # cast-ok: validated callable native binding
|
||||
|
||||
|
||||
def _as_atranscription(value: object) -> RustAtranscription | None:
|
||||
return cast(RustAtranscription, value) if callable(value) else None # cast-ok: validated callable native binding
|
||||
|
||||
|
||||
_TRANSCRIPTION: Final = COMPONENT.bind("transcription", validate=_as_transcription)
|
||||
_ATRANSCRIPTION: Final = COMPONENT.bind("atranscription", validate=_as_atranscription)
|
||||
|
||||
|
||||
def configure_rust_transcription(
|
||||
*,
|
||||
transcription: RustTranscription | None | BindingUnset = BINDING_UNSET,
|
||||
atranscription: RustAtranscription | None | BindingUnset = BINDING_UNSET,
|
||||
) -> None:
|
||||
_TRANSCRIPTION.configure(transcription)
|
||||
_ATRANSCRIPTION.configure(atranscription)
|
||||
|
||||
|
||||
def load_rust_transcription(*, context: CapabilityContext) -> RustTranscription | None:
|
||||
return COMPONENT.resolve(context).select(_TRANSCRIPTION)
|
||||
|
||||
|
||||
def load_rust_atranscription(*, context: CapabilityContext) -> RustAtranscription | None:
|
||||
return COMPONENT.resolve(context).select(_ATRANSCRIPTION)
|
||||
|
||||
|
||||
def transcription(
|
||||
*,
|
||||
model: str,
|
||||
audio: dict[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: dict[str, object] | None,
|
||||
optional_params: dict[str, object],
|
||||
timeout: float | httpx.Timeout | None,
|
||||
python_fallback: Callable[[], dict[str, object]] | None,
|
||||
) -> dict[str, object]:
|
||||
execution: Final = COMPONENT.resolve(CapabilityContext(provider=custom_llm_provider or "", model=model))
|
||||
rust_transcription: Final = execution.select(_TRANSCRIPTION)
|
||||
return invoke(
|
||||
execution=execution,
|
||||
native_call=(
|
||||
lambda: rust_transcription(
|
||||
model=model,
|
||||
audio=audio,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
optional_params=optional_params,
|
||||
timeout_seconds=timeout_to_seconds(timeout),
|
||||
)
|
||||
)
|
||||
if rust_transcription is not None
|
||||
else None,
|
||||
python_fallback=python_fallback,
|
||||
adapt=lambda response: response,
|
||||
context=BridgeErrorContext(route=COMPONENT.name.value, provider=custom_llm_provider or "", model=model),
|
||||
)
|
||||
|
||||
|
||||
async def atranscription(
|
||||
*,
|
||||
model: str,
|
||||
audio: dict[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: dict[str, object] | None,
|
||||
optional_params: dict[str, object],
|
||||
timeout: float | httpx.Timeout | None,
|
||||
python_fallback: Callable[[], Awaitable[dict[str, object]]] | None,
|
||||
) -> dict[str, object]:
|
||||
execution: Final = COMPONENT.resolve(CapabilityContext(provider=custom_llm_provider or "", model=model))
|
||||
rust_atranscription: Final = execution.select(_ATRANSCRIPTION)
|
||||
|
||||
async def adapt(response: dict[str, object]) -> dict[str, object]:
|
||||
return response
|
||||
|
||||
return await ainvoke(
|
||||
execution=execution,
|
||||
native_call=(
|
||||
lambda: rust_atranscription(
|
||||
model=model,
|
||||
audio=audio,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
optional_params=optional_params,
|
||||
timeout_seconds=timeout_to_seconds(timeout),
|
||||
)
|
||||
)
|
||||
if rust_atranscription is not None
|
||||
else None,
|
||||
python_fallback=python_fallback,
|
||||
adapt=adapt,
|
||||
context=BridgeErrorContext(route=COMPONENT.name.value, provider=custom_llm_provider or "", model=model),
|
||||
)
|
||||
"""Removed legacy transcription value adapter."""
|
||||
|
|
|
|||
|
|
@ -1,429 +1,154 @@
|
|||
"""Tests for the optional Rust-backed Anthropic Messages path."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from typing import cast
|
||||
import asyncio
|
||||
import inspect
|
||||
from collections.abc import Awaitable
|
||||
from types import SimpleNamespace
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
|
||||
from litellm.rust_bridge import configuration
|
||||
from litellm.types.llms.anthropic_messages.anthropic_response import (
|
||||
AnthropicMessagesResponse,
|
||||
from litellm.rust_bridge import bindings, configuration
|
||||
from litellm.rust_bridge.messages import lifecycle
|
||||
|
||||
|
||||
class RustBridgeDeclined(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RustBridgeUnavailable(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RustHostCallbackError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RustUpstreamError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
NATIVE_EXCEPTIONS: Final = SimpleNamespace(
|
||||
RustBridgeDeclined=RustBridgeDeclined,
|
||||
RustBridgeUnavailable=RustBridgeUnavailable,
|
||||
RustHostCallbackError=RustHostCallbackError,
|
||||
RustUpstreamError=RustUpstreamError,
|
||||
)
|
||||
|
||||
rust_messages = importlib.import_module("litellm.rust_bridge.messages")
|
||||
rust_bridge_loader = importlib.import_module("litellm.rust_bridge.loader")
|
||||
|
||||
FAKE_MESSAGES_RESPONSE: dict[str, object] = {
|
||||
"id": "msg_123",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"content": [{"type": "text", "text": "hello world"}],
|
||||
"stop_reason": "end_turn",
|
||||
"usage": {"input_tokens": 5, "output_tokens": 3},
|
||||
}
|
||||
|
||||
REQUEST_BODY: dict[str, object] = {
|
||||
"model": "claude-sonnet-4-5",
|
||||
"max_tokens": 64,
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
}
|
||||
PYTHON_MESSAGES_RESPONSE: dict[str, object] = {"id": "python_fallback"}
|
||||
|
||||
|
||||
class RecordingMessages:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[dict[str, object]] = []
|
||||
class RecordingSync:
|
||||
def __init__(self, error: BaseException | None = None) -> None:
|
||||
self.error: Final = error
|
||||
self.calls: Final[list[tuple[dict[str, object], tuple[object, ...], dict[str, object], object]]] = []
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
model: str,
|
||||
body: dict[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: dict[str, object] | None,
|
||||
timeout_seconds: float | None,
|
||||
has_agentic_hook: bool = False,
|
||||
on_request=None,
|
||||
) -> dict[str, object]:
|
||||
self.calls.append(
|
||||
{
|
||||
"model": model,
|
||||
"body": body,
|
||||
"api_key": api_key,
|
||||
"api_base": api_base,
|
||||
"custom_llm_provider": custom_llm_provider,
|
||||
"extra_headers": extra_headers,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
}
|
||||
)
|
||||
if on_request is not None:
|
||||
on_request()
|
||||
return dict(FAKE_MESSAGES_RESPONSE)
|
||||
|
||||
|
||||
class RecordingAsyncMessages:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[dict[str, object]] = []
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
model: str,
|
||||
body: dict[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: dict[str, object] | None,
|
||||
timeout_seconds: float | None,
|
||||
has_agentic_hook: bool = False,
|
||||
on_request=None,
|
||||
) -> dict[str, object]:
|
||||
self.calls.append(
|
||||
{
|
||||
"model": model,
|
||||
"body": body,
|
||||
"api_key": api_key,
|
||||
"api_base": api_base,
|
||||
"custom_llm_provider": custom_llm_provider,
|
||||
"extra_headers": extra_headers,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
}
|
||||
)
|
||||
if on_request is not None:
|
||||
on_request()
|
||||
return dict(FAKE_MESSAGES_RESPONSE)
|
||||
|
||||
|
||||
class ExplodingAsyncMessages:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
async def __call__(self, **kwargs: object) -> dict[str, object]:
|
||||
self.calls += 1
|
||||
raise AssertionError("bridge must not be called")
|
||||
|
||||
|
||||
class RaisingAsyncMessages:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
async def __call__(self, **kwargs: object) -> dict[str, object]:
|
||||
self.calls += 1
|
||||
raise RuntimeError("upstream request failed with status 400: bad request")
|
||||
|
||||
|
||||
class DecliningAsyncMessages:
|
||||
async def __call__(self, **kwargs: object) -> dict[str, object]:
|
||||
native = pytest.importorskip("litellm.rust_bridge._native")
|
||||
raise native.RustBridgeDeclined("unsupported request")
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> object:
|
||||
self.calls.append((request, args, kwargs, host))
|
||||
if self.error is not None:
|
||||
raise self.error
|
||||
return "native"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_rust_flag():
|
||||
rust_messages.set_rust_messages(messages=None, amessages=None)
|
||||
def reset_bridge(monkeypatch: pytest.MonkeyPatch):
|
||||
lifecycle.set_rust_messages(messages=None, amessages=None)
|
||||
configuration.reset_rust_configuration()
|
||||
rust_bridge_loader._cached_bridge = rust_bridge_loader._BRIDGE_SENTINEL
|
||||
yield
|
||||
rust_messages.set_rust_messages(messages=None, amessages=None)
|
||||
configuration.reset_rust_configuration()
|
||||
rust_bridge_loader._cached_bridge = rust_bridge_loader._BRIDGE_SENTINEL
|
||||
|
||||
|
||||
def test_load_rust_messages_returns_injected_impl():
|
||||
bridge = RecordingMessages()
|
||||
litellm.rust(True)
|
||||
rust_messages.set_rust_messages(messages=bridge)
|
||||
assert rust_messages.load_rust_messages() is bridge
|
||||
|
||||
|
||||
def test_load_rust_amessages_returns_injected_impl():
|
||||
bridge = RecordingAsyncMessages()
|
||||
litellm.rust(True)
|
||||
rust_messages.set_rust_messages(amessages=bridge)
|
||||
assert rust_messages.load_rust_amessages() is bridge
|
||||
|
||||
|
||||
def test_messages_wrapper_returns_fallback_when_bridge_absent(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
importlib.import_module("litellm.rust_bridge.bindings"),
|
||||
"get_native_bridge",
|
||||
lambda: None,
|
||||
)
|
||||
litellm.rust(True)
|
||||
assert rust_messages.load_rust_messages() is None
|
||||
result = rust_messages.messages(
|
||||
model="claude",
|
||||
body=REQUEST_BODY,
|
||||
api_key="k",
|
||||
api_base="b",
|
||||
custom_llm_provider="azure_ai",
|
||||
extra_headers={},
|
||||
timeout=30.0,
|
||||
python_fallback=lambda: dict(PYTHON_MESSAGES_RESPONSE),
|
||||
adapt=lambda response: response,
|
||||
)
|
||||
assert result == PYTHON_MESSAGES_RESPONSE
|
||||
|
||||
|
||||
def test_messages_wrapper_forwards_args_and_converts_timeout():
|
||||
bridge = RecordingMessages()
|
||||
litellm.rust(True)
|
||||
rust_messages.set_rust_messages(messages=bridge)
|
||||
|
||||
response = rust_messages.messages(
|
||||
model="claude-sonnet-4-5",
|
||||
body=REQUEST_BODY,
|
||||
api_key="sk-azure",
|
||||
api_base="https://resource.services.ai.azure.com/anthropic",
|
||||
custom_llm_provider="azure_ai",
|
||||
extra_headers={"anthropic-beta": "token-efficient-tools-2025-02-19"},
|
||||
timeout=httpx.Timeout(600.0, read=42.0),
|
||||
python_fallback=lambda: pytest.fail("native request should not fall back"),
|
||||
adapt=lambda response: response,
|
||||
)
|
||||
|
||||
assert response == FAKE_MESSAGES_RESPONSE
|
||||
assert bridge.calls[0] == {
|
||||
"model": "claude-sonnet-4-5",
|
||||
"body": REQUEST_BODY,
|
||||
"api_key": "sk-azure",
|
||||
"api_base": "https://resource.services.ai.azure.com/anthropic",
|
||||
"custom_llm_provider": "azure_ai",
|
||||
"extra_headers": {"anthropic-beta": "token-efficient-tools-2025-02-19"},
|
||||
"timeout_seconds": 42.0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_amessages_wrapper_forwards_args():
|
||||
bridge = RecordingAsyncMessages()
|
||||
litellm.rust(True)
|
||||
rust_messages.set_rust_messages(amessages=bridge)
|
||||
|
||||
async def python_fallback() -> dict[str, object]:
|
||||
pytest.fail("native request should not fall back")
|
||||
|
||||
async def adapt(response: dict[str, object]) -> dict[str, object]:
|
||||
return response
|
||||
|
||||
response = await rust_messages.amessages(
|
||||
model="claude-sonnet-4-5",
|
||||
body=REQUEST_BODY,
|
||||
api_key="sk-azure",
|
||||
api_base="https://resource.services.ai.azure.com/anthropic",
|
||||
custom_llm_provider="azure_ai",
|
||||
extra_headers=None,
|
||||
timeout=12.5,
|
||||
python_fallback=python_fallback,
|
||||
adapt=adapt,
|
||||
)
|
||||
|
||||
assert response == FAKE_MESSAGES_RESPONSE
|
||||
assert bridge.calls[0]["model"] == "claude-sonnet-4-5"
|
||||
assert bridge.calls[0]["timeout_seconds"] == 12.5
|
||||
|
||||
|
||||
async def _gate(**overrides):
|
||||
kwargs = {
|
||||
"custom_llm_provider": "azure_ai",
|
||||
"has_agentic_hook": False,
|
||||
"model": "claude-sonnet-4-5",
|
||||
"api_key": "sk-azure",
|
||||
"api_base": "https://resource.services.ai.azure.com/anthropic",
|
||||
"headers": {"x-api-key": "sk-azure", "anthropic-version": "2023-06-01"},
|
||||
"request_body": dict(REQUEST_BODY),
|
||||
"timeout": 30.0,
|
||||
}
|
||||
kwargs.update(overrides)
|
||||
request_body = kwargs.pop("request_body")
|
||||
|
||||
async def python_fallback() -> dict[str, object]:
|
||||
return dict(PYTHON_MESSAGES_RESPONSE)
|
||||
|
||||
async def adapt(response: dict[str, object]) -> dict[str, object]:
|
||||
adapted = dict(response)
|
||||
adapted["_hidden_params"] = {"additional_headers": {"x-litellm-rust": "true"}}
|
||||
return adapted
|
||||
|
||||
return await rust_messages.amessages(
|
||||
model=kwargs["model"],
|
||||
body={key: value for key, value in request_body.items() if key != "stream"},
|
||||
has_agentic_hook=kwargs["has_agentic_hook"],
|
||||
api_key=kwargs["api_key"],
|
||||
api_base=kwargs["api_base"],
|
||||
custom_llm_provider=kwargs["custom_llm_provider"],
|
||||
extra_headers=kwargs["headers"],
|
||||
timeout=kwargs["timeout"],
|
||||
python_fallback=python_fallback,
|
||||
adapt=adapt,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate_invokes_rust_and_marks_response_header():
|
||||
bridge = RecordingAsyncMessages()
|
||||
litellm.rust(True)
|
||||
rust_messages.set_rust_messages(amessages=bridge)
|
||||
|
||||
response = await _gate()
|
||||
|
||||
assert response is not None
|
||||
assert response["id"] == "msg_123"
|
||||
assert response["_hidden_params"]["additional_headers"] == {"x-litellm-rust": "true"}
|
||||
call = bridge.calls[0]
|
||||
assert call["model"] == "claude-sonnet-4-5"
|
||||
assert call["body"] == REQUEST_BODY
|
||||
assert call["api_key"] == "sk-azure"
|
||||
assert call["api_base"] == "https://resource.services.ai.azure.com/anthropic"
|
||||
assert call["extra_headers"] == {"x-api-key": "sk-azure", "anthropic-version": "2023-06-01"}
|
||||
assert call["timeout_seconds"] == 30.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate_propagates_unclassified_bridge_failure():
|
||||
bridge = RaisingAsyncMessages()
|
||||
litellm.rust(True)
|
||||
rust_messages.set_rust_messages(amessages=bridge)
|
||||
|
||||
with pytest.raises(RuntimeError, match="upstream request failed"):
|
||||
await _gate()
|
||||
assert bridge.calls == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate_skips_rust_when_flag_absent(monkeypatch):
|
||||
monkeypatch.delenv("LITELLM_RUST", raising=False)
|
||||
bridge = ExplodingAsyncMessages()
|
||||
rust_messages.set_rust_messages(amessages=bridge)
|
||||
|
||||
response = await _gate()
|
||||
|
||||
assert response == PYTHON_MESSAGES_RESPONSE
|
||||
assert bridge.calls == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate_uses_process_enable_without_request_override():
|
||||
bridge = RecordingAsyncMessages()
|
||||
rust_messages.set_rust_messages(amessages=bridge)
|
||||
litellm.rust(True)
|
||||
|
||||
response = await _gate()
|
||||
|
||||
assert response is not None
|
||||
assert bridge.calls[0]["custom_llm_provider"] == "azure_ai"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate_invokes_rust_for_native_anthropic_provider():
|
||||
bridge = RecordingAsyncMessages()
|
||||
litellm.rust(True)
|
||||
rust_messages.set_rust_messages(amessages=bridge)
|
||||
|
||||
response = await _gate(
|
||||
custom_llm_provider="anthropic",
|
||||
api_key="sk-ant",
|
||||
api_base="https://api.anthropic.com",
|
||||
headers={"x-api-key": "sk-ant", "anthropic-version": "2023-06-01"},
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response["_hidden_params"]["additional_headers"] == {"x-litellm-rust": "true"}
|
||||
assert bridge.calls[0]["custom_llm_provider"] == "anthropic"
|
||||
assert bridge.calls[0]["api_key"] == "sk-ant"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate_invokes_rust_when_env_var_set(monkeypatch):
|
||||
bridge = RecordingAsyncMessages()
|
||||
rust_messages.set_rust_messages(amessages=bridge)
|
||||
monkeypatch.setenv("LITELLM_RUST", "1")
|
||||
monkeypatch.setattr(bindings, "get_native_bridge", lambda: NATIVE_EXCEPTIONS)
|
||||
yield
|
||||
lifecycle.set_rust_messages(messages=None, amessages=None)
|
||||
configuration.reset_rust_configuration()
|
||||
|
||||
response = await _gate(
|
||||
custom_llm_provider="anthropic",
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert bridge.calls[0]["custom_llm_provider"] == "anthropic"
|
||||
def sync_python(max_tokens: int, messages: list[object], model: str, **kwargs: object) -> object:
|
||||
return max_tokens, messages, model, kwargs
|
||||
|
||||
|
||||
async def async_python(max_tokens: int, messages: list[object], model: str, **kwargs: object) -> object:
|
||||
return max_tokens, messages, model, kwargs
|
||||
|
||||
|
||||
def test_sync_boundary_enters_native_once_and_preserves_call_shape() -> None:
|
||||
rust: Final = RecordingSync()
|
||||
lifecycle.set_rust_messages(messages=rust)
|
||||
wrapped: Final = lifecycle.wrap_sync(sync_python)
|
||||
messages: Final[list[object]] = [{"role": "user", "content": "hi"}]
|
||||
|
||||
assert wrapped(64, messages, "anthropic/model", temperature=0.2) == "native"
|
||||
assert len(rust.calls) == 1
|
||||
request, args, kwargs, _ = rust.calls[0]
|
||||
assert args == (64, messages, "anthropic/model")
|
||||
assert kwargs == {"temperature": 0.2}
|
||||
assert request["model"] == "anthropic/model"
|
||||
assert request["body"] == {"max_tokens": 64, "messages": messages, "temperature": 0.2}
|
||||
assert inspect.signature(wrapped) == inspect.signature(sync_python)
|
||||
|
||||
|
||||
def test_advisor_interceptor_request_declines_native_host_ownership() -> None:
|
||||
rust: Final = RecordingSync(RustBridgeDeclined("host operations"))
|
||||
lifecycle.set_rust_messages(messages=rust)
|
||||
wrapped: Final = lifecycle.wrap_sync(sync_python)
|
||||
tools: Final[list[object]] = [{"type": "advisor_20260301", "model": "advisor-model"}]
|
||||
|
||||
wrapped(64, [], "anthropic/model", tools=tools)
|
||||
|
||||
request, _, _, _ = rust.calls[0]
|
||||
assert request["has_agentic_hook"] is True
|
||||
|
||||
|
||||
def test_decline_calls_captured_python_implementation_once() -> None:
|
||||
rust: Final = RecordingSync(RustBridgeDeclined("unsupported"))
|
||||
lifecycle.set_rust_messages(messages=rust)
|
||||
calls: Final[list[None]] = []
|
||||
|
||||
def python(max_tokens: int, messages: list[object], model: str) -> str:
|
||||
calls.append(None)
|
||||
return model
|
||||
|
||||
assert lifecycle.wrap_sync(python)(1, [], "anthropic/model") == "anthropic/model"
|
||||
assert len(rust.calls) == 1
|
||||
assert calls == [None]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate_env_var_falsey_does_not_enable(monkeypatch):
|
||||
bridge = ExplodingAsyncMessages()
|
||||
rust_messages.set_rust_messages(amessages=bridge)
|
||||
monkeypatch.setenv("LITELLM_RUST", "0")
|
||||
async def test_async_decline_is_caught_only_during_admission() -> None:
|
||||
def decline(
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> Awaitable[object]:
|
||||
raise RustBridgeDeclined("admission")
|
||||
|
||||
response = await _gate(
|
||||
custom_llm_provider="anthropic",
|
||||
)
|
||||
lifecycle.set_rust_messages(amessages=decline)
|
||||
wrapped: Final = lifecycle.wrap_async(async_python)
|
||||
assert await wrapped(1, [], "anthropic/model") == (1, [], "anthropic/model", {})
|
||||
|
||||
assert response == PYTHON_MESSAGES_RESPONSE
|
||||
assert bridge.calls == 0
|
||||
error: Final = RustBridgeDeclined("resume")
|
||||
|
||||
def fail(
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> Awaitable[object]:
|
||||
async def result() -> object:
|
||||
await asyncio.sleep(0)
|
||||
raise error
|
||||
|
||||
return result()
|
||||
|
||||
lifecycle.set_rust_messages(amessages=fail)
|
||||
with pytest.raises(RustBridgeDeclined) as caught:
|
||||
await wrapped(1, [], "anthropic/model")
|
||||
assert caught.value is error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate_falls_back_for_unsupported_provider():
|
||||
litellm.rust(True)
|
||||
rust_messages.set_rust_messages(amessages=DecliningAsyncMessages())
|
||||
response = await _gate(custom_llm_provider="openai", api_base="http://127.0.0.1:1")
|
||||
assert response == PYTHON_MESSAGES_RESPONSE
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate_falls_back_for_agentic_hook():
|
||||
litellm.rust(True)
|
||||
rust_messages.set_rust_messages(amessages=DecliningAsyncMessages())
|
||||
response = await _gate(has_agentic_hook=True, api_base="http://127.0.0.1:1")
|
||||
assert response == PYTHON_MESSAGES_RESPONSE
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate_streams_through_rust_when_eligible_and_strips_stream_flag():
|
||||
bridge = RecordingAsyncMessages()
|
||||
litellm.rust(True)
|
||||
rust_messages.set_rust_messages(amessages=bridge)
|
||||
|
||||
streaming_body = {**REQUEST_BODY, "stream": True}
|
||||
response = await _gate(
|
||||
has_agentic_hook=False,
|
||||
request_body=streaming_body,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response["_hidden_params"]["additional_headers"] == {"x-litellm-rust": "true"}
|
||||
assert "stream" not in bridge.calls[0]["body"]
|
||||
assert bridge.calls[0]["body"] == REQUEST_BODY
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fake_stream_wraps_rust_response_as_anthropic_sse():
|
||||
response = cast(AnthropicMessagesResponse, dict(FAKE_MESSAGES_RESPONSE))
|
||||
stream = BaseLLMHTTPHandler._rust_anthropic_messages_fake_stream(response)
|
||||
|
||||
assert stream._hidden_params["additional_headers"] == {"x-litellm-rust": "true"}
|
||||
|
||||
chunks = [chunk async for chunk in stream]
|
||||
joined = b"".join(chunks)
|
||||
|
||||
assert b"event: message_start" in joined
|
||||
assert b"event: content_block_delta" in joined
|
||||
assert b"hello world" in joined
|
||||
assert b"event: message_stop" in joined
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate_falls_back_when_bridge_unavailable(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
importlib.import_module("litellm.rust_bridge.bindings"),
|
||||
"get_native_bridge",
|
||||
lambda: None,
|
||||
)
|
||||
litellm.rust(True)
|
||||
|
||||
response = await _gate()
|
||||
|
||||
assert response == PYTHON_MESSAGES_RESPONSE
|
||||
def test_missing_binding_calls_python_without_native_entry(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(bindings, "get_native_bridge", lambda: None)
|
||||
assert lifecycle.wrap_sync(sync_python)(1, [], "anthropic/model") == (1, [], "anthropic/model", {})
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
Tests for the OCR `req_format` option in the SDK request path.
|
||||
"""
|
||||
|
||||
from litellm.rust_bridge.ocr import value as rust_ocr_bridge
|
||||
from litellm.rust_bridge.ocr.host import HOST
|
||||
|
||||
|
||||
def test_rust_ocr_response_retains_provider_native_response():
|
||||
provider_response = {"status": "succeeded", "analyzeResult": {"content": "native"}}
|
||||
response = rust_ocr_bridge.adapt_response(
|
||||
response = HOST.response(
|
||||
{
|
||||
"pages": [],
|
||||
"model": "prebuilt-layout",
|
||||
|
|
|
|||
|
|
@ -1,177 +1,173 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
from collections.abc import Awaitable
|
||||
from types import SimpleNamespace
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.rust_bridge import chat_completions as bridge
|
||||
from litellm.rust_bridge import configuration
|
||||
from litellm.types.utils import ModelResponse
|
||||
|
||||
native = pytest.importorskip("litellm.rust_bridge._native")
|
||||
|
||||
RUST_RESPONSE: Final = {
|
||||
"created": 1_700_000_000,
|
||||
"model": "claude-sonnet-4-5-20260101",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": "hello from rust"},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {"prompt_tokens": 11, "completion_tokens": 4, "total_tokens": 15},
|
||||
}
|
||||
MESSAGES: Final = [{"role": "user", "content": "hi"}]
|
||||
|
||||
_FakeDeclined = native.RustBridgeDeclined
|
||||
_FakeUpstream = native.RustUpstreamError
|
||||
from litellm.rust_bridge import bindings, configuration
|
||||
from litellm.rust_bridge.chat_completions import lifecycle
|
||||
|
||||
|
||||
class _FakeNative:
|
||||
RustBridgeDeclined = _FakeDeclined
|
||||
RustUpstreamError = _FakeUpstream
|
||||
class RustBridgeDeclined(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class _RecordingCall:
|
||||
def __init__(self, result: object = RUST_RESPONSE, error: Exception | None = None) -> None:
|
||||
class RustBridgeUnavailable(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RustHostCallbackError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RustUpstreamError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
NATIVE_EXCEPTIONS: Final = SimpleNamespace(
|
||||
RustBridgeDeclined=RustBridgeDeclined,
|
||||
RustBridgeUnavailable=RustBridgeUnavailable,
|
||||
RustHostCallbackError=RustHostCallbackError,
|
||||
RustUpstreamError=RustUpstreamError,
|
||||
)
|
||||
|
||||
|
||||
class _RecordingSync:
|
||||
def __init__(self, result: object = "native", error: BaseException | None = None) -> None:
|
||||
self.result: Final = result
|
||||
self.error: Final = error
|
||||
self.calls: Final[list[dict[str, object]]] = []
|
||||
self.calls: Final[list[tuple[dict[str, object], tuple[object, ...], dict[str, object], object]]] = []
|
||||
|
||||
def __call__(self, **kwargs: object) -> object:
|
||||
self.calls.append(kwargs)
|
||||
def __call__(
|
||||
self,
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> object:
|
||||
self.calls.append((request, args, kwargs, host))
|
||||
if self.error is not None:
|
||||
raise self.error
|
||||
on_request: Final = kwargs["on_request"]
|
||||
assert callable(on_request)
|
||||
on_request()
|
||||
return self.result
|
||||
|
||||
|
||||
class _RecordingAsyncCall(_RecordingCall):
|
||||
async def __call__(self, **kwargs: object) -> object:
|
||||
return super().__call__(**kwargs)
|
||||
class _RecordingAsync(_RecordingSync):
|
||||
def __call__(
|
||||
self,
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> Awaitable[object]:
|
||||
self.calls.append((request, args, kwargs, host))
|
||||
if self.error is not None:
|
||||
raise self.error
|
||||
|
||||
async def result() -> object:
|
||||
return self.result
|
||||
|
||||
return result()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_bridge(monkeypatch: pytest.MonkeyPatch):
|
||||
bridge.set_rust_chat_completions(chat_completions=None, achat_completions=None)
|
||||
lifecycle.set_rust_chat_completions(chat_completions=None, achat_completions=None)
|
||||
configuration.reset_rust_configuration()
|
||||
monkeypatch.setenv("LITELLM_RUST", "1")
|
||||
monkeypatch.setattr(bindings, "get_native_bridge", lambda: NATIVE_EXCEPTIONS)
|
||||
yield
|
||||
bridge.set_rust_chat_completions(chat_completions=None, achat_completions=None)
|
||||
lifecycle.set_rust_chat_completions(chat_completions=None, achat_completions=None)
|
||||
configuration.reset_rust_configuration()
|
||||
|
||||
|
||||
def _fake_native_bridge(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("litellm.rust_bridge.bindings.get_native_bridge", lambda: _FakeNative())
|
||||
def _sync_function(model: str, messages: list[object], **kwargs: object) -> object:
|
||||
return (model, messages, kwargs)
|
||||
|
||||
|
||||
def _call_kwargs(model_response: ModelResponse, fallback: object = "python") -> dict[str, object]:
|
||||
return {
|
||||
"model": "claude-sonnet-4-5",
|
||||
"messages": MESSAGES,
|
||||
"optional_params": {"max_tokens": 16},
|
||||
"model_response": model_response,
|
||||
"api_key": "sk-test",
|
||||
"api_base": None,
|
||||
"custom_llm_provider": "anthropic",
|
||||
"extra_headers": {},
|
||||
"timeout": 30.0,
|
||||
"python_fallback": lambda: fallback,
|
||||
}
|
||||
async def _async_function(model: str, messages: list[object], **kwargs: object) -> object:
|
||||
return (model, messages, kwargs)
|
||||
|
||||
|
||||
def test_sync_native_entrypoint_runs_once_and_logs_once() -> None:
|
||||
events: Final[list[str]] = []
|
||||
native_call: Final = _RecordingCall()
|
||||
bridge.set_rust_chat_completions(chat_completions=native_call)
|
||||
kwargs: Final = _call_kwargs(ModelResponse())
|
||||
kwargs.update({"on_request": lambda: events.append("pre"), "on_response": lambda _value: events.append("post")})
|
||||
def test_sync_boundary_enters_native_once_and_preserves_call_shape() -> None:
|
||||
rust: Final = _RecordingSync()
|
||||
lifecycle.set_rust_chat_completions(chat_completions=rust)
|
||||
wrapped: Final = lifecycle.wrap_sync(_sync_function)
|
||||
messages: Final[list[object]] = [{"role": "user", "content": "hi"}]
|
||||
|
||||
result: Final = bridge.chat_completions(**kwargs)
|
||||
|
||||
assert isinstance(result, ModelResponse)
|
||||
assert result.choices[0].message.content == "hello from rust"
|
||||
assert result._hidden_params["additional_headers"] == {"x-litellm-rust": "true"}
|
||||
assert len(native_call.calls) == 1
|
||||
assert events == ["pre", "post"]
|
||||
assert wrapped("anthropic/model", messages, temperature=0.2) == "native"
|
||||
assert len(rust.calls) == 1
|
||||
request, args, kwargs, _ = rust.calls[0]
|
||||
assert args == ("anthropic/model", messages)
|
||||
assert kwargs == {"temperature": 0.2}
|
||||
assert request["model"] == "anthropic/model"
|
||||
assert request["messages"] is messages
|
||||
assert inspect.signature(wrapped) == inspect.signature(_sync_function)
|
||||
|
||||
|
||||
def test_decline_has_no_logging_effect_and_runs_one_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_fake_native_bridge(monkeypatch)
|
||||
events: Final[list[str]] = []
|
||||
native_call: Final = _RecordingCall(error=_FakeDeclined("unsupported"))
|
||||
bridge.set_rust_chat_completions(chat_completions=native_call)
|
||||
kwargs: Final = _call_kwargs(ModelResponse(), fallback="python")
|
||||
kwargs.update({"on_request": lambda: events.append("pre"), "on_response": lambda _value: events.append("post")})
|
||||
def test_decline_calls_captured_python_implementation_once() -> None:
|
||||
rust: Final = _RecordingSync(error=RustBridgeDeclined("unsupported"))
|
||||
lifecycle.set_rust_chat_completions(chat_completions=rust)
|
||||
calls: Final[list[None]] = []
|
||||
|
||||
assert bridge.chat_completions(**kwargs) == "python"
|
||||
assert len(native_call.calls) == 1
|
||||
assert events == []
|
||||
def python(model: str, messages: list[object]) -> str:
|
||||
calls.append(None)
|
||||
return f"python:{model}:{len(messages)}"
|
||||
|
||||
wrapped: Final = lifecycle.wrap_sync(python)
|
||||
assert wrapped("anthropic/model", []) == "python:anthropic/model:0"
|
||||
assert len(rust.calls) == 1
|
||||
assert calls == [None]
|
||||
|
||||
|
||||
def test_streaming_decline_comes_from_the_native_call(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_fake_native_bridge(monkeypatch)
|
||||
native_call: Final = _RecordingCall(error=_FakeDeclined("streaming"))
|
||||
bridge.set_rust_chat_completions(chat_completions=native_call)
|
||||
kwargs: Final = _call_kwargs(ModelResponse())
|
||||
kwargs["stream"] = True
|
||||
assert bridge.chat_completions(**kwargs) == "python"
|
||||
assert len(native_call.calls) == 1
|
||||
assert native_call.calls[0]["host_facts"] == {
|
||||
"stream": True,
|
||||
"anthropic_user_id": False,
|
||||
"bedrock_metadata_owned": False,
|
||||
}
|
||||
def test_missing_binding_calls_python_without_native_entry(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
calls: Final[list[None]] = []
|
||||
monkeypatch.setattr(bindings, "get_native_bridge", lambda: None)
|
||||
|
||||
def python(model: str, messages: list[object]) -> str:
|
||||
calls.append(None)
|
||||
return model
|
||||
|
||||
def test_host_facts_reach_the_single_native_call(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
native_call: Final = _RecordingCall()
|
||||
bridge.set_rust_chat_completions(chat_completions=native_call)
|
||||
monkeypatch.setattr(litellm, "bedrock_request_metadata_fields", ["team_id"])
|
||||
kwargs: Final = _call_kwargs(ModelResponse())
|
||||
kwargs["litellm_params"] = {"metadata": {"user_id": "u-1"}}
|
||||
bridge.chat_completions(**kwargs)
|
||||
assert native_call.calls[0]["host_facts"] == {
|
||||
"stream": False,
|
||||
"anthropic_user_id": True,
|
||||
"bedrock_metadata_owned": True,
|
||||
}
|
||||
|
||||
|
||||
def test_upstream_and_adaptation_failures_never_fall_back(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_fake_native_bridge(monkeypatch)
|
||||
fallback_calls: Final[list[bool]] = []
|
||||
bridge.set_rust_chat_completions(chat_completions=_RecordingCall(error=_FakeUpstream(429, "rate limited")))
|
||||
kwargs: Final = _call_kwargs(ModelResponse())
|
||||
kwargs["python_fallback"] = lambda: fallback_calls.append(True)
|
||||
with pytest.raises(litellm.APIError, match="rate limited"):
|
||||
bridge.chat_completions(**kwargs)
|
||||
assert fallback_calls == []
|
||||
|
||||
bridge.set_rust_chat_completions(chat_completions=_RecordingCall())
|
||||
kwargs["on_response"] = lambda _value: (_ for _ in ()).throw(RuntimeError("adapt failed"))
|
||||
with pytest.raises(RuntimeError, match="adapt failed"):
|
||||
bridge.chat_completions(**kwargs)
|
||||
assert fallback_calls == []
|
||||
assert lifecycle.wrap_sync(python)("anthropic/model", []) == "anthropic/model"
|
||||
assert calls == [None]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_native_and_fallback_paths(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
native_call: Final = _RecordingAsyncCall()
|
||||
bridge.set_rust_chat_completions(achat_completions=native_call)
|
||||
async def test_async_decline_is_caught_only_while_obtaining_coroutine() -> None:
|
||||
entry_decline: Final = _RecordingAsync(error=RustBridgeDeclined("admission"))
|
||||
lifecycle.set_rust_chat_completions(achat_completions=entry_decline)
|
||||
wrapped: Final = lifecycle.wrap_async(_async_function)
|
||||
python_result: Final = await wrapped("anthropic/model", [])
|
||||
assert python_result == ("anthropic/model", [], {})
|
||||
|
||||
async def fallback() -> str:
|
||||
return "python"
|
||||
execution_error: Final = RustBridgeDeclined("resume")
|
||||
|
||||
kwargs: Final = _call_kwargs(ModelResponse())
|
||||
kwargs["python_fallback"] = fallback
|
||||
result: Final = await bridge.achat_completions(**kwargs)
|
||||
assert isinstance(result, ModelResponse)
|
||||
assert len(native_call.calls) == 1
|
||||
def fail_after_admission(
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> Awaitable[object]:
|
||||
async def fail() -> object:
|
||||
await asyncio.sleep(0)
|
||||
raise execution_error
|
||||
|
||||
configuration.rust(False)
|
||||
assert await bridge.achat_completions(**kwargs) == "python"
|
||||
return fail()
|
||||
|
||||
lifecycle.set_rust_chat_completions(achat_completions=fail_after_admission)
|
||||
with pytest.raises(RustBridgeDeclined) as caught:
|
||||
await wrapped("anthropic/model", [])
|
||||
assert caught.value is execution_error
|
||||
|
||||
|
||||
def test_streaming_is_declined_by_the_same_native_entry() -> None:
|
||||
rust: Final = _RecordingSync(error=RustBridgeDeclined("streaming"))
|
||||
lifecycle.set_rust_chat_completions(chat_completions=rust)
|
||||
wrapped: Final = lifecycle.wrap_sync(_sync_function)
|
||||
|
||||
assert wrapped("anthropic/model", [], stream=True) == ("anthropic/model", [], {"stream": True})
|
||||
assert len(rust.calls) == 1
|
||||
assert rust.calls[0][0]["host_facts"] == {"stream": True}
|
||||
|
|
|
|||
|
|
@ -64,10 +64,9 @@ def test_public_binding_keeps_positional_fields_and_defaults_out_of_native_hook_
|
|||
request: LiteLLMOcrRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: Mapping[str, object],
|
||||
asynchronous: bool,
|
||||
host: object,
|
||||
) -> OCRResponse:
|
||||
captured.append((request, args, kwargs, asynchronous))
|
||||
captured.append((request, args, kwargs))
|
||||
return OCRResponse(pages=[], model=request.model)
|
||||
|
||||
litellm.rust(True)
|
||||
|
|
@ -78,13 +77,12 @@ def test_public_binding_keeps_positional_fields_and_defaults_out_of_native_hook_
|
|||
NATIVE_OCR_LIFECYCLE.reset()
|
||||
configuration.reset_rust_configuration()
|
||||
|
||||
request, call_args, hook_kwargs, asynchronous = captured[0]
|
||||
request, call_args, hook_kwargs = captured[0]
|
||||
assert response.model == "mistral/mistral-ocr-latest"
|
||||
assert request.model == "mistral/mistral-ocr-latest"
|
||||
assert request.document is document
|
||||
assert call_args == ("mistral/mistral-ocr-latest", document)
|
||||
assert hook_kwargs == {}
|
||||
assert asynchronous is False
|
||||
|
||||
|
||||
def test_public_binding_keeps_keyword_model_and_document_in_native_hook_kwargs() -> None:
|
||||
|
|
@ -95,7 +93,6 @@ def test_public_binding_keeps_keyword_model_and_document_in_native_hook_kwargs()
|
|||
request: LiteLLMOcrRequest,
|
||||
args: tuple[object, ...],
|
||||
kwargs: Mapping[str, object],
|
||||
asynchronous: bool,
|
||||
host: object,
|
||||
) -> OCRResponse:
|
||||
assert args == ()
|
||||
|
|
@ -211,7 +208,7 @@ async def test_only_native_declines_replay_on_legacy(
|
|||
monkeypatch: pytest.MonkeyPatch, asynchronous: bool, declined: bool
|
||||
) -> None:
|
||||
failure: Final = Declined("unsupported") if declined else RuntimeError("provider already called")
|
||||
native: Final = AsyncMock(side_effect=failure) if asynchronous else Mock(side_effect=failure)
|
||||
native: Final = AsyncMock(side_effect=failure) if asynchronous and not declined else Mock(side_effect=failure)
|
||||
NATIVE_OCR_LIFECYCLE.override(native)
|
||||
monkeypatch.setattr(
|
||||
bindings,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import pytest
|
|||
|
||||
from litellm.exceptions import APIError
|
||||
from litellm.rust_bridge import bindings, runtime
|
||||
from litellm.rust_bridge.configuration import ExecutionDecision, ComponentName
|
||||
from litellm.rust_bridge.configuration import ComponentName, ExecutionDecision
|
||||
from litellm.rust_bridge.errors import RustRouteDeclinedError, RustRouteUnavailableError, RustRouteUnsupportedError
|
||||
from litellm.rust_bridge.route import ComponentExecution
|
||||
|
||||
|
|
@ -260,6 +260,45 @@ async def test_host_callback_failure_preserves_its_cause(asynchronous: bool) ->
|
|||
assert caught.value is callback_error
|
||||
|
||||
|
||||
def test_lifecycle_unavailable_during_execution_never_falls_back() -> None:
|
||||
fallback_calls: Final[list[None]] = []
|
||||
|
||||
with pytest.raises(RustBridgeUnavailable):
|
||||
runtime.invoke_lifecycle(
|
||||
execution=ComponentExecution(
|
||||
route_name=ComponentName.MESSAGES,
|
||||
decision=ExecutionDecision.RUST_WITH_FALLBACK,
|
||||
),
|
||||
native_call=lambda: (_ for _ in ()).throw(RustBridgeUnavailable()),
|
||||
python_fallback=lambda: fallback_calls.append(None),
|
||||
)
|
||||
|
||||
assert fallback_calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("error", (RustBridgeDeclined("resume"), RustBridgeUnavailable()))
|
||||
async def test_async_lifecycle_reserved_error_after_admission_never_falls_back(error: Exception) -> None:
|
||||
fallback_calls: Final[list[None]] = []
|
||||
|
||||
async def fail_after_admission() -> object:
|
||||
await asyncio.sleep(0)
|
||||
raise error
|
||||
|
||||
with pytest.raises(type(error)) as caught:
|
||||
await runtime.ainvoke_lifecycle(
|
||||
execution=ComponentExecution(
|
||||
route_name=ComponentName.MESSAGES,
|
||||
decision=ExecutionDecision.RUST_WITH_FALLBACK,
|
||||
),
|
||||
native_call=fail_after_admission,
|
||||
python_fallback=lambda: asyncio.sleep(0, result=fallback_calls.append(None)),
|
||||
)
|
||||
|
||||
assert caught.value is error
|
||||
assert fallback_calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("asynchronous", (False, True))
|
||||
async def test_unsupported_execution_runs_nothing(asynchronous: bool) -> None:
|
||||
|
|
|
|||
|
|
@ -1,271 +1,126 @@
|
|||
import importlib
|
||||
from collections.abc import Iterator
|
||||
from types import ModuleType
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
from collections.abc import Awaitable, Iterator
|
||||
from types import SimpleNamespace
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.exceptions import APIError
|
||||
from litellm.llms.bedrock.audio_transcription import BedrockAudioTranscriptionRustDispatch
|
||||
from litellm.rust_bridge import configuration
|
||||
from litellm.rust_bridge.errors import RustRouteDeclinedError, RustRouteUnavailableError
|
||||
|
||||
rust_bridge = importlib.import_module("litellm.rust_bridge.transcription")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_bridge() -> Iterator[None]:
|
||||
configuration.reset_rust_configuration()
|
||||
rust_bridge.configure_rust_transcription(transcription=None, atranscription=None)
|
||||
yield
|
||||
configuration.reset_rust_configuration()
|
||||
rust_bridge.configure_rust_transcription(transcription=None, atranscription=None)
|
||||
|
||||
|
||||
class SyncBridge:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[dict[str, object]] = []
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
model: str,
|
||||
audio: dict[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: dict[str, object] | None,
|
||||
optional_params: dict[str, object],
|
||||
timeout_seconds: float | None,
|
||||
) -> dict[str, object]:
|
||||
self.calls.append({"model": model, "audio": audio, "optional_params": optional_params})
|
||||
return {"text": "hello"}
|
||||
|
||||
|
||||
class AsyncBridge:
|
||||
async def __call__(
|
||||
self,
|
||||
model: str,
|
||||
audio: dict[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str | None,
|
||||
extra_headers: dict[str, object] | None,
|
||||
optional_params: dict[str, object],
|
||||
timeout_seconds: float | None,
|
||||
) -> dict[str, object]:
|
||||
return {"text": "async"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", (False, True))
|
||||
def test_enabled_sync_bridge_receives_audio(enabled: bool) -> None:
|
||||
configuration.rust(enabled)
|
||||
bridge = SyncBridge()
|
||||
rust_bridge.configure_rust_transcription(transcription=bridge)
|
||||
result = rust_bridge.transcription(
|
||||
model="mistral.voxtral-mini-3b-2507",
|
||||
audio={"data": "AQI=", "format": "wav", "filename": "audio.wav"},
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
custom_llm_provider="bedrock",
|
||||
extra_headers=None,
|
||||
optional_params={"temperature": 0},
|
||||
timeout=5.0,
|
||||
python_fallback=None,
|
||||
)
|
||||
assert result == {"text": "hello"}
|
||||
assert bridge.calls[0]["audio"] == {"data": "AQI=", "format": "wav", "filename": "audio.wav"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("enabled", (False, True))
|
||||
async def test_enabled_async_bridge(enabled: bool) -> None:
|
||||
configuration.rust(enabled)
|
||||
rust_bridge.configure_rust_transcription(atranscription=AsyncBridge())
|
||||
result = await rust_bridge.atranscription(
|
||||
model="mistral.voxtral-mini-3b-2507",
|
||||
audio={"data": "AQI=", "format": "wav", "filename": "audio.wav"},
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
custom_llm_provider="bedrock",
|
||||
extra_headers=None,
|
||||
optional_params={},
|
||||
timeout=None,
|
||||
python_fallback=None,
|
||||
)
|
||||
assert result == {"text": "async"}
|
||||
|
||||
|
||||
def test_loader_returns_none_without_native_extension(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
rust_bridge.configure_rust_transcription(transcription=None, atranscription=None)
|
||||
monkeypatch.setattr("litellm.rust_bridge.bindings.get_native_bridge", lambda: None)
|
||||
assert (
|
||||
rust_bridge.load_rust_transcription(context=configuration.CapabilityContext(provider="openai", model="test"))
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
rust_bridge.load_rust_atranscription(context=configuration.CapabilityContext(provider="openai", model="test"))
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_dispatch_sync_path_requires_bridge(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("litellm.rust_bridge.bindings.get_native_bridge", lambda: None)
|
||||
|
||||
with pytest.raises(RustRouteUnavailableError, match="bridge is unavailable"):
|
||||
BedrockAudioTranscriptionRustDispatch().audio_transcriptions(
|
||||
model="bedrock/mistral.voxtral-mini-3b-2507",
|
||||
audio_file=("audio.wav", b"audio", "audio/wav"),
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
custom_llm_provider="bedrock",
|
||||
extra_headers=None,
|
||||
optional_params={},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_async_path_requires_bridge(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("litellm.rust_bridge.bindings.get_native_bridge", lambda: None)
|
||||
|
||||
with pytest.raises(RustRouteUnavailableError, match="bridge is unavailable"):
|
||||
await BedrockAudioTranscriptionRustDispatch().async_audio_transcriptions(
|
||||
model="bedrock/mistral.voxtral-mini-3b-2507",
|
||||
audio_file=("audio.wav", b"audio", "audio/wav"),
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
custom_llm_provider="bedrock",
|
||||
extra_headers=None,
|
||||
optional_params={},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def test_bedrock_transcription_uses_rust_only_path() -> None:
|
||||
rust_bridge.configure_rust_transcription(
|
||||
transcription=lambda **_: {"text": "rust"},
|
||||
atranscription=None,
|
||||
)
|
||||
try:
|
||||
response = litellm.transcription(
|
||||
model="bedrock/mistral.voxtral-mini-3b-2507",
|
||||
file=("audio.wav", b"audio", "audio/wav"),
|
||||
)
|
||||
finally:
|
||||
rust_bridge.configure_rust_transcription(transcription=None, atranscription=None)
|
||||
|
||||
assert response.text == "rust"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bedrock_atranscription_uses_rust_only_path() -> None:
|
||||
async def rust_response(**_: object) -> dict[str, object]:
|
||||
return {"text": "rust"}
|
||||
|
||||
rust_bridge.configure_rust_transcription(transcription=None, atranscription=rust_response)
|
||||
try:
|
||||
response = await litellm.atranscription(
|
||||
model="bedrock/mistral.voxtral-mini-3b-2507",
|
||||
file=("audio.wav", b"audio", "audio/wav"),
|
||||
)
|
||||
finally:
|
||||
rust_bridge.configure_rust_transcription(transcription=None, atranscription=None)
|
||||
|
||||
assert response.text == "rust"
|
||||
from litellm.rust_bridge import bindings, configuration
|
||||
from litellm.rust_bridge.errors import RustRouteUnavailableError
|
||||
from litellm.rust_bridge.transcription import configure_rust_transcription
|
||||
from litellm.rust_bridge.transcription.lifecycle import wrap_async, wrap_sync
|
||||
|
||||
|
||||
class RustBridgeDeclined(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RustBridgeUnavailable(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RustHostCallbackError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RustUpstreamError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("error", "expected", "message"),
|
||||
(
|
||||
(RustBridgeDeclined("unsupported model"), RustRouteDeclinedError, "declined the request: unsupported model"),
|
||||
(RustUpstreamError(429, "rate limited"), APIError, "rate limited"),
|
||||
),
|
||||
NATIVE_EXCEPTIONS: Final = SimpleNamespace(
|
||||
RustBridgeDeclined=RustBridgeDeclined,
|
||||
RustBridgeUnavailable=RustBridgeUnavailable,
|
||||
RustHostCallbackError=RustHostCallbackError,
|
||||
RustUpstreamError=RustUpstreamError,
|
||||
)
|
||||
async def test_bedrock_transcription_errors_never_fall_back(
|
||||
monkeypatch: pytest.MonkeyPatch, error: Exception, expected: type[Exception], message: str
|
||||
) -> None:
|
||||
native: Final = ModuleType("native")
|
||||
setattr(native, "RustBridgeDeclined", RustBridgeDeclined)
|
||||
setattr(native, "RustUpstreamError", RustUpstreamError)
|
||||
monkeypatch.setattr("litellm.rust_bridge.bindings.get_native_bridge", lambda: native)
|
||||
|
||||
def fail(**_: object) -> dict[str, object]:
|
||||
raise error
|
||||
|
||||
async def afail(**_: object) -> dict[str, object]:
|
||||
raise error
|
||||
class RecordingSync:
|
||||
def __init__(self, result: object = "native") -> None:
|
||||
self.result: Final = result
|
||||
self.calls: Final[list[tuple[dict[str, object], tuple[object, ...], dict[str, object], object]]] = []
|
||||
|
||||
rust_bridge.configure_rust_transcription(transcription=fail, atranscription=afail)
|
||||
with pytest.raises(expected, match=message):
|
||||
rust_bridge.transcription(
|
||||
model="model",
|
||||
audio={},
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
custom_llm_provider="bedrock",
|
||||
extra_headers=None,
|
||||
optional_params={},
|
||||
timeout=None,
|
||||
python_fallback=None,
|
||||
)
|
||||
with pytest.raises(expected, match=message):
|
||||
await rust_bridge.atranscription(
|
||||
model="model",
|
||||
audio={},
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
custom_llm_provider="bedrock",
|
||||
extra_headers=None,
|
||||
optional_params={},
|
||||
timeout=None,
|
||||
python_fallback=None,
|
||||
)
|
||||
def __call__(
|
||||
self,
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> object:
|
||||
self.calls.append((request, args, kwargs, host))
|
||||
return self.result
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_bridge(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
||||
configuration.reset_rust_configuration()
|
||||
configure_rust_transcription(transcription=None, atranscription=None)
|
||||
monkeypatch.setenv("LITELLM_RUST", "1")
|
||||
monkeypatch.setattr(bindings, "get_native_bridge", lambda: NATIVE_EXCEPTIONS)
|
||||
yield
|
||||
configuration.reset_rust_configuration()
|
||||
configure_rust_transcription(transcription=None, atranscription=None)
|
||||
|
||||
|
||||
def sync_python(model: str, file: object, **kwargs: object) -> object:
|
||||
return model, file, kwargs
|
||||
|
||||
|
||||
async def async_python(model: str, file: object, **kwargs: object) -> object:
|
||||
return model, file, kwargs
|
||||
|
||||
|
||||
def test_public_boundary_enters_native_once_and_preserves_call_shape() -> None:
|
||||
rust: Final = RecordingSync()
|
||||
configure_rust_transcription(transcription=rust)
|
||||
wrapped: Final = wrap_sync(sync_python)
|
||||
audio: Final = ("audio.wav", b"audio", "audio/wav")
|
||||
|
||||
assert wrapped("bedrock/model", audio, temperature=0) == "native"
|
||||
assert len(rust.calls) == 1
|
||||
request, args, kwargs, _ = rust.calls[0]
|
||||
assert args == ("bedrock/model", audio)
|
||||
assert kwargs == {"temperature": 0}
|
||||
assert request["model"] == "bedrock/model"
|
||||
assert request["audio"] == {"format": "wav"}
|
||||
assert inspect.signature(wrapped) == inspect.signature(sync_python)
|
||||
|
||||
|
||||
def test_python_provider_never_enters_native() -> None:
|
||||
rust: Final = RecordingSync()
|
||||
configure_rust_transcription(transcription=rust)
|
||||
wrapped: Final = wrap_sync(sync_python)
|
||||
|
||||
assert wrapped("openai/whisper-1", b"audio") == ("openai/whisper-1", b"audio", {})
|
||||
assert rust.calls == []
|
||||
|
||||
|
||||
def test_bedrock_requires_native_binding(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(bindings, "get_native_bridge", lambda: None)
|
||||
with pytest.raises(RustRouteUnavailableError, match="bridge is unavailable"):
|
||||
wrap_sync(sync_python)("bedrock/model", b"audio")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_python_transcription_skips_rust_when_enabled() -> None:
|
||||
configuration.rust(True)
|
||||
async def test_async_post_admission_decline_does_not_fall_back() -> None:
|
||||
error: Final = RustBridgeDeclined("execution")
|
||||
|
||||
def unexpected(**_: object) -> dict[str, object]:
|
||||
pytest.fail("Python provider must not call Rust")
|
||||
def native(
|
||||
request: dict[str, object],
|
||||
args: tuple[object, ...],
|
||||
kwargs: dict[str, object],
|
||||
host: object,
|
||||
) -> Awaitable[object]:
|
||||
async def result() -> object:
|
||||
await asyncio.sleep(0)
|
||||
raise error
|
||||
|
||||
async def aunexpected(**_: object) -> dict[str, object]:
|
||||
pytest.fail("Python provider must not call Rust")
|
||||
return result()
|
||||
|
||||
rust_bridge.configure_rust_transcription(transcription=unexpected, atranscription=aunexpected)
|
||||
assert rust_bridge.transcription(
|
||||
model="model",
|
||||
audio={},
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
custom_llm_provider="openai",
|
||||
extra_headers=None,
|
||||
optional_params={},
|
||||
timeout=None,
|
||||
python_fallback=lambda: {"text": "python"},
|
||||
) == {"text": "python"}
|
||||
|
||||
async def python_fallback() -> dict[str, object]:
|
||||
return {"text": "python"}
|
||||
|
||||
assert await rust_bridge.atranscription(
|
||||
model="model",
|
||||
audio={},
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
custom_llm_provider="openai",
|
||||
extra_headers=None,
|
||||
optional_params={},
|
||||
timeout=None,
|
||||
python_fallback=python_fallback,
|
||||
) == {"text": "python"}
|
||||
configure_rust_transcription(atranscription=native)
|
||||
with pytest.raises(RustBridgeDeclined) as caught:
|
||||
await wrap_async(async_python)("bedrock/model", b"audio")
|
||||
assert caught.value is error
|
||||
|
|
|
|||
|
|
@ -596,7 +596,7 @@ def test_unstarted_native_coroutine_releases_input_without_reading_file(ocr_serv
|
|||
kwargs: Final = {"model": "mistral/mistral-ocr-latest", "document": {"type": "file", "file": file}}
|
||||
from litellm.rust_bridge.ocr.host import HOST
|
||||
|
||||
coroutine: Final = _native._ocr_lifecycle(_public_request("aocr", (), kwargs), (), kwargs, True, HOST)
|
||||
coroutine: Final = _native.aocr(_public_request("aocr", (), kwargs), (), kwargs, HOST)
|
||||
file.owner = coroutine
|
||||
coroutine.close()
|
||||
return weakref.ref(file)
|
||||
|
|
|
|||
88
tests/test_litellm_rust/test_chat_completions.py
Normal file
88
tests/test_litellm_rust/test_chat_completions.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
from typing import Final, Protocol, cast # noqa: TID251 # public callables have legacy partial annotations
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.types.utils import ModelResponse
|
||||
from tests.test_litellm_rust.support.recording_server import ResponseSpec, recording_service
|
||||
|
||||
pytestmark = pytest.mark.requires_rust_extension
|
||||
|
||||
ANTHROPIC_RESPONSE: Final = {
|
||||
"id": "msg_test",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-sonnet-4-5",
|
||||
"content": [{"type": "text", "text": "hello from rust lifecycle"}],
|
||||
"stop_reason": "end_turn",
|
||||
"stop_sequence": None,
|
||||
"usage": {"input_tokens": 3, "output_tokens": 4},
|
||||
}
|
||||
|
||||
|
||||
class SyncCompletion(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
messages: list[object],
|
||||
max_tokens: int,
|
||||
api_key: str,
|
||||
api_base: str,
|
||||
) -> object: ...
|
||||
|
||||
|
||||
class AsyncCompletion(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
messages: list[object],
|
||||
max_tokens: int,
|
||||
api_key: str,
|
||||
api_base: str,
|
||||
) -> Awaitable[object]: ...
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("asynchronous", (False, True))
|
||||
async def test_public_chat_uses_one_native_lifecycle_and_one_provider_request(asynchronous: bool) -> None:
|
||||
with recording_service() as service:
|
||||
service.enqueue(ResponseSpec(body=ANTHROPIC_RESPONSE))
|
||||
sync: Final = cast(SyncCompletion, litellm.completion) # pyright: ignore[reportUnknownMemberType] # legacy signature
|
||||
async_call: Final = cast(AsyncCompletion, litellm.acompletion) # pyright: ignore[reportUnknownMemberType] # legacy signature
|
||||
messages: Final[list[object]] = [{"role": "user", "content": "hi"}]
|
||||
response_value: Final = (
|
||||
await async_call(
|
||||
model="anthropic/claude-sonnet-4-5",
|
||||
messages=messages,
|
||||
max_tokens=16,
|
||||
api_key="test-key",
|
||||
api_base=service.base_url,
|
||||
)
|
||||
if asynchronous
|
||||
else sync(
|
||||
model="anthropic/claude-sonnet-4-5",
|
||||
messages=messages,
|
||||
max_tokens=16,
|
||||
api_key="test-key",
|
||||
api_base=service.base_url,
|
||||
)
|
||||
)
|
||||
|
||||
assert isinstance(response_value, ModelResponse)
|
||||
response: Final = response_value
|
||||
assert response.choices[0].message.content == "hello from rust lifecycle"
|
||||
assert response._hidden_params["additional_headers"] == { # pyright: ignore[reportPrivateUsage, reportUnknownMemberType] # public response metadata
|
||||
"x-litellm-rust": "true"
|
||||
}
|
||||
assert len(service.requests) == 1
|
||||
assert service.requests[0].path == "/v1/messages"
|
||||
assert service.requests[0].body == {
|
||||
"max_tokens": 16,
|
||||
"messages": [{"content": [{"text": "hi", "type": "text"}], "role": "user"}],
|
||||
"model": "claude-sonnet-4-5",
|
||||
}
|
||||
87
tests/test_litellm_rust/test_messages.py
Normal file
87
tests/test_litellm_rust/test_messages.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
from typing import Final, Protocol, cast # noqa: TID251 # public callables have legacy partial annotations
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from tests.test_litellm_rust.support.recording_server import ResponseSpec, recording_service
|
||||
|
||||
pytestmark = pytest.mark.requires_rust_extension
|
||||
|
||||
ANTHROPIC_RESPONSE: Final = {
|
||||
"id": "msg_test",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-sonnet-4-5",
|
||||
"content": [{"type": "text", "text": "hello from rust messages"}],
|
||||
"stop_reason": "end_turn",
|
||||
"stop_sequence": None,
|
||||
"usage": {"input_tokens": 3, "output_tokens": 4},
|
||||
}
|
||||
|
||||
|
||||
class SyncMessages(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
max_tokens: int,
|
||||
messages: list[object],
|
||||
model: str,
|
||||
api_key: str,
|
||||
api_base: str,
|
||||
) -> object: ...
|
||||
|
||||
|
||||
class AsyncMessages(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
max_tokens: int,
|
||||
messages: list[object],
|
||||
model: str,
|
||||
api_key: str,
|
||||
api_base: str,
|
||||
) -> Awaitable[object]: ...
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("asynchronous", (False, True))
|
||||
async def test_public_messages_uses_one_native_lifecycle_and_provider_request(asynchronous: bool) -> None:
|
||||
with recording_service() as service:
|
||||
service.enqueue(ResponseSpec(body=ANTHROPIC_RESPONSE))
|
||||
sync: Final = cast(SyncMessages, litellm.anthropic.create) # pyright: ignore[reportUnknownMemberType] # legacy signature
|
||||
async_call: Final = cast(AsyncMessages, litellm.anthropic.acreate) # pyright: ignore[reportUnknownMemberType] # legacy signature
|
||||
messages: Final[list[object]] = [{"role": "user", "content": "hi"}]
|
||||
value: Final = (
|
||||
await async_call(
|
||||
max_tokens=16,
|
||||
messages=messages,
|
||||
model="anthropic/claude-sonnet-4-5",
|
||||
api_key="test-key",
|
||||
api_base=service.base_url,
|
||||
)
|
||||
if asynchronous
|
||||
else sync(
|
||||
max_tokens=16,
|
||||
messages=messages,
|
||||
model="anthropic/claude-sonnet-4-5",
|
||||
api_key="test-key",
|
||||
api_base=service.base_url,
|
||||
)
|
||||
)
|
||||
|
||||
response: Final = cast(dict[str, object], value)
|
||||
content: Final = cast(list[dict[str, object]], response["content"])
|
||||
hidden: Final = cast(dict[str, object], response["_hidden_params"])
|
||||
assert content[0]["text"] == "hello from rust messages"
|
||||
assert hidden["additional_headers"] == {"x-litellm-rust": "true"}
|
||||
assert len(service.requests) == 1
|
||||
assert service.requests[0].path == "/v1/messages"
|
||||
assert service.requests[0].body == {
|
||||
"max_tokens": 16,
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"model": "claude-sonnet-4-5",
|
||||
"stream": False,
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ from typing import Final
|
|||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.rust_bridge import _native
|
||||
|
||||
pytestmark = pytest.mark.requires_rust_extension
|
||||
|
||||
|
|
@ -79,19 +78,17 @@ def test_native_ocr_with_compiled_rust_extension(
|
|||
host: Final = str(address[0])
|
||||
port: Final = int(address[1])
|
||||
|
||||
response: Final = _native.ocr(
|
||||
"mistral-ocr-latest",
|
||||
{"type": "document_url", "document_url": "data:application/pdf;base64,YWJj"},
|
||||
response: Final = litellm.ocr(
|
||||
model="mistral-ocr-latest",
|
||||
document={"type": "document_url", "document_url": "data:application/pdf;base64,YWJj"},
|
||||
api_key="test-key",
|
||||
api_base=f"http://{host}:{port}",
|
||||
custom_llm_provider="mistral",
|
||||
extra_headers=None,
|
||||
optional_params={},
|
||||
timeout_seconds=None,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response["pages"][0]["markdown"] == "native OCR response"
|
||||
assert response.pages[0].markdown == "native OCR response"
|
||||
assert len(requests) == 1
|
||||
assert not requests[0]["headers"].get("user-agent", "").startswith("python-httpx")
|
||||
assert requests[0]["body"] == {
|
||||
|
|
@ -219,13 +216,14 @@ async def test_native_ocr_failures_do_not_retry_on_python(ocr_server, asynchrono
|
|||
assert not requests[0]["headers"].get("user-agent", "").startswith("python-httpx")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("custom_provider", ["mistral", "not-a-provider"])
|
||||
def test_native_ocr_rejects_invalid_input_before_network(ocr_server, custom_provider):
|
||||
from litellm.rust_bridge import _native
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("custom_provider", "message"),
|
||||
[("mistral", "Document URL is required"), ("not-a-provider", "invalid provider")],
|
||||
)
|
||||
def test_native_ocr_rejects_invalid_input_before_network(ocr_server, custom_provider, message):
|
||||
server, requests = ocr_server
|
||||
with pytest.raises(ValueError, match="Document URL is required"):
|
||||
_native.ocr(
|
||||
with pytest.raises(Exception, match=message):
|
||||
litellm.ocr(
|
||||
model="mistral-ocr-latest",
|
||||
custom_llm_provider=custom_provider,
|
||||
document={"type": "document_url"},
|
||||
|
|
|
|||
|
|
@ -5,29 +5,21 @@ from typing import Final
|
|||
import pytest
|
||||
|
||||
from litellm.rust_bridge import _native
|
||||
from litellm.rust_bridge.bindings import NativeBinding
|
||||
from litellm.rust_bridge.catalog import NATIVE_EXPORTS
|
||||
from litellm.rust_bridge.configuration import ComponentName, ExecutionDecision
|
||||
from litellm.rust_bridge.embeddings.lifecycle import LIFECYCLE as EMBEDDINGS
|
||||
from litellm.rust_bridge.image_edit.lifecycle import LIFECYCLE as IMAGE_EDIT
|
||||
from litellm.rust_bridge.image_generation.lifecycle import LIFECYCLE as IMAGE_GENERATION
|
||||
from litellm.rust_bridge.moderation.lifecycle import LIFECYCLE as MODERATION
|
||||
from litellm.rust_bridge.rerank.lifecycle import LIFECYCLE as RERANK
|
||||
from litellm.rust_bridge.responses.lifecycle import LIFECYCLE as RESPONSES
|
||||
from litellm.rust_bridge.route import ComponentExecution, NativeLifecycle
|
||||
from litellm.rust_bridge.route import ComponentExecution
|
||||
from litellm.rust_bridge.runtime import BridgeErrorContext, invoke
|
||||
from litellm.rust_bridge.speech.lifecycle import LIFECYCLE as SPEECH
|
||||
|
||||
pytestmark = pytest.mark.requires_rust_extension
|
||||
|
||||
UNIMPLEMENTED: Final[dict[ComponentName, NativeBinding[NativeLifecycle[object, object]]]] = {
|
||||
ComponentName.EMBEDDINGS: EMBEDDINGS,
|
||||
ComponentName.RERANK: RERANK,
|
||||
ComponentName.IMAGE_GENERATION: IMAGE_GENERATION,
|
||||
ComponentName.IMAGE_EDIT: IMAGE_EDIT,
|
||||
ComponentName.SPEECH: SPEECH,
|
||||
ComponentName.MODERATION: MODERATION,
|
||||
ComponentName.RESPONSES: RESPONSES,
|
||||
UNIMPLEMENTED: Final = {
|
||||
ComponentName.EMBEDDINGS: ("embedding", "aembedding"),
|
||||
ComponentName.RERANK: ("rerank", "arerank"),
|
||||
ComponentName.IMAGE_GENERATION: ("image_generation", "aimage_generation"),
|
||||
ComponentName.IMAGE_EDIT: ("image_edit", "aimage_edit"),
|
||||
ComponentName.SPEECH: ("speech", "aspeech"),
|
||||
ComponentName.MODERATION: ("moderation", "amoderation"),
|
||||
ComponentName.RESPONSES: ("responses", "aresponses"),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -36,38 +28,51 @@ class UntouchedInput:
|
|||
raise AssertionError(f"unimplemented route inspected {name}")
|
||||
|
||||
|
||||
class HostileValue:
|
||||
def __getattribute__(self, name: str) -> object:
|
||||
raise AssertionError(f"admission inspected {name}")
|
||||
|
||||
|
||||
class RaisingHost:
|
||||
def __init__(self, error: BaseException) -> None:
|
||||
self.error: Final = error
|
||||
|
||||
def invoke(self, *args: object) -> object:
|
||||
raise self.error
|
||||
|
||||
|
||||
def test_catalog_exports_are_registered() -> None:
|
||||
assert all(hasattr(_native, export) for export in NATIVE_EXPORTS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("route_name", "binding"), tuple(UNIMPLEMENTED.items()))
|
||||
@pytest.mark.parametrize("asynchronous", (False, True))
|
||||
@pytest.mark.parametrize(("route_name", "exports"), tuple(UNIMPLEMENTED.items()))
|
||||
def test_package_lifecycle_binding_declines_without_input_reads(
|
||||
route_name: ComponentName,
|
||||
binding: NativeBinding[NativeLifecycle[object, object]],
|
||||
asynchronous: bool,
|
||||
exports: tuple[str, str],
|
||||
) -> None:
|
||||
native: Final = binding.load()
|
||||
assert native is not None
|
||||
request: Final = UntouchedInput()
|
||||
with pytest.raises(_native.RustBridgeDeclined, match=f"^{route_name.value} native lifecycle is not implemented$"):
|
||||
native(request, (request,), {"callback": request, "file": request}, asynchronous, request)
|
||||
for export in exports:
|
||||
native: Final = getattr(_native, export)
|
||||
request: Final = UntouchedInput()
|
||||
with pytest.raises(
|
||||
_native.RustBridgeDeclined,
|
||||
match=f"^{route_name.value} native lifecycle is not implemented$",
|
||||
):
|
||||
native(request, (request,), {"callback": request, "file": request}, request)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("route_name", "binding"), tuple(UNIMPLEMENTED.items()))
|
||||
@pytest.mark.parametrize(("route_name", "exports"), tuple(UNIMPLEMENTED.items()))
|
||||
def test_package_stub_decline_selects_python(
|
||||
route_name: ComponentName,
|
||||
binding: NativeBinding[NativeLifecycle[object, object]],
|
||||
exports: tuple[str, str],
|
||||
) -> None:
|
||||
native: Final[NativeLifecycle[object, object] | None] = binding.load()
|
||||
assert native is not None
|
||||
native: Final = getattr(_native, exports[0])
|
||||
execution: Final = ComponentExecution(
|
||||
route_name=route_name,
|
||||
decision=ExecutionDecision.RUST_WITH_FALLBACK,
|
||||
)
|
||||
result: Final = invoke(
|
||||
execution=execution,
|
||||
native_call=lambda: native(UntouchedInput(), (), {}, False, UntouchedInput()),
|
||||
native_call=lambda: native(UntouchedInput(), (), {}, UntouchedInput()),
|
||||
python_fallback=lambda: "python",
|
||||
adapt=str,
|
||||
context=BridgeErrorContext(route=route_name.value, model="unused", provider="unused"),
|
||||
|
|
@ -75,20 +80,62 @@ def test_package_stub_decline_selects_python(
|
|||
assert result == "python"
|
||||
|
||||
|
||||
def test_transcription_admission_declines_unsupported_provider_before_host_work() -> None:
|
||||
request: Final = {
|
||||
"model": "model",
|
||||
"audio": {"format": "wav", "data": "YQ=="},
|
||||
"custom_llm_provider": "unsupported",
|
||||
}
|
||||
for binding in (_native.transcription, _native.atranscription):
|
||||
with pytest.raises(_native.RustBridgeDeclined):
|
||||
binding(request, (), {}, UntouchedInput())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("asynchronous", (False, True))
|
||||
@pytest.mark.parametrize("route", ("messages", "chat_completions", "transcription", "ocr"))
|
||||
def test_value_admission_declines_unsupported_provider_before_credentials(route: str, asynchronous: bool) -> None:
|
||||
binding: Final = getattr(_native, ("a" if asynchronous else "") + route)
|
||||
payload: Final = [{"role": "user", "content": "hi"}] if route == "chat_completions" else {}
|
||||
def test_chat_admission_declines_unsupported_provider_before_host_work(asynchronous: bool) -> None:
|
||||
binding: Final = _native.achat_completions if asynchronous else _native.chat_completions
|
||||
request: Final = {
|
||||
"model": "model",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"custom_llm_provider": "unsupported",
|
||||
}
|
||||
with pytest.raises(_native.RustBridgeDeclined):
|
||||
binding("model", payload, custom_llm_provider="unsupported", api_base="http://127.0.0.1:1")
|
||||
binding(request, (), {}, UntouchedInput())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("asynchronous", (False, True))
|
||||
def test_chat_admission_declines_opaque_messages_without_touching_them(asynchronous: bool) -> None:
|
||||
binding: Final = _native.achat_completions if asynchronous else _native.chat_completions
|
||||
request: Final = {
|
||||
"model": "anthropic/model",
|
||||
"messages": [HostileValue()],
|
||||
}
|
||||
with pytest.raises(_native.RustBridgeDeclined, match="cannot be inspected"):
|
||||
binding(request, (), {}, UntouchedInput())
|
||||
|
||||
|
||||
def test_chat_host_reserved_error_is_terminal_and_preserves_identity() -> None:
|
||||
error: Final = _native.RustBridgeDeclined("raised by host")
|
||||
request: Final = {
|
||||
"model": "anthropic/model",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
}
|
||||
with pytest.raises(_native.RustHostCallbackError) as caught:
|
||||
_native.chat_completions(request, (), request, RaisingHost(error))
|
||||
assert caught.value.__cause__ is error
|
||||
|
||||
|
||||
@pytest.mark.parametrize("asynchronous", (False, True))
|
||||
def test_messages_declines_required_host_hook_before_preparation(asynchronous: bool) -> None:
|
||||
binding: Final = _native.amessages if asynchronous else _native.messages
|
||||
request: Final = {
|
||||
"model": "anthropic/model",
|
||||
"body": {},
|
||||
"custom_llm_provider": "anthropic",
|
||||
"has_agentic_hook": True,
|
||||
}
|
||||
with pytest.raises(_native.RustBridgeDeclined, match="host operations"):
|
||||
binding("model", {}, custom_llm_provider="anthropic", has_agentic_hook=True)
|
||||
binding(request, (), {}, UntouchedInput())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -101,23 +148,16 @@ def test_messages_declines_required_host_hook_before_preparation(asynchronous: b
|
|||
),
|
||||
)
|
||||
def test_chat_entrypoints_decline_before_the_host_callback(provider: str, facts: dict, headers: dict) -> None:
|
||||
calls: Final[list[bool]] = []
|
||||
for binding in (_native.chat_completions, _native.achat_completions):
|
||||
request: Final = {
|
||||
"model": "model",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"custom_llm_provider": provider,
|
||||
"host_facts": facts,
|
||||
"extra_headers": headers,
|
||||
}
|
||||
with pytest.raises(_native.RustBridgeDeclined):
|
||||
binding(
|
||||
"model",
|
||||
[{"role": "user", "content": "hi"}],
|
||||
custom_llm_provider=provider,
|
||||
host_facts=facts,
|
||||
extra_headers=headers,
|
||||
on_request=lambda: calls.append(True),
|
||||
)
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_transcription_declines_audio_format_before_credentials() -> None:
|
||||
with pytest.raises(_native.RustBridgeDeclined, match="audio format"):
|
||||
_native.transcription("model", {"format": "unsupported", "data": "YQ=="}, custom_llm_provider="bedrock")
|
||||
binding(request, (), {}, UntouchedInput())
|
||||
|
||||
|
||||
def test_transcription_lifecycle_declines_audio_format_before_host_work() -> None:
|
||||
|
|
@ -126,8 +166,9 @@ def test_transcription_lifecycle_declines_audio_format_before_host_work() -> Non
|
|||
"audio": {"format": "unsupported", "data": "YQ=="},
|
||||
"custom_llm_provider": "bedrock",
|
||||
}
|
||||
with pytest.raises(_native.RustBridgeDeclined, match="audio format"):
|
||||
_native._transcription_lifecycle(request, (), {}, False, UntouchedInput())
|
||||
for binding in (_native.transcription, _native.atranscription):
|
||||
with pytest.raises(_native.RustBridgeDeclined, match="audio format"):
|
||||
binding(request, (), {}, UntouchedInput())
|
||||
|
||||
|
||||
def test_websocket_declines_before_parsing_or_dialing_url() -> None:
|
||||
|
|
|
|||
59
tests/test_litellm_rust/test_transcription.py
Normal file
59
tests/test_litellm_rust/test_transcription.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
from typing import Final, Protocol, cast # noqa: TID251 # public callables have legacy partial annotations
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.types.utils import TranscriptionResponse
|
||||
from tests.test_litellm_rust.support.recording_server import ResponseSpec, recording_service
|
||||
|
||||
pytestmark = pytest.mark.requires_rust_extension
|
||||
|
||||
BEDROCK_RESPONSE: Final = {"output": {"message": {"content": [{"text": "hello from rust"}]}}}
|
||||
|
||||
|
||||
class SyncTranscription(Protocol):
|
||||
def __call__(self, *, model: str, file: object, api_base: str, **kwargs: object) -> object: ...
|
||||
|
||||
|
||||
class AsyncTranscription(Protocol):
|
||||
def __call__(self, *, model: str, file: object, api_base: str, **kwargs: object) -> Awaitable[object]: ...
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("asynchronous", (False, True))
|
||||
async def test_public_transcription_uses_one_native_lifecycle_and_provider_request(asynchronous: bool) -> None:
|
||||
with recording_service() as service:
|
||||
service.enqueue(ResponseSpec(body=BEDROCK_RESPONSE))
|
||||
sync: Final = cast(SyncTranscription, litellm.transcription) # pyright: ignore[reportUnknownMemberType] # legacy signature
|
||||
async_call: Final = cast(AsyncTranscription, litellm.atranscription) # pyright: ignore[reportUnknownMemberType] # legacy signature
|
||||
kwargs: Final[dict[str, object]] = {
|
||||
"aws_access_key_id": "access-key",
|
||||
"aws_secret_access_key": "secret-key",
|
||||
"aws_region_name": "us-east-1",
|
||||
}
|
||||
value: Final = (
|
||||
await async_call(
|
||||
model="bedrock/mistral.voxtral-mini-3b-2507",
|
||||
file=("audio.wav", b"audio", "audio/wav"),
|
||||
api_base=service.base_url,
|
||||
**kwargs,
|
||||
)
|
||||
if asynchronous
|
||||
else sync(
|
||||
model="bedrock/mistral.voxtral-mini-3b-2507",
|
||||
file=("audio.wav", b"audio", "audio/wav"),
|
||||
api_base=service.base_url,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
assert isinstance(value, TranscriptionResponse)
|
||||
assert value.text == "hello from rust"
|
||||
assert len(service.requests) == 1
|
||||
assert service.requests[0].path == "/model/mistral.voxtral-mini-3b-2507/converse"
|
||||
assert service.requests[0].headers["authorization"].startswith("AWS4-HMAC-SHA256")
|
||||
body: Final = cast(dict[str, object], service.requests[0].body)
|
||||
assert "YXVkaW8=" in str(body)
|
||||
Loading…
Add table
Reference in a new issue