mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
Merge 11fc0038cd into c2c2a623c0
This commit is contained in:
commit
46fe6a6cd3
8 changed files with 166 additions and 24 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -907,6 +914,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(
|
||||
|
|
@ -928,6 +936,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
organization=organization,
|
||||
client=client,
|
||||
shared_session=shared_session,
|
||||
ssl_verify=ssl_verify,
|
||||
)
|
||||
|
||||
## LOGGING
|
||||
|
|
@ -1024,6 +1033,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))
|
||||
|
|
@ -1037,6 +1047,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
max_retries=max_retries,
|
||||
organization=organization,
|
||||
client=client,
|
||||
ssl_verify=ssl_verify,
|
||||
)
|
||||
## LOGGING
|
||||
logging_obj.pre_call(
|
||||
|
|
@ -1109,6 +1120,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(
|
||||
|
|
|
|||
|
|
@ -352,6 +352,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,
|
||||
|
|
|
|||
|
|
@ -3834,6 +3834,7 @@ all_litellm_params = (
|
|||
"rust",
|
||||
"prompt_label",
|
||||
"shared_session",
|
||||
"ssl_verify",
|
||||
"search_tool_name",
|
||||
"order",
|
||||
"enable_tag_filtering",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -83,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 (
|
||||
|
|
@ -97,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):
|
||||
|
|
@ -117,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"
|
||||
|
|
@ -143,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
|
||||
|
|
@ -174,6 +163,81 @@ 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):
|
||||
monkeypatch.setattr(litellm, "client_session", None)
|
||||
monkeypatch.setattr(litellm, "network_mock", False)
|
||||
result = BaseOpenAILLM._get_sync_http_client(ssl_verify=False)
|
||||
|
||||
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):
|
||||
monkeypatch.setattr(litellm, "aclient_session", None)
|
||||
monkeypatch.setattr(litellm, "network_mock", False)
|
||||
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_uses_per_call_ssl_verify(monkeypatch):
|
||||
from litellm.caching.llm_caching_handler import LLMClientCache
|
||||
from litellm.llms.openai.openai import OpenAIChatCompletion
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
assert client is not None
|
||||
assert client._client._transport._pool._ssl_context.check_hostname is False
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -2361,6 +2361,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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -5403,6 +5403,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__()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue