feat: b6-openai-models — batch 6

This commit is contained in:
YeonGyu-Kim
2026-04-07 15:52:11 +09:00
parent 18d3c1918b
commit 1f968b359f
2 changed files with 268 additions and 3 deletions

View File

@@ -7,11 +7,17 @@ const NO_PROXY_KEYS: [&str; 2] = ["NO_PROXY", "no_proxy"];
/// Snapshot of the proxy-related environment variables that influence the
/// outbound HTTP client. Captured up front so callers can inspect, log, and
/// test the resolved configuration without re-reading the process environment.
///
/// When `proxy_url` is set it acts as a single catch-all proxy for both
/// HTTP and HTTPS traffic, taking precedence over the per-scheme fields.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ProxyConfig {
pub http_proxy: Option<String>,
pub https_proxy: Option<String>,
pub no_proxy: Option<String>,
/// Optional unified proxy URL that applies to both HTTP and HTTPS.
/// When set, this takes precedence over `http_proxy` and `https_proxy`.
pub proxy_url: Option<String>,
}
impl ProxyConfig {
@@ -22,6 +28,17 @@ impl ProxyConfig {
Self::from_lookup(|key| std::env::var(key).ok())
}
/// Create a proxy configuration from a single URL that applies to both
/// HTTP and HTTPS traffic. This is the config-file alternative to setting
/// `HTTP_PROXY` and `HTTPS_PROXY` environment variables separately.
#[must_use]
pub fn from_proxy_url(url: impl Into<String>) -> Self {
Self {
proxy_url: Some(url.into()),
..Self::default()
}
}
fn from_lookup<F>(mut lookup: F) -> Self
where
F: FnMut(&str) -> Option<String>,
@@ -30,12 +47,13 @@ impl ProxyConfig {
http_proxy: first_non_empty(&HTTP_PROXY_KEYS, &mut lookup),
https_proxy: first_non_empty(&HTTPS_PROXY_KEYS, &mut lookup),
no_proxy: first_non_empty(&NO_PROXY_KEYS, &mut lookup),
proxy_url: None,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.http_proxy.is_none() && self.https_proxy.is_none()
self.proxy_url.is_none() && self.http_proxy.is_none() && self.https_proxy.is_none()
}
}
@@ -58,6 +76,10 @@ pub fn build_http_client_or_default() -> reqwest::Client {
/// Build a `reqwest::Client` from an explicit [`ProxyConfig`]. Used by tests
/// and by callers that want to override process-level environment lookups.
///
/// When `config.proxy_url` is set it overrides the per-scheme `http_proxy`
/// and `https_proxy` fields and is registered as both an HTTP and HTTPS
/// proxy so a single value can route every outbound request.
pub fn build_http_client_with(config: &ProxyConfig) -> Result<reqwest::Client, ApiError> {
let mut builder = reqwest::Client::builder().no_proxy();
@@ -66,7 +88,12 @@ pub fn build_http_client_with(config: &ProxyConfig) -> Result<reqwest::Client, A
.as_deref()
.and_then(reqwest::NoProxy::from_string);
if let Some(url) = config.https_proxy.as_deref() {
let (http_proxy_url, https_proxy_url) = match config.proxy_url.as_deref() {
Some(unified) => (Some(unified), Some(unified)),
None => (config.http_proxy.as_deref(), config.https_proxy.as_deref()),
};
if let Some(url) = https_proxy_url {
let mut proxy = reqwest::Proxy::https(url)?;
if let Some(filter) = no_proxy.clone() {
proxy = proxy.no_proxy(Some(filter));
@@ -74,7 +101,7 @@ pub fn build_http_client_with(config: &ProxyConfig) -> Result<reqwest::Client, A
builder = builder.proxy(proxy);
}
if let Some(url) = config.http_proxy.as_deref() {
if let Some(url) = http_proxy_url {
let mut proxy = reqwest::Proxy::http(url)?;
if let Some(filter) = no_proxy.clone() {
proxy = proxy.no_proxy(Some(filter));
@@ -221,6 +248,7 @@ mod tests {
http_proxy: Some("http://proxy.internal:3128".to_string()),
https_proxy: Some("http://secure.internal:3129".to_string()),
no_proxy: Some("localhost,127.0.0.1".to_string()),
proxy_url: None,
};
// when
@@ -237,6 +265,7 @@ mod tests {
http_proxy: None,
https_proxy: Some("not a url".to_string()),
no_proxy: None,
proxy_url: None,
};
// when
@@ -249,4 +278,67 @@ mod tests {
"expected ApiError::Http for invalid proxy URL, got: {error:?}"
);
}
#[test]
fn from_proxy_url_sets_unified_field_and_leaves_per_scheme_empty() {
// given / when
let config = ProxyConfig::from_proxy_url("http://unified.internal:3128");
// then
assert_eq!(
config.proxy_url.as_deref(),
Some("http://unified.internal:3128")
);
assert!(config.http_proxy.is_none());
assert!(config.https_proxy.is_none());
assert!(!config.is_empty());
}
#[test]
fn build_http_client_succeeds_with_unified_proxy_url() {
// given
let config = ProxyConfig {
proxy_url: Some("http://unified.internal:3128".to_string()),
no_proxy: Some("localhost".to_string()),
..ProxyConfig::default()
};
// when
let result = build_http_client_with(&config);
// then
assert!(result.is_ok());
}
#[test]
fn proxy_url_takes_precedence_over_per_scheme_fields() {
// given both per-scheme and unified are set
let config = ProxyConfig {
http_proxy: Some("http://per-scheme.internal:1111".to_string()),
https_proxy: Some("http://per-scheme.internal:2222".to_string()),
no_proxy: None,
proxy_url: Some("http://unified.internal:3128".to_string()),
};
// when building succeeds (the unified URL is valid)
let result = build_http_client_with(&config);
// then
assert!(result.is_ok());
}
#[test]
fn build_http_client_returns_error_for_invalid_unified_proxy_url() {
// given
let config = ProxyConfig::from_proxy_url("not a url");
// when
let result = build_http_client_with(&config);
// then
assert!(
matches!(result, Err(crate::error::ApiError::Http(_))),
"invalid unified proxy URL should fail: {result:?}"
);
}
}