mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
reqwest and hyper each read HTTP(S)_PROXY, ALL_PROXY and NO_PROXY from the process on their own, so tests could not inject them and the pooled client key ignored proxy changes. EnvironmentProxies now reads them through Lookup with the same precedence hyper used, the resolved config carries them (empty when the transport does not trust the env), and both the provider clients and the media fetcher build from that one value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
419 lines
16 KiB
Rust
419 lines
16 KiB
Rust
use std::{
|
|
path::{Path, PathBuf},
|
|
time::Duration,
|
|
};
|
|
|
|
use litellm_core_utils::settings::{Layer, Lookup, merge};
|
|
|
|
use crate::proxy::EnvironmentProxies;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub enum SslVerify {
|
|
Enabled,
|
|
Disabled,
|
|
CaBundle(PathBuf),
|
|
}
|
|
|
|
impl SslVerify {
|
|
pub fn parse(value: &str) -> Self {
|
|
match value.trim().to_ascii_lowercase().as_str() {
|
|
"true" => Self::Enabled,
|
|
"false" => Self::Disabled,
|
|
_ => Self::CaBundle(PathBuf::from(value)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub struct TcpKeepalive {
|
|
pub idle: Duration,
|
|
pub interval: Duration,
|
|
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>,
|
|
pub proxies: Option<EnvironmentProxies>,
|
|
}
|
|
|
|
impl HttpSettingsLayer {
|
|
pub fn from_environment(env: &impl Lookup) -> Self {
|
|
let seconds = |name: &str, default: u32| {
|
|
Duration::from_secs(u64::from(env.parsed::<u32>(name).unwrap_or(default)))
|
|
};
|
|
Self {
|
|
ssl_verify: env.get("SSL_VERIFY").map(|value| SslVerify::parse(&value)),
|
|
ssl_cert_file: env.truthy("SSL_CERT_FILE").map(PathBuf::from),
|
|
ssl_certificate: env.get("SSL_CERTIFICATE").map(PathBuf::from),
|
|
ssl_security_level: env.get("SSL_SECURITY_LEVEL"),
|
|
ssl_ecdh_curve: env.get("SSL_ECDH_CURVE"),
|
|
force_ipv4: None,
|
|
http2: env.enabled("LITELLM_HTTP2"),
|
|
aiohttp_trust_env: env.enabled("AIOHTTP_TRUST_ENV"),
|
|
disable_aiohttp_trust_env: env.enabled("DISABLE_AIOHTTP_TRUST_ENV"),
|
|
disable_aiohttp_transport: env.enabled("DISABLE_AIOHTTP_TRANSPORT"),
|
|
user_agent: env.get("LITELLM_USER_AGENT"),
|
|
tcp_keepalive: env.enabled("AIOHTTP_SO_KEEPALIVE").map(|_| TcpKeepalive {
|
|
idle: seconds("AIOHTTP_TCP_KEEPIDLE", 60),
|
|
interval: seconds("AIOHTTP_TCP_KEEPINTVL", 30),
|
|
retries: env.parsed("AIOHTTP_TCP_KEEPCNT").unwrap_or(5),
|
|
}),
|
|
pool_idle_timeout: env
|
|
.parsed::<u32>("AIOHTTP_KEEPALIVE_TIMEOUT")
|
|
.map(|timeout| Duration::from_secs(u64::from(timeout))),
|
|
proxies: Some(EnvironmentProxies::from_environment(env))
|
|
.filter(|proxies| *proxies != EnvironmentProxies::default()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Layer for HttpSettingsLayer {
|
|
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),
|
|
proxies: self.proxies.or(lower.proxies),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct HttpSettings {
|
|
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: bool,
|
|
pub http2: bool,
|
|
pub user_agent: Option<String>,
|
|
pub trust_proxy_env: bool,
|
|
pub proxies: EnvironmentProxies,
|
|
pub connect_timeout: Duration,
|
|
pub tcp_keepalive: Option<TcpKeepalive>,
|
|
pub pool_idle_timeout: Duration,
|
|
}
|
|
|
|
impl Default for HttpSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
ssl_verify: None,
|
|
ssl_cert_file: None,
|
|
ssl_certificate: None,
|
|
ssl_security_level: None,
|
|
ssl_ecdh_curve: None,
|
|
force_ipv4: false,
|
|
http2: false,
|
|
user_agent: None,
|
|
trust_proxy_env: true,
|
|
proxies: EnvironmentProxies::default(),
|
|
connect_timeout: Duration::from_secs(10),
|
|
tcp_keepalive: None,
|
|
pool_idle_timeout: Duration::from_secs(120),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HttpSettings {
|
|
pub fn from_layers(
|
|
highest_precedence_first: impl IntoIterator<Item = HttpSettingsLayer>,
|
|
) -> Self {
|
|
let merged = merge(highest_precedence_first);
|
|
let defaults = Self::default();
|
|
let http2 = merged.http2.unwrap_or(defaults.http2);
|
|
Self {
|
|
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: 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),
|
|
proxies: merged.proxies.unwrap_or_default(),
|
|
..defaults
|
|
}
|
|
}
|
|
|
|
pub fn without_missing_files(self, exists: &dyn Fn(&Path) -> bool) -> Self {
|
|
Self {
|
|
ssl_verify: match self.ssl_verify {
|
|
Some(SslVerify::CaBundle(path)) if !exists(&path) => Some(SslVerify::Enabled),
|
|
other => other,
|
|
},
|
|
ssl_cert_file: self.ssl_cert_file.filter(|path| exists(path)),
|
|
..self
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use rstest::rstest;
|
|
|
|
use super::*;
|
|
|
|
fn no_env(_: &str) -> Option<String> {
|
|
None
|
|
}
|
|
|
|
fn env_of(values: &'static [(&'static str, &'static str)]) -> impl Fn(&str) -> Option<String> {
|
|
move |name| {
|
|
values
|
|
.iter()
|
|
.find(|(key, _)| *key == name)
|
|
.map(|(_, value)| value.to_string())
|
|
}
|
|
}
|
|
|
|
#[rstest]
|
|
#[case("true", SslVerify::Enabled)]
|
|
#[case(" True ", SslVerify::Enabled)]
|
|
#[case("FALSE", SslVerify::Disabled)]
|
|
#[case("/etc/ssl/bundle.pem", SslVerify::CaBundle("/etc/ssl/bundle.pem".into()))]
|
|
fn ssl_verify_parses_bools_and_treats_anything_else_as_a_bundle_path(
|
|
#[case] value: &str,
|
|
#[case] expected: SslVerify,
|
|
) {
|
|
assert_eq!(SslVerify::parse(value), expected);
|
|
}
|
|
|
|
#[test]
|
|
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()),
|
|
..HttpSettingsLayer::default()
|
|
};
|
|
let environment = HttpSettingsLayer::from_environment(&env_of(&[
|
|
("SSL_VERIFY", "false"),
|
|
("SSL_CERT_FILE", "/env/roots.pem"),
|
|
("SSL_CERTIFICATE", "/env/client.pem"),
|
|
("SSL_SECURITY_LEVEL", "DEFAULT@SECLEVEL=1"),
|
|
("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()));
|
|
assert_eq!(
|
|
settings.ssl_security_level.as_deref(),
|
|
Some("DEFAULT@SECLEVEL=1")
|
|
);
|
|
assert_eq!(settings.ssl_ecdh_curve.as_deref(), Some("X25519"));
|
|
assert_eq!(settings.user_agent.as_deref(), Some("env/2"));
|
|
}
|
|
|
|
#[test]
|
|
fn an_explicit_false_in_a_higher_layer_beats_a_lower_true() {
|
|
let higher = HttpSettingsLayer {
|
|
http2: Some(false),
|
|
force_ipv4: Some(false),
|
|
..HttpSettingsLayer::default()
|
|
};
|
|
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 configured = HttpSettingsLayer {
|
|
ssl_certificate: Some("/configured/client.pem".into()),
|
|
ssl_security_level: Some("configured".into()),
|
|
ssl_ecdh_curve: Some("X25519".into()),
|
|
..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);
|
|
}
|
|
|
|
#[test]
|
|
fn socket_keepalive_follows_the_aiohttp_variables_with_python_defaults() {
|
|
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 {
|
|
idle: Duration::from_secs(45),
|
|
interval: Duration::from_secs(30),
|
|
retries: 5,
|
|
})
|
|
);
|
|
assert_eq!(tuned.pool_idle_timeout, Duration::from_secs(30));
|
|
}
|
|
|
|
#[test]
|
|
fn socket_keepalive_stays_off_unless_enabled() {
|
|
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 {
|
|
ssl_verify: Some(SslVerify::CaBundle("/absent/roots.pem".into())),
|
|
ssl_cert_file: Some("/absent/env.pem".into()),
|
|
..HttpSettings::default()
|
|
}
|
|
.without_missing_files(&|_| false);
|
|
assert_eq!(settings.ssl_verify, Some(SslVerify::Enabled));
|
|
assert_eq!(settings.ssl_cert_file, None);
|
|
}
|
|
|
|
#[test]
|
|
fn existing_files_are_kept() {
|
|
let settings = HttpSettings {
|
|
ssl_verify: Some(SslVerify::CaBundle("/present/roots.pem".into())),
|
|
ssl_cert_file: Some("/present/env.pem".into()),
|
|
..HttpSettings::default()
|
|
};
|
|
assert_eq!(settings.clone().without_missing_files(&|_| true), settings);
|
|
}
|
|
|
|
#[rstest]
|
|
#[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"
|
|
| "DISABLE_AIOHTTP_TRANSPORT"
|
|
| "DISABLE_AIOHTTP_TRUST_ENV" => Some(value.to_string()),
|
|
_ => None,
|
|
};
|
|
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);
|
|
}
|
|
}
|