Skip to content
Open
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
31 changes: 30 additions & 1 deletion litellm/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

## LiteLLM versions of the OpenAI Exception Types

from typing import Optional
from typing import Any, Dict, Optional

import httpx
import openai
Expand Down Expand Up @@ -1016,6 +1016,35 @@ def __repr__(self):
return self.__str__()


class ModifyResponseException(Exception):
"""
Exception raised when a guardrail wants to modify the response.

This exception carries the synthetic response that should be returned
to the user instead of calling the LLM or instead of the LLM's response.
It should be caught by the proxy and returned with a 200 status code.

This is a base exception that all guardrails can use to replace responses,
allowing violation messages to be returned as successful responses
rather than errors.
"""

def __init__(
self,
message: str,
model: str,
request_data: Dict[str, Any],
guardrail_name: Optional[str] = None,
detection_info: Optional[Dict[str, Any]] = None,
):
self.message = message
self.model = model
self.request_data = request_data
self.guardrail_name = guardrail_name
self.detection_info = detection_info or {}
super().__init__(message)


class GuardrailInterventionNormalStringError(
Exception
): # custom exception to raise when a guardrail intervenes, but we want to return a normal string to the user
Expand Down
47 changes: 8 additions & 39 deletions litellm/integrations/custom_guardrail.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,43 +41,7 @@
dc = DualCache()


class ModifyResponseException(Exception):
"""
Exception raised when a guardrail wants to modify the response.

This exception carries the synthetic response that should be returned
to the user instead of calling the LLM or instead of the LLM's response.
It should be caught by the proxy and returned with a 200 status code.

This is a base exception that all guardrails can use to replace responses,
allowing violation messages to be returned as successful responses
rather than errors.
"""

def __init__(
self,
message: str,
model: str,
request_data: Dict[str, Any],
guardrail_name: Optional[str] = None,
detection_info: Optional[Dict[str, Any]] = None,
):
"""
Initialize the modify response exception.

Args:
message: The violation message to return to the user
model: The model that was being called
request_data: The original request data
guardrail_name: Name of the guardrail that raised this exception
detection_info: Additional detection metadata (scores, rules, etc.)
"""
self.message = message
self.model = model
self.request_data = request_data
self.guardrail_name = guardrail_name
self.detection_info = detection_info or {}
super().__init__(message)
from litellm.exceptions import ModifyResponseException as ModifyResponseException

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'ModifyResponseException' may not be defined if module
litellm.exceptions
is imported before module
litellm.integrations.custom_guardrail
, as the
definition
of ModifyResponseException occurs after the cyclic
import
of litellm.integrations.custom_guardrail.
'ModifyResponseException' may not be defined if module
litellm.exceptions
is imported before module
litellm.integrations.custom_guardrail
, as the
definition
of ModifyResponseException occurs after the cyclic
import
of litellm.integrations.custom_guardrail.


class CustomGuardrail(CustomLogger):
Expand Down Expand Up @@ -417,7 +381,9 @@
"""
requested_guardrails = self.get_guardrail_from_metadata(data)
disable_global_guardrail = self.get_disable_global_guardrail(data)
opted_out_global_guardrails = self.get_opted_out_global_guardrails_from_metadata(data)
opted_out_global_guardrails = (
self.get_opted_out_global_guardrails_from_metadata(data)
)
verbose_logger.debug(
"inside should_run_guardrail for guardrail=%s event_type= %s guardrail_supported_event_hooks= %s requested_guardrails= %s self.default_on= %s",
self.guardrail_name,
Expand All @@ -426,7 +392,10 @@
requested_guardrails,
self.default_on,
)
if self.default_on is True and self.guardrail_name in opted_out_global_guardrails:
if (
self.default_on is True
and self.guardrail_name in opted_out_global_guardrails
):
return False

if self.default_on is True and disable_global_guardrail is not True:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from litellm._logging import verbose_proxy_logger
from litellm.caching.caching import DualCache
from litellm.cost_calculator import _infer_call_type
from litellm.integrations.custom_guardrail import CustomGuardrail

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module
litellm.integrations.custom_guardrail
is imported before module
litellm.proxy.guardrails.guardrail_hooks.unified_guardrail.unified_guardrail
, as the
definition
of CustomGuardrail occurs after the cyclic
import
of unified_guardrail.unified_guardrail.
'CustomGuardrail' may not be defined if module [litellm.integrations.custom_guardrail](
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.api_route_to_call_types import get_call_types_for_route
from litellm.llms import load_guardrail_translation_mappings
Expand Down Expand Up @@ -227,6 +227,16 @@
if call_type is None:
call_type = _infer_call_type(call_type=None, completion_response=response) # type: ignore

# Fallback: resolve call_type from logging_obj for pass-through endpoints
if call_type is None:
litellm_logging_obj = data.get("litellm_logging_obj")
if (
litellm_logging_obj is not None
and getattr(litellm_logging_obj, "call_type", None)
== CallTypes.pass_through.value
):

Check warning

Code scanning / CodeQL

Log Injection Medium

This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a
user-provided value
.
This log entry depends on a [user-p
call_type = CallTypes.pass_through.value

if call_type is None:
return response

Expand Down
78 changes: 73 additions & 5 deletions litellm/proxy/pass_through_endpoints/pass_through_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@
custom_llm_provider: Optional field - custom LLM provider for the endpoint
guardrails_config: Optional field - guardrails configuration for passthrough endpoint
"""
from litellm.exceptions import ModifyResponseException

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
litellm.exceptions
begins an import cycle.
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.proxy.pass_through_endpoints.passthrough_guardrails import (
PassthroughGuardrailHandler,
Expand Down Expand Up @@ -915,8 +916,41 @@

content = await response.aread()

## LOG SUCCESS
## POST-CALL GUARDRAILS ##
_content_modified = False
response_body: Optional[dict] = get_response_body(response)
if response_body is not None and guardrails_to_run:
# Build an enriched data dict: _parsed_body has been stripped of
# `metadata` by both pre_call_hook and _init_kwargs_for_pass_through_endpoint,
# so we re-attach the configured guardrails here so should_run_guardrail
# sees them.
hook_data = dict(_parsed_body or {})
existing_metadata = hook_data.get("metadata")
if not isinstance(existing_metadata, dict):
existing_metadata = {}
hook_data["metadata"] = {
**existing_metadata,
"guardrails": guardrails_to_run,
}
response_body = await proxy_logging_obj.post_call_success_hook(
data=hook_data,
user_api_key_dict=user_api_key_dict,
response=response_body, # type: ignore[arg-type]
)
if isinstance(response_body, dict):
content = json.dumps(response_body).encode("utf-8")
_content_modified = True
else:
verbose_proxy_logger.debug(
"pass_through_endpoint: post_call_success_hook returned %s, expected dict — using original response",
type(response_body).__name__,
)
elif response_body is None:
verbose_proxy_logger.debug(
"pass_through_endpoint: response body not JSON-parseable, skipping post-call guardrails"
)

## LOG SUCCESS
passthrough_logging_payload["response_body"] = response_body
end_time = datetime.now()
asyncio.create_task(
Expand Down Expand Up @@ -944,13 +978,47 @@
api_base=str(url._uri_reference),
)

response_headers = HttpPassThroughEndpointHelpers.get_response_headers(
headers=response.headers,
custom_headers=custom_headers,
)
if _content_modified:
response_headers.pop("content-length", None)

return Response(
content=content,
status_code=response.status_code,
headers=HttpPassThroughEndpointHelpers.get_response_headers(
headers=response.headers,
custom_headers=custom_headers,
),
headers=response_headers,
)
except ModifyResponseException as e:
verbose_proxy_logger.info(
"pass_through_endpoint: Guardrail %s modified response: %s",
e.guardrail_name,
str(e.message or "")[:200],
)
try:
await proxy_logging_obj.post_call_failure_hook(
user_api_key_dict=user_api_key_dict,
original_exception=e,
request_data=e.request_data,
)
except Exception:
verbose_proxy_logger.warning(
"pass_through_endpoint: post_call_failure_hook raised during guardrail block",
exc_info=True,
)
error_body = {
"error": {
"message": e.message or "Response blocked by guardrail",
"type": "content_filter",
"guardrail_name": e.guardrail_name,
"model": e.model,
}
}
return Response(
content=json.dumps(error_body),
status_code=200,
media_type="application/json",
)
except Exception as e:
custom_headers = ProxyBaseLLMRequestProcessing.get_custom_headers(
Expand Down
Loading
Loading