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
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ async def apply_guardrail(inputs, request_data, input_type):
from fastapi import HTTPException

from litellm._logging import verbose_proxy_logger
from litellm.exceptions import ModifyResponseException
from litellm.integrations.custom_guardrail import (
CustomGuardrail,
log_guardrail_information,
Expand Down Expand Up @@ -253,6 +254,9 @@ async def apply_guardrail(
except HTTPException:
# Re-raise HTTP exceptions (from block action)
raise
except ModifyResponseException:
# Pre-call block uses passthrough; must not wrap as execution error (500)
raise
except Exception as e:
verbose_proxy_logger.error(
f"Custom code guardrail '{self.guardrail_name}' execution error: {e}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1202,7 +1202,7 @@ def _handle_conditional_match(
)
verbose_proxy_logger.warning(error_msg)
raise HTTPException(
status_code=403,
status_code=400,
detail={
"error": error_msg,
"category": category_name,
Expand Down Expand Up @@ -1242,7 +1242,7 @@ def _handle_category_keyword_match(
)
verbose_proxy_logger.warning(error_msg)
raise HTTPException(
status_code=403,
status_code=400,
detail={
"error": error_msg,
"category": category_name,
Expand Down Expand Up @@ -1285,7 +1285,7 @@ def _handle_pattern_match(
error_msg = f"Content blocked: {pattern_name} pattern detected"
verbose_proxy_logger.warning(error_msg)
raise HTTPException(
status_code=403,
status_code=400,
detail={"error": error_msg, "pattern": pattern_name},
)
elif action == ContentFilterAction.MASK:
Expand Down Expand Up @@ -1325,7 +1325,7 @@ def _handle_blocked_word_match(
error_msg += f" ({description})"
verbose_proxy_logger.warning(error_msg)
raise HTTPException(
status_code=403,
status_code=400,
detail={
"error": error_msg,
"keyword": keyword,
Expand Down Expand Up @@ -1677,7 +1677,7 @@ def _apply_competitor_intent_policy(
"ContentFilterGuardrail: competitor intent refuse - %s", intent_val
)
raise HTTPException(
status_code=403,
status_code=400,
detail={
"error": msg,
"intent": intent_val,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def _run(checker, text: str) -> dict:
checker.check(text)
return {"decision": "ALLOW", "score": 0.0, "matched_topic": None}
except HTTPException as e:
if e.status_code == 403:
if e.status_code == 400:
detail: Dict[str, Any] = e.detail if isinstance(e.detail, dict) else {}
return {
"decision": "BLOCK",
Expand Down Expand Up @@ -542,7 +542,7 @@ def check(self, text: str) -> str:

if "BLOCK" in decision:
raise HTTPException(
status_code=403,
status_code=400,
detail={
"error": "Content blocked by LLM judge",
"topic": "financial_advice",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ async def test_apply_guardrail_with_competitor_intent_refuse(self):
await guardrail.apply_guardrail(
inputs, request_data={}, input_type="request"
)
assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400


# Exact config from litellm/proxy/_new_secret_config.yaml (lines 27-53).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ async def test_apply_guardrail_block(self):
input_type="request",
)

assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400
assert "us_ssn" in str(exc_info.value.detail)

@pytest.mark.asyncio
Expand Down Expand Up @@ -563,7 +563,7 @@ async def mock_stream():
):
pass

assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400
assert "us_ssn" in str(exc_info.value.detail)

@pytest.mark.asyncio
Expand Down Expand Up @@ -1010,7 +1010,7 @@ async def test_apply_guardrail_blocks_on_any_blocked_keyword(self):
input_type="request",
)

assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400
assert "danger_word" in str(exc_info.value.detail)

@pytest.mark.asyncio
Expand Down Expand Up @@ -1298,7 +1298,7 @@ async def test_harm_toxic_abuse_blocks_abusive_input(self):
input_type="request",
)

assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400
detail = exc_info.value.detail
if isinstance(detail, dict):
assert detail.get("category") == "harm_toxic_abuse"
Expand Down Expand Up @@ -1327,7 +1327,7 @@ async def test_harm_toxic_abuse_blocks_sht_ai(self):
input_type="request",
)

assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400
detail = exc_info.value.detail
if isinstance(detail, dict):
assert detail.get("category") == "harm_toxic_abuse"
Expand Down Expand Up @@ -1375,7 +1375,7 @@ async def test_category_keywords_with_asterisks_match_actual_text(self):
input_type="request",
)

assert exc_info.value.status_code == 403, f"Failed to block: '{test_input}'"
assert exc_info.value.status_code == 400, f"Failed to block: '{test_input}'"
detail = exc_info.value.detail
if isinstance(detail, dict):
assert detail.get("category") == "harm_toxic_abuse"
Expand Down Expand Up @@ -1443,7 +1443,7 @@ async def test_blocked_words_with_asterisks_custom(self):
input_type="request",
)

assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400
assert "te*st" in str(exc_info.value.detail)

def test_check_category_keywords_asterisk_pattern_matching(self):
Expand Down Expand Up @@ -1510,7 +1510,7 @@ async def test_nsfw_keywords_in_harm_toxic_abuse(self):
input_type="request",
)

assert exc_info.value.status_code == 403, f"Failed to block: '{test_input}'"
assert exc_info.value.status_code == 400, f"Failed to block: '{test_input}'"
detail = exc_info.value.detail
if isinstance(detail, dict):
assert detail.get("category") == "harm_toxic_abuse"
Expand Down Expand Up @@ -1560,7 +1560,7 @@ async def test_solicitation_keywords_block_escort_requests(self):
input_type="request",
)

assert exc_info.value.status_code == 403, f"Failed to block: '{test_input}'"
assert exc_info.value.status_code == 400, f"Failed to block: '{test_input}'"
detail = exc_info.value.detail
if isinstance(detail, dict):
assert detail.get("category") == "harm_toxic_abuse"
Expand Down Expand Up @@ -1646,7 +1646,7 @@ async def test_multilanguage_harm_toxic_abuse_spanish(self):
)

assert (
exc_info.value.status_code == 403
exc_info.value.status_code == 400
), f"Failed to block Spanish: '{test_input}'"

@pytest.mark.asyncio
Expand Down Expand Up @@ -1683,7 +1683,7 @@ async def test_multilanguage_harm_toxic_abuse_french(self):
)

assert (
exc_info.value.status_code == 403
exc_info.value.status_code == 400
), f"Failed to block French: '{test_input}'"

@pytest.mark.asyncio
Expand Down Expand Up @@ -1720,7 +1720,7 @@ async def test_multilanguage_harm_toxic_abuse_german(self):
)

assert (
exc_info.value.status_code == 403
exc_info.value.status_code == 400
), f"Failed to block German: '{test_input}'"

@pytest.mark.asyncio
Expand Down Expand Up @@ -1766,7 +1766,7 @@ async def test_multilanguage_harm_toxic_abuse_australian(self):
)

assert (
exc_info.value.status_code == 403
exc_info.value.status_code == 400
), f"Failed to block Australian: '{test_input}'"

async def test_html_tags_in_messages_not_blocked(self):
Expand Down Expand Up @@ -1942,7 +1942,7 @@ async def test_conditional_child_safety_category(self):
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400
assert "harmful_child_safety" in str(exc_info.value.detail)

# Test case 2: Should BLOCK - identifier + block word combination
Expand All @@ -1956,7 +1956,7 @@ async def test_conditional_child_safety_category(self):
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400

# Test case 3: Should BLOCK - explicit content + minors
with pytest.raises(HTTPException) as exc_info:
Expand All @@ -1967,7 +1967,7 @@ async def test_conditional_child_safety_category(self):
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400

# Test case 4: Should NOT block - identifier word alone (no block word)
result = await guardrail.apply_guardrail(
Expand Down Expand Up @@ -2009,7 +2009,7 @@ async def test_conditional_child_safety_category(self):
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400

@pytest.mark.asyncio
async def test_conditional_category_sentence_boundaries(self):
Expand Down Expand Up @@ -2093,7 +2093,7 @@ async def test_conditional_racial_bias_category(self):
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400
assert "bias_racial" in str(exc_info.value.detail)

# Test case 2: Should BLOCK - identifier + dehumanizing language
Expand All @@ -2107,7 +2107,7 @@ async def test_conditional_racial_bias_category(self):
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400

# Test case 3: Should BLOCK - supremacist content
with pytest.raises(HTTPException) as exc_info:
Expand All @@ -2120,7 +2120,7 @@ async def test_conditional_racial_bias_category(self):
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400

# Test case 4: Should BLOCK - elimination rhetoric
with pytest.raises(HTTPException) as exc_info:
Expand All @@ -2133,7 +2133,7 @@ async def test_conditional_racial_bias_category(self):
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400

# Test case 5: Should NOT block - identifier word alone (no block word)
result = await guardrail.apply_guardrail(
Expand Down Expand Up @@ -2171,7 +2171,7 @@ async def test_conditional_racial_bias_category(self):
request_data={},
input_type="request",
)
assert exc_info.value.status_code == 403
assert exc_info.value.status_code == 400

# Test case 9: Should NOT block - block word alone (no identifier)
result = await guardrail.apply_guardrail(
Expand Down
46 changes: 45 additions & 1 deletion tests/test_litellm/proxy/guardrails/test_custom_code_security.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import pytest
from fastapi import HTTPException

from litellm.exceptions import ModifyResponseException
from litellm.proxy.guardrails.guardrail_hooks.custom_code.custom_code_guardrail import (
CustomCodeCompilationError,
CustomCodeGuardrail,
)


# str.mro() + generator gi_code + code.replace(co_names=...) + __setattr__
# to swap a function's bytecode and read http_get's real builtins dict.
BYTECODE_REWRITE_PAYLOAD = (
Expand Down Expand Up @@ -153,6 +154,49 @@ async def test_async_guardrail_compiles_and_runs():
assert result["texts"][0] == "test"


@pytest.mark.asyncio
async def test_custom_code_pre_call_block_uses_passthrough():
code = (
"def apply_guardrail(inputs, request_data, input_type):\n"
' return block("blocked by test")\n'
)
guardrail = _compile(code)

with pytest.raises(ModifyResponseException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": ["test"]},
request_data={"model": "test-model"},
input_type="request",
)

assert exc_info.value.message == "blocked by test"
assert exc_info.value.model == "test-model"
assert exc_info.value.guardrail_name == "t"


@pytest.mark.asyncio
async def test_custom_code_post_call_block_raises_http_400():
code = (
"def apply_guardrail(inputs, request_data, input_type):\n"
' return block("blocked by test")\n'
)
guardrail = _compile(code)

with pytest.raises(HTTPException) as exc_info:
await guardrail.apply_guardrail(
inputs={"texts": ["test"]},
request_data={"model": "test-model"},
input_type="response",
)

assert exc_info.value.status_code == 400
assert exc_info.value.detail == {
"error": "blocked by test",
"guardrail": "t",
"detection_info": {},
}


def test_typical_sync_guardrail_still_works():
code = (
"def apply_guardrail(inputs, request_data, input_type):\n"
Expand Down
Loading
Loading