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>
This commit is contained in:
yassin 2026-09-16 00:22:48 +00:00
parent b7af51cc4a
commit 241b177f05
4 changed files with 26 additions and 2 deletions

View file

@ -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,

View file

@ -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 = (

View file

@ -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:

View file

@ -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()