From e14f99dd387a3315a8265e892c49750d5f9abdf5 Mon Sep 17 00:00:00 2001 From: King Star Date: Tue, 25 Aug 2026 15:43:51 +0800 Subject: [PATCH 1/6] fix(openai): honor per-call SSL verification --- litellm/llms/openai/common_utils.py | 9 +- litellm/llms/openai/openai.py | 16 +++- .../llms/openai/test_openai_common_utils.py | 91 ++++++++++++++++++- 3 files changed, 109 insertions(+), 7 deletions(-) diff --git a/litellm/llms/openai/common_utils.py b/litellm/llms/openai/common_utils.py index 2db6d78a218..21e43578122 100644 --- a/litellm/llms/openai/common_utils.py +++ b/litellm/llms/openai/common_utils.py @@ -33,6 +33,7 @@ from litellm.llms.custom_httpx.http_handler import ( AsyncHTTPHandler, get_ssl_configuration, ) +from litellm.types.llms.custom_http import VerifyTypes def _get_client_init_params(cls: type) -> tuple[str, ...]: @@ -278,6 +279,7 @@ class BaseOpenAILLM: "organization", "api_base", "workload_identity_config", + "ssl_verify", ) openai_client_fields: Final = ( BaseOpenAILLM.get_openai_client_initialization_param_fields(client_type=client_type) @@ -303,6 +305,7 @@ class BaseOpenAILLM: @staticmethod def _get_async_http_client( shared_session: Optional["ClientSession"] = None, + ssl_verify: VerifyTypes | None = None, ) -> httpx.AsyncClient | None: if litellm.aclient_session is not None: return litellm.aclient_session @@ -313,7 +316,7 @@ class BaseOpenAILLM: return httpx.AsyncClient(transport=MockOpenAITransport()) # Get unified SSL configuration - ssl_config: Final = get_ssl_configuration() + ssl_config: Final = get_ssl_configuration(ssl_verify=ssl_verify) transport: Final = AsyncHTTPHandler._create_async_transport( ssl_context=(ssl_config if isinstance(ssl_config, ssl.SSLContext) else None), ssl_verify=ssl_config if isinstance(ssl_config, bool) else None, @@ -328,7 +331,7 @@ class BaseOpenAILLM: ) @staticmethod - def _get_sync_http_client() -> httpx.Client | None: + def _get_sync_http_client(ssl_verify: VerifyTypes | None = None) -> httpx.Client | None: if litellm.client_session is not None: return litellm.client_session @@ -338,7 +341,7 @@ class BaseOpenAILLM: return httpx.Client(transport=MockOpenAITransport()) # Get unified SSL configuration - ssl_config: Final = get_ssl_configuration() + ssl_config: Final = get_ssl_configuration(ssl_verify=ssl_verify) return httpx.Client( verify=ssl_config, diff --git a/litellm/llms/openai/openai.py b/litellm/llms/openai/openai.py index edc8d64d9c2..7c5e5d6e731 100644 --- a/litellm/llms/openai/openai.py +++ b/litellm/llms/openai/openai.py @@ -31,6 +31,7 @@ from litellm.litellm_core_utils.logging_utils import speech_request_body, track_ from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException from litellm.llms.bedrock.chat.invoke_handler import MockResponseIterator +from litellm.types.llms.custom_http import VerifyTypes from litellm.types.utils import ( EmbeddingResponse, ImageResponse, @@ -382,6 +383,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): organization: str | None = None, client: OpenAI | AsyncOpenAI | None = None, shared_session: Optional["ClientSession"] = None, + ssl_verify: VerifyTypes | None = None, ) -> OpenAI | AsyncOpenAI | None: workload_identity_config: Final = resolve_openai_workload_identity_config(api_key=api_key, api_base=api_base) client_initialization_params: Final[dict] = locals() @@ -400,7 +402,9 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): if isinstance(cached_client, OpenAI) or isinstance(cached_client, AsyncOpenAI): return cached_client if is_async: - async_http_client: Final = OpenAIChatCompletion._get_async_http_client(shared_session=shared_session) + async_http_client: Final = OpenAIChatCompletion._get_async_http_client( + shared_session=shared_session, ssl_verify=ssl_verify + ) http_client: httpx.Client | httpx.AsyncClient | None = async_http_client _new_client: OpenAI | AsyncOpenAI = ( AsyncOpenAI( @@ -422,7 +426,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): ) ) else: - sync_http_client: Final = OpenAIChatCompletion._get_sync_http_client() + sync_http_client: Final = OpenAIChatCompletion._get_sync_http_client(ssl_verify=ssl_verify) http_client = sync_http_client _new_client = ( OpenAI( @@ -750,6 +754,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): drop_params=drop_params, fake_stream=fake_stream, shared_session=shared_session, + ssl_verify=litellm_params.get("ssl_verify"), ) data = provider_config.transform_request( @@ -773,6 +778,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): max_retries=max_retries, organization=organization, stream_options=stream_options, + ssl_verify=litellm_params.get("ssl_verify"), ) else: if not isinstance(max_retries, int): @@ -786,6 +792,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): max_retries=max_retries, organization=organization, client=client, + ssl_verify=litellm_params.get("ssl_verify"), ) ## LOGGING @@ -906,6 +913,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): stream_options: dict | None = None, fake_stream: bool = False, shared_session: Optional["ClientSession"] = None, + ssl_verify: VerifyTypes | None = None, ): response = None data = await provider_config.async_transform_request( @@ -927,6 +935,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): organization=organization, client=client, shared_session=shared_session, + ssl_verify=ssl_verify, ) ## LOGGING @@ -1022,6 +1031,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): max_retries=None, headers=None, stream_options: dict | None = None, + ssl_verify: VerifyTypes | None = None, ): data["stream"] = True data.update(self.get_stream_options(stream_options=stream_options, api_base=api_base)) @@ -1035,6 +1045,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): max_retries=max_retries, organization=organization, client=client, + ssl_verify=ssl_verify, ) ## LOGGING logging_obj.pre_call( @@ -1107,6 +1118,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): organization=organization, client=client, shared_session=shared_session, + ssl_verify=litellm_params.get("ssl_verify"), ) ## LOGGING logging_obj.pre_call( diff --git a/tests/test_litellm/llms/openai/test_openai_common_utils.py b/tests/test_litellm/llms/openai/test_openai_common_utils.py index d3c21c5bd5a..f54783a59cd 100644 --- a/tests/test_litellm/llms/openai/test_openai_common_utils.py +++ b/tests/test_litellm/llms/openai/test_openai_common_utils.py @@ -1,10 +1,9 @@ -from unittest.mock import MagicMock, call, patch +from unittest.mock import MagicMock, patch import httpx import openai import pytest - import litellm from litellm.litellm_core_utils.token_counter import token_counter from litellm.llms.openai.common_utils import BaseOpenAILLM, is_openai_backed_api_base @@ -174,6 +173,94 @@ def test_get_openai_client_cache_key(client_type): assert "api_key=sk-test" in key +def test_get_openai_client_cache_key_includes_ssl_verify(): + first_key = BaseOpenAILLM.get_openai_client_cache_key( + client_initialization_params={"api_key": "sk-test", "ssl_verify": "/tmp/first-ca.pem"}, + client_type="openai", + ) + second_key = BaseOpenAILLM.get_openai_client_cache_key( + client_initialization_params={"api_key": "sk-test", "ssl_verify": "/tmp/second-ca.pem"}, + client_type="openai", + ) + + assert first_key != second_key + + +def test_get_sync_http_client_uses_per_call_ssl_verify(monkeypatch): + from litellm.llms.openai import common_utils + + monkeypatch.setattr(litellm, "client_session", None) + monkeypatch.setattr(litellm, "network_mock", False) + with ( + patch.object( # test-quality-ok: verify the per-call SSL setting reaches the resolver + common_utils, "get_ssl_configuration", return_value=False + ) as get_ssl_configuration, + patch.object( # test-quality-ok: capture the constructed HTTP client options + common_utils.httpx, "Client" + ) as http_client, + ): + result = BaseOpenAILLM._get_sync_http_client(ssl_verify="/tmp/custom-ca.pem") + + assert result is http_client.return_value + get_ssl_configuration.assert_called_once_with(ssl_verify="/tmp/custom-ca.pem") + http_client.assert_called_once_with(verify=False, follow_redirects=True) + + +@pytest.mark.asyncio +async def test_get_async_http_client_uses_per_call_ssl_verify(monkeypatch): + from litellm.llms.openai import common_utils + + monkeypatch.setattr(litellm, "aclient_session", None) + monkeypatch.setattr(litellm, "network_mock", False) + with ( + patch.object( # test-quality-ok: verify the per-call SSL setting reaches the resolver + common_utils, "get_ssl_configuration", return_value=False + ) as get_ssl_configuration, + patch.object( # test-quality-ok: capture async transport options + common_utils.AsyncHTTPHandler, "_create_async_transport", return_value=None + ) as transport, + patch.object( # test-quality-ok: capture the constructed HTTP client options + common_utils.httpx, "AsyncClient" + ) as http_client, + ): + result = BaseOpenAILLM._get_async_http_client(ssl_verify="/tmp/custom-ca.pem") + + assert result is http_client.return_value + get_ssl_configuration.assert_called_once_with(ssl_verify="/tmp/custom-ca.pem") + transport.assert_called_once_with(ssl_context=None, ssl_verify=False, shared_session=None) + http_client.assert_called_once_with(verify=False, transport=None, follow_redirects=True) + + +def test_openai_client_ssl_verify(monkeypatch): # test-quality-ok: verifies per-call SSL reaches the client boundary + from litellm.llms.openai.openai import OpenAIChatCompletion + + monkeypatch.setattr(litellm, "in_memory_llm_clients_cache", MagicMock()) + with ( + patch.object( # test-quality-ok: force the fresh-client path + BaseOpenAILLM, "get_cached_openai_client", return_value=None + ), + patch.object( # test-quality-ok: avoid mutating the shared client cache + BaseOpenAILLM, "set_cached_openai_client" + ), + patch.object( # test-quality-ok: observe the HTTP client boundary + OpenAIChatCompletion, "_get_sync_http_client" + ) as get_http_client, + patch( # test-quality-ok: avoid constructing a provider client + "litellm.llms.openai.openai.OpenAI" + ) as openai_client, + ): + OpenAIChatCompletion()._get_openai_client( + is_async=False, + api_key="sk-test", + api_base="https://example.test/v1", + max_retries=2, + ssl_verify="/tmp/custom-ca.pem", + ) + + get_http_client.assert_called_once_with(ssl_verify="/tmp/custom-ca.pem") + assert openai_client.call_count == 1 + + def test_evicting_a_client_built_on_the_callers_session_leaves_that_session_open(monkeypatch): """`litellm.aclient_session` belongs to the caller, who goes on using it. From eaca525fc77ca2fd0be9bd9618b4cdfaa61da19e Mon Sep 17 00:00:00 2001 From: King Star Date: Tue, 25 Aug 2026 16:02:01 +0800 Subject: [PATCH 2/6] test(openai): cover per-call TLS client setup --- .../llms/openai/test_openai_common_utils.py | 63 +++++-------------- 1 file changed, 14 insertions(+), 49 deletions(-) diff --git a/tests/test_litellm/llms/openai/test_openai_common_utils.py b/tests/test_litellm/llms/openai/test_openai_common_utils.py index f54783a59cd..9adf631e04c 100644 --- a/tests/test_litellm/llms/openai/test_openai_common_utils.py +++ b/tests/test_litellm/llms/openai/test_openai_common_utils.py @@ -82,11 +82,7 @@ async def test_openai_client_reuse(function_name, is_async, args): """ # Determine which client class to mock based on whether the test is async - client_path = ( - "litellm.llms.openai.openai.AsyncOpenAI" - if is_async - else "litellm.llms.openai.openai.OpenAI" - ) + client_path = "litellm.llms.openai.openai.AsyncOpenAI" if is_async else "litellm.llms.openai.openai.OpenAI" # Create the appropriate patches with ( @@ -96,9 +92,7 @@ async def test_openai_client_reuse(function_name, is_async, args): ): # Setup the mock to return None first time (cache miss) then a client for subsequent calls mock_client = MagicMock() - mock_get_cache.side_effect = [None] + [ - mock_client - ] * 9 # First call returns None, rest return the mock client + mock_get_cache.side_effect = [None] + [mock_client] * 9 # First call returns None, rest return the mock client # Make 10 API calls for _ in range(10): @@ -116,9 +110,9 @@ async def test_openai_client_reuse(function_name, is_async, args): pass # Verify client was created only once - assert ( - mock_client_class.call_count == 1 - ), f"{'Async' if is_async else ''}OpenAI client should be created only once" + assert mock_client_class.call_count == 1, ( + f"{'Async' if is_async else ''}OpenAI client should be created only once" + ) # Verify the client was cached assert mock_set_cache.call_count == 1, "Client should be cached once" @@ -142,12 +136,8 @@ def test_precomputed_init_params_match_inspect_signature(): _OPENAI_INIT_PARAMS, ) - expected_openai = tuple( - p for p in inspect.signature(OpenAI.__init__).parameters if p != "self" - ) - expected_azure = tuple( - p for p in inspect.signature(AzureOpenAI.__init__).parameters if p != "self" - ) + expected_openai = tuple(p for p in inspect.signature(OpenAI.__init__).parameters if p != "self") + expected_azure = tuple(p for p in inspect.signature(AzureOpenAI.__init__).parameters if p != "self") assert _OPENAI_INIT_PARAMS == expected_openai assert _AZURE_OPENAI_INIT_PARAMS == expected_azure @@ -187,48 +177,23 @@ def test_get_openai_client_cache_key_includes_ssl_verify(): def test_get_sync_http_client_uses_per_call_ssl_verify(monkeypatch): - from litellm.llms.openai import common_utils - monkeypatch.setattr(litellm, "client_session", None) monkeypatch.setattr(litellm, "network_mock", False) - with ( - patch.object( # test-quality-ok: verify the per-call SSL setting reaches the resolver - common_utils, "get_ssl_configuration", return_value=False - ) as get_ssl_configuration, - patch.object( # test-quality-ok: capture the constructed HTTP client options - common_utils.httpx, "Client" - ) as http_client, - ): - result = BaseOpenAILLM._get_sync_http_client(ssl_verify="/tmp/custom-ca.pem") + result = BaseOpenAILLM._get_sync_http_client(ssl_verify=False) - assert result is http_client.return_value - get_ssl_configuration.assert_called_once_with(ssl_verify="/tmp/custom-ca.pem") - http_client.assert_called_once_with(verify=False, follow_redirects=True) + assert result is not None + assert result._transport._pool._ssl_context.check_hostname is False + result.close() @pytest.mark.asyncio async def test_get_async_http_client_uses_per_call_ssl_verify(monkeypatch): - from litellm.llms.openai import common_utils - monkeypatch.setattr(litellm, "aclient_session", None) monkeypatch.setattr(litellm, "network_mock", False) - with ( - patch.object( # test-quality-ok: verify the per-call SSL setting reaches the resolver - common_utils, "get_ssl_configuration", return_value=False - ) as get_ssl_configuration, - patch.object( # test-quality-ok: capture async transport options - common_utils.AsyncHTTPHandler, "_create_async_transport", return_value=None - ) as transport, - patch.object( # test-quality-ok: capture the constructed HTTP client options - common_utils.httpx, "AsyncClient" - ) as http_client, - ): - result = BaseOpenAILLM._get_async_http_client(ssl_verify="/tmp/custom-ca.pem") + result = BaseOpenAILLM._get_async_http_client(ssl_verify=False) - assert result is http_client.return_value - get_ssl_configuration.assert_called_once_with(ssl_verify="/tmp/custom-ca.pem") - transport.assert_called_once_with(ssl_context=None, ssl_verify=False, shared_session=None) - http_client.assert_called_once_with(verify=False, transport=None, follow_redirects=True) + assert result is not None + await result.aclose() def test_openai_client_ssl_verify(monkeypatch): # test-quality-ok: verifies per-call SSL reaches the client boundary From 340b1a2b2e352339e0f76d6eef810930458f68ce Mon Sep 17 00:00:00 2001 From: King Star Date: Wed, 26 Aug 2026 18:24:00 +0800 Subject: [PATCH 3/6] test(openai): avoid class-level TLS client patches --- .../llms/openai/test_openai_common_utils.py | 41 ++++++++----------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/tests/test_litellm/llms/openai/test_openai_common_utils.py b/tests/test_litellm/llms/openai/test_openai_common_utils.py index 9adf631e04c..cab0f63e6f0 100644 --- a/tests/test_litellm/llms/openai/test_openai_common_utils.py +++ b/tests/test_litellm/llms/openai/test_openai_common_utils.py @@ -193,37 +193,28 @@ async def test_get_async_http_client_uses_per_call_ssl_verify(monkeypatch): result = BaseOpenAILLM._get_async_http_client(ssl_verify=False) assert result is not None + assert result._transport._ssl_verify is False await result.aclose() -def test_openai_client_ssl_verify(monkeypatch): # test-quality-ok: verifies per-call SSL reaches the client boundary +def test_openai_client_uses_per_call_ssl_verify(monkeypatch): + from litellm.caching.llm_caching_handler import LLMClientCache from litellm.llms.openai.openai import OpenAIChatCompletion - monkeypatch.setattr(litellm, "in_memory_llm_clients_cache", MagicMock()) - with ( - patch.object( # test-quality-ok: force the fresh-client path - BaseOpenAILLM, "get_cached_openai_client", return_value=None - ), - patch.object( # test-quality-ok: avoid mutating the shared client cache - BaseOpenAILLM, "set_cached_openai_client" - ), - patch.object( # test-quality-ok: observe the HTTP client boundary - OpenAIChatCompletion, "_get_sync_http_client" - ) as get_http_client, - patch( # test-quality-ok: avoid constructing a provider client - "litellm.llms.openai.openai.OpenAI" - ) as openai_client, - ): - OpenAIChatCompletion()._get_openai_client( - is_async=False, - api_key="sk-test", - api_base="https://example.test/v1", - max_retries=2, - ssl_verify="/tmp/custom-ca.pem", - ) + monkeypatch.setattr(litellm, "client_session", None) + monkeypatch.setattr(litellm, "network_mock", False) + monkeypatch.setattr(litellm, "in_memory_llm_clients_cache", LLMClientCache()) + client = OpenAIChatCompletion()._get_openai_client( + is_async=False, + api_key="sk-test", + api_base="https://example.test/v1", + max_retries=2, + ssl_verify=False, + ) - get_http_client.assert_called_once_with(ssl_verify="/tmp/custom-ca.pem") - assert openai_client.call_count == 1 + assert client is not None + assert client._client._transport._pool._ssl_context.check_hostname is False + client.close() def test_evicting_a_client_built_on_the_callers_session_leaves_that_session_open(monkeypatch): From 4fa710628c758bbc8706c56ebf8977a107c98ff2 Mon Sep 17 00:00:00 2001 From: King Star Date: Thu, 10 Sep 2026 06:00:54 +0800 Subject: [PATCH 4/6] fix(openai): keep ssl_verify out of provider params --- litellm/types/utils.py | 1 + tests/test_litellm/test_utils.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index d62f00f3676..b61f4bfe96e 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3791,6 +3791,7 @@ all_litellm_params = ( "rust", "prompt_label", "shared_session", + "ssl_verify", "search_tool_name", "order", "enable_tag_filtering", diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index e42608c9904..f6ddc9044c2 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -5268,6 +5268,19 @@ def test_client_side_timeout_marker_never_reaches_the_provider(): ) +def test_ssl_verify_never_reaches_the_provider_params(): + """SSL transport configuration must stay in litellm_params instead of extra_body.""" + kwargs = {"a_real_provider_specific_param": 1, "ssl_verify": "/tmp/ca.pem"} + + non_default = get_non_default_completion_params(kwargs) + + assert non_default == {"a_real_provider_specific_param": 1}, ( + "ssl_verify leaked into the provider params: " + f"{sorted(set(non_default) - {'a_real_provider_specific_param'})}" + ) + assert "ssl_verify" in all_litellm_params + + class _RecordingDeploymentFailureLogger(CustomLogger): def __init__(self) -> None: super().__init__() From 5077db873b0ebb4ad9c4bc704ac8c7419fa7ed20 Mon Sep 17 00:00:00 2001 From: King Star Date: Thu, 10 Sep 2026 06:17:00 +0800 Subject: [PATCH 5/6] test(openai): cover async TLS client propagation --- .../llms/openai/test_openai_common_utils.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_litellm/llms/openai/test_openai_common_utils.py b/tests/test_litellm/llms/openai/test_openai_common_utils.py index cab0f63e6f0..47d22a696fb 100644 --- a/tests/test_litellm/llms/openai/test_openai_common_utils.py +++ b/tests/test_litellm/llms/openai/test_openai_common_utils.py @@ -217,6 +217,27 @@ def test_openai_client_uses_per_call_ssl_verify(monkeypatch): client.close() +@pytest.mark.asyncio +async def test_async_openai_client_uses_per_call_ssl_verify(monkeypatch): + from litellm.caching.llm_caching_handler import LLMClientCache + from litellm.llms.openai.openai import OpenAIChatCompletion + + monkeypatch.setattr(litellm, "aclient_session", None) + monkeypatch.setattr(litellm, "network_mock", False) + monkeypatch.setattr(litellm, "in_memory_llm_clients_cache", LLMClientCache()) + client = OpenAIChatCompletion()._get_openai_client( + is_async=True, + api_key="sk-test", + api_base="https://example.test/v1", + max_retries=2, + ssl_verify=False, + ) + + assert client is not None + assert client._client._transport._ssl_verify is False + await client.close() + + def test_evicting_a_client_built_on_the_callers_session_leaves_that_session_open(monkeypatch): """`litellm.aclient_session` belongs to the caller, who goes on using it. From 11fc0038cdc809ef12601a6bd1c5b33c875f90e7 Mon Sep 17 00:00:00 2001 From: King Star Date: Thu, 10 Sep 2026 08:55:36 +0800 Subject: [PATCH 6/6] fix(proxy): gate caller-controlled SSL verification --- litellm/proxy/auth/auth_utils.py | 1 + .../proxy/auth/test_auth_utils.py | 47 +++++++++++++++++++ .../auth/test_banned_params_extra_body.py | 1 + 3 files changed, 49 insertions(+) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index f78c4221f5a..0619c35b40a 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -350,6 +350,7 @@ _BANNED_REQUEST_BODY_PARAMS: Final[tuple[str, ...]] = ( # the request away from the admin's pinned configuration. "nvcf_function_id", "use_ssl", + "ssl_verify", # Per-deployment opt-in that hands the whole call to the Rust core. It is a # deployment decision, not a request one: the Rust path uses its own client # rather than the one the deployment configured, and reports no post_call, diff --git a/tests/test_litellm/proxy/auth/test_auth_utils.py b/tests/test_litellm/proxy/auth/test_auth_utils.py index aaf630ad29b..3906fb3aad0 100644 --- a/tests/test_litellm/proxy/auth/test_auth_utils.py +++ b/tests/test_litellm/proxy/auth/test_auth_utils.py @@ -2329,6 +2329,53 @@ class TestIsRequestBodySafeBlocksEndpointTargetingFields: ) +class TestIsRequestBodySafeBlocksTLSVerificationOverride: + @pytest.mark.parametrize("ssl_verify", [False, "/tmp/custom-ca.pem"]) + def test_ssl_verify_in_request_body_is_rejected(self, ssl_verify): + with pytest.raises(ValueError, match="ssl_verify"): + is_request_body_safe( + request_body={"model": "gpt-4", "ssl_verify": ssl_verify}, + general_settings={}, + llm_router=None, + model="gpt-4", + ) + + def test_admin_opt_in_proxy_wide_allows_ssl_verify(self): + assert ( + is_request_body_safe( + request_body={"model": "gpt-4", "ssl_verify": False}, + general_settings={"allow_client_side_credentials": True}, + llm_router=None, + model="gpt-4", + ) + is True + ) + + def test_admin_opt_in_per_deployment_allows_ssl_verify(self): + from litellm import Router + + router = Router( + model_list=[ + { + "model_name": "gpt-4", + "litellm_params": { + "model": "openai/gpt-4", + "configurable_clientside_auth_params": ["ssl_verify"], + }, + } + ] + ) + assert ( + is_request_body_safe( + request_body={"model": "gpt-4", "ssl_verify": "/tmp/custom-ca.pem"}, + general_settings={}, + llm_router=router, + model="gpt-4", + ) + is True + ) + + class TestIsRequestBodySafeBlocksBedrockProjectOverride: """``aws_bedrock_project_id`` pins a deployment to a Bedrock project so that project's data-retention policy applies to its requests. A diff --git a/tests/test_litellm/proxy/auth/test_banned_params_extra_body.py b/tests/test_litellm/proxy/auth/test_banned_params_extra_body.py index e87b206a40a..8e9ccd4a3b8 100644 --- a/tests/test_litellm/proxy/auth/test_banned_params_extra_body.py +++ b/tests/test_litellm/proxy/auth/test_banned_params_extra_body.py @@ -28,6 +28,7 @@ from litellm.proxy.auth.auth_utils import is_request_body_safe # noqa: E402 "base_url", "vertex_credentials", "azure_ad_token", + "ssl_verify", ], ) def test_banned_param_under_extra_body_is_rejected(banned_param):