Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added MCP_SSL_CHANGES_SUMMARY.md
Empty file.
46 changes: 45 additions & 1 deletion litellm/experimental_mcp_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import asyncio
import base64
from datetime import timedelta
from typing import Dict, List, Optional, Union
from typing import Callable, Dict, List, Optional, Union

import httpx
from mcp import ClientSession, StdioServerParameters
from mcp.client.sse import sse_client
from mcp.client.stdio import stdio_client
Expand All @@ -17,6 +18,8 @@
from mcp.types import Tool as MCPTool

from litellm._logging import verbose_logger
from litellm.llms.custom_httpx.http_handler import get_ssl_configuration
from litellm.types.llms.custom_http import VerifyTypes
from litellm.types.mcp import (
MCPAuth,
MCPAuthType,
Expand Down Expand Up @@ -48,6 +51,7 @@ def __init__(
timeout: float = 60.0,
stdio_config: Optional[MCPStdioConfig] = None,
extra_headers: Optional[Dict[str, str]] = None,
ssl_verify: Optional[VerifyTypes] = None,
):
self.server_url: str = server_url
self.transport_type: MCPTransport = transport_type
Expand All @@ -62,6 +66,7 @@ def __init__(
self._task: Optional[asyncio.Task] = None
self.stdio_config: Optional[MCPStdioConfig] = stdio_config
self.extra_headers: Optional[Dict[str, str]] = extra_headers
self.ssl_verify: Optional[VerifyTypes] = ssl_verify
# handle the basic auth value if provided
if auth_value:
self.update_auth_value(auth_value)
Expand Down Expand Up @@ -104,10 +109,12 @@ async def connect(self):
await self._session.initialize()
elif self.transport_type == MCPTransport.sse:
headers = self._get_auth_headers()
httpx_client_factory = self._create_httpx_client_factory()
self._transport_ctx = sse_client(
url=self.server_url,
timeout=self.timeout,
headers=headers,
httpx_client_factory=httpx_client_factory,
)
self._transport = await self._transport_ctx.__aenter__()
self._session_ctx = ClientSession(
Expand All @@ -117,13 +124,15 @@ async def connect(self):
await self._session.initialize()
else: # http
headers = self._get_auth_headers()
httpx_client_factory = self._create_httpx_client_factory()
verbose_logger.debug(
"litellm headers for streamablehttp_client: ", headers
)
self._transport_ctx = streamablehttp_client(
url=self.server_url,
timeout=timedelta(seconds=self.timeout),
headers=headers,
httpx_client_factory=httpx_client_factory,
)
self._transport = await self._transport_ctx.__aenter__()
self._session_ctx = ClientSession(
Expand Down Expand Up @@ -215,6 +224,41 @@ def _get_auth_headers(self) -> dict:

return headers

def _create_httpx_client_factory(self) -> Callable[..., httpx.AsyncClient]:
"""
Create a custom httpx client factory that uses LiteLLM's SSL configuration.

This factory follows the same CA bundle path logic as http_handler.py:
1. Check ssl_verify parameter (can be SSLContext, bool, or path to CA bundle)
2. Check SSL_VERIFY environment variable
3. Check SSL_CERT_FILE environment variable
4. Fall back to certifi CA bundle
"""

def factory(
*,
headers: Optional[Dict[str, str]] = None,
timeout: Optional[httpx.Timeout] = None,
auth: Optional[httpx.Auth] = None,
) -> httpx.AsyncClient:
"""Create an httpx.AsyncClient with LiteLLM's SSL configuration."""
# Get unified SSL configuration using the same logic as http_handler.py
ssl_config = get_ssl_configuration(self.ssl_verify)

verbose_logger.debug(
f"MCP client using SSL configuration: {type(ssl_config).__name__}"
)

return httpx.AsyncClient(
headers=headers,
timeout=timeout,
auth=auth,
verify=ssl_config,
follow_redirects=True,
)

return factory

async def list_tools(self) -> List[MCPTool]:
"""List available tools from the server."""
if not self._session:
Expand Down
64 changes: 55 additions & 9 deletions litellm/llms/azure/responses/transformation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple

import httpx
from openai.types.responses import ResponseReasoningItem

from litellm._logging import verbose_logger
from litellm.llms.azure.common_utils import BaseAzureLLM
Expand Down Expand Up @@ -38,6 +39,50 @@ def get_stripped_model_name(self, model: str) -> str:
model = model.replace("o_series/", "")
return model

def _handle_reasoning_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
"""
Handle reasoning items specifically to filter out status=None using OpenAI's model.
Issue: https://github.com/BerriAI/litellm/issues/13484
OpenAI API does not accept ReasoningItem(status=None), so we need to:
1. Check if the item is a reasoning type
2. Create a ResponseReasoningItem object with the item data
3. Convert it back to dict with exclude_none=True to filter None values
"""
if item.get("type") == "reasoning":
try:
# Ensure required fields are present for ResponseReasoningItem
item_data = dict(item)
if "id" not in item_data:
item_data["id"] = f"reasoning_{hash(str(item_data))}"
if "summary" not in item_data:
item_data["summary"] = (
item_data.get("reasoning_content", "")[:100] + "..."
if len(item_data.get("reasoning_content", "")) > 100
else item_data.get("reasoning_content", "")
)

# Create ResponseReasoningItem object from the item data
reasoning_item = ResponseReasoningItem(**item_data)

# Convert back to dict with exclude_none=True to exclude None fields
dict_reasoning_item = reasoning_item.model_dump(exclude_none=True)
dict_reasoning_item.pop("status", None)

return dict_reasoning_item
except Exception as e:
verbose_logger.debug(
f"Failed to create ResponseReasoningItem, falling back to manual filtering: {e}"
)
# Fallback: manually filter out known None fields
filtered_item = {
k: v
for k, v in item.items()
if v is not None
or k not in {"status", "content", "encrypted_content"}
}
return filtered_item
return item

def transform_responses_api_request(
self,
model: str,
Expand All @@ -48,12 +93,13 @@ def transform_responses_api_request(
) -> Dict:
"""No transform applied since inputs are in OpenAI spec already"""
stripped_model_name = self.get_stripped_model_name(model)
return dict(
ResponsesAPIRequestParams(
model=stripped_model_name,
input=input,
**response_api_optional_request_params,
)

return super().transform_responses_api_request(
model=stripped_model_name,
input=input,
response_api_optional_request_params=response_api_optional_request_params,
litellm_params=litellm_params,
headers=headers,
)

def get_complete_url(
Expand Down Expand Up @@ -217,15 +263,15 @@ def transform_cancel_response_api_request(
at the correct location (before any query parameters).
"""
from urllib.parse import urlparse, urlunparse

# Parse the URL to separate its components
parsed_url = urlparse(api_base)

# Insert the response_id and /cancel at the end of the path component
# Remove trailing slash if present to avoid double slashes
path = parsed_url.path.rstrip("/")
new_path = f"{path}/{response_id}/cancel"

# Reconstruct the URL with all original components but with the modified path
cancel_url = urlunparse(
(
Expand Down
11 changes: 1 addition & 10 deletions litellm/llms/openai/responses/transformation.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
from typing import (
TYPE_CHECKING,
Any,
Dict,
Optional,
Union,
cast,
get_type_hints,
)
from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast, get_type_hints

import httpx
from openai.types.responses import ResponseReasoningItem
Expand Down Expand Up @@ -127,7 +119,6 @@ def _handle_reasoning_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
2. Create a ResponseReasoningItem object with the item data
3. Convert it back to dict with exclude_none=True to filter None values
"""
verbose_logger.debug(f"Handling reasoning item: {item}")
if item.get("type") == "reasoning":
try:
# Ensure required fields are present for ResponseReasoningItem
Expand Down
1 change: 0 additions & 1 deletion litellm/proxy/_experimental/out/onboarding.html

This file was deleted.

35 changes: 4 additions & 31 deletions litellm/proxy/_new_secret_config.yaml
Original file line number Diff line number Diff line change
@@ -1,33 +1,6 @@
model_list:
- model_name: openai/gpt-4o
- model_name: gpt-5-mini
litellm_params:
model: openai/gpt-4o-mini
api_base: "https://webhook.site/2f385e05-00aa-402b-86d1-efc9261471a5"
api_key: dummy
- model_name: "byok-wildcard/*"
litellm_params:
model: openai/*
- model_name: xai-grok-3
litellm_params:
model: xai/grok-3
- model_name: hosted_vllm/whisper-v3
litellm_params:
model: hosted_vllm/whisper-v3
api_base: "https://webhook.site/2f385e05-00aa-402b-86d1-efc9261471a5"
api_key: dummy

# mcp_servers:
# github_mcp:
# url: "https://api.githubcopilot.com/mcp"
# auth_type: oauth2
# authorization_url: https://github.com/login/oauth/authorize
# token_url: https://github.com/login/oauth/access_token
# client_id: os.environ/GITHUB_OAUTH_CLIENT_ID
# client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET
# scopes: ["public_repo", "user:email"]
# allowed_tools: ["list_tools"]
# # disallowed_tools: ["repo_delete"]

litellm_settings:
callbacks: ["prometheus"]
custom_prometheus_metadata_labels: ["metadata.initiative", "metadata.business-unit"]
model: azure/gpt-5-mini-2
api_key: os.environ/AZURE_API_KEY_ALT
api_base: os.environ/AZURE_API_BASE_ALT
54 changes: 53 additions & 1 deletion tests/llm_responses_api_testing/test_azure_responses_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
from base_responses_api import BaseResponsesAPITest


class TestAzureResponsesAPITest(BaseResponsesAPITest):
def get_base_completion_call_args(self):
return {
Expand All @@ -43,4 +44,55 @@ async def test_azure_responses_api_preview_api_version():
api_base=os.getenv("AZURE_RESPONSES_OPENAI_ENDPOINT"),
api_key=os.getenv("AZURE_RESPONSES_OPENAI_API_KEY"),
input="Hello, can you tell me a short joke?",
)
)


@pytest.mark.asyncio
async def test_azure_responses_api_status_error():
"""
Ensure new azure preview api version is working
"""
litellm._turn_on_debug()

request_data = {
"model": "gpt-5-mini",
"input": [
{"content": "tell me an interesting fact", "role": "user"},
{
"id": "rs_0ab687487834d9df0068e462a1b2d88197aabbc832c9ba5316",
"summary": [],
"type": "reasoning",
"content": None,
"encrypted_content": None,
"status": "completed",
},
{
"id": "msg_0ab687487834d9df0068e462a1df188197b74b1eef05102c18",
"content": [
{
"annotations": [],
"text": "Octopuses have three hearts: two pump blood to the gills, while the third pumps it to the rest of the body. Even more unusual, their blood is blue because it uses the copper-containing protein hemocyanin to carry oxygen, which is more efficient than hemoglobin in cold, low-oxygen environments.",
"type": "output_text",
"logprobs": [],
}
],
"role": "assistant",
"status": "completed",
"type": "message",
},
{"role": "user", "content": "tell me another"},
],
"include": [],
"instructions": "You are a helpful assistant.",
"reasoning": {"effort": "minimal"},
"stream": False,
"tools": [],
}
response = await litellm.aresponses(
model="azure/gpt-5-mini-2",
truncation="auto",
api_version="preview",
api_base=os.getenv("AZURE_GPT5_MINI_API_BASE"),
api_key=os.getenv("AZURE_GPT5_MINI_API_KEY"),
input=request_data["input"],
)
Loading
Loading