use std::time::Duration; use litellm_ai_gateway::io::ocr::{ocr as run_ocr, OcrRequest}; use litellm_core::error::CoreError; use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyAny, PyDict}; use serde_json::{Map, Value}; mod gil; type MarshaledOcrInputs = ( Value, Option>, Map, Option, ); fn py_to_json(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult { let json = py.import("json")?; let encoded: String = json.call_method1("dumps", (value,))?.extract()?; serde_json::from_str(&encoded).map_err(|err| PyValueError::new_err(err.to_string())) } fn json_to_py(py: Python<'_>, value: Value) -> PyResult> { let json = py.import("json")?; let encoded = serde_json::to_string(&value).map_err(|err| PyValueError::new_err(err.to_string()))?; Ok(json.call_method1("loads", (encoded,))?.unbind()) } fn core_error_to_pyerr(err: CoreError) -> PyErr { match err { CoreError::Auth(message) => PyValueError::new_err(message), CoreError::InvalidProvider(_) | CoreError::InvalidRequest(_) | CoreError::InvalidType { .. } | CoreError::MissingField(_) => PyValueError::new_err(err.to_string()), other => PyRuntimeError::new_err(other.to_string()), } } fn optional_object_to_map( py: Python<'_>, name: &'static str, value: Option>, ) -> PyResult> { match value { Some(value) => match py_to_json(py, value.bind(py))? { Value::Object(map) => Ok(map), _ => Err(PyValueError::new_err(format!("{name} must be a dict"))), }, None => Ok(Map::new()), } } fn optional_timeout(timeout_seconds: Option) -> Option { timeout_seconds.and_then(|secs| { if secs.is_finite() && secs > 0.0 { Some(Duration::from_secs_f64(secs)) } else { None } }) } fn marshal_inputs( py: Python<'_>, document: Py, extra_headers: Option>, optional_params: Option>, timeout_seconds: Option, ) -> PyResult { let document = py_to_json(py, document.bind(py))?; let extra_headers = match extra_headers { Some(headers) => Some(optional_object_to_map(py, "extra_headers", Some(headers))?), None => None, }; let optional_params = optional_object_to_map(py, "optional_params", optional_params)?; let timeout = optional_timeout(timeout_seconds); Ok((document, extra_headers, optional_params, timeout)) } #[pyfunction] #[pyo3(signature = (model, document, api_key=None, api_base=None, custom_llm_provider=None, extra_headers=None, optional_params=None, timeout_seconds=None))] #[allow(clippy::too_many_arguments)] fn ocr( py: Python<'_>, model: String, document: Py, api_key: Option, api_base: Option, custom_llm_provider: Option, extra_headers: Option>, optional_params: Option>, timeout_seconds: Option, ) -> PyResult> { let (document, extra_headers, optional_params, timeout) = marshal_inputs( py, document, extra_headers, optional_params, timeout_seconds, )?; let result = gil::release_gil(py, || { pyo3_async_runtimes::tokio::get_runtime().block_on(run_ocr(OcrRequest { model: &model, document, 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, callbacks: Vec::new(), guardrails: Vec::new(), request_metadata: Default::default(), litellm_call_id: None, })) }); match result { Ok(value) => json_to_py(py, value), Err(err) => Err(core_error_to_pyerr(err)), } } #[pyfunction] #[pyo3(signature = (model, document, api_key=None, api_base=None, custom_llm_provider=None, extra_headers=None, optional_params=None, timeout_seconds=None))] #[allow(clippy::too_many_arguments)] fn aocr( py: Python<'_>, model: String, document: Py, api_key: Option, api_base: Option, custom_llm_provider: Option, extra_headers: Option>, optional_params: Option>, timeout_seconds: Option, ) -> PyResult> { let (document, extra_headers, optional_params, timeout) = marshal_inputs( py, document, extra_headers, optional_params, timeout_seconds, )?; pyo3_async_runtimes::tokio::future_into_py(py, async move { let value = run_ocr(OcrRequest { model: &model, document, 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, callbacks: Vec::new(), guardrails: Vec::new(), request_metadata: Default::default(), litellm_call_id: None, }) .await .map_err(core_error_to_pyerr)?; Python::with_gil(|py| json_to_py(py, value)) }) } #[pyfunction] fn gil_stats(py: Python<'_>) -> PyResult> { let stats = PyDict::new(py); stats.set_item("releases", gil::release_count())?; Ok(stats.into_any().unbind()) } #[pymodule] fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(ocr, module)?)?; module.add_function(wrap_pyfunction!(aocr, module)?)?; module.add_function(wrap_pyfunction!(gil_stats, module)?)?; Ok(()) }