mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +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
226 lines
7.6 KiB
Rust
226 lines
7.6 KiB
Rust
mod auth;
|
|
mod constants;
|
|
mod diagnostics;
|
|
mod errors;
|
|
mod execution;
|
|
#[cfg(feature = "trace-parity")]
|
|
mod function_trace;
|
|
mod lifecycle;
|
|
mod marshal;
|
|
mod routes;
|
|
mod token_counter;
|
|
|
|
use litellm_core::responses::websocket::ResponsesWebSocketConnection as RustResponsesWebSocketConnection;
|
|
use pyo3::prelude::*;
|
|
use pyo3::types::PyAny;
|
|
use serde_json::Value;
|
|
|
|
use crate::errors::core_error_to_pyerr;
|
|
use crate::marshal::{marshal_headers, optional_timeout};
|
|
|
|
#[pyclass]
|
|
struct ResponsesWebSocketConnection {
|
|
inner: RustResponsesWebSocketConnection,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl ResponsesWebSocketConnection {
|
|
#[classmethod]
|
|
#[pyo3(signature = (url, headers=None, timeout_seconds=None))]
|
|
fn connect<'py>(
|
|
_cls: &Bound<'py, pyo3::types::PyType>,
|
|
py: Python<'py>,
|
|
url: String,
|
|
#[pyo3(from_py_with = litellm_python_interop::from_py)] headers: Option<Value>,
|
|
timeout_seconds: Option<f64>,
|
|
) -> PyResult<Bound<'py, PyAny>> {
|
|
let headers = marshal_headers(headers)?;
|
|
let timeout = optional_timeout(timeout_seconds);
|
|
pyo3_async_runtimes::tokio::future_into_py(py, async move {
|
|
let inner = RustResponsesWebSocketConnection::connect_url(&url, &headers, timeout)
|
|
.await
|
|
.map_err(core_error_to_pyerr)?;
|
|
Ok(ResponsesWebSocketConnection { inner })
|
|
})
|
|
}
|
|
|
|
fn send_text<'py>(&self, py: Python<'py>, text: String) -> PyResult<Bound<'py, PyAny>> {
|
|
let inner = self.inner.clone();
|
|
pyo3_async_runtimes::tokio::future_into_py(py, async move {
|
|
inner.send_text(text).await.map_err(core_error_to_pyerr)
|
|
})
|
|
}
|
|
|
|
fn recv_text<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
|
let inner = self.inner.clone();
|
|
pyo3_async_runtimes::tokio::future_into_py(py, async move {
|
|
inner.recv_text().await.map_err(core_error_to_pyerr)
|
|
})
|
|
}
|
|
|
|
fn close<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
|
let inner = self.inner.clone();
|
|
pyo3_async_runtimes::tokio::future_into_py(py, async move {
|
|
inner.close().await.map_err(core_error_to_pyerr)
|
|
})
|
|
}
|
|
}
|
|
|
|
#[pymodule(gil_used = true)]
|
|
mod _native {
|
|
use pyo3::prelude::*;
|
|
|
|
#[pymodule_init]
|
|
fn init(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
super::errors::register(module)?;
|
|
super::routes::register(module)?;
|
|
module.add_class::<super::ResponsesWebSocketConnection>()?;
|
|
super::token_counter::register(module)?;
|
|
super::diagnostics::register(module)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::ffi::CString;
|
|
use std::time::Duration;
|
|
|
|
use futures_util::{SinkExt, StreamExt};
|
|
use pyo3::types::PyDict;
|
|
use tokio::net::TcpListener;
|
|
use tokio_tungstenite::{accept_async, tungstenite::Message};
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn module_registration_preserves_the_public_surface() {
|
|
Python::initialize();
|
|
Python::attach(|py| {
|
|
let module = pyo3::wrap_pymodule!(_native)(py).into_bound(py);
|
|
|
|
let expected = [
|
|
"RustBridgeDeclined",
|
|
"RustUpstreamError",
|
|
"ocr",
|
|
"aocr",
|
|
"transcription",
|
|
"atranscription",
|
|
"messages",
|
|
"amessages",
|
|
"chat_completions_decline",
|
|
"chat_completions",
|
|
"achat_completions",
|
|
"ResponsesWebSocketConnection",
|
|
"TokenCounter",
|
|
"gil_stats",
|
|
];
|
|
|
|
let public_names: Vec<String> = module
|
|
.dict()
|
|
.keys()
|
|
.extract::<Vec<String>>()
|
|
.expect("module names should be strings")
|
|
.into_iter()
|
|
.filter(|name| !name.starts_with('_'))
|
|
.collect();
|
|
assert_eq!(public_names, expected);
|
|
|
|
#[cfg(not(feature = "trace-parity"))]
|
|
assert!(!module.hasattr("_trace").expect("module lookup should work"));
|
|
|
|
#[cfg(feature = "trace-parity")]
|
|
{
|
|
let trace = module
|
|
.getattr("_trace")
|
|
.expect("trace build should expose its diagnostic namespace");
|
|
let trace_names: Vec<String> = trace
|
|
.cast::<PyModule>()
|
|
.expect("trace namespace should be a module")
|
|
.dict()
|
|
.keys()
|
|
.extract::<Vec<String>>()
|
|
.expect("trace names should be strings")
|
|
.into_iter()
|
|
.filter(|name| !name.starts_with("__"))
|
|
.collect();
|
|
assert_eq!(
|
|
trace_names,
|
|
[
|
|
"ocr",
|
|
"aocr",
|
|
"transcription",
|
|
"atranscription",
|
|
"messages",
|
|
"amessages",
|
|
"chat_completions",
|
|
"achat_completions",
|
|
]
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn responses_websocket_connection_round_trips_through_python() {
|
|
Python::initialize();
|
|
let runtime = pyo3_async_runtimes::tokio::get_runtime();
|
|
let listener = runtime
|
|
.block_on(TcpListener::bind("127.0.0.1:0"))
|
|
.expect("listener should bind");
|
|
let address = listener
|
|
.local_addr()
|
|
.expect("listener should have an address");
|
|
let server = runtime.spawn(async move {
|
|
let (stream, _) = listener.accept().await.expect("server should accept");
|
|
let mut socket = accept_async(stream)
|
|
.await
|
|
.expect("handshake should succeed");
|
|
|
|
let message = socket
|
|
.next()
|
|
.await
|
|
.expect("client should send a frame")
|
|
.expect("client frame should be valid");
|
|
assert_eq!(message, Message::Text("from-python".into()));
|
|
socket
|
|
.send(Message::Text("from-server".into()))
|
|
.await
|
|
.expect("server should reply");
|
|
assert!(matches!(socket.next().await, Some(Ok(Message::Close(_)))));
|
|
});
|
|
|
|
Python::attach(|py| {
|
|
let module = pyo3::wrap_pymodule!(_native)(py).into_bound(py);
|
|
let locals = PyDict::new(py);
|
|
locals
|
|
.set_item("native", &module)
|
|
.expect("module should enter Python locals");
|
|
locals
|
|
.set_item("url", format!("ws://{address}"))
|
|
.expect("URL should enter Python locals");
|
|
let code = CString::new(
|
|
r#"
|
|
import asyncio
|
|
|
|
async def exercise():
|
|
connection = await native.ResponsesWebSocketConnection.connect(url)
|
|
assert type(connection) is native.ResponsesWebSocketConnection
|
|
await connection.send_text("from-python")
|
|
assert await connection.recv_text() == "from-server"
|
|
await connection.close()
|
|
assert await connection.recv_text() is None
|
|
|
|
asyncio.run(asyncio.wait_for(exercise(), timeout=5))
|
|
"#,
|
|
)
|
|
.expect("Python source should not contain null bytes");
|
|
py.run(&code, Some(&locals), Some(&locals))
|
|
.expect("Python WebSocket methods should round trip");
|
|
});
|
|
|
|
runtime
|
|
.block_on(async { tokio::time::timeout(Duration::from_secs(5), server).await })
|
|
.expect("server should finish")
|
|
.expect("server task should not panic");
|
|
}
|
|
}
|