mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
* refactor(ocr): extract call completion boundary * fix(ocr): release completion state after dispatch * test(ocr): prove wrapper completion handoff * test(ocr): narrow mapped failure assertion * fix(ocr): preserve wrapper invocation kwargs * fix(ocr): retain completion through finalization * fix(ocr): make completion ownership explicit * refactor(ocr): resolve logging executor explicitly * fix(callbacks): preserve completion lifecycle behavior * refactor(ocr): move public OCR into native lifecycle * refactor(ocr): remove unused rust bridge capability * wip * wip * refactor * wip * fix(ocr): preserve reducto native compatibility * wip * fix(ocr): document native callable casts * perf(ocr): bound responses and reduce native scheduling overhead * refactor(python-bridge): organize placeholder routes * refactor test * fix(ocr): normalize DeepSeek document content * perf(ocr): skip unused callback work and benchmark callback overhead * fix(ocr): align conversion contracts * test(ocr): cover official provider response shapes * fix(ocr): restore Python fallback and honor Rust opt-out * fixes and refactor * fix(ocr): preserve Azure Document Intelligence authentication * fix(rust): enforce OCR response limits and lint contracts * test(rust): align native OCR contract coverage * test(ocr): isolate Azure auth precedence coverage
153 lines
4.3 KiB
Rust
153 lines
4.3 KiB
Rust
use std::any::Any;
|
|
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
|
|
use pyo3::exceptions::PyValueError;
|
|
use pyo3::panic::PanicException;
|
|
use pyo3::prelude::*;
|
|
use serde::Serialize;
|
|
use serde::de::DeserializeOwned;
|
|
|
|
pub fn from_py<T>(value: &Bound<'_, PyAny>) -> PyResult<T>
|
|
where
|
|
T: DeserializeOwned,
|
|
{
|
|
pythonize::depythonize(value).map_err(|error| PyValueError::new_err(error.to_string()))
|
|
}
|
|
|
|
pub fn from_py_preserving_errors<T>(value: &Bound<'_, PyAny>) -> PyResult<T>
|
|
where
|
|
T: DeserializeOwned,
|
|
{
|
|
pythonize::depythonize(value).map_err(PyErr::from)
|
|
}
|
|
|
|
pub fn to_py<T>(py: Python<'_>, value: &T) -> PyResult<Py<PyAny>>
|
|
where
|
|
T: Serialize + ?Sized,
|
|
{
|
|
pythonize::pythonize(py, value)
|
|
.map(Bound::unbind)
|
|
.map_err(|error| PyValueError::new_err(error.to_string()))
|
|
}
|
|
|
|
pub fn to_py_preserving_errors<T>(py: Python<'_>, value: &T) -> PyResult<Py<PyAny>>
|
|
where
|
|
T: Serialize + ?Sized,
|
|
{
|
|
pythonize::pythonize(py, value)
|
|
.map(Bound::unbind)
|
|
.map_err(PyErr::from)
|
|
}
|
|
|
|
pub struct Pythonized<T>(pub T);
|
|
|
|
impl<'py, T> IntoPyObject<'py> for Pythonized<T>
|
|
where
|
|
T: Serialize,
|
|
{
|
|
type Target = PyAny;
|
|
type Output = Bound<'py, PyAny>;
|
|
type Error = PyErr;
|
|
|
|
fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
|
|
catch_unwind(AssertUnwindSafe(|| pythonize::pythonize(py, &self.0)))
|
|
.map_err(panic_to_pyerr)?
|
|
.map_err(|error| PyValueError::new_err(error.to_string()))
|
|
}
|
|
}
|
|
|
|
pub fn panic_to_pyerr(payload: Box<dyn Any + Send>) -> PyErr {
|
|
let message = payload
|
|
.downcast_ref::<String>()
|
|
.map(String::as_str)
|
|
.or_else(|| payload.downcast_ref::<&str>().copied())
|
|
.unwrap_or("panic from Rust code");
|
|
PanicException::new_err(message.to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use serde::Serializer;
|
|
|
|
use super::*;
|
|
|
|
struct PanickingSerializer;
|
|
|
|
impl Serialize for PanickingSerializer {
|
|
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
panic!("serializer panicked")
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pythonized_converts_on_the_attached_thread() {
|
|
Python::initialize();
|
|
Python::attach(|py| {
|
|
let value: Vec<i32> = Pythonized(vec![1, 2, 3])
|
|
.into_pyobject(py)
|
|
.and_then(|value| value.extract())
|
|
.expect("value should convert");
|
|
assert_eq!(value, vec![1, 2, 3]);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn pythonized_maps_serializer_panics_to_a_base_exception() {
|
|
Python::initialize();
|
|
Python::attach(|py| {
|
|
let error = Pythonized(PanickingSerializer)
|
|
.into_pyobject(py)
|
|
.expect_err("serializer panic should become a Python exception");
|
|
assert!(error.is_instance_of::<PanicException>(py));
|
|
assert_eq!(error.to_string(), "PanicException: serializer panicked");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn depythonize_preserves_python_exception_identity_and_traceback() {
|
|
Python::initialize();
|
|
Python::attach(|py| {
|
|
let locals = pyo3::types::PyDict::new(py);
|
|
py.run(
|
|
pyo3::ffi::c_str!(
|
|
r#"
|
|
failure = LookupError('conversion failed')
|
|
cause = ValueError('cause')
|
|
class Broken:
|
|
def __index__(self):
|
|
raise failure from cause
|
|
value = Broken()
|
|
"#
|
|
),
|
|
Some(&locals),
|
|
Some(&locals),
|
|
)
|
|
.unwrap();
|
|
let value = locals.get_item("value").unwrap().unwrap();
|
|
let legacy_error = from_py::<i64>(&value).unwrap_err();
|
|
assert!(legacy_error.is_instance_of::<PyValueError>(py));
|
|
assert!(
|
|
!legacy_error
|
|
.value(py)
|
|
.is(locals.get_item("failure").unwrap().unwrap())
|
|
);
|
|
let error = from_py_preserving_errors::<i64>(&value).unwrap_err();
|
|
assert!(
|
|
error
|
|
.value(py)
|
|
.is(locals.get_item("failure").unwrap().unwrap())
|
|
);
|
|
assert!(
|
|
error
|
|
.cause(py)
|
|
.unwrap()
|
|
.value(py)
|
|
.is(locals.get_item("cause").unwrap().unwrap())
|
|
);
|
|
assert!(error.traceback(py).is_some());
|
|
});
|
|
}
|
|
}
|