litellm/litellm-rust/crates/python-bridge/src/auth.rs
yujonglee 347b642bdd
refactor(ocr): complete native lifecycle and preserve Azure auth (#40734)
* 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
2026-09-12 11:56:49 -07:00

194 lines
6.5 KiB
Rust

use litellm_core::auth::{ResolvedCredential, SecretValue};
use pyo3::exceptions::{PyException, PyRuntimeError, PyTypeError};
use pyo3::gc::{PyTraverseError, PyVisit};
use pyo3::prelude::*;
use pyo3::types::PyString;
#[derive(Clone, Copy)]
pub(crate) struct TokenProviderContract {
callable_error: &'static str,
token_type_error: &'static str,
callback_error: &'static str,
}
pub(crate) const AZURE_AD_TOKEN_PROVIDER: TokenProviderContract = TokenProviderContract {
callable_error: "Azure AD token provider must be callable",
token_type_error: "Azure AD token must be a string, got {}",
callback_error: "Failed to get Azure AD token: {}",
};
pub(crate) struct PythonTokenProvider {
callback: Py<PyAny>,
contract: TokenProviderContract,
}
impl PythonTokenProvider {
pub(crate) fn select(
provider: Bound<'_, PyAny>,
contract: TokenProviderContract,
) -> Option<Self> {
(provider.is_callable() && provider.is_truthy().unwrap_or(false)).then(|| Self {
callback: provider.unbind(),
contract,
})
}
pub(crate) fn acquire(&self, py: Python<'_>) -> PyResult<ResolvedCredential> {
let provider = self.callback.bind(py);
if !provider.is_callable() {
return Err(PyTypeError::new_err(self.contract.callable_error));
}
let token = (|| {
let token = provider.call0()?;
if !token.is_instance_of::<PyString>() {
let message = PyString::new(py, self.contract.token_type_error)
.call_method1("format", (token.get_type(),))?;
return Err(PyTypeError::new_err(message.unbind()));
}
Ok(token)
})()
.map_err(|error| {
if error.is_instance_of::<PyTypeError>(py) || !error.is_instance_of::<PyException>(py) {
return error;
}
match PyString::new(py, self.contract.callback_error)
.call_method1("format", (error.value(py),))
{
Ok(message) => {
let wrapped = PyRuntimeError::new_err(message.unbind());
wrapped.set_context(py, Some(error.clone_ref(py)));
wrapped.set_cause(py, Some(error));
wrapped
}
Err(format_error) => {
format_error.set_context(py, Some(error));
format_error
}
}
})?;
Ok(ResolvedCredential::AccessToken {
token: SecretValue::new(token.extract::<String>()?),
expires_on: None,
})
}
pub(crate) fn traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> {
visit.call(&self.callback)
}
}
#[cfg(test)]
mod tests {
use pyo3::exceptions::PyRuntimeError;
use pyo3::types::PyDict;
use super::*;
#[test]
fn token_callback_preserves_exception_identity_and_explicit_chaining() {
Python::initialize();
Python::attach(|py| {
let locals = PyDict::new(py);
py.run(
pyo3::ffi::c_str!(
r#"
class ProviderError(Exception):
def __format__(self, specification):
return 'unavailable'
ordinary = ProviderError('must use __format__')
type_error = TypeError('signature')
abort = KeyboardInterrupt('cancelled')
def provider(error):
def acquire():
raise error
return acquire
"#
),
Some(&locals),
Some(&locals),
)
.unwrap();
for name in ["ordinary", "type_error", "abort"] {
let original = locals.get_item(name).unwrap().unwrap();
let callback = locals
.get_item("provider")
.unwrap()
.unwrap()
.call1((&original,))
.unwrap();
let provider =
PythonTokenProvider::select(callback, AZURE_AD_TOKEN_PROVIDER).unwrap();
let error = provider.acquire(py).unwrap_err();
if name == "ordinary" {
assert!(error.is_instance_of::<PyRuntimeError>(py));
assert!(error.cause(py).unwrap().value(py).is(&original));
assert!(
error
.value(py)
.getattr("__context__")
.unwrap()
.is(&original)
);
assert_eq!(
error.value(py).str().unwrap().to_str().unwrap(),
"Failed to get Azure AD token: unavailable"
);
} else {
assert!(error.value(py).is(&original));
}
}
});
}
#[test]
fn invalid_token_type_formatting_preserves_python_failure_semantics() {
Python::initialize();
Python::attach(|py| {
let locals = PyDict::new(py);
py.run(
pyo3::ffi::c_str!(
r#"
failure = ValueError('formatting failed')
class TokenType(type):
def __format__(cls, specification):
raise failure
class Token(metaclass=TokenType):
pass
def provider():
return Token()
"#
),
Some(&locals),
Some(&locals),
)
.unwrap();
let provider = PythonTokenProvider::select(
locals.get_item("provider").unwrap().unwrap(),
AZURE_AD_TOKEN_PROVIDER,
)
.unwrap();
let error = provider.acquire(py).unwrap_err();
assert!(error.is_instance_of::<PyRuntimeError>(py));
assert!(
error
.cause(py)
.unwrap()
.value(py)
.is(locals.get_item("failure").unwrap().unwrap())
);
});
}
#[test]
fn token_string_extraction_errors_are_not_wrapped_as_callback_failures() {
Python::initialize();
Python::attach(|py| {
let callback = py
.eval(pyo3::ffi::c_str!("lambda: '\\ud800'"), None, None)
.unwrap();
let provider = PythonTokenProvider::select(callback, AZURE_AD_TOKEN_PROVIDER).unwrap();
let error = provider.acquire(py).unwrap_err();
assert!(error.is_instance_of::<pyo3::exceptions::PyUnicodeEncodeError>(py));
});
}
}