fix(rust): keep native OCR on the proxy by declining only a supplied client

The proxy attaches its shared aiohttp session to every request as shared_session, so declining on it sent every proxy OCR call to Python, which never uses that session for OCR. aclient_session is a litellm global and never a call argument, so that check could not match. The proxy-shaped lifecycle test now asserts the call was served by Rust
This commit is contained in:
Yujong Lee 2026-09-18 18:47:36 -07:00
parent 157fa58947
commit c635c35b3d
2 changed files with 22 additions and 25 deletions

View file

@ -12,8 +12,6 @@ use crate::{errors::RustBridgeDeclined, python_settings::PythonSettings};
static POOL: LazyLock<HttpClientPool> =
LazyLock::new(|| HttpClientPool::new(Arc::new(PublicDnsResolver)));
const LIVE_CLIENT_ARGUMENTS: [&str; 3] = ["client", "shared_session", "aclient_session"];
pub(crate) fn pool() -> &'static HttpClientPool {
&POOL
}
@ -23,7 +21,7 @@ pub(crate) fn call_config(
kwargs: &Bound<'_, PyDict>,
asynchronous: bool,
) -> PyResult<HttpClientConfig> {
decline_live_clients(kwargs)?;
decline_live_client(kwargs)?;
decline_custom_url_policy(&PythonSettings::UrlPolicy.read(py)?)?;
let configured = settings(&PythonSettings::Http.read(py)?)?
.with_environment(&|name| std::env::var(name).ok());
@ -53,13 +51,14 @@ fn for_call(
}
}
pub(crate) fn decline_live_clients(kwargs: &Bound<'_, PyDict>) -> PyResult<()> {
for name in LIVE_CLIENT_ARGUMENTS {
if kwargs.get_item(name)?.is_some_and(|value| !value.is_none()) {
return Err(RustBridgeDeclined::new_err(format!(
"{name} is a live Python HTTP client and cannot be used by the Rust route"
)));
}
fn decline_live_client(kwargs: &Bound<'_, PyDict>) -> PyResult<()> {
if kwargs
.get_item("client")?
.is_some_and(|value| !value.is_none())
{
return Err(RustBridgeDeclined::new_err(
"client is a live Python HTTP client and cannot be used by the Rust route",
));
}
Ok(())
}
@ -362,32 +361,29 @@ user_agent='litellm/9.9.9',
assert_eq!(config.trust_proxy_env, expected);
}
#[rstest]
#[case::client("client")]
#[case::shared_session("shared_session")]
#[case::aclient_session("aclient_session")]
fn live_python_clients_decline_before_dispatch(#[case] name: &str) {
#[test]
fn live_python_client_declines_before_dispatch() {
Python::initialize();
Python::attach(|py| {
let kwargs = PyDict::new(py);
kwargs
.set_item(name, py.eval(c"object()", None, None).unwrap())
.set_item("client", py.eval(c"object()", None, None).unwrap())
.unwrap();
let error = decline_live_clients(&kwargs).unwrap_err();
let error = decline_live_client(&kwargs).unwrap_err();
assert!(error.is_instance_of::<RustBridgeDeclined>(py));
assert!(error.value(py).to_string().contains(name));
});
}
#[test]
fn none_valued_client_arguments_are_not_live_clients() {
#[rstest]
#[case::absent_client("{}")]
#[case::none_client("{'client': None}")]
#[case::proxy_shared_session("{'shared_session': object()}")]
fn calls_without_a_python_client_stay_on_the_rust_route(#[case] kwargs: &str) {
Python::initialize();
Python::attach(|py| {
let kwargs = PyDict::new(py);
for name in LIVE_CLIENT_ARGUMENTS {
kwargs.set_item(name, py.None()).unwrap();
}
decline_live_clients(&kwargs).unwrap();
let source = std::ffi::CString::new(kwargs).unwrap();
let kwargs = py.eval(&source, None, None).unwrap();
decline_live_client(kwargs.cast::<PyDict>().unwrap()).unwrap();
});
}
}

View file

@ -39,6 +39,7 @@ async def test_proxy_metadata_remains_python_owned(ocr_server: RecordingServer)
)
events: Final = await recorder.wait_for_async("async_log_success_event")
assert response.pages[0].markdown == "native OCR response"
assert response._hidden_params["additional_headers"]["x-litellm-rust"] == "true"
assert events[0].kwargs["litellm_params"]["metadata"]["user_api_key_auth"].user_id == "ocr-user"
assert "metadata" not in ocr_server.requests[0].body