mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
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:
parent
80dbb2a28a
commit
bae4f22d3a
4 changed files with 278 additions and 188 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue