diff --git a/.github/workflows/publish-docker-offline-amd64.yml b/.github/workflows/publish-docker-offline-amd64.yml index e8f078f..8cabc30 100644 --- a/.github/workflows/publish-docker-offline-amd64.yml +++ b/.github/workflows/publish-docker-offline-amd64.yml @@ -13,6 +13,14 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Free up disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + docker system prune -af + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/publish-docker-offline-arm64.yml b/.github/workflows/publish-docker-offline-arm64.yml index 7006866..9f72716 100644 --- a/.github/workflows/publish-docker-offline-arm64.yml +++ b/.github/workflows/publish-docker-offline-arm64.yml @@ -13,6 +13,14 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Free up disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + docker system prune -af + - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/README.md b/README.md index 7b580b0..17f0623 100644 --- a/README.md +++ b/README.md @@ -102,10 +102,18 @@ docker run -p 8000:8000 ghcr.io/codelion/optillm:latest 2024-10-22 07:45:06,293 - INFO - Starting server with approach: auto ``` -To use optillm without local inference and only as a proxy you can add the `-proxy` suffix. +**Available Docker image variants:** + +- **Full image** (`latest`): Includes all dependencies for local inference and plugins +- **Proxy-only** (`latest-proxy`): Lightweight image without local inference capabilities +- **Offline** (`latest-offline`): Self-contained image with pre-downloaded models (spaCy) for fully offline operation ```bash +# Proxy-only (smallest) docker pull ghcr.io/codelion/optillm:latest-proxy + +# Offline (largest, includes pre-downloaded models) +docker pull ghcr.io/codelion/optillm:latest-offline ``` ### Install from source @@ -120,6 +128,32 @@ source .venv/bin/activate pip install -r requirements.txt ``` +## 🔒 SSL Configuration + +OptILLM supports SSL certificate verification configuration for working with self-signed certificates or corporate proxies. + +**Disable SSL verification (development only):** +```bash +# Command line +optillm --no-ssl-verify + +# Environment variable +export OPTILLM_SSL_VERIFY=false +optillm +``` + +**Use custom CA certificate:** +```bash +# Command line +optillm --ssl-cert-path /path/to/ca-bundle.crt + +# Environment variable +export OPTILLM_SSL_CERT_PATH=/path/to/ca-bundle.crt +optillm +``` + +⚠️ **Security Note**: Disabling SSL verification is insecure and should only be used in development. For production environments with custom CAs, use `--ssl-cert-path` instead. See [SSL_CONFIGURATION.md](SSL_CONFIGURATION.md) for details. + ## Implemented techniques | Approach | Slug | Description | diff --git a/SSL_CONFIGURATION.md b/SSL_CONFIGURATION.md new file mode 100644 index 0000000..8f091f0 --- /dev/null +++ b/SSL_CONFIGURATION.md @@ -0,0 +1,88 @@ +# SSL Certificate Configuration + +OptILLM now supports SSL certificate verification configuration to work with self-signed certificates or corporate proxies. + +## Usage + +### Disable SSL Verification (Development Only) + +**⚠️ WARNING: Only use this in development environments. Disabling SSL verification is insecure.** + +#### Via Command Line +```bash +python optillm.py --no-ssl-verify +``` + +#### Via Environment Variable +```bash +export OPTILLM_SSL_VERIFY=false +python optillm.py +``` + +### Use Custom CA Certificate Bundle + +For corporate environments with custom Certificate Authorities: + +#### Via Command Line +```bash +python optillm.py --ssl-cert-path /path/to/ca-bundle.crt +``` + +#### Via Environment Variable +```bash +export OPTILLM_SSL_CERT_PATH=/path/to/ca-bundle.crt +python optillm.py +``` + +## Configuration Options + +| Option | Environment Variable | Default | Description | +|--------|---------------------|---------|-------------| +| `--ssl-verify` / `--no-ssl-verify` | `OPTILLM_SSL_VERIFY` | `true` | Enable/disable SSL certificate verification | +| `--ssl-cert-path` | `OPTILLM_SSL_CERT_PATH` | `""` | Path to custom CA certificate bundle | + +## Affected Components + +SSL configuration applies to: +- **OpenAI API clients** (OpenAI, Azure, Cerebras) +- **HTTP plugins** (readurls, deep_research) +- **All external HTTPS connections** + +## Examples + +### Development with Self-Signed Certificate +```bash +# Disable SSL verification temporarily +python optillm.py --no-ssl-verify --base-url https://localhost:8443/v1 +``` + +### Production with Corporate CA +```bash +# Use corporate certificate bundle +python optillm.py --ssl-cert-path /etc/ssl/certs/corporate-ca-bundle.crt +``` + +### Docker Environment +```bash +docker run -e OPTILLM_SSL_VERIFY=false optillm +``` + +## Security Notes + +1. **Never disable SSL verification in production** - This makes your application vulnerable to man-in-the-middle attacks +2. **Use custom CA bundles instead** - For corporate environments, provide the proper CA certificate path +3. **Warning messages** - When SSL verification is disabled, OptILLM will log a warning message for security awareness + +## Testing + +Run the SSL configuration test suite: +```bash +python -m unittest tests.test_ssl_config -v +``` + +This validates: +- CLI argument parsing +- Environment variable configuration +- HTTP client SSL settings +- Plugin SSL propagation +- Warning messages \ No newline at end of file diff --git a/optillm/__init__.py b/optillm/__init__.py index f22e988..dee0c8b 100644 --- a/optillm/__init__.py +++ b/optillm/__init__.py @@ -1,5 +1,5 @@ # Version information -__version__ = "0.3.1" +__version__ = "0.3.2" # Import from server module from .server import ( diff --git a/optillm/plugins/deep_research_plugin.py b/optillm/plugins/deep_research_plugin.py index 62717d9..75a58f0 100644 --- a/optillm/plugins/deep_research_plugin.py +++ b/optillm/plugins/deep_research_plugin.py @@ -66,6 +66,9 @@ def create(self, **kwargs): ) else: # OpenAI or AzureOpenAI + # Get existing http_client to preserve SSL settings + existing_http_client = getattr(self.parent.client, '_client', None) + if 'Azure' in self.parent.client.__class__.__name__: from openai import AzureOpenAI # AzureOpenAI has different parameters @@ -75,7 +78,8 @@ def create(self, **kwargs): azure_endpoint=getattr(self.parent.client, 'azure_endpoint', None), azure_ad_token_provider=getattr(self.parent.client, 'azure_ad_token_provider', None), timeout=self.parent.timeout, - max_retries=self.parent.max_retries + max_retries=self.parent.max_retries, + http_client=existing_http_client ) else: from openai import OpenAI @@ -83,7 +87,8 @@ def create(self, **kwargs): api_key=self.parent.client.api_key, base_url=getattr(self.parent.client, 'base_url', None), timeout=self.parent.timeout, - max_retries=self.parent.max_retries + max_retries=self.parent.max_retries, + http_client=existing_http_client ) return custom_client.chat.completions.create(**kwargs) except Exception as e: diff --git a/optillm/plugins/readurls_plugin.py b/optillm/plugins/readurls_plugin.py index cf9024b..b799bca 100644 --- a/optillm/plugins/readurls_plugin.py +++ b/optillm/plugins/readurls_plugin.py @@ -1,10 +1,10 @@ import re -from typing import Tuple, List +from typing import Tuple, List, Optional import requests import os from bs4 import BeautifulSoup from urllib.parse import urlparse -from optillm import __version__ +from optillm import __version__, server_config SLUG = "readurls" @@ -24,13 +24,27 @@ def extract_urls(text: str) -> List[str]: return cleaned_urls -def fetch_webpage_content(url: str, max_length: int = 100000) -> str: +def fetch_webpage_content(url: str, max_length: int = 100000, verify_ssl: Optional[bool] = None, cert_path: Optional[str] = None) -> str: try: headers = { 'User-Agent': f'optillm/{__version__} (https://github.com/codelion/optillm)' } - - response = requests.get(url, headers=headers, timeout=10) + + # Use SSL configuration from server_config if not explicitly provided + if verify_ssl is None: + verify_ssl = server_config.get('ssl_verify', True) + if cert_path is None: + cert_path = server_config.get('ssl_cert_path', '') + + # Determine verify parameter for requests + if not verify_ssl: + verify = False + elif cert_path: + verify = cert_path + else: + verify = True + + response = requests.get(url, headers=headers, timeout=10, verify=verify) response.raise_for_status() # Make a soup diff --git a/optillm/server.py b/optillm/server.py index c8237f4..29a0313 100644 --- a/optillm/server.py +++ b/optillm/server.py @@ -58,7 +58,24 @@ conversation_logger = None def get_config(): + import httpx + API_KEY = None + + # Create httpx client with SSL configuration + ssl_verify = server_config.get('ssl_verify', True) + ssl_cert_path = server_config.get('ssl_cert_path', '') + + # Determine SSL verification setting + if not ssl_verify: + logger.warning("SSL certificate verification is DISABLED. This is insecure and should only be used for development.") + http_client = httpx.Client(verify=False) + elif ssl_cert_path: + logger.info(f"Using custom CA certificate bundle: {ssl_cert_path}") + http_client = httpx.Client(verify=ssl_cert_path) + else: + http_client = httpx.Client(verify=True) + if os.environ.get("OPTILLM_API_KEY"): # Use local inference engine from optillm.inference import create_inference_client @@ -69,16 +86,16 @@ def get_config(): API_KEY = os.environ.get("CEREBRAS_API_KEY") base_url = server_config['base_url'] if base_url != "": - default_client = Cerebras(api_key=API_KEY, base_url=base_url) + default_client = Cerebras(api_key=API_KEY, base_url=base_url, http_client=http_client) else: - default_client = Cerebras(api_key=API_KEY) + default_client = Cerebras(api_key=API_KEY, http_client=http_client) elif os.environ.get("OPENAI_API_KEY"): API_KEY = os.environ.get("OPENAI_API_KEY") base_url = server_config['base_url'] if base_url != "": - default_client = OpenAI(api_key=API_KEY, base_url=base_url) + default_client = OpenAI(api_key=API_KEY, base_url=base_url, http_client=http_client) else: - default_client = OpenAI(api_key=API_KEY) + default_client = OpenAI(api_key=API_KEY, http_client=http_client) elif os.environ.get("AZURE_OPENAI_API_KEY"): API_KEY = os.environ.get("AZURE_OPENAI_API_KEY") API_VERSION = os.environ.get("AZURE_API_VERSION") @@ -88,6 +105,7 @@ def get_config(): api_key=API_KEY, api_version=API_VERSION, azure_endpoint=AZURE_ENDPOINT, + http_client=http_client ) else: from azure.identity import DefaultAzureCredential, get_bearer_token_provider @@ -96,7 +114,8 @@ def get_config(): default_client = AzureOpenAI( api_version=API_VERSION, azure_endpoint=AZURE_ENDPOINT, - azure_ad_token_provider=token_provider + azure_ad_token_provider=token_provider, + http_client=http_client ) else: # Import the LiteLLM wrapper @@ -152,7 +171,7 @@ def count_reasoning_tokens(text: str, tokenizer=None) -> int: # Server configuration server_config = { - 'approach': 'none', + 'approach': 'none', 'mcts_simulations': 2, 'mcts_exploration': 0.2, 'mcts_depth': 1, @@ -167,6 +186,8 @@ def count_reasoning_tokens(text: str, tokenizer=None) -> int: 'return_full_response': False, 'port': 8000, 'log': 'info', + 'ssl_verify': True, + 'ssl_cert_path': '', } # List of known approaches @@ -977,7 +998,19 @@ def parse_args(): base_url_default = os.environ.get("OPTILLM_BASE_URL", "") parser.add_argument("--base-url", "--base_url", dest="base_url", type=str, default=base_url_default, help="Base url for OpenAI compatible endpoint") - + + # SSL configuration arguments + ssl_verify_default = os.environ.get("OPTILLM_SSL_VERIFY", "true").lower() in ("true", "1", "yes") + parser.add_argument("--ssl-verify", dest="ssl_verify", action="store_true" if ssl_verify_default else "store_false", + default=ssl_verify_default, + help="Enable SSL certificate verification (default: True)") + parser.add_argument("--no-ssl-verify", dest="ssl_verify", action="store_false", + help="Disable SSL certificate verification") + + ssl_cert_path_default = os.environ.get("OPTILLM_SSL_CERT_PATH", "") + parser.add_argument("--ssl-cert-path", dest="ssl_cert_path", type=str, default=ssl_cert_path_default, + help="Path to custom CA certificate bundle for SSL verification") + # Use the function to get the default path default_config_path = get_config_path() diff --git a/pyproject.toml b/pyproject.toml index 02dd082..f822c07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "optillm" -version = "0.3.1" +version = "0.3.2" description = "An optimizing inference proxy for LLMs." readme = "README.md" license = "Apache-2.0" diff --git a/tests/test_ssl_config.py b/tests/test_ssl_config.py new file mode 100644 index 0000000..8f58c6a --- /dev/null +++ b/tests/test_ssl_config.py @@ -0,0 +1,354 @@ +""" +Unit tests for SSL configuration support in optillm. + +Tests verify that SSL certificate verification can be configured via: +- Command-line arguments (--ssl-verify, --no-ssl-verify, --ssl-cert-path) +- Environment variables (OPTILLM_SSL_VERIFY, OPTILLM_SSL_CERT_PATH) +- And that SSL settings are properly propagated to HTTP clients +""" + +import unittest +from unittest.mock import Mock, patch, MagicMock, call +import sys +import os +import tempfile +import httpx + +# Add parent directory to path to import optillm modules +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from optillm import server_config, parse_args + + +class TestSSLConfiguration(unittest.TestCase): + """Test SSL configuration via CLI arguments and environment variables.""" + + def setUp(self): + """Reset server_config before each test.""" + # Save original config + self.original_config = server_config.copy() + + # Clear SSL-related environment variables + for key in ['OPTILLM_SSL_VERIFY', 'OPTILLM_SSL_CERT_PATH']: + if key in os.environ: + del os.environ[key] + + def tearDown(self): + """Restore original server_config after each test.""" + server_config.clear() + server_config.update(self.original_config) + + def test_default_ssl_verify_enabled(self): + """Test that SSL verification is enabled by default.""" + self.assertTrue(server_config.get('ssl_verify', True)) + self.assertEqual(server_config.get('ssl_cert_path', ''), '') + + def test_cli_no_ssl_verify_flag(self): + """Test --no-ssl-verify CLI flag disables SSL verification.""" + with patch('sys.argv', ['optillm', '--no-ssl-verify']): + args = parse_args() + self.assertFalse(args.ssl_verify) + + def test_cli_ssl_cert_path(self): + """Test --ssl-cert-path CLI argument.""" + test_cert_path = '/path/to/ca-bundle.crt' + with patch('sys.argv', ['optillm', '--ssl-cert-path', test_cert_path]): + args = parse_args() + self.assertEqual(args.ssl_cert_path, test_cert_path) + + def test_env_ssl_verify_false(self): + """Test OPTILLM_SSL_VERIFY=false environment variable.""" + os.environ['OPTILLM_SSL_VERIFY'] = 'false' + with patch('sys.argv', ['optillm']): + args = parse_args() + self.assertFalse(args.ssl_verify) + + def test_env_ssl_verify_true(self): + """Test OPTILLM_SSL_VERIFY=true environment variable.""" + os.environ['OPTILLM_SSL_VERIFY'] = 'true' + with patch('sys.argv', ['optillm']): + args = parse_args() + self.assertTrue(args.ssl_verify) + + def test_env_ssl_cert_path(self): + """Test OPTILLM_SSL_CERT_PATH environment variable.""" + test_cert_path = '/etc/ssl/certs/custom-ca.pem' + os.environ['OPTILLM_SSL_CERT_PATH'] = test_cert_path + with patch('sys.argv', ['optillm']): + args = parse_args() + self.assertEqual(args.ssl_cert_path, test_cert_path) + + def test_cli_overrides_env(self): + """Test that CLI arguments override environment variables.""" + os.environ['OPTILLM_SSL_VERIFY'] = 'true' + with patch('sys.argv', ['optillm', '--no-ssl-verify']): + args = parse_args() + self.assertFalse(args.ssl_verify) + + +class TestHTTPClientSSLConfiguration(unittest.TestCase): + """Test that SSL configuration is properly applied to HTTP clients.""" + + def setUp(self): + """Set up test environment.""" + self.original_config = server_config.copy() + + def tearDown(self): + """Restore original server_config.""" + server_config.clear() + server_config.update(self.original_config) + + @patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key'}) + def test_httpx_client_ssl_verify_disabled(self): + """Test httpx.Client created with verify=False when SSL disabled.""" + from optillm.server import get_config + + # Configure to disable SSL verification + server_config['ssl_verify'] = False + server_config['ssl_cert_path'] = '' + + # Create client + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + get_config() + # Verify httpx.Client was called with verify=False + mock_httpx_client.assert_called_once_with(verify=False) + + @patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key'}) + def test_httpx_client_ssl_verify_enabled(self): + """Test httpx.Client created with verify=True by default.""" + from optillm.server import get_config + + # Configure to enable SSL verification (default) + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = '' + + # Create client + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + get_config() + # Verify httpx.Client was called with verify=True + mock_httpx_client.assert_called_once_with(verify=True) + + @patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key'}) + def test_httpx_client_custom_cert_path(self): + """Test httpx.Client created with custom certificate path.""" + from optillm.server import get_config + + # Configure custom certificate path + test_cert_path = '/path/to/custom-ca.pem' + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = test_cert_path + + # Create client + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + get_config() + # Verify httpx.Client was called with custom cert path + mock_httpx_client.assert_called_once_with(verify=test_cert_path) + + @patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key'}) + def test_openai_client_receives_http_client(self): + """Test that OpenAI client receives the configured httpx client.""" + from optillm.server import get_config + + server_config['ssl_verify'] = False + server_config['ssl_cert_path'] = '' + server_config['base_url'] = '' + + mock_http_client_instance = MagicMock() + + with patch('httpx.Client', return_value=mock_http_client_instance) as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai: + get_config() + + # Verify OpenAI was called with http_client parameter + mock_openai.assert_called_once() + call_kwargs = mock_openai.call_args[1] + self.assertIn('http_client', call_kwargs) + self.assertEqual(call_kwargs['http_client'], mock_http_client_instance) + + @patch.dict(os.environ, {'CEREBRAS_API_KEY': 'test-key'}) + def test_cerebras_client_receives_http_client(self): + """Test that Cerebras client receives the configured httpx client.""" + from optillm.server import get_config + + server_config['ssl_verify'] = False + server_config['ssl_cert_path'] = '' + server_config['base_url'] = '' + + mock_http_client_instance = MagicMock() + + with patch('httpx.Client', return_value=mock_http_client_instance) as mock_httpx_client, \ + patch('optillm.server.Cerebras') as mock_cerebras: + get_config() + + # Verify Cerebras was called with http_client parameter + mock_cerebras.assert_called_once() + call_kwargs = mock_cerebras.call_args[1] + self.assertIn('http_client', call_kwargs) + self.assertEqual(call_kwargs['http_client'], mock_http_client_instance) + + @patch.dict(os.environ, {'AZURE_OPENAI_API_KEY': 'test-key', 'AZURE_API_VERSION': '2024-02-15-preview', 'AZURE_API_BASE': 'https://test.openai.azure.com'}) + def test_azure_client_receives_http_client(self): + """Test that AzureOpenAI client receives the configured httpx client.""" + from optillm.server import get_config + + server_config['ssl_verify'] = False + server_config['ssl_cert_path'] = '' + + mock_http_client_instance = MagicMock() + + with patch('httpx.Client', return_value=mock_http_client_instance) as mock_httpx_client, \ + patch('optillm.server.AzureOpenAI') as mock_azure: + get_config() + + # Verify AzureOpenAI was called with http_client parameter + mock_azure.assert_called_once() + call_kwargs = mock_azure.call_args[1] + self.assertIn('http_client', call_kwargs) + self.assertEqual(call_kwargs['http_client'], mock_http_client_instance) + + +class TestPluginSSLConfiguration(unittest.TestCase): + """Test that plugins properly use SSL configuration.""" + + def setUp(self): + """Set up test environment.""" + self.original_config = server_config.copy() + + def tearDown(self): + """Restore original server_config.""" + server_config.clear() + server_config.update(self.original_config) + + @patch('optillm.plugins.readurls_plugin.requests.get') + def test_readurls_plugin_ssl_verify_disabled(self, mock_requests_get): + """Test readurls plugin respects SSL verification disabled.""" + from optillm.plugins.readurls_plugin import fetch_webpage_content + + # Configure to disable SSL verification + server_config['ssl_verify'] = False + server_config['ssl_cert_path'] = '' + + # Mock response + mock_response = MagicMock() + mock_response.content = b'
Test content
' + mock_response.raise_for_status = MagicMock() + mock_requests_get.return_value = mock_response + + # Fetch webpage + fetch_webpage_content('https://example.com') + + # Verify requests.get was called with verify=False + mock_requests_get.assert_called_once() + call_kwargs = mock_requests_get.call_args[1] + self.assertIn('verify', call_kwargs) + self.assertFalse(call_kwargs['verify']) + + @patch('optillm.plugins.readurls_plugin.requests.get') + def test_readurls_plugin_ssl_verify_enabled(self, mock_requests_get): + """Test readurls plugin respects SSL verification enabled.""" + from optillm.plugins.readurls_plugin import fetch_webpage_content + + # Configure to enable SSL verification + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = '' + + # Mock response + mock_response = MagicMock() + mock_response.content = b'Test content
' + mock_response.raise_for_status = MagicMock() + mock_requests_get.return_value = mock_response + + # Fetch webpage + fetch_webpage_content('https://example.com') + + # Verify requests.get was called with verify=True + mock_requests_get.assert_called_once() + call_kwargs = mock_requests_get.call_args[1] + self.assertIn('verify', call_kwargs) + self.assertTrue(call_kwargs['verify']) + + @patch('optillm.plugins.readurls_plugin.requests.get') + def test_readurls_plugin_custom_cert_path(self, mock_requests_get): + """Test readurls plugin uses custom certificate path.""" + from optillm.plugins.readurls_plugin import fetch_webpage_content + + # Configure custom certificate path + test_cert_path = '/path/to/custom-ca.pem' + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = test_cert_path + + # Mock response + mock_response = MagicMock() + mock_response.content = b'Test content
' + mock_response.raise_for_status = MagicMock() + mock_requests_get.return_value = mock_response + + # Fetch webpage + fetch_webpage_content('https://example.com') + + # Verify requests.get was called with custom cert path + mock_requests_get.assert_called_once() + call_kwargs = mock_requests_get.call_args[1] + self.assertIn('verify', call_kwargs) + self.assertEqual(call_kwargs['verify'], test_cert_path) + + +class TestSSLWarnings(unittest.TestCase): + """Test that appropriate warnings are shown when SSL is disabled.""" + + def setUp(self): + """Set up test environment.""" + self.original_config = server_config.copy() + + def tearDown(self): + """Restore original server_config.""" + server_config.clear() + server_config.update(self.original_config) + + @patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key'}) + def test_warning_when_ssl_disabled(self): + """Test that a warning is logged when SSL verification is disabled.""" + from optillm.server import get_config + + # Configure to disable SSL verification + server_config['ssl_verify'] = False + server_config['ssl_cert_path'] = '' + + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai, \ + patch('optillm.server.logger.warning') as mock_logger_warning: + get_config() + + # Verify warning was logged + mock_logger_warning.assert_called() + warning_message = mock_logger_warning.call_args[0][0] + self.assertIn('SSL certificate verification is DISABLED', warning_message) + self.assertIn('insecure', warning_message.lower()) + + @patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key'}) + def test_info_when_custom_cert_used(self): + """Test that an info message is logged when using custom certificate.""" + from optillm.server import get_config + + # Configure custom certificate path + test_cert_path = '/path/to/custom-ca.pem' + server_config['ssl_verify'] = True + server_config['ssl_cert_path'] = test_cert_path + + with patch('httpx.Client') as mock_httpx_client, \ + patch('optillm.server.OpenAI') as mock_openai, \ + patch('optillm.server.logger.info') as mock_logger_info: + get_config() + + # Verify info message was logged + mock_logger_info.assert_called() + info_message = mock_logger_info.call_args[0][0] + self.assertIn('custom CA certificate bundle', info_message) + self.assertIn(test_cert_path, info_message) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file