mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
fix(proxy): name the blocking guardrail in x-litellm-applied-guardrails
When a guardrail hook raises, the common ProxyLogging dispatch (sequential and parallel pre_call, pipeline block, during_call and post_call metrics wrapper, streaming iterator wrapper) now records that guardrail in applied_guardrails before re-raising, and pre_call_hook folds request-declared guardrails in on its raising path. Buffered streams rebuild their response headers after the first chunk so a post_call block reached while buffering carries the blocker too Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
4b368bf066
commit
9fa85c5da5
8 changed files with 234 additions and 20 deletions
|
|
@ -2576,11 +2576,14 @@ class ProxyBaseLLMRequestProcessing:
|
|||
)
|
||||
|
||||
async def refresh_stream_headers() -> Mapping[str, str]:
|
||||
"""`custom_headers` rebuilt for whichever deployment served the stream."""
|
||||
if not getattr(response, "fallback_headers_adopted", False):
|
||||
return custom_headers
|
||||
"""`custom_headers` rebuilt once the first chunk is buffered, from `self.data` as the
|
||||
guardrails left it and for whichever deployment served the stream."""
|
||||
return self._stream_response_headers(
|
||||
hidden_params=get_hidden_params_dict(response),
|
||||
hidden_params=(
|
||||
get_hidden_params_dict(response)
|
||||
if getattr(response, "fallback_headers_adopted", False)
|
||||
else hidden_params
|
||||
),
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
logging_obj=logging_obj,
|
||||
version=version,
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ from litellm.proxy._types import (
|
|||
UserAPIKeyAuth,
|
||||
)
|
||||
from litellm.proxy.auth.route_checks import RouteChecks
|
||||
from litellm.proxy.common_utils.callback_utils import add_guardrail_to_applied_guardrails_header
|
||||
from litellm.proxy.common_utils.config_sync_pubsub import publish_config_param_change
|
||||
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
|
||||
from litellm.proxy.db.create_views import (
|
||||
|
|
@ -437,6 +438,12 @@ def _enrich_http_exception_with_guardrail_context(exc: BaseException, callback:
|
|||
detail.setdefault("guardrail_mode", event_hook)
|
||||
|
||||
|
||||
def _record_raising_guardrail(request_data: Mapping[str, object], callback: object) -> None:
|
||||
guardrail_name: Final[object] = getattr(callback, "guardrail_name", None)
|
||||
if isinstance(request_data, dict) and isinstance(guardrail_name, str):
|
||||
add_guardrail_to_applied_guardrails_header(request_data=request_data, guardrail_name=guardrail_name)
|
||||
|
||||
|
||||
def _is_client_error_exception(exc: Exception) -> bool:
|
||||
if isinstance(exc, HTTPException):
|
||||
return exc.status_code < 500
|
||||
|
|
@ -1795,13 +1802,19 @@ class ProxyLogging:
|
|||
)
|
||||
if expected_if_unmutated is not None:
|
||||
callback.mark_pre_call_hook_ran(expected_if_unmutated)
|
||||
result: Final = await self._process_guardrail_callback(
|
||||
callback=callback,
|
||||
data=input_data,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
call_type=call_type,
|
||||
event_type=GuardrailEventHooks.pre_call,
|
||||
)
|
||||
try:
|
||||
result: Final = await self._process_guardrail_callback(
|
||||
callback=callback,
|
||||
data=input_data,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
call_type=call_type,
|
||||
event_type=GuardrailEventHooks.pre_call,
|
||||
)
|
||||
except SensitiveDataRouteException:
|
||||
raise
|
||||
except Exception:
|
||||
_record_raising_guardrail(data, callback)
|
||||
raise
|
||||
if (
|
||||
scans_raw_request
|
||||
and expected_if_unmutated is not None
|
||||
|
|
@ -2031,6 +2044,7 @@ class ProxyLogging:
|
|||
callback: Final = PipelineExecutor.find_guardrail_callback(blocking_step.guardrail_name)
|
||||
if callback is not None:
|
||||
_enrich_http_exception_with_guardrail_context(original_exception, callback)
|
||||
_record_raising_guardrail(data, callback)
|
||||
raise original_exception
|
||||
|
||||
step_results_serializable: Final = [
|
||||
|
|
@ -2296,8 +2310,10 @@ class ProxyLogging:
|
|||
if data is not None:
|
||||
self._process_guardrail_metadata(data)
|
||||
return data
|
||||
except Exception as e:
|
||||
raise e
|
||||
except Exception:
|
||||
if data is not None:
|
||||
self._process_guardrail_metadata(data)
|
||||
raise
|
||||
|
||||
async def _run_parallel_pre_call_guardrails(
|
||||
self,
|
||||
|
|
@ -2355,6 +2371,8 @@ class ProxyLogging:
|
|||
# live kwargs.
|
||||
if callback.scan_raw_request and not isinstance(result, BaseException) and result is not None:
|
||||
callback.mark_pre_call_hook_ran(data)
|
||||
if isinstance(result, BaseException) and not isinstance(result, SensitiveDataRouteException):
|
||||
_record_raising_guardrail(data, callback)
|
||||
raised: Final = tuple(result for result in results if isinstance(result, BaseException))
|
||||
blocking: Final = next((exc for exc in raised if not _exception_changes_request_flow(exc)), None)
|
||||
if blocking is not None:
|
||||
|
|
@ -2433,7 +2451,12 @@ class ProxyLogging:
|
|||
break
|
||||
|
||||
@staticmethod
|
||||
async def _run_guardrail_with_metrics(callback: object, coro: Awaitable[_T], hook_type: str) -> _T:
|
||||
async def _run_guardrail_with_metrics(
|
||||
callback: object,
|
||||
coro: Awaitable[_T],
|
||||
hook_type: str,
|
||||
request_data: Mapping[str, object],
|
||||
) -> _T:
|
||||
"""
|
||||
Await `coro`, recording its latency and status to the
|
||||
`litellm_guardrail_latency_seconds` metric under `hook_type`, and
|
||||
|
|
@ -2453,6 +2476,7 @@ class ProxyLogging:
|
|||
status = "error"
|
||||
error_type = type(e).__name__
|
||||
_enrich_http_exception_with_guardrail_context(e, callback)
|
||||
_record_raising_guardrail(request_data, callback)
|
||||
raise
|
||||
finally:
|
||||
ProxyLogging._emit_guardrail_metrics(
|
||||
|
|
@ -2465,7 +2489,9 @@ class ProxyLogging:
|
|||
|
||||
@staticmethod
|
||||
async def _wrap_streaming_iterator_with_enrichment(
|
||||
callback: object, gen: AsyncGenerator[_T, None]
|
||||
callback: object,
|
||||
gen: AsyncGenerator[_T, None],
|
||||
request_data: Mapping[str, object],
|
||||
) -> AsyncGenerator[_T, None]:
|
||||
"""
|
||||
Yield from `gen`; if iteration raises an HTTPException with dict detail,
|
||||
|
|
@ -2480,6 +2506,7 @@ class ProxyLogging:
|
|||
yield chunk
|
||||
except Exception as e:
|
||||
_enrich_http_exception_with_guardrail_context(e, callback)
|
||||
_record_raising_guardrail(request_data, callback)
|
||||
raise
|
||||
|
||||
# Cache for callback-capability detection. Keyed on a signature of
|
||||
|
|
@ -2714,6 +2741,7 @@ class ProxyLogging:
|
|||
call_type=call_type,
|
||||
),
|
||||
"during_call",
|
||||
request_data=data,
|
||||
)
|
||||
return
|
||||
await self._run_guardrail_with_metrics(
|
||||
|
|
@ -2724,6 +2752,7 @@ class ProxyLogging:
|
|||
call_type=call_type,
|
||||
),
|
||||
"during_call",
|
||||
request_data=data,
|
||||
)
|
||||
|
||||
async def failed_tracking_alert(
|
||||
|
|
@ -3242,6 +3271,7 @@ class ProxyLogging:
|
|||
response=response,
|
||||
),
|
||||
"post_call",
|
||||
request_data=data,
|
||||
)
|
||||
else:
|
||||
guardrail_response = await self._run_guardrail_with_metrics(
|
||||
|
|
@ -3252,6 +3282,7 @@ class ProxyLogging:
|
|||
response=response,
|
||||
),
|
||||
"post_call",
|
||||
request_data=data,
|
||||
)
|
||||
|
||||
if guardrail_response is not None:
|
||||
|
|
@ -3315,6 +3346,7 @@ class ProxyLogging:
|
|||
response=response,
|
||||
),
|
||||
"post_call",
|
||||
request_data=data,
|
||||
)
|
||||
else:
|
||||
await self._run_guardrail_with_metrics(
|
||||
|
|
@ -3325,6 +3357,7 @@ class ProxyLogging:
|
|||
response=response,
|
||||
),
|
||||
"post_call",
|
||||
request_data=data,
|
||||
)
|
||||
|
||||
results: Final = await asyncio.gather(
|
||||
|
|
@ -3388,6 +3421,7 @@ class ProxyLogging:
|
|||
request_data=request_data,
|
||||
),
|
||||
"post_mcp_call",
|
||||
request_data=request_data,
|
||||
)
|
||||
return response
|
||||
|
||||
|
|
@ -3637,6 +3671,7 @@ class ProxyLogging:
|
|||
response=current_response,
|
||||
request_data=request_data,
|
||||
),
|
||||
request_data=request_data,
|
||||
)
|
||||
else:
|
||||
# kind == "apply_guardrail": route through unified_guardrail
|
||||
|
|
@ -3649,6 +3684,7 @@ class ProxyLogging:
|
|||
guardrail_to_apply=resolved_callback,
|
||||
buffer_until_moderated_default=(kind == "override"),
|
||||
),
|
||||
request_data=request_data,
|
||||
)
|
||||
|
||||
pipeline_translation: Final = (
|
||||
|
|
|
|||
|
|
@ -40,9 +40,12 @@ from litellm.proxy.common_request_processing import (
|
|||
_parse_event_data_for_error,
|
||||
_resolve_per_request_model_group_alias,
|
||||
_should_return_raw_model_name,
|
||||
_sse_error_frames,
|
||||
_UpstreamClosingStreamingResponse,
|
||||
create_response,
|
||||
sse_error_payload,
|
||||
)
|
||||
from litellm.proxy.common_utils.callback_utils import add_guardrail_to_applied_guardrails_header
|
||||
from litellm.proxy.dd_span_tagger import DDSpanTagger
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.proxy._types import ProxyErrorTypes, ProxyException
|
||||
|
|
@ -8952,6 +8955,60 @@ class TestStreamingResponseHeadersFollowFallback:
|
|||
assert "llm_provider-stale-marker" not in result.headers
|
||||
assert result.headers["x-callback-header"] == "kept"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_block_headers_name_the_blocking_guardrail(self, monkeypatch):
|
||||
processor_data: dict[str, object] = {"model": "oa", "stream": True, "metadata": {}}
|
||||
|
||||
def select_data_generator(**kwargs):
|
||||
async def generator():
|
||||
add_guardrail_to_applied_guardrails_header(processor_data, "stream-blocker")
|
||||
_, error_obj = sse_error_payload(HTTPException(status_code=400, detail="blocked"))
|
||||
for frame in _sse_error_frames(error_obj):
|
||||
yield frame
|
||||
|
||||
return generator()
|
||||
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.litellm_call_id = "lit-7144-call"
|
||||
logging_obj._defer_async_logging = False
|
||||
logging_obj._on_deferred_stream_complete = None
|
||||
logging_obj.cost_breakdown = None
|
||||
processor_data["litellm_logging_obj"] = logging_obj
|
||||
processor = ProxyBaseLLMRequestProcessing(data=processor_data)
|
||||
|
||||
proxy_logging_obj = MagicMock(spec=ProxyLogging)
|
||||
proxy_logging_obj.during_call_hook = AsyncMock(return_value=None)
|
||||
proxy_logging_obj.update_request_status = AsyncMock(return_value=None)
|
||||
proxy_logging_obj.post_call_success_hook = AsyncMock(
|
||||
side_effect=lambda data, user_api_key_dict, response: response
|
||||
)
|
||||
proxy_logging_obj.post_call_response_headers_hook = AsyncMock(return_value={})
|
||||
|
||||
async def fake_route_request(**kwargs):
|
||||
async def call():
|
||||
return SimpleNamespace(_hidden_params={}, fallback_headers_adopted=False)
|
||||
|
||||
return call()
|
||||
|
||||
monkeypatch.setattr(litellm.proxy.common_request_processing, "route_request", fake_route_request)
|
||||
|
||||
result = await processor.base_process_llm_request(
|
||||
request=Request(scope={"type": "http", "headers": []}),
|
||||
fastapi_response=Response(),
|
||||
user_api_key_dict=ProxyUserAPIKeyAuth(api_key="sk-test"),
|
||||
route_type="acompletion",
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
general_settings={},
|
||||
proxy_config=MagicMock(spec=ProxyConfig),
|
||||
select_data_generator=select_data_generator,
|
||||
is_streaming_request=True,
|
||||
skip_pre_call_logic=True,
|
||||
)
|
||||
|
||||
assert isinstance(result, JSONResponse)
|
||||
assert result.status_code == 400
|
||||
assert result.headers["x-litellm-applied-guardrails"] == "stream-blocker"
|
||||
|
||||
|
||||
class _MessagesFallbackStream:
|
||||
def __init__(self) -> None:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from typing import Any, Dict
|
|||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
import litellm
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
|
|
@ -84,3 +85,20 @@ async def test_during_call_hook_guardrail_error_raises(proxy_logging, make_user_
|
|||
user_api_key_dict=make_user_api_key_auth(),
|
||||
call_type="completion",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_during_call_block_names_the_blocking_guardrail_in_applied_guardrails(
|
||||
proxy_logging, make_user_api_key_auth, monkeypatch
|
||||
):
|
||||
g = _make_guardrail("blocker")
|
||||
g.async_moderation_hook = AsyncMock(side_effect=HTTPException(status_code=400, detail="blocked"))
|
||||
monkeypatch.setattr(litellm, "callbacks", [_make_guardrail("passer"), g])
|
||||
data = {"model": "m", "metadata": {}}
|
||||
with pytest.raises(HTTPException):
|
||||
await proxy_logging.during_call_hook(
|
||||
data=data,
|
||||
user_api_key_dict=make_user_api_key_auth(),
|
||||
call_type="completion",
|
||||
)
|
||||
assert "blocker" in data["metadata"]["applied_guardrails"]
|
||||
|
|
|
|||
|
|
@ -527,17 +527,19 @@ def test_handle_pipeline_result_block_enriches_with_guardrail_name_and_mode():
|
|||
result.step_results = [MagicMock(guardrail_name="g")]
|
||||
result.original_exception = original
|
||||
|
||||
data: dict[str, object] = {"model": "m"}
|
||||
saved = litellm.callbacks
|
||||
litellm.callbacks = [cb]
|
||||
try:
|
||||
with pytest.raises(HTTPException) as info:
|
||||
ProxyLogging._handle_pipeline_result(result=result, data={"model": "m"}, policy_name="p")
|
||||
ProxyLogging._handle_pipeline_result(result=result, data=data, policy_name="p")
|
||||
finally:
|
||||
litellm.callbacks = saved
|
||||
|
||||
assert info.value is original
|
||||
assert info.value.detail["guardrail_name"] == "g"
|
||||
assert info.value.detail["guardrail_mode"] == GuardrailEventHooks.pre_call
|
||||
assert data["metadata"] == {"applied_guardrails": ["g"]}
|
||||
|
||||
|
||||
def test_handle_pipeline_result_block_does_not_reraise_sensitive_data_route():
|
||||
|
|
@ -617,7 +619,7 @@ async def test_run_guardrail_with_metrics_passes_result_and_records_success(monk
|
|||
monkeypatch.setattr(litellm, "callbacks", [prom])
|
||||
|
||||
out = await ProxyLogging._run_guardrail_with_metrics(
|
||||
callback=MagicMock(guardrail_name="g"), coro=task(), hook_type="during_call"
|
||||
callback=MagicMock(guardrail_name="g"), coro=task(), hook_type="during_call", request_data={}
|
||||
)
|
||||
|
||||
assert out == {"a": 1, "b": 2, "c": 3}
|
||||
|
|
@ -643,7 +645,7 @@ async def test_run_guardrail_with_metrics_records_error_and_enriches(monkeypatch
|
|||
monkeypatch.setattr(litellm, "callbacks", [prom])
|
||||
|
||||
with pytest.raises(HTTPException):
|
||||
await ProxyLogging._run_guardrail_with_metrics(callback=cb, coro=task(), hook_type="post_call")
|
||||
await ProxyLogging._run_guardrail_with_metrics(callback=cb, coro=task(), hook_type="post_call", request_data={})
|
||||
|
||||
assert detail["guardrail_name"] == "presidio"
|
||||
recorded = prom._record_guardrail_metrics.call_args.kwargs
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from typing import Any
|
|||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
import litellm
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
|
|
@ -96,3 +97,26 @@ async def test_post_call_success_hook_guardrail_returns_modified_response(
|
|||
data={}, response={"orig": True}, user_api_key_dict=make_user_api_key_auth()
|
||||
)
|
||||
assert out == modified
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("run_in_parallel", [False, True], ids=["sequential", "parallel"])
|
||||
async def test_post_call_block_names_the_blocking_guardrail_in_applied_guardrails(
|
||||
proxy_logging, make_user_api_key_auth, monkeypatch, run_in_parallel
|
||||
):
|
||||
def _passer_that_records(data, user_api_key_dict, response):
|
||||
data["metadata"]["applied_guardrails"] = ["passer"]
|
||||
|
||||
passer = _make_guardrail("passer")
|
||||
passer.async_post_call_success_hook = AsyncMock(side_effect=_passer_that_records)
|
||||
passer.run_in_parallel = run_in_parallel
|
||||
blocker = _make_guardrail("blocker")
|
||||
blocker.async_post_call_success_hook = AsyncMock(side_effect=HTTPException(status_code=400, detail="blocked"))
|
||||
blocker.run_in_parallel = run_in_parallel
|
||||
monkeypatch.setattr(litellm, "callbacks", [passer, blocker])
|
||||
data = {"model": "m", "metadata": {}}
|
||||
with pytest.raises(HTTPException):
|
||||
await proxy_logging.post_call_success_hook(
|
||||
data=data, response=MagicMock(), user_api_key_dict=make_user_api_key_auth()
|
||||
)
|
||||
assert data["metadata"]["applied_guardrails"] == ["passer", "blocker"]
|
||||
|
|
|
|||
|
|
@ -905,3 +905,43 @@ async def test_scan_raw_request_warns_on_in_place_mutation_returning_none(
|
|||
)
|
||||
mock_logger.warning.assert_called_once()
|
||||
assert "scan_raw_request" in str(mock_logger.warning.call_args)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"blocker_kwargs",
|
||||
[
|
||||
pytest.param({}, id="sequential"),
|
||||
pytest.param({"scan_raw_request": True}, id="scan_raw_request"),
|
||||
pytest.param({"run_in_parallel": True}, id="parallel"),
|
||||
],
|
||||
)
|
||||
async def test_pre_call_block_names_the_blocking_guardrail_in_applied_guardrails(
|
||||
proxy_logging, make_user_api_key_auth, monkeypatch, blocker_kwargs
|
||||
):
|
||||
monkeypatch.setattr(litellm, "callbacks", [_BlockOnSecretGuardrail(**blocker_kwargs)])
|
||||
proxy_logging.slack_alerting_instance = MagicMock(alerting=None)
|
||||
data = _secret_request()
|
||||
with pytest.raises(HTTPException, match="blocked"):
|
||||
await proxy_logging.pre_call_hook(
|
||||
user_api_key_dict=make_user_api_key_auth(),
|
||||
data=data,
|
||||
call_type="completion",
|
||||
)
|
||||
assert data["metadata"]["applied_guardrails"] == ["blocker"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pre_call_block_keeps_request_declared_guardrail_in_applied_guardrails(
|
||||
proxy_logging, make_user_api_key_auth, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr(litellm, "callbacks", [_BlockOnSecretGuardrail(default_on=False)])
|
||||
proxy_logging.slack_alerting_instance = MagicMock(alerting=None)
|
||||
data = {**_secret_request(), "metadata": {"guardrails": ["blocker", "declared-post-call"]}}
|
||||
with pytest.raises(HTTPException, match="blocked"):
|
||||
await proxy_logging.pre_call_hook(
|
||||
user_api_key_dict=make_user_api_key_auth(),
|
||||
data=data,
|
||||
call_type="completion",
|
||||
)
|
||||
assert data["metadata"]["applied_guardrails"] == ["blocker", "declared-post-call"]
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from fastapi import HTTPException
|
|||
|
||||
import litellm
|
||||
from litellm.exceptions import GuardrailRaisedException
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.streaming_iterator import (
|
||||
|
|
@ -27,6 +28,7 @@ from litellm.llms.anthropic.experimental_pass_through.messages.streaming_iterato
|
|||
)
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
from litellm.types.utils import Usage
|
||||
|
||||
|
||||
|
|
@ -175,7 +177,7 @@ async def test_wrap_streaming_iterator_with_enrichment_passes_through_chunks(pro
|
|||
yield ch
|
||||
|
||||
cb = MagicMock(guardrail_name="g", event_hook="pre_call")
|
||||
wrapped = proxy_logging._wrap_streaming_iterator_with_enrichment(callback=cb, gen=gen())
|
||||
wrapped = proxy_logging._wrap_streaming_iterator_with_enrichment(callback=cb, gen=gen(), request_data={})
|
||||
out = [ch async for ch in wrapped]
|
||||
snapshot = {
|
||||
"chunks": out,
|
||||
|
|
@ -201,7 +203,7 @@ async def test_wrap_streaming_iterator_with_enrichment_enriches_http_exception_r
|
|||
raise HTTPException(status_code=400, detail=detail)
|
||||
|
||||
cb = MagicMock(guardrail_name="presidio", event_hook="post_call")
|
||||
wrapped = proxy_logging._wrap_streaming_iterator_with_enrichment(callback=cb, gen=boom_gen())
|
||||
wrapped = proxy_logging._wrap_streaming_iterator_with_enrichment(callback=cb, gen=boom_gen(), request_data={})
|
||||
with pytest.raises(HTTPException):
|
||||
async for _ in wrapped:
|
||||
pass
|
||||
|
|
@ -696,3 +698,35 @@ async def test_post_call_response_headers_hook_swallows_callback_error(proxy_log
|
|||
data={}, user_api_key_dict=make_user_api_key_auth(), response=response
|
||||
)
|
||||
assert out == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_guardrail_block_names_the_blocking_guardrail_in_applied_guardrails(
|
||||
proxy_logging, make_user_api_key_auth, monkeypatch
|
||||
):
|
||||
class _StreamBlocker(CustomGuardrail):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(guardrail_name="stream-blocker", event_hook=GuardrailEventHooks.post_call, default_on=True)
|
||||
|
||||
async def async_post_call_streaming_iterator_hook(
|
||||
self, user_api_key_dict: UserAPIKeyAuth, response: AsyncIterator[object], request_data: dict[str, object]
|
||||
) -> AsyncGenerator[object, None]:
|
||||
async for _ in response:
|
||||
raise HTTPException(status_code=400, detail={"error": "blocked"})
|
||||
yield # pragma: no cover
|
||||
|
||||
monkeypatch.setattr(litellm, "callbacks", [_StreamBlocker()])
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
||||
|
||||
async def upstream():
|
||||
yield "chunk"
|
||||
|
||||
request_data: dict[str, object] = {"metadata": {}}
|
||||
with pytest.raises(HTTPException):
|
||||
async for _ in proxy_logging.async_post_call_streaming_iterator_hook(
|
||||
response=upstream(),
|
||||
user_api_key_dict=make_user_api_key_auth(),
|
||||
request_data=request_data,
|
||||
):
|
||||
pass
|
||||
assert request_data["metadata"]["applied_guardrails"] == ["stream-blocker"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue