diff --git a/litellm-rust/Cargo.lock b/litellm-rust/Cargo.lock index b3dac5ca935..33ce4d91cd5 100644 --- a/litellm-rust/Cargo.lock +++ b/litellm-rust/Cargo.lock @@ -1456,6 +1456,7 @@ dependencies = [ "litellm-python-interop", "pyo3", "pyo3-async-runtimes", + "pyo3-build-config", "serde", "serde_json", "tokio", diff --git a/litellm-rust/crates/python-bridge/Cargo.toml b/litellm-rust/crates/python-bridge/Cargo.toml index 637e5580170..0ca3399ecc1 100644 --- a/litellm-rust/crates/python-bridge/Cargo.toml +++ b/litellm-rust/crates/python-bridge/Cargo.toml @@ -28,6 +28,9 @@ serde.workspace = true serde_json.workspace = true tokio.workspace = true +[build-dependencies] +pyo3-build-config = "0.29.2" + [dev-dependencies] criterion = "0.8.2" tokio-tungstenite.workspace = true diff --git a/litellm-rust/crates/python-bridge/build.rs b/litellm-rust/crates/python-bridge/build.rs index 0f7293007b2..10b53bcc965 100644 --- a/litellm-rust/crates/python-bridge/build.rs +++ b/litellm-rust/crates/python-bridge/build.rs @@ -1,6 +1,34 @@ +use std::path::Path; + +fn pyo3_version_from_lock() -> Option { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?; + let lockfile = Path::new(&manifest_dir).join("../../Cargo.lock"); + let contents = std::fs::read_to_string(lockfile).ok()?; + let lines: Vec<&str> = contents.lines().collect(); + for pair in lines.windows(2) { + if pair[0].trim() != "name = \"pyo3\"" { + continue; + } + if let Some(version) = pair[1].trim().strip_prefix("version = ") { + return Some(version.trim_matches('"').to_owned()); + } + } + None +} + fn main() { + pyo3_build_config::use_pyo3_cfgs(); if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") { println!("cargo:rustc-cdylib-link-arg=-undefined"); println!("cargo:rustc-cdylib-link-arg=dynamic_lookup"); } + println!( + "cargo:rustc-env=LITELLM_NATIVE_PROFILE={}", + std::env::var("PROFILE").unwrap_or_else(|_| "unknown".to_owned()) + ); + println!( + "cargo:rustc-env=LITELLM_PYO3_VERSION={}", + pyo3_version_from_lock().unwrap_or_else(|| "unknown".to_owned()) + ); + println!("cargo:rerun-if-changed=../../Cargo.lock"); } diff --git a/litellm-rust/crates/python-bridge/src/diagnostics.rs b/litellm-rust/crates/python-bridge/src/diagnostics.rs index cc153a89b8f..a60f9ec9815 100644 --- a/litellm-rust/crates/python-bridge/src/diagnostics.rs +++ b/litellm-rust/crates/python-bridge/src/diagnostics.rs @@ -9,6 +9,17 @@ fn gil_stats(py: Python<'_>) -> PyResult> { Ok(stats.into_any().unbind()) } +#[pyfunction] +fn build_info(py: Python<'_>) -> PyResult> { + let info = PyDict::new(py); + info.set_item("profile", env!("LITELLM_NATIVE_PROFILE"))?; + info.set_item("pyo3", env!("LITELLM_PYO3_VERSION"))?; + info.set_item("abi3", cfg!(Py_LIMITED_API))?; + info.set_item("gil_disabled", cfg!(Py_GIL_DISABLED))?; + info.set_item("debug_assertions", cfg!(debug_assertions))?; + Ok(info.into_any().unbind()) +} + #[cfg(feature = "panic-test")] #[pyfunction] fn _panic_for_test() { @@ -17,6 +28,7 @@ fn _panic_for_test() { pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(gil_stats, module)?)?; + module.add_function(wrap_pyfunction!(build_info, module)?)?; #[cfg(feature = "panic-test")] module.add_function(wrap_pyfunction!(_panic_for_test, module)?)?; Ok(()) diff --git a/litellm-rust/crates/python-bridge/src/lib.rs b/litellm-rust/crates/python-bridge/src/lib.rs index 5f36a22370a..1dbe15b563f 100644 --- a/litellm-rust/crates/python-bridge/src/lib.rs +++ b/litellm-rust/crates/python-bridge/src/lib.rs @@ -68,6 +68,7 @@ mod _native { #[pymodule_init] fn init(module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add("__version__", env!("CARGO_PKG_VERSION"))?; super::errors::register(module)?; super::routes::register(module)?; module.add_class::()?; @@ -107,6 +108,7 @@ mod tests { "achat_completions", "ResponsesWebSocketConnection", "gil_stats", + "build_info", ]; let public_names: Vec = module @@ -118,6 +120,58 @@ mod tests { .filter(|name| !name.starts_with("__")) .collect(); assert_eq!(public_names, expected); + + let version = module + .getattr("__version__") + .expect("module should expose __version__") + .extract::() + .expect("__version__ should be a string"); + assert_eq!(version, env!("CARGO_PKG_VERSION")); + }); + } + + #[test] + fn version_and_build_info_report_expected_shape() { + Python::initialize(); + Python::attach(|py| { + let module = pyo3::wrap_pymodule!(_native)(py).into_bound(py); + + let version = module + .getattr("__version__") + .expect("module should expose __version__") + .extract::() + .expect("__version__ should be a string"); + let segments: Vec<&str> = version.split('.').collect(); + assert_eq!(segments.len(), 3, "__version__ should be x.y.z: {version}"); + for segment in segments { + assert!( + !segment.is_empty() && segment.bytes().all(|b| b.is_ascii_digit()), + "__version__ segments should be numeric: {version}" + ); + } + + let build_info = module + .getattr("build_info") + .expect("module should expose build_info") + .call0() + .expect("build_info should be callable") + .cast_into::() + .expect("build_info should return a dict"); + let mut keys: Vec = build_info + .keys() + .extract::>() + .expect("build_info keys should be strings"); + keys.sort(); + assert_eq!( + keys, + [ + "abi3", + "debug_assertions", + "gil_disabled", + "profile", + "pyo3" + ] + ); }); }