From 241b177f05a9dfa56621eea65f85ee24294e4d1f Mon Sep 17 00:00:00 2001 From: yassin Date: Wed, 16 Sep 2026 00:22:48 +0000 Subject: [PATCH] fix(proxy): log the internal model access denial reason for MCP sampling denials MCP sampling catches the denial itself and returns ErrorData, so the central ProxyException handler never sees it. Log the sanitized internal reason there and share the CR/LF stripping through ModelAccessDeniedProxyException.sanitized_internal_message Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../mcp_server/sampling_handler.py | 5 ++++- litellm/proxy/_types.py | 3 +++ litellm/proxy/proxy_server.py | 2 +- .../test_mcp_sampling_model_access.py | 18 ++++++++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/sampling_handler.py b/litellm/proxy/_experimental/mcp_server/sampling_handler.py index 125dc3d773d..6072e747bfe 100644 --- a/litellm/proxy/_experimental/mcp_server/sampling_handler.py +++ b/litellm/proxy/_experimental/mcp_server/sampling_handler.py @@ -771,6 +771,7 @@ async def _check_model_access(model: str, user_api_key_auth: "UserAPIKeyAuth | N try: import litellm + from litellm.proxy._types import ModelAccessDeniedProxyException from litellm.proxy.auth.auth_checks import ( _check_team_member_model_access, can_key_call_model, @@ -887,7 +888,9 @@ async def _check_model_access(model: str, user_api_key_auth: "UserAPIKeyAuth | N verbose_logger.warning( "MCP sampling: model access denied for model=%s: %s", model, - access_err, + access_err.sanitized_internal_message() + if isinstance(access_err, ModelAccessDeniedProxyException) + else access_err, ) return ErrorData( code=-1, diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 89a4f583dcd..e9bfb7ab5ab 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -4042,6 +4042,9 @@ class ModelAccessDeniedProxyException(ProxyException): super().__init__(message=message, type=type, param=param, code=code) self.internal_message: Final = internal_message + def sanitized_internal_message(self) -> str: + return self.internal_message.replace("\r", "").replace("\n", "") + class CommonProxyErrors(str, enum.Enum): db_not_connected_error = ( diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9deb70a9277..12d62f6620d 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1684,7 +1684,7 @@ async def openai_exception_handler(request: Request, exc: ProxyException): def _log_model_access_denial(exc: ProxyException) -> None: if not litellm.model_access_denied_message or not isinstance(exc, ModelAccessDeniedProxyException): return - verbose_proxy_logger.warning(exc.internal_message.replace("\r", "").replace("\n", "")) + verbose_proxy_logger.warning(exc.sanitized_internal_message()) def _close_dangling_otel_server_span(request: Request, status_code: int, exc: Exception | None = None) -> None: diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_sampling_model_access.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_sampling_model_access.py index f141cb2e316..d98db5518c3 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_sampling_model_access.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_sampling_model_access.py @@ -137,6 +137,24 @@ class TestCheckModelAccess: assert result.code == -1 assert "claude-3-opus-20240229" in result.message + @pytest.mark.asyncio + async def test_should_log_internal_denial_reason_when_client_message_is_configured(self, monkeypatch, caplog): + import litellm + from litellm.proxy._types import UserAPIKeyAuth + + monkeypatch.setattr(litellm, "model_access_denied_message", "The model `{model}` is unavailable for this API key.") + auth = UserAPIKeyAuth(api_key="sk-test-key", models=["gpt-3.5-turbo"]) + + with caplog.at_level("WARNING", logger="LiteLLM"): + result = await _check_model_access("gpt-4o\r\nforged", user_api_key_auth=auth) + + assert result is not None + assert "gpt-4o\r\nforged" in result.message + assert "gpt-3.5-turbo" not in result.message + denial_records = [r for r in caplog.records if "gpt-3.5-turbo" in r.getMessage()] + assert len(denial_records) == 1 + assert "Tried to access gpt-4oforged" in denial_records[0].getMessage() + @pytest.mark.asyncio async def test_should_deny_empty_oauth_passthrough_placeholder(self): """Regression: process_mcp_request() returns an empty UserAPIKeyAuth()