mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
Merge da4f6974ea into e638540de9
This commit is contained in:
commit
53a0e8dccc
7 changed files with 176 additions and 8 deletions
|
|
@ -34,6 +34,7 @@ from litellm.llms.custom_httpx.http_handler import (
|
|||
get_ssl_configuration,
|
||||
http2_enabled,
|
||||
)
|
||||
from litellm.types.llms.custom_http import VerifyTypes
|
||||
|
||||
|
||||
def _get_client_init_params(cls: type) -> tuple[str, ...]:
|
||||
|
|
@ -279,6 +280,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)
|
||||
|
|
@ -304,6 +306,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
|
||||
|
|
@ -314,7 +317,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)
|
||||
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,
|
||||
|
|
@ -337,7 +340,9 @@ class BaseOpenAILLM:
|
|||
return cls._get_async_http_client(shared_session)
|
||||
|
||||
@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
|
||||
|
||||
|
|
@ -347,7 +352,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)
|
||||
|
||||
return httpx.Client(
|
||||
verify=ssl_config,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ from typing import TYPE_CHECKING, Final, Literal, Optional, cast
|
|||
|
||||
import httpx
|
||||
|
||||
from litellm.types.llms.custom_http import VerifyTypes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiohttp import ClientSession
|
||||
|
||||
|
|
@ -390,6 +392,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
client: OpenAI | AsyncOpenAI | None = None,
|
||||
shared_session: Optional["ClientSession"] = None,
|
||||
litellm_params: Mapping[str, object] | None = 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, litellm_params=litellm_params
|
||||
|
|
@ -410,7 +413,10 @@ 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(
|
||||
|
|
@ -432,7 +438,9 @@ 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(
|
||||
|
|
@ -796,6 +804,9 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
headers=headers or {},
|
||||
)
|
||||
if stream is True and fake_stream is False:
|
||||
stream_ssl_verify: VerifyTypes | None = (
|
||||
cast(VerifyTypes | None, litellm_params.get("ssl_verify", None)) if litellm_params else None
|
||||
)
|
||||
return self.streaming(
|
||||
logging_obj=logging_obj,
|
||||
headers=headers,
|
||||
|
|
@ -810,10 +821,14 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
organization=organization,
|
||||
stream_options=stream_options,
|
||||
litellm_params=litellm_params,
|
||||
ssl_verify=stream_ssl_verify,
|
||||
)
|
||||
else:
|
||||
if not isinstance(max_retries, int):
|
||||
raise OpenAIError(status_code=422, message="max retries must be an int")
|
||||
ssl_verify: VerifyTypes | None = (
|
||||
cast(VerifyTypes | None, litellm_params.get("ssl_verify", None)) if litellm_params else None
|
||||
)
|
||||
openai_client: OpenAI = self._get_openai_client(
|
||||
is_async=False,
|
||||
api_key=api_key,
|
||||
|
|
@ -824,6 +839,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
organization=organization,
|
||||
client=client,
|
||||
litellm_params=litellm_params,
|
||||
ssl_verify=ssl_verify,
|
||||
)
|
||||
|
||||
## LOGGING
|
||||
|
|
@ -956,6 +972,9 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
)
|
||||
for _ in range(2): # if call fails due to alternating messages, retry with reformatted message
|
||||
try:
|
||||
ssl_verify: VerifyTypes | None = (
|
||||
cast(VerifyTypes | None, litellm_params.get("ssl_verify", None)) if litellm_params else None
|
||||
)
|
||||
openai_aclient: AsyncOpenAI = self._get_openai_client(
|
||||
is_async=True,
|
||||
api_key=api_key,
|
||||
|
|
@ -967,6 +986,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
client=client,
|
||||
shared_session=shared_session,
|
||||
litellm_params=litellm_params,
|
||||
ssl_verify=ssl_verify,
|
||||
)
|
||||
|
||||
## LOGGING
|
||||
|
|
@ -1064,6 +1084,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
headers=None,
|
||||
stream_options: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
ssl_verify: VerifyTypes | None = None,
|
||||
):
|
||||
data["stream"] = True
|
||||
data.update(self.get_stream_options(stream_options=stream_options, api_base=api_base))
|
||||
|
|
@ -1078,6 +1099,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
organization=organization,
|
||||
client=client,
|
||||
litellm_params=litellm_params,
|
||||
ssl_verify=ssl_verify,
|
||||
)
|
||||
## LOGGING
|
||||
logging_obj.pre_call(
|
||||
|
|
@ -1140,6 +1162,9 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
data.update(self.get_stream_options(stream_options=stream_options, api_base=api_base))
|
||||
for _ in range(2):
|
||||
try:
|
||||
ssl_verify: VerifyTypes | None = (
|
||||
cast(VerifyTypes | None, litellm_params.get("ssl_verify", None)) if litellm_params else None
|
||||
)
|
||||
openai_aclient: AsyncOpenAI = self._get_openai_client(
|
||||
is_async=True,
|
||||
api_key=api_key,
|
||||
|
|
@ -1151,6 +1176,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
client=client,
|
||||
shared_session=shared_session,
|
||||
litellm_params=litellm_params,
|
||||
ssl_verify=ssl_verify,
|
||||
)
|
||||
## LOGGING
|
||||
logging_obj.pre_call(
|
||||
|
|
|
|||
|
|
@ -5372,7 +5372,7 @@ def completion(
|
|||
context_window_fallback_dict: Final = kwargs.get("context_window_fallback_dict", None)
|
||||
organization: Final = kwargs.get("organization", None)
|
||||
### VERIFY SSL ###
|
||||
ssl_verify: Final = kwargs.get("ssl_verify", None)
|
||||
ssl_verify: Final = kwargs.pop("ssl_verify", None)
|
||||
### CUSTOM MODEL COST ###
|
||||
input_cost_per_token: Final = kwargs.get("input_cost_per_token", None)
|
||||
output_cost_per_token: Final = kwargs.get("output_cost_per_token", None)
|
||||
|
|
@ -7356,7 +7356,7 @@ def embedding(
|
|||
optional_params=optional_params,
|
||||
client=client,
|
||||
aembedding=aembedding,
|
||||
litellm_params={"ssl_verify": kwargs.get("ssl_verify", None)},
|
||||
litellm_params={"ssl_verify": kwargs.pop("ssl_verify", None)},
|
||||
)
|
||||
elif custom_llm_provider == "perplexity":
|
||||
response = base_llm_http_handler.embedding(
|
||||
|
|
|
|||
|
|
@ -259,6 +259,7 @@ LITELLM_TRACE_CONTROL_METADATA_FIELDS: Final = frozenset(
|
|||
)
|
||||
|
||||
_UNTRUSTED_ROOT_CONTROL_FIELDS: Final = (
|
||||
"ssl_verify",
|
||||
"weights",
|
||||
"_router_weights",
|
||||
"proxy_server_request",
|
||||
|
|
|
|||
|
|
@ -5087,7 +5087,7 @@ def add_provider_specific_params_to_optional_params(
|
|||
if _should_drop_param(k="extra_body", additional_drop_params=additional_drop_params) is False:
|
||||
extra_body: Final = dict(passed_params.pop("extra_body", None) or {})
|
||||
for k in passed_params:
|
||||
if k not in openai_params and passed_params[k] is not None:
|
||||
if k not in openai_params and k != "ssl_verify" and passed_params[k] is not None:
|
||||
extra_body[k] = passed_params[k]
|
||||
if not isinstance(optional_params.get("extra_body"), dict):
|
||||
optional_params["extra_body"] = {}
|
||||
|
|
|
|||
134
tests/unit/llms/openai/test_openai_ssl_verify.py
Normal file
134
tests/unit/llms/openai/test_openai_ssl_verify.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import json
|
||||
import ssl
|
||||
from typing import Final
|
||||
from unittest.mock import patch
|
||||
|
||||
import certifi
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.llms.openai.common_utils import BaseOpenAILLM
|
||||
from litellm.utils import add_provider_specific_params_to_optional_params
|
||||
|
||||
|
||||
def test_ssl_verify_not_in_extra_body():
|
||||
"""
|
||||
Ensure ssl_verify is NOT dumped into extra_body for openai and openai-compatible providers.
|
||||
Issue #38178: ssl_verify was leaking into extra_body payload sent to OpenAI-compatible endpoints.
|
||||
"""
|
||||
optional_params = {}
|
||||
passed_params = {
|
||||
"ssl_verify": "/custom/path/ca.pem",
|
||||
"temperature": 0.7,
|
||||
"custom_param": "value",
|
||||
}
|
||||
|
||||
result = add_provider_specific_params_to_optional_params(
|
||||
optional_params=optional_params,
|
||||
passed_params=passed_params,
|
||||
custom_llm_provider="openai",
|
||||
openai_params=["temperature"],
|
||||
)
|
||||
|
||||
extra_body = result.get("extra_body", {})
|
||||
assert "ssl_verify" not in extra_body
|
||||
assert extra_body.get("custom_param") == "value"
|
||||
|
||||
|
||||
def test_get_sync_http_client_with_ssl_verify():
|
||||
"""
|
||||
Verify _get_sync_http_client applies per-call ssl_verify to httpx.Client(verify=...).
|
||||
"""
|
||||
client_false = BaseOpenAILLM._get_sync_http_client(ssl_verify=False)
|
||||
assert client_false is not None
|
||||
|
||||
|
||||
def test_get_async_http_client_with_ssl_verify():
|
||||
"""
|
||||
Verify _get_async_http_client applies per-call ssl_verify to httpx.AsyncClient(verify=...).
|
||||
"""
|
||||
client_false = BaseOpenAILLM._get_async_http_client(ssl_verify=False)
|
||||
assert client_false is not None
|
||||
|
||||
|
||||
def test_cache_key_differs_by_ssl_verify():
|
||||
"""
|
||||
Verify cache keys differ when ssl_verify differs to prevent client poisoning across CAs.
|
||||
"""
|
||||
params_ca1 = {
|
||||
"api_key": "sk-1234",
|
||||
"is_async": True,
|
||||
"ssl_verify": "/path/ca1.pem",
|
||||
}
|
||||
params_ca2 = {
|
||||
"api_key": "sk-1234",
|
||||
"is_async": True,
|
||||
"ssl_verify": "/path/ca2.pem",
|
||||
}
|
||||
|
||||
key1 = BaseOpenAILLM.get_openai_client_cache_key(params_ca1, "openai")
|
||||
key2 = BaseOpenAILLM.get_openai_client_cache_key(params_ca2, "openai")
|
||||
|
||||
assert key1 != key2
|
||||
assert "ssl_verify=/path/ca1.pem" in key1
|
||||
assert "ssl_verify=/path/ca2.pem" in key2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"verify", [False, certifi.where(), None, True], ids=["disabled", "custom-ca", "default", "enabled"]
|
||||
)
|
||||
async def test_ssl_verify_http_client(verify: bool | str | None):
|
||||
def respond(request: httpx.Request) -> httpx.Response:
|
||||
payload: Final = json.loads(request.content)
|
||||
assert "ssl_verify" not in payload
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 1,
|
||||
"model": "gpt-3.5-turbo",
|
||||
"choices": [
|
||||
{"index": 0, "message": {"role": "assistant", "content": "hello"}, "finish_reason": "stop"}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
class TLSClient(httpx.AsyncClient):
|
||||
def __init__(self, **kwargs: object):
|
||||
verify_config: Final = kwargs.get("verify", True)
|
||||
|
||||
def verified_response(request: httpx.Request) -> httpx.Response:
|
||||
if verify_setting is False:
|
||||
assert verify_config is False
|
||||
else:
|
||||
assert isinstance(verify_config, ssl.SSLContext)
|
||||
assert verify_config.verify_mode == ssl.CERT_REQUIRED
|
||||
assert verify_config.check_hostname
|
||||
return respond(request)
|
||||
|
||||
client_kwargs: Final = {key: value for key, value in kwargs.items() if key not in {"transport", "mounts"}}
|
||||
super().__init__(**client_kwargs, transport=httpx.MockTransport(verified_response))
|
||||
|
||||
verify_setting: Final = verify
|
||||
router: Final = litellm.Router(
|
||||
num_retries=0,
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "tls-test",
|
||||
"litellm_params": {
|
||||
"model": "openai/gpt-3.5-turbo",
|
||||
"api_key": f"sk-test-{verify}",
|
||||
"ssl_verify": verify,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
with patch("httpx.AsyncClient", TLSClient):
|
||||
response: Final = await router.acompletion(
|
||||
model="tls-test",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
)
|
||||
assert response.choices[0].message.content == "hello"
|
||||
|
|
@ -1088,6 +1088,7 @@ async def test_add_litellm_data_to_request_strips_user_control_fields():
|
|||
"model": "gpt-3.5-turbo",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"mock_response": "free response",
|
||||
"ssl_verify": False,
|
||||
"mock_tool_calls": [{"id": "call_1"}],
|
||||
"disable_global_guardrails": True,
|
||||
"enable_prompt_caching": True,
|
||||
|
|
@ -1108,6 +1109,7 @@ async def test_add_litellm_data_to_request_strips_user_control_fields():
|
|||
version="test-version",
|
||||
)
|
||||
|
||||
assert "ssl_verify" not in updated
|
||||
assert "mock_response" not in updated
|
||||
assert "mock_tool_calls" not in updated
|
||||
assert "disable_global_guardrails" not in updated
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue