Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a6943ed
feat(guardrails): add headroom guardrail for message compression
krrish-berri-2 Jun 26, 2026
4534060
fix(guardrails/headroom): add @log_guardrail_information to populate …
krrish-berri-2 Jun 26, 2026
1787704
style: fix ruff format violations
krrish-berri-2 Jun 26, 2026
47759cb
fix(lint): replace deprecated typing aliases with builtin generics (U…
krrish-berri-2 Jun 26, 2026
19b56b8
fix(guardrails): only write back structured_messages when guardrail a…
krrish-berri-2 Jun 26, 2026
3c528a1
fix(guardrails/headroom): raise 502 when compression returns empty me…
krrish-berri-2 Jun 26, 2026
633d1db
fix(guardrails/headroom): catch transport errors and fix stale debug log
krrish-berri-2 Jun 26, 2026
2ccd027
fix(guardrails/anthropic): strip system messages before anthropic_mes…
krrish-berri-2 Jun 26, 2026
f46bdcf
fix(guardrails/anthropic): strip cache_control from thinking blocks a…
krrish-berri-2 Jun 26, 2026
41a1f37
debug(headroom): add INFO logging to trace guardrail execution
krrish-berri-2 Jun 26, 2026
225a556
debug(headroom): use print() for immediate visibility
krrish-berri-2 Jun 26, 2026
c7c1d28
debug(headroom): print request_data keys to diagnose metadata dict mi…
krrish-berri-2 Jun 26, 2026
7563aac
fix(guardrails/anthropic): propagate guardrail info to logging_obj.me…
krrish-berri-2 Jun 26, 2026
b84db5a
fix: use model_call_details litellm_params metadata on Logging object
krrish-berri-2 Jun 26, 2026
75fbd64
fix(guardrails/anthropic): write guardrail info to litellm_params att…
krrish-berri-2 Jun 26, 2026
a1d5e1f
fix: read slg_info from litellm_metadata when metadata key absent
krrish-berri-2 Jun 26, 2026
2ff1f2f
fix: write slg_info to both litellm_params attr and model_call_detail…
krrish-berri-2 Jun 27, 2026
7d99315
chore: remove debug prints; fix now verified end-to-end
krrish-berri-2 Jun 27, 2026
caf42dc
refactor(guardrails): move spend-log sync to shared helper in custom_…
krrish-berri-2 Jun 27, 2026
581dde2
fix(lint): reduce _sync_guardrail_info_to_logging_obj complexity belo…
krrish-berri-2 Jun 27, 2026
f91ac7b
fix(lint): simplify _sync_guardrail_info_to_logging_obj to reduce McC…
krrish-berri-2 Jun 27, 2026
b315b27
fix(lint): extract _append_slg_to_litellm_params to reduce McCabe com…
krrish-berri-2 Jun 27, 2026
bc8c1bf
fix(lint): extract _write_back_structured_messages to reduce process_…
krrish-berri-2 Jun 27, 2026
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
45 changes: 45 additions & 0 deletions litellm/integrations/custom_guardrail.py
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,45 @@ def get_guardrails_messages_for_call_type(
return None


def _append_slg_to_litellm_params(lp: object, entries: list) -> None:
"""Merge guardrail entries into a single litellm_params dict."""
if not isinstance(lp, dict):
return
if lp.get("metadata") is None:
lp["metadata"] = {}
existing = lp["metadata"].setdefault("standard_logging_guardrail_information", [])
for entry in entries:
if entry not in existing:
existing.append(entry)


def _sync_guardrail_info_to_logging_obj(
request_data: dict, logging_obj: object
) -> None:
"""Copy standard_logging_guardrail_information from request_data into logging_obj.

The @log_guardrail_information decorator writes guardrail info to
request_data["metadata"] or request_data["litellm_metadata"]. For
passthrough routes (/v1/messages, /v1/responses) the spend-log payload is
built from logging_obj.litellm_params["metadata"], which is a separate dict
that does not share identity with the one in request_data. This helper
bridges that gap so guardrail_information is non-null in spend logs for all
routes, not just /v1/chat/completions.
"""
if logging_obj is None:
return
meta_src = (
request_data.get("metadata") or request_data.get("litellm_metadata") or {}
)
slg_info = meta_src.get("standard_logging_guardrail_information")
if not slg_info:
return
entries: list = slg_info if isinstance(slg_info, list) else [slg_info]
mcd = getattr(logging_obj, "model_call_details", None) or {}
_append_slg_to_litellm_params(getattr(logging_obj, "litellm_params", None), entries)
_append_slg_to_litellm_params(mcd.get("litellm_params"), entries)


def log_guardrail_information(func):
"""
Decorator to add standard logging guardrail information to any function
Expand Down Expand Up @@ -1153,6 +1192,7 @@ async def async_wrapper(*args, **kwargs):
if func.__name__ == "apply_guardrail" and "inputs" in kwargs:
original_inputs = kwargs.get("inputs")

logging_obj = kwargs.get("logging_obj")
entries_before = _count_recorded_guardrail_entries(request_data)
try:
response = await func(*args, **kwargs)
Expand All @@ -1178,6 +1218,8 @@ async def async_wrapper(*args, **kwargs):
duration=(datetime.now() - start_time).total_seconds(),
event_type=event_type,
)
finally:
_sync_guardrail_info_to_logging_obj(request_data, logging_obj)

@functools.wraps(func)
def sync_wrapper(*args, **kwargs):
Expand All @@ -1191,6 +1233,7 @@ def sync_wrapper(*args, **kwargs):
if func.__name__ == "apply_guardrail" and "inputs" in kwargs:
original_inputs = kwargs.get("inputs")

logging_obj = kwargs.get("logging_obj")
entries_before = _count_recorded_guardrail_entries(request_data)
try:
response = func(*args, **kwargs)
Expand All @@ -1212,6 +1255,8 @@ def sync_wrapper(*args, **kwargs):
duration=(datetime.now() - start_time).total_seconds(),
event_type=event_type,
)
finally:
_sync_guardrail_info_to_logging_obj(request_data, logging_obj)

@functools.wraps(func)
def wrapper(*args, **kwargs):
Expand Down
1 change: 0 additions & 1 deletion litellm/litellm_core_utils/litellm_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5959,7 +5959,6 @@ def get_standard_logging_object_payload(
),
standard_built_in_tools_params=standard_built_in_tools_params,
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert this whitespace-only change.

This PR removes a blank line in get_standard_logging_object_payload; it is unrelated to headroom or guardrail logging sync.

Change: restore the blank line so this file has zero diff vs base:

         )
+
         # emit_standard_logging_payload(payload) - Moved to success_handler to prevent double emitting

Keeps the PR scope clean and avoids noisy merge conflicts on an unrelated 6k-line file.


# emit_standard_logging_payload(payload) - Moved to success_handler to prevent double emitting

return payload
Expand Down
42 changes: 37 additions & 5 deletions litellm/llms/anthropic/chat/guardrail_translation/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ async def process_input_messages(
inputs["images"] = images_to_check
if tools_to_check:
inputs["tools"] = tools_to_check
original_structured_messages = structured_messages
if structured_messages:
inputs["structured_messages"] = structured_messages
# Include model information if available
Expand All @@ -175,19 +176,50 @@ async def process_input_messages(
# Note: MCP servers are handled separately in the main transformation
data["tools"] = anthropic_tools

# Step 3: Map guardrail responses back to original message structure
await self._apply_guardrail_responses_to_input(
messages=messages,
responses=guardrailed_texts,
task_mappings=task_mappings,
guardrailed_structured_messages = guardrailed_inputs.get(
"structured_messages"
)
if (
guardrailed_structured_messages is not None
and guardrailed_structured_messages is not original_structured_messages
):
self._write_back_structured_messages(
data, guardrailed_structured_messages
)
else:
# Step 3: Map guardrail responses back to original message structure
await self._apply_guardrail_responses_to_input(
messages=messages,
responses=guardrailed_texts,
Comment thread
greptile-apps[bot] marked this conversation as resolved.
task_mappings=task_mappings,
)

verbose_proxy_logger.debug(
"Anthropic Messages: Processed input messages: %s", messages
)

return data

@staticmethod
def _write_back_structured_messages(data: dict, structured_messages: list) -> None:
"""Convert compressed structured_messages back to Anthropic format and write to data."""
from litellm.litellm_core_utils.prompt_templates.factory import (
anthropic_messages_pt,
)

model = str(data.get("model") or "")
non_system = [m for m in structured_messages if m.get("role") != "system"]
converted = anthropic_messages_pt(
messages=non_system, model=model, llm_provider="anthropic"
)
for msg in converted:
content = msg.get("content")
if isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get("type") == "thinking":
block.pop("cache_control", None)
data["messages"] = converted

def extract_request_tool_names(self, data: dict) -> List[str]:
"""Extract tool names from Anthropic messages request (tools[].name)."""
names: List[str] = []
Expand Down
43 changes: 26 additions & 17 deletions litellm/llms/openai/chat/guardrail_translation/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ async def process_input_messages(
if model:
inputs["model"] = model

original_structured_messages = inputs.get("structured_messages")
guardrailed_inputs = await guardrail_to_apply.apply_guardrail(
inputs=inputs,
request_data=data,
Expand All @@ -137,26 +138,34 @@ async def process_input_messages(
if guardrailed_tools is not None:
data["tools"] = guardrailed_tools

# Step 3: Map guardrail responses back to original message structure
if guardrailed_texts and texts_to_check:
await self._apply_guardrail_responses_to_input_texts(
messages=messages,
responses=guardrailed_texts,
task_mappings=text_task_mappings,
)
guardrailed_structured_messages = guardrailed_inputs.get(
"structured_messages"
)
if (
guardrailed_structured_messages is not None
and guardrailed_structured_messages is not original_structured_messages
):
data["messages"] = guardrailed_structured_messages
else:
# Step 3: Map guardrail responses back to original message structure
if guardrailed_texts and texts_to_check:
await self._apply_guardrail_responses_to_input_texts(
messages=messages,
responses=guardrailed_texts,
task_mappings=text_task_mappings,
)

# Step 4: Apply guardrailed tool calls back to messages
if guardrailed_tool_calls:
# Note: The guardrail may modify tool_calls_to_check in place
# or we may need to handle returned tool calls differently
await self._apply_guardrail_responses_to_input_tool_calls(
messages=messages,
tool_calls=guardrailed_tool_calls, # type: ignore
task_mappings=tool_call_task_mappings,
)
# Step 4: Apply guardrailed tool calls back to messages
if guardrailed_tool_calls:
await self._apply_guardrail_responses_to_input_tool_calls(
messages=messages,
tool_calls=guardrailed_tool_calls, # type: ignore
task_mappings=tool_call_task_mappings,
)

verbose_proxy_logger.debug(
"OpenAI Chat Completions: Processed input messages: %s", messages
"OpenAI Chat Completions: Processed input messages: %s",
data.get("messages"),
)

return data
Expand Down
52 changes: 52 additions & 0 deletions litellm/proxy/guardrails/guardrail_hooks/headroom/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from litellm.types.guardrails import (
GuardrailEventHooks,
Mode,
SupportedGuardrailIntegrations,
)

from .headroom import HeadroomGuardrail

if TYPE_CHECKING:
from litellm.types.guardrails import Guardrail, LitellmParams


def _coerce_event_hook(
mode: str | list[str] | Mode,
) -> GuardrailEventHooks | list[GuardrailEventHooks] | Mode:
if isinstance(mode, Mode):
return mode
if isinstance(mode, list):
return [GuardrailEventHooks(item) for item in mode]
return GuardrailEventHooks(mode)


def initialize_guardrail(
litellm_params: LitellmParams, guardrail: Guardrail
) -> HeadroomGuardrail:
import litellm

_callback = HeadroomGuardrail(
api_base=litellm_params.api_base,
api_key=litellm_params.api_key,
model=litellm_params.model,
guardrail_name=guardrail["guardrail_name"],
event_hook=_coerce_event_hook(litellm_params.mode),
default_on=litellm_params.default_on or False,
)
litellm.logging_callback_manager.add_litellm_callback( # pyright: ignore[reportUnknownMemberType]
_callback
)
return _callback


guardrail_initializer_registry = {
SupportedGuardrailIntegrations.HEADROOM.value: initialize_guardrail,
}

guardrail_class_registry = {
SupportedGuardrailIntegrations.HEADROOM.value: HeadroomGuardrail,
}
Loading
Loading