|
11 | 11 | - Circuit Breaker integration for provider resilience |
12 | 12 | - Per-provider health tracking |
13 | 13 | - Automatic failure detection and recovery |
| 14 | +
|
| 15 | +NEW in v2.7 (Dec 5, 2025): |
| 16 | + - HttpConfig for enterprise SSL/proxy support |
| 17 | + - Auto-detect HTTPS_PROXY, HTTP_PROXY, SSL_CERT_FILE from environment |
| 18 | + - Custom CA bundle support for corporate environments |
14 | 19 | """ |
15 | 20 |
|
16 | 21 | import asyncio |
17 | 22 | import logging |
18 | 23 | import math |
| 24 | +import os |
19 | 25 | import random |
| 26 | +import warnings |
20 | 27 | from abc import ABC, abstractmethod |
21 | 28 | from collections.abc import AsyncIterator |
22 | 29 | from dataclasses import dataclass, field |
23 | 30 | from enum import Enum |
24 | | -from typing import TYPE_CHECKING, Any, Optional |
| 31 | +from typing import TYPE_CHECKING, Any, Optional, Union |
25 | 32 |
|
26 | 33 | if TYPE_CHECKING: |
27 | 34 | from cascadeflow.resilience import CircuitBreaker |
@@ -110,6 +117,155 @@ def get_summary(self) -> dict[str, Any]: |
110 | 117 | } |
111 | 118 |
|
112 | 119 |
|
| 120 | +# ============================================================================ |
| 121 | +# HTTP CONFIGURATION FOR ENTERPRISE (SSL, PROXY) |
| 122 | +# ============================================================================ |
| 123 | + |
| 124 | + |
| 125 | +@dataclass |
| 126 | +class HttpConfig: |
| 127 | + """ |
| 128 | + HTTP configuration for enterprise environments. |
| 129 | +
|
| 130 | + Supports SSL certificate verification, custom CA bundles, and proxy settings. |
| 131 | + Auto-detects from standard environment variables when using from_env(). |
| 132 | +
|
| 133 | + Environment Variables (auto-detected): |
| 134 | + - HTTPS_PROXY, HTTP_PROXY: Proxy URL |
| 135 | + - NO_PROXY: Comma-separated list of hosts to bypass proxy |
| 136 | + - SSL_CERT_FILE, REQUESTS_CA_BUNDLE: Path to CA bundle file |
| 137 | + - CURL_CA_BUNDLE: Alternative CA bundle path |
| 138 | +
|
| 139 | + Attributes: |
| 140 | + verify: SSL verification setting: |
| 141 | + - True (default): Use system CA bundle |
| 142 | + - False: Disable SSL verification (WARNING: insecure!) |
| 143 | + - str: Path to custom CA bundle file |
| 144 | + proxy: Proxy URL (e.g., "http://proxy.corp.com:8080") |
| 145 | + timeout: Request timeout in seconds |
| 146 | + no_proxy: Comma-separated list of hosts to bypass proxy |
| 147 | +
|
| 148 | + Examples: |
| 149 | + # Default: Auto-detect from environment |
| 150 | + config = HttpConfig.from_env() |
| 151 | +
|
| 152 | + # Custom CA bundle (enterprise) |
| 153 | + config = HttpConfig(verify="/path/to/corporate-ca.pem") |
| 154 | +
|
| 155 | + # Explicit proxy |
| 156 | + config = HttpConfig( |
| 157 | + verify=True, |
| 158 | + proxy="http://proxy.corp.com:8080" |
| 159 | + ) |
| 160 | +
|
| 161 | + # Disable SSL verification (development only!) |
| 162 | + config = HttpConfig(verify=False) # Emits warning |
| 163 | + """ |
| 164 | + |
| 165 | + verify: Union[bool, str] = True |
| 166 | + proxy: Optional[str] = None |
| 167 | + timeout: float = 60.0 |
| 168 | + no_proxy: Optional[str] = None |
| 169 | + |
| 170 | + def __post_init__(self): |
| 171 | + """Emit warning if SSL verification is disabled.""" |
| 172 | + if self.verify is False: |
| 173 | + warnings.warn( |
| 174 | + "SSL verification is DISABLED. This is insecure and should only be used " |
| 175 | + "for development/testing. Set verify=True or provide a CA bundle path " |
| 176 | + "for production use.", |
| 177 | + UserWarning, |
| 178 | + stacklevel=3, |
| 179 | + ) |
| 180 | + logger.warning( |
| 181 | + "HttpConfig: SSL verification disabled. This is insecure! " |
| 182 | + "Only use for development/testing." |
| 183 | + ) |
| 184 | + |
| 185 | + @classmethod |
| 186 | + def from_env(cls) -> "HttpConfig": |
| 187 | + """ |
| 188 | + Create HttpConfig from environment variables. |
| 189 | +
|
| 190 | + Auto-detects: |
| 191 | + - HTTPS_PROXY or HTTP_PROXY for proxy settings |
| 192 | + - NO_PROXY for proxy bypass |
| 193 | + - SSL_CERT_FILE, REQUESTS_CA_BUNDLE, or CURL_CA_BUNDLE for CA bundle |
| 194 | +
|
| 195 | + Returns: |
| 196 | + HttpConfig with settings from environment |
| 197 | +
|
| 198 | + Example: |
| 199 | + # In shell: |
| 200 | + export HTTPS_PROXY=http://proxy.corp.com:8080 |
| 201 | + export SSL_CERT_FILE=/path/to/ca-bundle.crt |
| 202 | +
|
| 203 | + # In Python: |
| 204 | + config = HttpConfig.from_env() |
| 205 | + # config.proxy = "http://proxy.corp.com:8080" |
| 206 | + # config.verify = "/path/to/ca-bundle.crt" |
| 207 | + """ |
| 208 | + # Detect proxy from environment (HTTPS_PROXY takes priority) |
| 209 | + proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY") |
| 210 | + if not proxy: |
| 211 | + # Also check lowercase variants |
| 212 | + proxy = os.environ.get("https_proxy") or os.environ.get("http_proxy") |
| 213 | + |
| 214 | + # Detect no_proxy |
| 215 | + no_proxy = os.environ.get("NO_PROXY") or os.environ.get("no_proxy") |
| 216 | + |
| 217 | + # Detect CA bundle from environment |
| 218 | + # Check multiple environment variables in priority order |
| 219 | + ca_bundle = ( |
| 220 | + os.environ.get("SSL_CERT_FILE") |
| 221 | + or os.environ.get("REQUESTS_CA_BUNDLE") |
| 222 | + or os.environ.get("CURL_CA_BUNDLE") |
| 223 | + ) |
| 224 | + |
| 225 | + # If CA bundle specified and file exists, use it; otherwise use system default |
| 226 | + verify: Union[bool, str] = True |
| 227 | + if ca_bundle: |
| 228 | + if os.path.isfile(ca_bundle): |
| 229 | + verify = ca_bundle |
| 230 | + logger.info(f"HttpConfig: Using CA bundle from environment: {ca_bundle}") |
| 231 | + else: |
| 232 | + logger.warning( |
| 233 | + f"HttpConfig: CA bundle path from environment does not exist: {ca_bundle}. " |
| 234 | + f"Using system default." |
| 235 | + ) |
| 236 | + |
| 237 | + if proxy: |
| 238 | + logger.info(f"HttpConfig: Using proxy from environment: {proxy}") |
| 239 | + |
| 240 | + return cls( |
| 241 | + verify=verify, |
| 242 | + proxy=proxy, |
| 243 | + no_proxy=no_proxy, |
| 244 | + ) |
| 245 | + |
| 246 | + def get_httpx_kwargs(self) -> dict[str, Any]: |
| 247 | + """ |
| 248 | + Get kwargs for httpx.AsyncClient initialization. |
| 249 | +
|
| 250 | + Returns: |
| 251 | + Dictionary of kwargs for httpx.AsyncClient |
| 252 | +
|
| 253 | + Example: |
| 254 | + config = HttpConfig.from_env() |
| 255 | + client = httpx.AsyncClient(**config.get_httpx_kwargs()) |
| 256 | + """ |
| 257 | + kwargs: dict[str, Any] = { |
| 258 | + "verify": self.verify, |
| 259 | + "timeout": self.timeout, |
| 260 | + } |
| 261 | + |
| 262 | + if self.proxy: |
| 263 | + # httpx uses 'proxy' for single proxy URL |
| 264 | + kwargs["proxy"] = self.proxy |
| 265 | + |
| 266 | + return kwargs |
| 267 | + |
| 268 | + |
113 | 269 | # ============================================================================ |
114 | 270 | # MODEL RESPONSE |
115 | 271 | # ============================================================================ |
@@ -208,21 +364,44 @@ def __init__( |
208 | 364 | api_key: Optional[str] = None, |
209 | 365 | retry_config: Optional[RetryConfig] = None, |
210 | 366 | enable_circuit_breaker: bool = True, |
| 367 | + http_config: Optional[HttpConfig] = None, |
211 | 368 | ): |
212 | 369 | """ |
213 | | - Initialize provider with retry logic and circuit breaker. |
| 370 | + Initialize provider with retry logic, circuit breaker, and HTTP config. |
214 | 371 |
|
215 | 372 | Args: |
216 | 373 | api_key: API key for the provider (if needed) |
217 | 374 | retry_config: Custom retry configuration (optional) |
218 | 375 | enable_circuit_breaker: Enable circuit breaker for resilience (default: True) |
| 376 | + http_config: HTTP configuration for SSL/proxy (default: auto-detect from env) |
| 377 | +
|
| 378 | + Example: |
| 379 | + # Auto-detect proxy and SSL from environment |
| 380 | + provider = OpenAIProvider() # Uses HttpConfig.from_env() |
| 381 | +
|
| 382 | + # Custom CA bundle for corporate environment |
| 383 | + provider = OpenAIProvider( |
| 384 | + http_config=HttpConfig(verify="/path/to/corp-ca.pem") |
| 385 | + ) |
| 386 | +
|
| 387 | + # Explicit proxy configuration |
| 388 | + provider = OpenAIProvider( |
| 389 | + http_config=HttpConfig( |
| 390 | + proxy="http://proxy.corp.com:8080", |
| 391 | + verify=True |
| 392 | + ) |
| 393 | + ) |
219 | 394 | """ |
220 | 395 | # Load API key from parameter or environment |
221 | 396 | if api_key: |
222 | 397 | self.api_key = api_key |
223 | 398 | else: |
224 | 399 | self.api_key = self._load_api_key() |
225 | 400 |
|
| 401 | + # HTTP configuration for enterprise (SSL/proxy) |
| 402 | + # Auto-detect from environment if not provided |
| 403 | + self.http_config = http_config or HttpConfig.from_env() |
| 404 | + |
226 | 405 | # Circuit breaker integration |
227 | 406 | self._enable_circuit_breaker = enable_circuit_breaker |
228 | 407 | self._circuit_breaker: Optional[CircuitBreaker] = None |
|
0 commit comments