litellm/litellm-rust/crates/python-bridge/src/python_settings.rs
yujonglee dff8131f14
refactor(rust_bridge): route Python state reads through host modules (#45689)
* refactor(rust_bridge): group the Python modules Rust imports by name under rust_bridge/host

Rust reads Python-owned state (legacy callback registries, model capabilities) through
modules it imports by path. Put them in one folder with an AGENTS.md that states the
contract, and update the module path constants on the Rust side.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(rust_bridge): route Python state reads through host accessors

* docs(rust_bridge): point test_support at the moved host contract test

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(rust_bridge): move secret manager, transport and timeout accessors under host

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore(greptile): register rust_bridge/host AGENTS.md in files.json

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-11 14:10:16 -07:00

329 lines
10 KiB
Rust

use pyo3::{exceptions::PyModuleNotFoundError, prelude::*};
use strum::IntoStaticStr;
use crate::coercion::{FieldSpec, ProjectionError};
const MODULE: &str = "litellm.rust_bridge.host.settings";
#[derive(Clone, Copy, Debug, IntoStaticStr, PartialEq, Eq)]
pub(crate) enum PythonSettings {
#[strum(serialize = "http_settings")]
Http,
#[strum(serialize = "url_policy")]
UrlPolicy,
#[strum(serialize = "provider_defaults")]
ProviderDefaults,
#[strum(serialize = "secret_manager")]
SecretManager,
#[strum(serialize = "secret_manager_binding")]
SecretManagerBinding,
}
pub(crate) struct Snapshot<'py> {
group: PythonSettings,
value: Bound<'py, PyAny>,
}
impl Snapshot<'_> {
pub(crate) fn read<T>(&self, spec: &FieldSpec<T>) -> Result<T, ProjectionError> {
spec.read(&self.value, self.group.into())
}
}
impl PythonSettings {
pub(crate) fn read(self, py: Python<'_>) -> PyResult<Snapshot<'_>> {
let value = py
.import(MODULE)?
.getattr(<&'static str>::from(self))?
.call0()?;
Ok(Snapshot { group: self, value })
}
/// Reads the accessor, or `None` when the litellm package is not installed
/// (a bare extension module), meaning there are no configured values.
pub(crate) fn read_or_unset(self, py: Python<'_>) -> PyResult<Option<Snapshot<'_>>> {
match self.read(py) {
Ok(snapshot) => Ok(Some(snapshot)),
Err(error) => {
if missing_module(py, &error, "litellm")? {
Ok(None)
} else {
Err(error)
}
}
}
}
#[cfg(test)]
pub(crate) fn snapshot(self, value: Bound<'_, PyAny>) -> Snapshot<'_> {
Snapshot { group: self, value }
}
}
pub(crate) fn missing_module(py: Python<'_>, error: &PyErr, expected: &str) -> PyResult<bool> {
if !error.is_instance_of::<PyModuleNotFoundError>(py) {
return Ok(false);
}
Ok(error
.value(py)
.getattr("name")?
.extract::<Option<String>>()?
.is_some_and(|name| name == expected || name.starts_with(&format!("{expected}."))))
}
#[cfg(test)]
mod tests {
use pyo3::{
exceptions::{PyImportError, PyModuleNotFoundError, PyRuntimeError},
prelude::*,
types::PyDict,
};
use super::PythonSettings;
use crate::coercion::FieldSpec;
#[test]
fn declarations_select_the_decoder_and_read_only_the_requested_field() {
const TRUTHY: FieldSpec<bool> = FieldSpec::new("flag", |field| field.truthy());
const EXACT: FieldSpec<bool> = FieldSpec::new("flag", |field| Ok(field.exact_true()));
Python::initialize();
Python::attach(|py| {
let locals = PyDict::new(py);
py.run(
c"
reads = []
class Settings:
value = 1
@property
def flag(self):
reads.append('flag')
return self.value
@property
def unrelated(self):
raise AssertionError('unrequested field')
settings = Settings()
",
Some(&locals),
Some(&locals),
)
.unwrap();
let value = locals.get_item("settings").unwrap().unwrap();
let snapshot = PythonSettings::Http.snapshot(value.clone());
assert!(snapshot.read(&TRUTHY).unwrap());
assert!(!snapshot.read(&EXACT).unwrap());
value.setattr("value", true).unwrap();
assert!(snapshot.read(&EXACT).unwrap());
assert_eq!(
locals
.get_item("reads")
.unwrap()
.unwrap()
.extract::<Vec<String>>()
.unwrap(),
["flag", "flag", "flag"]
);
});
}
#[test]
fn declared_reads_preserve_descriptor_and_decoder_failures_and_name_missing_fields() {
const FLAG: FieldSpec<bool> = FieldSpec::new("flag", |field| field.truthy());
Python::initialize();
Python::attach(|py| {
let locals = PyDict::new(py);
py.run(
c"
from types import SimpleNamespace
failure = AttributeError('read failed')
class Descriptor:
@property
def flag(self): raise failure
class Truth:
def __bool__(self): raise failure
values = (Descriptor(), SimpleNamespace(flag=Truth()))
",
Some(&locals),
Some(&locals),
)
.unwrap();
let failure = locals.get_item("failure").unwrap().unwrap();
for value in locals
.get_item("values")
.unwrap()
.unwrap()
.try_iter()
.unwrap()
{
let snapshot = PythonSettings::Http.snapshot(value.unwrap());
let error = PyErr::from(snapshot.read(&FLAG).unwrap_err());
assert!(error.value(py).is(&failure));
assert!(error.traceback(py).is_some());
}
let missing = PythonSettings::Http.snapshot(py.eval(c"object()", None, None).unwrap());
let error = PyErr::from(missing.read(&FLAG).unwrap_err());
assert!(error.is_instance_of::<PyRuntimeError>(py));
assert!(
error
.to_string()
.contains("http_settings.flag: missing snapshot field")
);
});
}
#[test]
fn read_or_unset_returns_none_when_litellm_is_missing() {
Python::initialize();
Python::attach(|py| {
let locals = PyDict::new(py);
py.run(
c"
import sys
class MissingLitellm:
def find_spec(self, fullname, path=None, target=None):
if fullname == 'litellm':
raise ModuleNotFoundError('No module named litellm', name='litellm')
finder = MissingLitellm()
previous_litellm = sys.modules.get('litellm')
had_litellm = 'litellm' in sys.modules
sys.meta_path.insert(0, finder)
sys.modules.pop('litellm', None)
",
Some(&locals),
Some(&locals),
)
.unwrap();
let result = PythonSettings::Http.read_or_unset(py);
assert!(result.unwrap().is_none());
py.run(
c"
sys.meta_path.remove(finder)
if had_litellm:
sys.modules['litellm'] = previous_litellm
else:
sys.modules.pop('litellm', None)
",
Some(&locals),
Some(&locals),
)
.unwrap();
});
}
#[test]
fn read_or_unset_propagates_nested_module_not_found_errors() {
Python::initialize();
Python::attach(|py| {
let locals = PyDict::new(py);
py.run(
c"
import sys
import types
previous_modules = {
name: sys.modules[name]
for name in ('litellm', 'litellm.rust_bridge', 'litellm.rust_bridge.host', 'litellm.rust_bridge.host.settings')
if name in sys.modules
}
litellm = types.ModuleType('litellm')
litellm.__path__ = []
rust_bridge = types.ModuleType('litellm.rust_bridge')
rust_bridge.__path__ = []
host = types.ModuleType('litellm.rust_bridge.host')
host.__path__ = []
settings = types.ModuleType('litellm.rust_bridge.host.settings')
def http_settings():
raise ModuleNotFoundError('No module named certifi', name='certifi')
settings.http_settings = http_settings
litellm.rust_bridge = rust_bridge
rust_bridge.host = host
host.settings = settings
sys.modules['litellm'] = litellm
sys.modules['litellm.rust_bridge'] = rust_bridge
sys.modules['litellm.rust_bridge.host'] = host
sys.modules['litellm.rust_bridge.host.settings'] = settings
",
Some(&locals),
Some(&locals),
)
.unwrap();
let error = match PythonSettings::Http.read_or_unset(py) {
Ok(_) => panic!("nested module errors must propagate"),
Err(error) => error,
};
assert!(error.is_instance_of::<PyModuleNotFoundError>(py));
assert_eq!(
error
.value(py)
.getattr("name")
.unwrap()
.extract::<String>()
.unwrap(),
"certifi"
);
py.run(
c"
for name in ('litellm.rust_bridge.host.settings', 'litellm.rust_bridge.host', 'litellm.rust_bridge', 'litellm'):
sys.modules.pop(name, None)
sys.modules.update(previous_modules)
",
Some(&locals),
Some(&locals),
)
.unwrap();
});
}
#[test]
fn read_or_unset_propagates_import_errors() {
Python::initialize();
Python::attach(|py| {
let locals = PyDict::new(py);
py.run(
c"
import sys
import types
previous_modules = {
name: sys.modules[name]
for name in ('litellm', 'litellm.rust_bridge', 'litellm.rust_bridge.host', 'litellm.rust_bridge.host.settings')
if name in sys.modules
}
litellm = types.ModuleType('litellm')
litellm.__path__ = []
rust_bridge = types.ModuleType('litellm.rust_bridge')
rust_bridge.__path__ = []
host = types.ModuleType('litellm.rust_bridge.host')
host.__path__ = []
settings = types.ModuleType('litellm.rust_bridge.host.settings')
def http_settings():
raise ImportError('cannot import name setting')
settings.http_settings = http_settings
litellm.rust_bridge = rust_bridge
rust_bridge.host = host
host.settings = settings
sys.modules['litellm'] = litellm
sys.modules['litellm.rust_bridge'] = rust_bridge
sys.modules['litellm.rust_bridge.host'] = host
sys.modules['litellm.rust_bridge.host.settings'] = settings
",
Some(&locals),
Some(&locals),
)
.unwrap();
let error = match PythonSettings::Http.read_or_unset(py) {
Ok(_) => panic!("import errors must propagate"),
Err(error) => error,
};
assert!(error.is_instance_of::<PyImportError>(py));
assert_eq!(error.to_string(), "ImportError: cannot import name setting");
py.run(
c"
for name in ('litellm.rust_bridge.host.settings', 'litellm.rust_bridge.host', 'litellm.rust_bridge', 'litellm'):
sys.modules.pop(name, None)
sys.modules.update(previous_modules)
",
Some(&locals),
Some(&locals),
)
.unwrap();
});
}
}