refactor(rust): merge http settings from per-source layers

Each source (per-call kwargs, environment variables, the Python module) now
builds an HttpSettingsLayer, and HttpSettings::from_layers merges them with
explicit precedence. The aiohttp and httpx proxy-env rule is resolved once in
the merge, so HttpSettings carries a single trust_proxy_env flag
This commit is contained in:
Yujong Lee 2026-09-18 19:53:14 -07:00
parent 80dbb2a28a
commit bae4f22d3a
4 changed files with 278 additions and 188 deletions

View file

@ -72,7 +72,7 @@ impl From<&HttpSettings> for Resolution {
force_ipv4: settings.force_ipv4,
http2: settings.http2,
user_agent: settings.user_agent.clone(),
trust_proxy_env: settings.trusts_proxy_env(),
trust_proxy_env: settings.trust_proxy_env,
connect_timeout: settings.connect_timeout,
tcp_keepalive: settings.tcp_keepalive,
pool_idle_timeout: settings.pool_idle_timeout,
@ -125,17 +125,12 @@ mod tests {
use super::*;
fn no_env(_: &str) -> Option<String> {
None
}
fn settings(ssl_verify: Option<SslVerify>, ssl_cert_file: Option<&str>) -> HttpSettings {
HttpSettings {
ssl_verify,
ssl_cert_file: ssl_cert_file.map(PathBuf::from),
..HttpSettings::default()
}
.with_environment(&no_env)
}
#[rstest]
@ -161,17 +156,6 @@ mod tests {
assert_eq!(config.verify, expected);
}
#[test]
fn ssl_verify_environment_variable_beats_the_configured_setting() {
let settings = HttpSettings {
ssl_verify: Some(SslVerify::Disabled),
..HttpSettings::default()
}
.with_environment(&|name: &str| (name == "SSL_VERIFY").then(|| "true".to_string()));
let config = Resolution::from(&settings).config;
assert_eq!(config.verify, Verify::BuiltInRoots);
}
#[rstest]
#[case::x25519("X25519", Some(KeyExchangeGroup::X25519))]
#[case::openssl_p256("prime256v1", Some(KeyExchangeGroup::Secp256r1))]
@ -280,29 +264,6 @@ mod tests {
);
}
#[rstest]
#[case::aiohttp_default(HttpSettings::default(), true)]
#[case::aiohttp_opted_out(HttpSettings { ignore_proxy_env: true, ..HttpSettings::default() }, false)]
#[case::session_trust_env_beats_opt_out(
HttpSettings { ignore_proxy_env: true, trust_proxy_env: true, ..HttpSettings::default() },
true
)]
#[case::http2_uses_httpx(
HttpSettings { ignore_proxy_env: true, http2: true, ..HttpSettings::default() },
true
)]
#[case::aiohttp_disabled(
HttpSettings { ignore_proxy_env: true, httpx_transport: true, ..HttpSettings::default() },
true
)]
fn environment_proxies_apply_unless_the_aiohttp_transport_opts_out(
#[case] settings: HttpSettings,
#[case] expected: bool,
) {
let config = Resolution::from(&settings).config;
assert_eq!(config.trust_proxy_env, expected);
}
#[test]
fn missing_ca_bundle_is_a_read_error() {
let path = std::env::temp_dir().join("litellm-http-missing-bundle.pem");

View file

@ -9,5 +9,5 @@ pub use config::{HttpClientConfig, Resolution, Verify};
pub use error::Error;
pub use pool::{ClientVariant, HttpClientPool};
pub use proxy::EnvironmentProxies;
pub use settings::{HttpSettings, SslVerify, TcpKeepalive};
pub use settings::{HttpSettings, HttpSettingsLayer, SslVerify, TcpKeepalive};
pub use tls::{KeyExchangeGroup, Tls12CipherSuite, Unsupported};

View file

@ -27,6 +27,79 @@ pub struct TcpKeepalive {
pub retries: u32,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HttpSettingsLayer {
pub ssl_verify: Option<SslVerify>,
pub ssl_cert_file: Option<PathBuf>,
pub ssl_certificate: Option<PathBuf>,
pub ssl_security_level: Option<String>,
pub ssl_ecdh_curve: Option<String>,
pub force_ipv4: Option<bool>,
pub http2: Option<bool>,
pub aiohttp_trust_env: Option<bool>,
pub disable_aiohttp_trust_env: Option<bool>,
pub disable_aiohttp_transport: Option<bool>,
pub user_agent: Option<String>,
pub tcp_keepalive: Option<TcpKeepalive>,
pub pool_idle_timeout: Option<Duration>,
}
impl HttpSettingsLayer {
pub fn from_environment(env: &(dyn Fn(&str) -> Option<String> + Sync)) -> Self {
let enabled = |name: &str| {
env(name)
.is_some_and(|value| value.trim().eq_ignore_ascii_case("true"))
.then_some(true)
};
let number = |name: &str| env(name).and_then(|value| value.trim().parse::<u32>().ok());
let seconds = |name: &str, default: u32| {
Duration::from_secs(u64::from(number(name).unwrap_or(default)))
};
Self {
ssl_verify: env("SSL_VERIFY").map(|value| SslVerify::parse(&value)),
ssl_cert_file: env("SSL_CERT_FILE").map(PathBuf::from),
ssl_certificate: env("SSL_CERTIFICATE").map(PathBuf::from),
ssl_security_level: env("SSL_SECURITY_LEVEL"),
ssl_ecdh_curve: env("SSL_ECDH_CURVE"),
force_ipv4: None,
http2: enabled("LITELLM_HTTP2"),
aiohttp_trust_env: enabled("AIOHTTP_TRUST_ENV"),
disable_aiohttp_trust_env: enabled("DISABLE_AIOHTTP_TRUST_ENV"),
disable_aiohttp_transport: enabled("DISABLE_AIOHTTP_TRANSPORT"),
user_agent: env("LITELLM_USER_AGENT"),
tcp_keepalive: enabled("AIOHTTP_SO_KEEPALIVE").map(|_| TcpKeepalive {
idle: seconds("AIOHTTP_TCP_KEEPIDLE", 60),
interval: seconds("AIOHTTP_TCP_KEEPINTVL", 30),
retries: number("AIOHTTP_TCP_KEEPCNT").unwrap_or(5),
}),
pool_idle_timeout: number("AIOHTTP_KEEPALIVE_TIMEOUT")
.map(|timeout| Duration::from_secs(u64::from(timeout))),
}
}
fn or(self, lower: Self) -> Self {
Self {
ssl_verify: self.ssl_verify.or(lower.ssl_verify),
ssl_cert_file: self.ssl_cert_file.or(lower.ssl_cert_file),
ssl_certificate: self.ssl_certificate.or(lower.ssl_certificate),
ssl_security_level: self.ssl_security_level.or(lower.ssl_security_level),
ssl_ecdh_curve: self.ssl_ecdh_curve.or(lower.ssl_ecdh_curve),
force_ipv4: self.force_ipv4.or(lower.force_ipv4),
http2: self.http2.or(lower.http2),
aiohttp_trust_env: self.aiohttp_trust_env.or(lower.aiohttp_trust_env),
disable_aiohttp_trust_env: self
.disable_aiohttp_trust_env
.or(lower.disable_aiohttp_trust_env),
disable_aiohttp_transport: self
.disable_aiohttp_transport
.or(lower.disable_aiohttp_transport),
user_agent: self.user_agent.or(lower.user_agent),
tcp_keepalive: self.tcp_keepalive.or(lower.tcp_keepalive),
pool_idle_timeout: self.pool_idle_timeout.or(lower.pool_idle_timeout),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HttpSettings {
pub ssl_verify: Option<SslVerify>,
@ -36,10 +109,8 @@ pub struct HttpSettings {
pub ssl_ecdh_curve: Option<String>,
pub force_ipv4: bool,
pub http2: bool,
pub httpx_transport: bool,
pub user_agent: Option<String>,
pub trust_proxy_env: bool,
pub ignore_proxy_env: bool,
pub connect_timeout: Duration,
pub tcp_keepalive: Option<TcpKeepalive>,
pub pool_idle_timeout: Duration,
@ -55,10 +126,8 @@ impl Default for HttpSettings {
ssl_ecdh_curve: None,
force_ipv4: false,
http2: false,
httpx_transport: false,
user_agent: None,
trust_proxy_env: false,
ignore_proxy_env: false,
trust_proxy_env: true,
connect_timeout: Duration::from_secs(10),
tcp_keepalive: None,
pool_idle_timeout: Duration::from_secs(120),
@ -67,54 +136,38 @@ impl Default for HttpSettings {
}
impl HttpSettings {
pub fn with_environment(self, env: &(dyn Fn(&str) -> Option<String> + Sync)) -> Self {
let enabled =
|name: &str| env(name).is_some_and(|value| value.trim().eq_ignore_ascii_case("true"));
let number = |name: &str| env(name).and_then(|value| value.trim().parse::<u32>().ok());
let seconds = |name: &str, default: u32| {
Duration::from_secs(u64::from(number(name).unwrap_or(default)))
};
pub fn from_layers(
highest_precedence_first: impl IntoIterator<Item = HttpSettingsLayer>,
) -> Self {
let merged = highest_precedence_first
.into_iter()
.reduce(HttpSettingsLayer::or)
.unwrap_or_default();
let defaults = Self::default();
let http2 = merged.http2.unwrap_or(defaults.http2);
Self {
ssl_verify: env("SSL_VERIFY")
.map(|value| SslVerify::parse(&value))
.or(self.ssl_verify),
ssl_cert_file: env("SSL_CERT_FILE")
.map(PathBuf::from)
.or(self.ssl_cert_file),
ssl_certificate: env("SSL_CERTIFICATE")
.map(PathBuf::from)
.or(self.ssl_certificate)
ssl_verify: merged.ssl_verify,
ssl_cert_file: merged.ssl_cert_file,
ssl_certificate: merged
.ssl_certificate
.filter(|path| !path.as_os_str().is_empty()),
ssl_security_level: env("SSL_SECURITY_LEVEL")
.or(self.ssl_security_level)
.filter(|level| !level.is_empty()),
ssl_ecdh_curve: env("SSL_ECDH_CURVE")
.or(self.ssl_ecdh_curve)
.filter(|curve| !curve.is_empty()),
http2: self.http2 || enabled("LITELLM_HTTP2"),
httpx_transport: self.httpx_transport || enabled("DISABLE_AIOHTTP_TRANSPORT"),
user_agent: env("LITELLM_USER_AGENT").or(self.user_agent),
trust_proxy_env: self.trust_proxy_env || enabled("AIOHTTP_TRUST_ENV"),
ignore_proxy_env: self.ignore_proxy_env || enabled("DISABLE_AIOHTTP_TRUST_ENV"),
tcp_keepalive: enabled("AIOHTTP_SO_KEEPALIVE")
.then(|| TcpKeepalive {
idle: seconds("AIOHTTP_TCP_KEEPIDLE", 60),
interval: seconds("AIOHTTP_TCP_KEEPINTVL", 30),
retries: number("AIOHTTP_TCP_KEEPCNT").unwrap_or(5),
})
.or(self.tcp_keepalive),
pool_idle_timeout: number("AIOHTTP_KEEPALIVE_TIMEOUT")
.map_or(self.pool_idle_timeout, |timeout| {
Duration::from_secs(u64::from(timeout))
}),
..self
ssl_security_level: merged.ssl_security_level.filter(|level| !level.is_empty()),
ssl_ecdh_curve: merged.ssl_ecdh_curve.filter(|curve| !curve.is_empty()),
force_ipv4: merged.force_ipv4.unwrap_or(defaults.force_ipv4),
http2,
user_agent: merged.user_agent,
trust_proxy_env: !merged.disable_aiohttp_trust_env.unwrap_or(false)
|| merged.aiohttp_trust_env.unwrap_or(false)
|| merged.disable_aiohttp_transport.unwrap_or(false)
|| http2,
tcp_keepalive: merged.tcp_keepalive,
pool_idle_timeout: merged
.pool_idle_timeout
.unwrap_or(defaults.pool_idle_timeout),
..defaults
}
}
pub fn trusts_proxy_env(&self) -> bool {
!self.ignore_proxy_env || self.trust_proxy_env || self.http2 || self.httpx_transport
}
pub fn without_missing_files(self, exists: &dyn Fn(&Path) -> bool) -> Self {
Self {
ssl_verify: match self.ssl_verify {
@ -161,15 +214,15 @@ mod tests {
}
#[test]
fn environment_overrides_configured_ssl_values() {
let settings = HttpSettings {
fn higher_layers_override_lower_ones() {
let configured = HttpSettingsLayer {
ssl_verify: Some(SslVerify::Enabled),
ssl_certificate: Some("/configured/client.pem".into()),
ssl_security_level: Some("configured".into()),
user_agent: Some("configured/1".into()),
..HttpSettings::default()
}
.with_environment(&env_of(&[
..HttpSettingsLayer::default()
};
let environment = HttpSettingsLayer::from_environment(&env_of(&[
("SSL_VERIFY", "false"),
("SSL_CERT_FILE", "/env/roots.pem"),
("SSL_CERTIFICATE", "/env/client.pem"),
@ -177,6 +230,7 @@ mod tests {
("SSL_ECDH_CURVE", "X25519"),
("LITELLM_USER_AGENT", "env/2"),
]));
let settings = HttpSettings::from_layers([environment, configured]);
assert_eq!(settings.ssl_verify, Some(SslVerify::Disabled));
assert_eq!(settings.ssl_cert_file, Some("/env/roots.pem".into()));
assert_eq!(settings.ssl_certificate, Some("/env/client.pem".into()));
@ -189,30 +243,60 @@ mod tests {
}
#[test]
fn missing_environment_keeps_configured_values() {
let configured = HttpSettings {
ssl_verify: Some(SslVerify::CaBundle("/configured/roots.pem".into())),
http2: true,
trust_proxy_env: true,
user_agent: Some("configured/1".into()),
..HttpSettings::default()
fn an_explicit_false_in_a_higher_layer_beats_a_lower_true() {
let higher = HttpSettingsLayer {
http2: Some(false),
force_ipv4: Some(false),
..HttpSettingsLayer::default()
};
assert_eq!(configured.clone().with_environment(&no_env), configured);
let lower = HttpSettingsLayer {
http2: Some(true),
force_ipv4: Some(true),
..HttpSettingsLayer::default()
};
let settings = HttpSettings::from_layers([higher, lower]);
assert!(!settings.http2);
assert!(!settings.force_ipv4);
}
#[test]
fn an_empty_environment_is_an_empty_layer_so_lower_layers_and_defaults_apply() {
assert_eq!(
HttpSettingsLayer::from_environment(&no_env),
HttpSettingsLayer::default()
);
let configured = HttpSettingsLayer {
ssl_verify: Some(SslVerify::CaBundle("/configured/roots.pem".into())),
http2: Some(true),
user_agent: Some("configured/1".into()),
..HttpSettingsLayer::default()
};
assert_eq!(
HttpSettings::from_layers([HttpSettingsLayer::default(), configured]),
HttpSettings {
ssl_verify: Some(SslVerify::CaBundle("/configured/roots.pem".into())),
http2: true,
user_agent: Some("configured/1".into()),
..HttpSettings::default()
}
);
assert_eq!(HttpSettings::from_layers([]), HttpSettings::default());
}
#[test]
fn empty_environment_values_clear_the_setting_like_python_truthiness() {
let settings = HttpSettings {
let configured = HttpSettingsLayer {
ssl_certificate: Some("/configured/client.pem".into()),
ssl_security_level: Some("configured".into()),
ssl_ecdh_curve: Some("X25519".into()),
..HttpSettings::default()
}
.with_environment(&env_of(&[
..HttpSettingsLayer::default()
};
let environment = HttpSettingsLayer::from_environment(&env_of(&[
("SSL_CERTIFICATE", ""),
("SSL_SECURITY_LEVEL", ""),
("SSL_ECDH_CURVE", ""),
]));
let settings = HttpSettings::from_layers([environment, configured]);
assert_eq!(settings.ssl_certificate, None);
assert_eq!(settings.ssl_security_level, None);
assert_eq!(settings.ssl_ecdh_curve, None);
@ -220,11 +304,11 @@ mod tests {
#[test]
fn socket_keepalive_follows_the_aiohttp_variables_with_python_defaults() {
let tuned = HttpSettings::default().with_environment(&env_of(&[
let tuned = HttpSettings::from_layers([HttpSettingsLayer::from_environment(&env_of(&[
("AIOHTTP_SO_KEEPALIVE", "True"),
("AIOHTTP_TCP_KEEPIDLE", "45"),
("AIOHTTP_KEEPALIVE_TIMEOUT", "30"),
]));
]))]);
assert_eq!(
tuned.tcp_keepalive,
Some(TcpKeepalive {
@ -238,12 +322,53 @@ mod tests {
#[test]
fn socket_keepalive_stays_off_unless_enabled() {
let settings =
HttpSettings::default().with_environment(&env_of(&[("AIOHTTP_TCP_KEEPIDLE", "45")]));
let settings = HttpSettings::from_layers([HttpSettingsLayer::from_environment(&env_of(
&[("AIOHTTP_TCP_KEEPIDLE", "45")],
))]);
assert_eq!(settings.tcp_keepalive, None);
assert_eq!(settings.pool_idle_timeout, Duration::from_secs(120));
}
fn proxy_flags(
aiohttp_trust_env: bool,
disable_aiohttp_trust_env: bool,
disable_aiohttp_transport: bool,
http2: bool,
) -> HttpSettingsLayer {
HttpSettingsLayer {
aiohttp_trust_env: Some(aiohttp_trust_env),
disable_aiohttp_trust_env: Some(disable_aiohttp_trust_env),
disable_aiohttp_transport: Some(disable_aiohttp_transport),
http2: Some(http2),
..HttpSettingsLayer::default()
}
}
#[rstest]
#[case::aiohttp_default(proxy_flags(false, false, false, false), true)]
#[case::aiohttp_opted_out(proxy_flags(false, true, false, false), false)]
#[case::session_trust_env_beats_opt_out(proxy_flags(true, true, false, false), true)]
#[case::http2_uses_httpx(proxy_flags(false, true, false, true), true)]
#[case::aiohttp_disabled(proxy_flags(false, true, true, false), true)]
fn environment_proxies_apply_unless_the_aiohttp_transport_opts_out(
#[case] layer: HttpSettingsLayer,
#[case] expected: bool,
) {
assert_eq!(HttpSettings::from_layers([layer]).trust_proxy_env, expected);
}
#[test]
fn a_proxy_opt_out_in_one_source_still_yields_to_trust_env_from_another() {
let environment =
HttpSettingsLayer::from_environment(&env_of(&[("DISABLE_AIOHTTP_TRUST_ENV", "true")]));
let configured = HttpSettingsLayer {
aiohttp_trust_env: Some(true),
..HttpSettingsLayer::default()
};
assert!(!HttpSettings::from_layers([environment.clone()]).trust_proxy_env);
assert!(HttpSettings::from_layers([environment, configured]).trust_proxy_env);
}
#[test]
fn missing_files_fall_back_to_default_verification() {
let settings = HttpSettings {
@ -267,11 +392,14 @@ mod tests {
}
#[rstest]
#[case("true", true)]
#[case("True", true)]
#[case("false", false)]
#[case("1", false)]
fn boolean_switches_only_turn_on_for_true(#[case] value: &'static str, #[case] expected: bool) {
#[case("true", Some(true))]
#[case("True", Some(true))]
#[case("false", None)]
#[case("1", None)]
fn boolean_switches_only_turn_on_for_true(
#[case] value: &'static str,
#[case] expected: Option<bool>,
) {
let env = move |name: &str| match name {
"LITELLM_HTTP2"
| "AIOHTTP_TRUST_ENV"
@ -279,10 +407,10 @@ mod tests {
| "DISABLE_AIOHTTP_TRUST_ENV" => Some(value.to_string()),
_ => None,
};
let settings = HttpSettings::default().with_environment(&env);
assert_eq!(settings.http2, expected);
assert_eq!(settings.httpx_transport, expected);
assert_eq!(settings.trust_proxy_env, expected);
assert_eq!(settings.ignore_proxy_env, expected);
let layer = HttpSettingsLayer::from_environment(&env);
assert_eq!(layer.http2, expected);
assert_eq!(layer.aiohttp_trust_env, expected);
assert_eq!(layer.disable_aiohttp_transport, expected);
assert_eq!(layer.disable_aiohttp_trust_env, expected);
}
}

View file

@ -5,7 +5,8 @@ use std::{
};
use litellm_http::{
HttpClientConfig, HttpClientPool, HttpSettings, Resolution, SslVerify, Unsupported,
HttpClientConfig, HttpClientPool, HttpSettings, HttpSettingsLayer, Resolution, SslVerify,
Unsupported,
};
use litellm_llms::custom_httpx::media::{PublicDnsResolver, UrlPolicy};
use pyo3::{prelude::*, types::PyDict};
@ -26,10 +27,12 @@ pub(crate) fn call_config(
kwargs: &Bound<'_, PyDict>,
asynchronous: bool,
) -> PyResult<HttpClientConfig> {
let configured = settings(&PythonSettings::Http.read(py)?)?
.with_environment(&|name| std::env::var(name).ok());
let settings = for_call(configured, call_ssl_verify(kwargs)?, asynchronous)
.without_missing_files(&|path: &Path| path.exists());
let settings = HttpSettings::from_layers([
for_call(call_ssl_verify(kwargs)?, asynchronous),
HttpSettingsLayer::from_environment(&|name| std::env::var(name).ok()),
configured(&PythonSettings::Http.read(py)?)?,
])
.without_missing_files(&|path: &Path| path.exists());
let resolution = Resolution::from(&settings);
for unsupported in unreported(&REPORTED_UNSUPPORTED, resolution.unsupported) {
PythonSettings::warn(py, &unsupported.to_string())?;
@ -70,15 +73,11 @@ fn call_ssl_verify(kwargs: &Bound<'_, PyDict>) -> PyResult<Option<SslVerify>> {
.and_then(|value| ssl_verify(&value)))
}
fn for_call(
configured: HttpSettings,
call_ssl_verify: Option<SslVerify>,
asynchronous: bool,
) -> HttpSettings {
HttpSettings {
ssl_verify: call_ssl_verify.or(configured.ssl_verify),
httpx_transport: configured.httpx_transport || !asynchronous,
..configured
fn for_call(call_ssl_verify: Option<SslVerify>, asynchronous: bool) -> HttpSettingsLayer {
HttpSettingsLayer {
ssl_verify: call_ssl_verify,
disable_aiohttp_transport: (!asynchronous).then_some(true),
..HttpSettingsLayer::default()
}
}
@ -102,24 +101,24 @@ struct PythonHttpSettings<'py> {
user_agent: String,
}
fn settings(value: &Bound<'_, PyAny>) -> PyResult<HttpSettings> {
fn configured(value: &Bound<'_, PyAny>) -> PyResult<HttpSettingsLayer> {
let python: PythonHttpSettings = value.extract().map_err(|error: PyErr| {
RustBridgeDeclined::new_err(format!(
"litellm HTTP settings cannot be used by the Rust route: {error}"
))
})?;
Ok(HttpSettings {
Ok(HttpSettingsLayer {
ssl_verify: ssl_verify(&python.ssl_verify),
ssl_certificate: python.ssl_certificate.map(PathBuf::from),
ssl_security_level: python.ssl_security_level,
ssl_ecdh_curve: python.ssl_ecdh_curve,
force_ipv4: python.force_ipv4,
http2: python.http2,
httpx_transport: python.disable_aiohttp_transport,
force_ipv4: Some(python.force_ipv4),
http2: Some(python.http2),
aiohttp_trust_env: Some(python.aiohttp_trust_env),
disable_aiohttp_trust_env: Some(python.disable_aiohttp_trust_env),
disable_aiohttp_transport: Some(python.disable_aiohttp_transport),
user_agent: Some(python.user_agent),
trust_proxy_env: python.aiohttp_trust_env,
ignore_proxy_env: python.disable_aiohttp_trust_env,
..HttpSettings::default()
..HttpSettingsLayer::default()
})
}
@ -174,12 +173,12 @@ settings = types.SimpleNamespace(**{{name: defaults[name] for name in json.loads
}
#[test]
fn default_python_settings_produce_default_settings_with_verification_on() {
fn default_python_settings_resolve_to_default_settings_with_verification_on() {
Python::initialize();
Python::attach(|py| {
let settings = settings(&python_settings(py, "")).unwrap();
let layer = configured(&python_settings(py, "")).unwrap();
assert_eq!(
settings,
HttpSettings::from_layers([layer]),
HttpSettings {
ssl_verify: Some(SslVerify::Enabled),
user_agent: Some("litellm/test".into()),
@ -190,10 +189,10 @@ settings = types.SimpleNamespace(**{{name: defaults[name] for name in json.loads
}
#[test]
fn python_settings_flow_into_settings() {
fn python_settings_flow_into_the_configured_layer() {
Python::initialize();
Python::attach(|py| {
let settings = settings(&python_settings(
let layer = configured(&python_settings(
py,
"
ssl_verify='/etc/ssl/corp.pem',
@ -210,19 +209,19 @@ user_agent='litellm/9.9.9',
))
.unwrap();
assert_eq!(
settings,
HttpSettings {
layer,
HttpSettingsLayer {
ssl_verify: Some(SslVerify::CaBundle("/etc/ssl/corp.pem".into())),
ssl_certificate: Some("/etc/ssl/client.pem".into()),
ssl_security_level: Some("2".into()),
ssl_ecdh_curve: Some("X25519".into()),
force_ipv4: true,
http2: true,
httpx_transport: true,
force_ipv4: Some(true),
http2: Some(true),
aiohttp_trust_env: Some(true),
disable_aiohttp_trust_env: Some(true),
disable_aiohttp_transport: Some(true),
user_agent: Some("litellm/9.9.9".into()),
trust_proxy_env: true,
ignore_proxy_env: true,
..HttpSettings::default()
..HttpSettingsLayer::default()
}
);
});
@ -232,11 +231,12 @@ user_agent='litellm/9.9.9',
fn user_agent_environment_variable_beats_the_python_default() {
Python::initialize();
Python::attach(|py| {
let settings = settings(&python_settings(py, ""))
.unwrap()
.with_environment(&|name| {
let settings = HttpSettings::from_layers([
HttpSettingsLayer::from_environment(&|name| {
(name == "LITELLM_USER_AGENT").then(|| "operator/1".to_string())
});
}),
configured(&python_settings(py, "")).unwrap(),
]);
assert_eq!(settings.user_agent.as_deref(), Some("operator/1"));
});
}
@ -252,8 +252,8 @@ user_agent='litellm/9.9.9',
) {
Python::initialize();
Python::attach(|py| {
let settings = settings(&python_settings(py, overrides)).unwrap();
let config = Resolution::from(&settings).config;
let layer = configured(&python_settings(py, overrides)).unwrap();
let config = Resolution::from(&HttpSettings::from_layers([layer])).config;
assert_eq!(config.verify, expected);
});
}
@ -262,8 +262,8 @@ user_agent='litellm/9.9.9',
fn ssl_context_global_is_ignored_so_environment_and_defaults_apply() {
Python::initialize();
Python::attach(|py| {
let settings = settings(&python_settings(py, "ssl_verify=object()")).unwrap();
assert_eq!(settings.ssl_verify, None);
let layer = configured(&python_settings(py, "ssl_verify=object()")).unwrap();
assert_eq!(layer.ssl_verify, None);
});
}
@ -283,22 +283,27 @@ user_agent='litellm/9.9.9',
fn mistyped_python_settings_decline_instead_of_raising() {
Python::initialize();
Python::attach(|py| {
let error = settings(&python_settings(py, "force_ipv4='yes'")).unwrap_err();
let error = configured(&python_settings(py, "force_ipv4='yes'")).unwrap_err();
assert!(error.is_instance_of::<RustBridgeDeclined>(py));
});
}
fn configured_ssl_verify(ssl_verify: SslVerify) -> HttpSettingsLayer {
HttpSettingsLayer {
ssl_verify: Some(ssl_verify),
..HttpSettingsLayer::default()
}
}
#[test]
fn call_ssl_verify_beats_the_configured_and_environment_value() {
fn call_ssl_verify_beats_the_configured_value() {
Python::initialize();
Python::attach(|py| {
let kwargs = PyDict::new(py);
kwargs.set_item("ssl_verify", false).unwrap();
let configured = HttpSettings {
ssl_verify: Some(SslVerify::Enabled),
..HttpSettings::default()
};
let settings = for_call(configured, call_ssl_verify(&kwargs).unwrap(), true);
let call = for_call(call_ssl_verify(&kwargs).unwrap(), true);
let settings =
HttpSettings::from_layers([call, configured_ssl_verify(SslVerify::Enabled)]);
assert_eq!(settings.ssl_verify, Some(SslVerify::Disabled));
});
}
@ -309,12 +314,10 @@ user_agent='litellm/9.9.9',
Python::attach(|py| {
let kwargs = PyDict::new(py);
kwargs.set_item("ssl_verify", py.None()).unwrap();
let configured = HttpSettings {
ssl_verify: Some(SslVerify::Disabled),
..HttpSettings::default()
};
let settings = for_call(configured.clone(), call_ssl_verify(&kwargs).unwrap(), true);
assert_eq!(settings, configured);
let call = for_call(call_ssl_verify(&kwargs).unwrap(), true);
let settings =
HttpSettings::from_layers([call, configured_ssl_verify(SslVerify::Disabled)]);
assert_eq!(settings.ssl_verify, Some(SslVerify::Disabled));
});
}
@ -326,12 +329,10 @@ user_agent='litellm/9.9.9',
kwargs
.set_item("ssl_verify", py.eval(c"object()", None, None).unwrap())
.unwrap();
let configured = HttpSettings {
ssl_verify: Some(SslVerify::Disabled),
..HttpSettings::default()
};
let settings = for_call(configured.clone(), call_ssl_verify(&kwargs).unwrap(), true);
assert_eq!(settings, configured);
let call = for_call(call_ssl_verify(&kwargs).unwrap(), true);
let settings =
HttpSettings::from_layers([call, configured_ssl_verify(SslVerify::Disabled)]);
assert_eq!(settings.ssl_verify, Some(SslVerify::Disabled));
});
}
@ -342,12 +343,12 @@ user_agent='litellm/9.9.9',
#[case] asynchronous: bool,
#[case] expected: bool,
) {
let opted_out = HttpSettings {
ignore_proxy_env: true,
..HttpSettings::default()
let opted_out = HttpSettingsLayer {
disable_aiohttp_trust_env: Some(true),
disable_aiohttp_transport: Some(false),
..HttpSettingsLayer::default()
};
let settings = for_call(opted_out, None, asynchronous);
let config = Resolution::from(&settings).config;
assert_eq!(config.trust_proxy_env, expected);
let settings = HttpSettings::from_layers([for_call(None, asynchronous), opted_out]);
assert_eq!(settings.trust_proxy_env, expected);
}
}