Skip to content

Commit 25fd5fa

Browse files
committed
feat(python): add enterprise HTTP configuration support
Implement HttpConfig dataclass for enterprise SSL/proxy configuration: - Add HttpConfig with verify, proxy, timeout, no_proxy attributes - Implement from_env() for auto-detection from environment variables - Support HTTPS_PROXY, SSL_CERT_FILE, REQUESTS_CA_BUNDLE, NO_PROXY - Add get_httpx_kwargs() for seamless httpx integration - Update all 8 Python providers to accept http_config parameter - Include security warnings when SSL verification is disabled Enables zero-config enterprise deployments via environment variables.
1 parent 19576f9 commit 25fd5fa

10 files changed

Lines changed: 390 additions & 51 deletions

File tree

cascadeflow/providers/anthropic.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import httpx
1010

1111
from ..exceptions import ModelError, ProviderError
12-
from .base import BaseProvider, ModelResponse, RetryConfig
12+
from .base import BaseProvider, HttpConfig, ModelResponse, RetryConfig
1313

1414
# ==============================================================================
1515
# REASONING MODEL SUPPORT
@@ -170,19 +170,43 @@ class AnthropicProvider(BaseProvider):
170170
... print(f"Args: {tool_call['arguments']}")
171171
"""
172172

173-
def __init__(self, api_key: Optional[str] = None, retry_config: Optional[RetryConfig] = None):
173+
def __init__(
174+
self,
175+
api_key: Optional[str] = None,
176+
retry_config: Optional[RetryConfig] = None,
177+
http_config: Optional[HttpConfig] = None,
178+
):
174179
"""
175-
Initialize Anthropic provider with automatic retry logic.
180+
Initialize Anthropic provider with automatic retry logic and enterprise HTTP support.
176181
177182
Args:
178183
api_key: Anthropic API key. If None, reads from ANTHROPIC_API_KEY env var.
179184
retry_config: Custom retry configuration (optional). If None, uses defaults:
180185
- max_attempts: 3
181186
- initial_delay: 1.0s
182187
- rate_limit_backoff: 30.0s
188+
http_config: HTTP configuration for SSL/proxy (default: auto-detect from env).
189+
Supports:
190+
- Custom CA bundles (SSL_CERT_FILE, REQUESTS_CA_BUNDLE)
191+
- Proxy servers (HTTPS_PROXY, HTTP_PROXY)
192+
- SSL verification control
193+
194+
Example:
195+
# Auto-detect from environment (default)
196+
provider = AnthropicProvider()
197+
198+
# Corporate environment with custom CA bundle
199+
provider = AnthropicProvider(
200+
http_config=HttpConfig(verify="/path/to/corporate-ca.pem")
201+
)
202+
203+
# With proxy
204+
provider = AnthropicProvider(
205+
http_config=HttpConfig(proxy="http://proxy.corp.com:8080")
206+
)
183207
"""
184-
# Call parent init to load API key, check logprobs support, and setup retry
185-
super().__init__(api_key=api_key, retry_config=retry_config)
208+
# Call parent init to load API key, check logprobs support, setup retry, and http_config
209+
super().__init__(api_key=api_key, retry_config=retry_config, http_config=http_config)
186210

187211
# Verify API key is set
188212
if not self.api_key:
@@ -191,16 +215,20 @@ def __init__(self, api_key: Optional[str] = None, retry_config: Optional[RetryCo
191215
"variable or pass api_key parameter."
192216
)
193217

194-
# Initialize HTTP client with the loaded API key
218+
# Initialize HTTP client with the loaded API key and HTTP config
195219
self.base_url = "https://api.anthropic.com/v1"
196220
self.api_version = "2023-06-01"
221+
222+
# Get httpx kwargs from http_config (includes verify, proxy, timeout)
223+
httpx_kwargs = self.http_config.get_httpx_kwargs()
224+
197225
self.client = httpx.AsyncClient(
198226
headers={
199227
"x-api-key": self.api_key,
200228
"anthropic-version": self.api_version,
201229
"Content-Type": "application/json",
202230
},
203-
timeout=60.0,
231+
**httpx_kwargs,
204232
)
205233

206234
def _load_api_key(self) -> Optional[str]:

cascadeflow/providers/base.py

Lines changed: 181 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,24 @@
1111
- Circuit Breaker integration for provider resilience
1212
- Per-provider health tracking
1313
- 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
1419
"""
1520

1621
import asyncio
1722
import logging
1823
import math
24+
import os
1925
import random
26+
import warnings
2027
from abc import ABC, abstractmethod
2128
from collections.abc import AsyncIterator
2229
from dataclasses import dataclass, field
2330
from enum import Enum
24-
from typing import TYPE_CHECKING, Any, Optional
31+
from typing import TYPE_CHECKING, Any, Optional, Union
2532

2633
if TYPE_CHECKING:
2734
from cascadeflow.resilience import CircuitBreaker
@@ -110,6 +117,155 @@ def get_summary(self) -> dict[str, Any]:
110117
}
111118

112119

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+
113269
# ============================================================================
114270
# MODEL RESPONSE
115271
# ============================================================================
@@ -208,21 +364,44 @@ def __init__(
208364
api_key: Optional[str] = None,
209365
retry_config: Optional[RetryConfig] = None,
210366
enable_circuit_breaker: bool = True,
367+
http_config: Optional[HttpConfig] = None,
211368
):
212369
"""
213-
Initialize provider with retry logic and circuit breaker.
370+
Initialize provider with retry logic, circuit breaker, and HTTP config.
214371
215372
Args:
216373
api_key: API key for the provider (if needed)
217374
retry_config: Custom retry configuration (optional)
218375
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+
)
219394
"""
220395
# Load API key from parameter or environment
221396
if api_key:
222397
self.api_key = api_key
223398
else:
224399
self.api_key = self._load_api_key()
225400

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+
226405
# Circuit breaker integration
227406
self._enable_circuit_breaker = enable_circuit_breaker
228407
self._circuit_breaker: Optional[CircuitBreaker] = None

cascadeflow/providers/groq.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import httpx
1010

1111
from ..exceptions import ModelError, ProviderError
12-
from .base import BaseProvider, ModelResponse, RetryConfig
12+
from .base import BaseProvider, HttpConfig, ModelResponse, RetryConfig
1313

1414

1515
class GroqProvider(BaseProvider):
@@ -90,19 +90,38 @@ class GroqProvider(BaseProvider):
9090
... )
9191
"""
9292

93-
def __init__(self, api_key: Optional[str] = None, retry_config: Optional[RetryConfig] = None):
93+
def __init__(
94+
self,
95+
api_key: Optional[str] = None,
96+
retry_config: Optional[RetryConfig] = None,
97+
http_config: Optional[HttpConfig] = None,
98+
):
9499
"""
95-
Initialize Groq provider with automatic retry logic.
100+
Initialize Groq provider with automatic retry logic and enterprise HTTP support.
96101
97102
Args:
98103
api_key: Groq API key. If None, reads from GROQ_API_KEY env var.
99104
retry_config: Custom retry configuration (optional). If None, uses defaults:
100105
- max_attempts: 3
101106
- initial_delay: 1.0s
102107
- rate_limit_backoff: 30.0s
108+
http_config: HTTP configuration for SSL/proxy (default: auto-detect from env).
109+
Supports:
110+
- Custom CA bundles (SSL_CERT_FILE, REQUESTS_CA_BUNDLE)
111+
- Proxy servers (HTTPS_PROXY, HTTP_PROXY)
112+
- SSL verification control
113+
114+
Example:
115+
# Auto-detect from environment (default)
116+
provider = GroqProvider()
117+
118+
# Corporate environment with custom CA bundle
119+
provider = GroqProvider(
120+
http_config=HttpConfig(verify="/path/to/corporate-ca.pem")
121+
)
103122
"""
104-
# Call parent init to load API key, check logprobs support, and setup retry
105-
super().__init__(api_key=api_key, retry_config=retry_config)
123+
# Call parent init to load API key, check logprobs support, setup retry, and http_config
124+
super().__init__(api_key=api_key, retry_config=retry_config, http_config=http_config)
106125

107126
# Verify API key is set
108127
if not self.api_key:
@@ -112,11 +131,16 @@ def __init__(self, api_key: Optional[str] = None, retry_config: Optional[RetryCo
112131
"https://console.groq.com"
113132
)
114133

115-
# Initialize HTTP client
134+
# Initialize HTTP client with enterprise HTTP config
116135
self.base_url = "https://api.groq.com/openai/v1"
136+
137+
# Get httpx kwargs from http_config (includes verify, proxy, timeout)
138+
httpx_kwargs = self.http_config.get_httpx_kwargs()
139+
httpx_kwargs["timeout"] = 60.0 # Groq-specific timeout
140+
117141
self.client = httpx.AsyncClient(
118142
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
119-
timeout=60.0,
143+
**httpx_kwargs,
120144
)
121145

122146
def _load_api_key(self) -> Optional[str]:

0 commit comments

Comments
 (0)