mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(proxy): type the new pipeline tests and keep tag values out of the deferral warning
Every test this PR adds now annotates its fixture and parametrize parameters. The submit-time warning for a tag-matched deferred policy names only the policies, since a wildcard attachment pattern would let caller-provided tag text reach the log.
This commit is contained in:
parent
0c58346ba9
commit
94f9230d13
3 changed files with 155 additions and 109 deletions
|
|
@ -620,19 +620,19 @@ def _defer_post_call_pipelines(
|
||||||
"retrieval re-matches only the key, team, and model scopes, so a tag carried in the request body "
|
"retrieval re-matches only the key, team, and model scopes, so a tag carried in the request body "
|
||||||
"does not govern the completed response: %s",
|
"does not govern the completed response: %s",
|
||||||
response.id,
|
response.id,
|
||||||
", ".join(f"{policy_name} ({source})" for policy_name, source in tag_matched),
|
", ".join(tag_matched),
|
||||||
)
|
)
|
||||||
_withdraw_deferred_claims(data, deferred)
|
_withdraw_deferred_claims(data, deferred)
|
||||||
|
|
||||||
|
|
||||||
def _tag_matched_deferrals(
|
def _tag_matched_deferrals(
|
||||||
data: Mapping[str, object], deferred: Sequence[tuple[str, "GuardrailPipeline"]]
|
data: Mapping[str, object], deferred: Sequence[tuple[str, "GuardrailPipeline"]]
|
||||||
) -> tuple[tuple[str, str], ...]:
|
) -> tuple[str, ...]:
|
||||||
sources: Final = _policy_state_metadata(data).get("policy_sources")
|
sources: Final = _policy_state_metadata(data).get("policy_sources")
|
||||||
if not isinstance(sources, dict):
|
if not isinstance(sources, dict):
|
||||||
return ()
|
return ()
|
||||||
return tuple(
|
return tuple(
|
||||||
(policy_name, str(sources[policy_name]))
|
policy_name
|
||||||
for policy_name, _pipeline in deferred
|
for policy_name, _pipeline in deferred
|
||||||
if policy_name in sources and "tag:" in str(sources[policy_name])
|
if policy_name in sources and "tag:" in str(sources[policy_name])
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Mapping
|
from collections.abc import Iterator, Mapping
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
@ -60,7 +60,7 @@ def _pipeline_policy(guardrail: str, mode: str = "post_call") -> dict[str, objec
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def policy_engine():
|
def policy_engine() -> Iterator[None]:
|
||||||
policy_registry = get_policy_registry()
|
policy_registry = get_policy_registry()
|
||||||
attachment_registry = get_attachment_registry()
|
attachment_registry = get_attachment_registry()
|
||||||
policy_registry.load_policies(
|
policy_registry.load_policies(
|
||||||
|
|
@ -97,7 +97,7 @@ def _attached_pipelines(data: Mapping[str, object]) -> tuple[tuple[str, str], ..
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_attaches_model_scoped_post_call_pipeline_to_retrieval(policy_engine):
|
def test_attaches_model_scoped_post_call_pipeline_to_retrieval(policy_engine: None) -> None:
|
||||||
data = _retrieval_data(GOVERNED_MODEL_ID)
|
data = _retrieval_data(GOVERNED_MODEL_ID)
|
||||||
|
|
||||||
attach_post_call_pipelines_to_retrieval(data=data, user_api_key_dict=UserAPIKeyAuth(), llm_router=_router())
|
attach_post_call_pipelines_to_retrieval(data=data, user_api_key_dict=UserAPIKeyAuth(), llm_router=_router())
|
||||||
|
|
@ -111,7 +111,7 @@ def test_attaches_model_scoped_post_call_pipeline_to_retrieval(policy_engine):
|
||||||
assert "guardrails" not in data["litellm_metadata"]
|
assert "guardrails" not in data["litellm_metadata"]
|
||||||
|
|
||||||
|
|
||||||
def test_key_and_team_context_also_governs_retrieval(policy_engine):
|
def test_key_and_team_context_also_governs_retrieval(policy_engine: None) -> None:
|
||||||
data = _retrieval_data(UNGOVERNED_MODEL_ID)
|
data = _retrieval_data(UNGOVERNED_MODEL_ID)
|
||||||
|
|
||||||
attach_post_call_pipelines_to_retrieval(
|
attach_post_call_pipelines_to_retrieval(
|
||||||
|
|
@ -121,7 +121,7 @@ def test_key_and_team_context_also_governs_retrieval(policy_engine):
|
||||||
assert _attached_pipelines(data) == (("team-governance", "team-word-filter"),)
|
assert _attached_pipelines(data) == (("team-governance", "team-word-filter"),)
|
||||||
|
|
||||||
|
|
||||||
def test_tag_attached_policy_is_not_re_matched_when_the_retrieval_carries_no_tag(policy_engine):
|
def test_tag_attached_policy_is_not_re_matched_when_the_retrieval_carries_no_tag(policy_engine: None) -> None:
|
||||||
data = _retrieval_data(GOVERNED_MODEL_ID)
|
data = _retrieval_data(GOVERNED_MODEL_ID)
|
||||||
|
|
||||||
attach_post_call_pipelines_to_retrieval(data=data, user_api_key_dict=UserAPIKeyAuth(), llm_router=_router())
|
attach_post_call_pipelines_to_retrieval(data=data, user_api_key_dict=UserAPIKeyAuth(), llm_router=_router())
|
||||||
|
|
@ -129,7 +129,7 @@ def test_tag_attached_policy_is_not_re_matched_when_the_retrieval_carries_no_tag
|
||||||
assert _attached_pipelines(data) == (("response-governance", "output-word-filter"),)
|
assert _attached_pipelines(data) == (("response-governance", "output-word-filter"),)
|
||||||
|
|
||||||
|
|
||||||
def test_tag_attached_policy_governs_a_retrieval_whose_metadata_carries_the_tag(policy_engine):
|
def test_tag_attached_policy_governs_a_retrieval_whose_metadata_carries_the_tag(policy_engine: None) -> None:
|
||||||
data: dict[str, object] = {
|
data: dict[str, object] = {
|
||||||
"response_id": _encoded_response_id(UNGOVERNED_MODEL_ID),
|
"response_id": _encoded_response_id(UNGOVERNED_MODEL_ID),
|
||||||
"litellm_metadata": {"tags": ["governed"]},
|
"litellm_metadata": {"tags": ["governed"]},
|
||||||
|
|
@ -141,7 +141,7 @@ def test_tag_attached_policy_governs_a_retrieval_whose_metadata_carries_the_tag(
|
||||||
assert data["litellm_metadata"]["policy_sources"] == {"tag-governance": "tag:governed"}
|
assert data["litellm_metadata"]["policy_sources"] == {"tag-governance": "tag:governed"}
|
||||||
|
|
||||||
|
|
||||||
def test_retrieval_of_an_ungoverned_model_attaches_nothing(policy_engine):
|
def test_retrieval_of_an_ungoverned_model_attaches_nothing(policy_engine: None) -> None:
|
||||||
data = _retrieval_data(UNGOVERNED_MODEL_ID)
|
data = _retrieval_data(UNGOVERNED_MODEL_ID)
|
||||||
|
|
||||||
attach_post_call_pipelines_to_retrieval(data=data, user_api_key_dict=UserAPIKeyAuth(), llm_router=_router())
|
attach_post_call_pipelines_to_retrieval(data=data, user_api_key_dict=UserAPIKeyAuth(), llm_router=_router())
|
||||||
|
|
@ -149,7 +149,7 @@ def test_retrieval_of_an_ungoverned_model_attaches_nothing(policy_engine):
|
||||||
assert data == _retrieval_data(UNGOVERNED_MODEL_ID)
|
assert data == _retrieval_data(UNGOVERNED_MODEL_ID)
|
||||||
|
|
||||||
|
|
||||||
def test_already_attached_policy_is_not_attached_twice(policy_engine):
|
def test_already_attached_policy_is_not_attached_twice(policy_engine: None) -> None:
|
||||||
data = _retrieval_data(GOVERNED_MODEL_ID)
|
data = _retrieval_data(GOVERNED_MODEL_ID)
|
||||||
router = _router()
|
router = _router()
|
||||||
attach_post_call_pipelines_to_retrieval(data=data, user_api_key_dict=UserAPIKeyAuth(), llm_router=router)
|
attach_post_call_pipelines_to_retrieval(data=data, user_api_key_dict=UserAPIKeyAuth(), llm_router=router)
|
||||||
|
|
@ -168,7 +168,9 @@ def _hidden_submit_model_warnings(caplog: pytest.LogCaptureFixture) -> list[str]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_wildcard_deployment_attaches_nothing_for_the_submitted_model_and_warns(policy_engine, caplog):
|
def test_wildcard_deployment_attaches_nothing_for_the_submitted_model_and_warns(
|
||||||
|
policy_engine: None, caplog: pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
data = _retrieval_data(WILDCARD_MODEL_ID)
|
data = _retrieval_data(WILDCARD_MODEL_ID)
|
||||||
|
|
||||||
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
|
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
|
||||||
|
|
@ -176,11 +178,14 @@ def test_wildcard_deployment_attaches_nothing_for_the_submitted_model_and_warns(
|
||||||
|
|
||||||
assert data == _retrieval_data(WILDCARD_MODEL_ID)
|
assert data == _retrieval_data(WILDCARD_MODEL_ID)
|
||||||
assert [
|
assert [
|
||||||
"as model group openai/* (a wildcard deployment)" in message for message in _hidden_submit_model_warnings(caplog)
|
"as model group openai/* (a wildcard deployment)" in message
|
||||||
|
for message in _hidden_submit_model_warnings(caplog)
|
||||||
] == [True]
|
] == [True]
|
||||||
|
|
||||||
|
|
||||||
def test_aliased_model_group_still_attaches_its_own_policies_and_warns(policy_engine, caplog):
|
def test_aliased_model_group_still_attaches_its_own_policies_and_warns(
|
||||||
|
policy_engine: None, caplog: pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
data = _retrieval_data(GOVERNED_MODEL_ID)
|
data = _retrieval_data(GOVERNED_MODEL_ID)
|
||||||
router = _router({"gpt-mini": GOVERNED_MODEL_GROUP, "gpt-hidden": {"model": GOVERNED_MODEL_GROUP, "hidden": True}})
|
router = _router({"gpt-mini": GOVERNED_MODEL_GROUP, "gpt-hidden": {"model": GOVERNED_MODEL_GROUP, "hidden": True}})
|
||||||
|
|
||||||
|
|
@ -194,7 +199,9 @@ def test_aliased_model_group_still_attaches_its_own_policies_and_warns(policy_en
|
||||||
] == [True]
|
] == [True]
|
||||||
|
|
||||||
|
|
||||||
def test_plain_model_group_retrieval_does_not_warn_about_the_submitted_model(policy_engine, caplog):
|
def test_plain_model_group_retrieval_does_not_warn_about_the_submitted_model(
|
||||||
|
policy_engine: None, caplog: pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
|
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
|
||||||
attach_post_call_pipelines_to_retrieval(
|
attach_post_call_pipelines_to_retrieval(
|
||||||
data=_retrieval_data(GOVERNED_MODEL_ID),
|
data=_retrieval_data(GOVERNED_MODEL_ID),
|
||||||
|
|
@ -209,7 +216,8 @@ def _ungoverned_retrieval_warnings(caplog: pytest.LogCaptureFixture) -> list[str
|
||||||
return [
|
return [
|
||||||
record.getMessage()
|
record.getMessage()
|
||||||
for record in caplog.records
|
for record in caplog.records
|
||||||
if record.levelno == logging.WARNING and "retrieved without its post_call policy pipelines" in record.getMessage()
|
if record.levelno == logging.WARNING
|
||||||
|
and "retrieved without its post_call policy pipelines" in record.getMessage()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -221,7 +229,9 @@ def _ungoverned_retrieval_warnings(caplog: pytest.LogCaptureFixture) -> list[str
|
||||||
(None, "response id names no deployment"),
|
(None, "response id names no deployment"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_unresolvable_response_id_attaches_nothing_and_warns(policy_engine, caplog, response_id, reason):
|
def test_unresolvable_response_id_attaches_nothing_and_warns(
|
||||||
|
policy_engine: None, caplog: pytest.LogCaptureFixture, response_id: str, reason: str
|
||||||
|
) -> None:
|
||||||
data = {"response_id": response_id, "litellm_metadata": {}}
|
data = {"response_id": response_id, "litellm_metadata": {}}
|
||||||
|
|
||||||
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
|
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
|
||||||
|
|
@ -231,7 +241,7 @@ def test_unresolvable_response_id_attaches_nothing_and_warns(policy_engine, capl
|
||||||
assert [message.endswith(f"({reason})") for message in _ungoverned_retrieval_warnings(caplog)] == [True]
|
assert [message.endswith(f"({reason})") for message in _ungoverned_retrieval_warnings(caplog)] == [True]
|
||||||
|
|
||||||
|
|
||||||
def test_without_a_router_attaches_nothing_and_warns(policy_engine, caplog):
|
def test_without_a_router_attaches_nothing_and_warns(policy_engine: None, caplog: pytest.LogCaptureFixture) -> None:
|
||||||
data = _retrieval_data(GOVERNED_MODEL_ID)
|
data = _retrieval_data(GOVERNED_MODEL_ID)
|
||||||
|
|
||||||
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
|
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
|
||||||
|
|
@ -241,7 +251,7 @@ def test_without_a_router_attaches_nothing_and_warns(policy_engine, caplog):
|
||||||
assert [message.endswith("(no router)") for message in _ungoverned_retrieval_warnings(caplog)] == [True]
|
assert [message.endswith("(no router)") for message in _ungoverned_retrieval_warnings(caplog)] == [True]
|
||||||
|
|
||||||
|
|
||||||
def test_without_policy_engine_attaches_nothing_quietly(caplog):
|
def test_without_policy_engine_attaches_nothing_quietly(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
get_policy_registry().clear()
|
get_policy_registry().clear()
|
||||||
data = _retrieval_data(GOVERNED_MODEL_ID)
|
data = _retrieval_data(GOVERNED_MODEL_ID)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from collections.abc import Iterator
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Any, Callable, Dict, List
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
|
@ -155,9 +156,7 @@ async def test_execute_guardrail_hook_unknown_hook_type_raises(proxy_logging, ma
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_execute_guardrail_with_load_balancing_routes_through_router(
|
async def test_execute_guardrail_with_load_balancing_routes_through_router(proxy_logging, make_user_api_key_auth):
|
||||||
proxy_logging, make_user_api_key_auth
|
|
||||||
):
|
|
||||||
cb = _make_guardrail()
|
cb = _make_guardrail()
|
||||||
router = MagicMock()
|
router = MagicMock()
|
||||||
router.get_available_guardrail = MagicMock(return_value={"callback": cb})
|
router.get_available_guardrail = MagicMock(return_value={"callback": cb})
|
||||||
|
|
@ -173,9 +172,7 @@ async def test_execute_guardrail_with_load_balancing_routes_through_router(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_execute_guardrail_with_load_balancing_router_none_raises(
|
async def test_execute_guardrail_with_load_balancing_router_none_raises(proxy_logging, make_user_api_key_auth):
|
||||||
proxy_logging, make_user_api_key_auth
|
|
||||||
):
|
|
||||||
with patch("litellm.proxy.proxy_server.llm_router", None):
|
with patch("litellm.proxy.proxy_server.llm_router", None):
|
||||||
with pytest.raises(ValueError, match="Router not initialized"):
|
with pytest.raises(ValueError, match="Router not initialized"):
|
||||||
await proxy_logging._execute_guardrail_with_load_balancing(
|
await proxy_logging._execute_guardrail_with_load_balancing(
|
||||||
|
|
@ -188,9 +185,7 @@ async def test_execute_guardrail_with_load_balancing_router_none_raises(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_execute_guardrail_with_load_balancing_no_callback_raises(
|
async def test_execute_guardrail_with_load_balancing_no_callback_raises(proxy_logging, make_user_api_key_auth):
|
||||||
proxy_logging, make_user_api_key_auth
|
|
||||||
):
|
|
||||||
router = MagicMock()
|
router = MagicMock()
|
||||||
router.get_available_guardrail = MagicMock(return_value={"callback": None})
|
router.get_available_guardrail = MagicMock(return_value={"callback": None})
|
||||||
with patch("litellm.proxy.proxy_server.llm_router", router):
|
with patch("litellm.proxy.proxy_server.llm_router", router):
|
||||||
|
|
@ -210,9 +205,7 @@ async def test_execute_guardrail_with_load_balancing_no_callback_raises(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_process_guardrail_callback_skipped_when_should_run_false(
|
async def test_process_guardrail_callback_skipped_when_should_run_false(proxy_logging, make_user_api_key_auth):
|
||||||
proxy_logging, make_user_api_key_auth
|
|
||||||
):
|
|
||||||
cb = _make_guardrail()
|
cb = _make_guardrail()
|
||||||
cb.should_run_guardrail = MagicMock(return_value=False)
|
cb.should_run_guardrail = MagicMock(return_value=False)
|
||||||
out = await proxy_logging._process_guardrail_callback(
|
out = await proxy_logging._process_guardrail_callback(
|
||||||
|
|
@ -226,9 +219,7 @@ async def test_process_guardrail_callback_skipped_when_should_run_false(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_process_guardrail_callback_returns_data_on_success(
|
async def test_process_guardrail_callback_returns_data_on_success(proxy_logging, make_user_api_key_auth, monkeypatch):
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch
|
|
||||||
):
|
|
||||||
cb = _make_guardrail()
|
cb = _make_guardrail()
|
||||||
cb.should_run_guardrail = MagicMock(return_value=True)
|
cb.should_run_guardrail = MagicMock(return_value=True)
|
||||||
proxy_logging._should_use_guardrail_load_balancing = MagicMock(return_value=False)
|
proxy_logging._should_use_guardrail_load_balancing = MagicMock(return_value=False)
|
||||||
|
|
@ -343,14 +334,14 @@ async def test_maybe_execute_pipelines_no_pipelines_returns_data(proxy_logging,
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_maybe_execute_pipelines_skips_pipelines_with_other_mode(proxy_logging, make_user_api_key_auth, monkeypatch):
|
async def test_maybe_execute_pipelines_skips_pipelines_with_other_mode(
|
||||||
|
proxy_logging, make_user_api_key_auth, monkeypatch
|
||||||
|
):
|
||||||
pipeline = MagicMock()
|
pipeline = MagicMock()
|
||||||
pipeline.mode = "post_call" # not pre_call
|
pipeline.mode = "post_call" # not pre_call
|
||||||
data = {"metadata": {"_guardrail_pipelines": [("p1", pipeline)]}, "model": "m", "messages": []}
|
data = {"metadata": {"_guardrail_pipelines": [("p1", pipeline)]}, "model": "m", "messages": []}
|
||||||
executed = MagicMock()
|
executed = MagicMock()
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr("litellm.proxy.policy_engine.pipeline_executor.PipelineExecutor.execute_steps", executed)
|
||||||
"litellm.proxy.policy_engine.pipeline_executor.PipelineExecutor.execute_steps", executed
|
|
||||||
)
|
|
||||||
out, replacement = await proxy_logging._maybe_execute_pipelines(
|
out, replacement = await proxy_logging._maybe_execute_pipelines(
|
||||||
data=data,
|
data=data,
|
||||||
user_api_key_dict=make_user_api_key_auth(),
|
user_api_key_dict=make_user_api_key_auth(),
|
||||||
|
|
@ -538,9 +529,7 @@ def test_handle_pipeline_result_block_enriches_with_guardrail_name_and_mode():
|
||||||
litellm.callbacks = [cb]
|
litellm.callbacks = [cb]
|
||||||
try:
|
try:
|
||||||
with pytest.raises(HTTPException) as info:
|
with pytest.raises(HTTPException) as info:
|
||||||
ProxyLogging._handle_pipeline_result(
|
ProxyLogging._handle_pipeline_result(result=result, data={"model": "m"}, policy_name="p")
|
||||||
result=result, data={"model": "m"}, policy_name="p"
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
litellm.callbacks = saved
|
litellm.callbacks = saved
|
||||||
|
|
||||||
|
|
@ -652,9 +641,7 @@ async def test_run_guardrail_with_metrics_records_error_and_enriches(monkeypatch
|
||||||
monkeypatch.setattr(litellm, "callbacks", [prom])
|
monkeypatch.setattr(litellm, "callbacks", [prom])
|
||||||
|
|
||||||
with pytest.raises(HTTPException):
|
with pytest.raises(HTTPException):
|
||||||
await ProxyLogging._run_guardrail_with_metrics(
|
await ProxyLogging._run_guardrail_with_metrics(callback=cb, coro=task(), hook_type="post_call")
|
||||||
callback=cb, coro=task(), hook_type="post_call"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert detail["guardrail_name"] == "presidio"
|
assert detail["guardrail_name"] == "presidio"
|
||||||
recorded = prom._record_guardrail_metrics.call_args.kwargs
|
recorded = prom._record_guardrail_metrics.call_args.kwargs
|
||||||
|
|
@ -682,9 +669,7 @@ def _moderation_guardrail() -> MagicMock:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_during_call_hook_records_latency_metric(
|
async def test_during_call_hook_records_latency_metric(proxy_logging, make_user_api_key_auth, monkeypatch):
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch
|
|
||||||
):
|
|
||||||
cb = _moderation_guardrail()
|
cb = _moderation_guardrail()
|
||||||
prom = _prometheus_callback()
|
prom = _prometheus_callback()
|
||||||
monkeypatch.setattr(litellm, "callbacks", [prom, cb])
|
monkeypatch.setattr(litellm, "callbacks", [prom, cb])
|
||||||
|
|
@ -703,9 +688,7 @@ async def test_during_call_hook_records_latency_metric(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_post_call_success_hook_records_latency_metric(
|
async def test_post_call_success_hook_records_latency_metric(proxy_logging, make_user_api_key_auth, monkeypatch):
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch
|
|
||||||
):
|
|
||||||
cb = _moderation_guardrail()
|
cb = _moderation_guardrail()
|
||||||
prom = _prometheus_callback()
|
prom = _prometheus_callback()
|
||||||
monkeypatch.setattr(litellm, "callbacks", [prom, cb])
|
monkeypatch.setattr(litellm, "callbacks", [prom, cb])
|
||||||
|
|
@ -733,9 +716,7 @@ async def test_post_call_success_hook_records_latency_metric(
|
||||||
async def test_process_prompt_template_no_op_when_no_prompt_spec(proxy_logging, monkeypatch):
|
async def test_process_prompt_template_no_op_when_no_prompt_spec(proxy_logging, monkeypatch):
|
||||||
from litellm.proxy.prompts import prompt_registry
|
from litellm.proxy.prompts import prompt_registry
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(prompt_registry.IN_MEMORY_PROMPT_REGISTRY, "resolve_prompt_spec", lambda *a, **kw: None)
|
||||||
prompt_registry.IN_MEMORY_PROMPT_REGISTRY, "resolve_prompt_spec", lambda *a, **kw: None
|
|
||||||
)
|
|
||||||
data: Dict[str, Any] = {"messages": [{"role": "user"}], "model": "m", "temperature": 0.1}
|
data: Dict[str, Any] = {"messages": [{"role": "user"}], "model": "m", "temperature": 0.1}
|
||||||
await proxy_logging._process_prompt_template(
|
await proxy_logging._process_prompt_template(
|
||||||
data=data,
|
data=data,
|
||||||
|
|
@ -760,9 +741,7 @@ async def test_process_prompt_template_applies_when_spec_resolves(proxy_logging,
|
||||||
"get_prompt_callback_for_prompt",
|
"get_prompt_callback_for_prompt",
|
||||||
lambda *a, **kw: custom_logger,
|
lambda *a, **kw: custom_logger,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(prompt_registry.IN_MEMORY_PROMPT_REGISTRY, "resolve_prompt_spec", lambda *a, **kw: prompt_spec)
|
||||||
prompt_registry.IN_MEMORY_PROMPT_REGISTRY, "resolve_prompt_spec", lambda *a, **kw: prompt_spec
|
|
||||||
)
|
|
||||||
|
|
||||||
logging_obj = MagicMock()
|
logging_obj = MagicMock()
|
||||||
logging_obj.async_get_chat_completion_prompt = AsyncMock(
|
logging_obj.async_get_chat_completion_prompt = AsyncMock(
|
||||||
|
|
@ -810,9 +789,7 @@ async def test_process_prompt_template_async_get_prompt_error_raises(proxy_loggi
|
||||||
"get_prompt_callback_for_prompt",
|
"get_prompt_callback_for_prompt",
|
||||||
lambda *a, **kw: custom_logger,
|
lambda *a, **kw: custom_logger,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(prompt_registry.IN_MEMORY_PROMPT_REGISTRY, "resolve_prompt_spec", lambda *a, **kw: prompt_spec)
|
||||||
prompt_registry.IN_MEMORY_PROMPT_REGISTRY, "resolve_prompt_spec", lambda *a, **kw: prompt_spec
|
|
||||||
)
|
|
||||||
logging_obj = MagicMock()
|
logging_obj = MagicMock()
|
||||||
logging_obj.async_get_chat_completion_prompt = AsyncMock(side_effect=RuntimeError("bad prompt"))
|
logging_obj.async_get_chat_completion_prompt = AsyncMock(side_effect=RuntimeError("bad prompt"))
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
|
|
@ -913,9 +890,7 @@ async def test_process_prompt_template_aresponses_swaps_model_and_merges_input(p
|
||||||
"get_prompt_callback_for_prompt",
|
"get_prompt_callback_for_prompt",
|
||||||
lambda *a, **kw: custom_logger,
|
lambda *a, **kw: custom_logger,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(prompt_registry.IN_MEMORY_PROMPT_REGISTRY, "resolve_prompt_spec", lambda *a, **kw: prompt_spec)
|
||||||
prompt_registry.IN_MEMORY_PROMPT_REGISTRY, "resolve_prompt_spec", lambda *a, **kw: prompt_spec
|
|
||||||
)
|
|
||||||
|
|
||||||
logging_obj = MagicMock()
|
logging_obj = MagicMock()
|
||||||
logging_obj.async_get_chat_completion_prompt = AsyncMock(
|
logging_obj.async_get_chat_completion_prompt = AsyncMock(
|
||||||
|
|
@ -1124,9 +1099,7 @@ async def test_pre_call_hook_still_runs_guardrail_managed_only_by_post_call_pipe
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
await proxy_logging.pre_call_hook(
|
await proxy_logging.pre_call_hook(user_api_key_dict=make_user_api_key_auth(), data=data, call_type="completion")
|
||||||
user_api_key_dict=make_user_api_key_auth(), data=data, call_type="completion"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert seen["count"] == 1
|
assert seen["count"] == 1
|
||||||
|
|
||||||
|
|
@ -1289,11 +1262,7 @@ async def test_post_call_pipeline_block_keeps_guardrail_metadata_writes(
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
litellm,
|
litellm,
|
||||||
"callbacks",
|
"callbacks",
|
||||||
[
|
[BlockingWriterGuardrail(guardrail_name="gr-post", event_hook=GuardrailEventHooks.post_call, default_on=False)],
|
||||||
BlockingWriterGuardrail(
|
|
||||||
guardrail_name="gr-post", event_hook=GuardrailEventHooks.post_call, default_on=False
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
||||||
data = _post_call_pipeline_data()
|
data = _post_call_pipeline_data()
|
||||||
|
|
@ -1379,9 +1348,7 @@ async def test_pre_call_pipeline_managed_parallel_guardrail_runs_exactly_once(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
await proxy_logging.pre_call_hook(
|
await proxy_logging.pre_call_hook(user_api_key_dict=make_user_api_key_auth(), data=data, call_type="completion")
|
||||||
user_api_key_dict=make_user_api_key_auth(), data=data, call_type="completion"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert seen["count"] == 1
|
assert seen["count"] == 1
|
||||||
|
|
||||||
|
|
@ -1433,14 +1400,20 @@ def _output_blocking_callbacks(seen: dict[str, object]) -> list[CustomGuardrail]
|
||||||
seen["response"] = response
|
seen["response"] = response
|
||||||
raise HTTPException(status_code=400, detail={"error": "output blocked"})
|
raise HTTPException(status_code=400, detail={"error": "output blocked"})
|
||||||
|
|
||||||
return [OutputBlockingGuardrail(guardrail_name="gr-post", event_hook=GuardrailEventHooks.post_call, default_on=False)]
|
return [
|
||||||
|
OutputBlockingGuardrail(guardrail_name="gr-post", event_hook=GuardrailEventHooks.post_call, default_on=False)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("pending_status", ["queued", "in_progress"])
|
@pytest.mark.parametrize("pending_status", ["queued", "in_progress"])
|
||||||
async def test_post_call_success_hook_waits_for_pending_background_response_before_running_pipeline(
|
async def test_post_call_success_hook_waits_for_pending_background_response_before_running_pipeline(
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch, caplog, pending_status
|
proxy_logging: ProxyLogging,
|
||||||
):
|
make_user_api_key_auth: Callable[..., UserAPIKeyAuth],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
pending_status: str,
|
||||||
|
) -> None:
|
||||||
seen: dict[str, object] = {}
|
seen: dict[str, object] = {}
|
||||||
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks(seen))
|
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks(seen))
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
||||||
|
|
@ -1464,8 +1437,11 @@ async def test_post_call_success_hook_waits_for_pending_background_response_befo
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("final_status", ["completed", "incomplete"])
|
@pytest.mark.parametrize("final_status", ["completed", "incomplete"])
|
||||||
async def test_post_call_success_hook_runs_pipeline_on_retrieved_background_response(
|
async def test_post_call_success_hook_runs_pipeline_on_retrieved_background_response(
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch, final_status
|
proxy_logging: ProxyLogging,
|
||||||
):
|
make_user_api_key_auth: Callable[..., UserAPIKeyAuth],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
final_status: str,
|
||||||
|
) -> None:
|
||||||
seen: dict[str, object] = {}
|
seen: dict[str, object] = {}
|
||||||
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks(seen))
|
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks(seen))
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
||||||
|
|
@ -1486,7 +1462,9 @@ def _output_passing_callbacks() -> list[CustomGuardrail]:
|
||||||
async def async_post_call_success_hook(self, data, user_api_key_dict, response):
|
async def async_post_call_success_hook(self, data, user_api_key_dict, response):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
return [OutputPassingGuardrail(guardrail_name="gr-post", event_hook=GuardrailEventHooks.post_call, default_on=False)]
|
return [
|
||||||
|
OutputPassingGuardrail(guardrail_name="gr-post", event_hook=GuardrailEventHooks.post_call, default_on=False)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _claimed_post_call_pipeline_data(
|
def _claimed_post_call_pipeline_data(
|
||||||
|
|
@ -1519,7 +1497,7 @@ def _claimed_post_call_pipeline_data(
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def clear_policy_registry():
|
def clear_policy_registry() -> Iterator[None]:
|
||||||
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
|
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
@ -1528,8 +1506,11 @@ def clear_policy_registry():
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pending_background_response_withdraws_the_deferred_policy_claims(
|
async def test_pending_background_response_withdraws_the_deferred_policy_claims(
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch, clear_policy_registry
|
proxy_logging: ProxyLogging,
|
||||||
):
|
make_user_api_key_auth: Callable[..., UserAPIKeyAuth],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
clear_policy_registry: None,
|
||||||
|
) -> None:
|
||||||
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks({}))
|
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks({}))
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
||||||
data = _claimed_post_call_pipeline_data("response-governance")
|
data = _claimed_post_call_pipeline_data("response-governance")
|
||||||
|
|
@ -1546,8 +1527,12 @@ async def test_pending_background_response_withdraws_the_deferred_policy_claims(
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pending_background_response_warns_when_the_deferred_policy_was_matched_through_a_tag(
|
async def test_pending_background_response_warns_when_the_deferred_policy_was_matched_through_a_tag(
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch, clear_policy_registry, caplog
|
proxy_logging: ProxyLogging,
|
||||||
):
|
make_user_api_key_auth: Callable[..., UserAPIKeyAuth],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
clear_policy_registry: None,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks({}))
|
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks({}))
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
||||||
data = _claimed_post_call_pipeline_data("response-governance", policy_source="tag:governed+model:m")
|
data = _claimed_post_call_pipeline_data("response-governance", policy_source="tag:governed+model:m")
|
||||||
|
|
@ -1559,19 +1544,21 @@ async def test_pending_background_response_warns_when_the_deferred_policy_was_ma
|
||||||
|
|
||||||
assert out.status == "queued"
|
assert out.status == "queued"
|
||||||
assert "policy_sources" not in data["metadata"]
|
assert "policy_sources" not in data["metadata"]
|
||||||
assert [
|
assert [message for message in _warnings(caplog) if "through a request tag" in message] == [
|
||||||
message for message in _warnings(caplog) if "response-governance (tag:governed+model:m)" in message
|
|
||||||
] == [
|
|
||||||
"Policy engine: background response resp_bg matched post_call policies through a request tag at submit; "
|
"Policy engine: background response resp_bg matched post_call policies through a request tag at submit; "
|
||||||
"retrieval re-matches only the key, team, and model scopes, so a tag carried in the request body "
|
"retrieval re-matches only the key, team, and model scopes, so a tag carried in the request body "
|
||||||
"does not govern the completed response: response-governance (tag:governed+model:m)"
|
"does not govern the completed response: response-governance"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pending_background_response_matched_through_its_model_does_not_warn(
|
async def test_pending_background_response_matched_through_its_model_does_not_warn(
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch, clear_policy_registry, caplog
|
proxy_logging: ProxyLogging,
|
||||||
):
|
make_user_api_key_auth: Callable[..., UserAPIKeyAuth],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
clear_policy_registry: None,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks({}))
|
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks({}))
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
||||||
|
|
||||||
|
|
@ -1587,12 +1574,17 @@ async def test_pending_background_response_matched_through_its_model_does_not_wa
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pending_background_response_keeps_the_claim_of_a_policy_that_runs_outside_its_pipeline(
|
async def test_pending_background_response_keeps_the_claim_of_a_policy_that_runs_outside_its_pipeline(
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch, clear_policy_registry
|
proxy_logging: ProxyLogging,
|
||||||
):
|
make_user_api_key_auth: Callable[..., UserAPIKeyAuth],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
clear_policy_registry: None,
|
||||||
|
) -> None:
|
||||||
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks({}))
|
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks({}))
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
||||||
data = _claimed_post_call_pipeline_data(
|
data = _claimed_post_call_pipeline_data(
|
||||||
"input-and-output-governance", "response-governance", extra_guardrails={"input-and-output-governance": ["gr-pre"]}
|
"input-and-output-governance",
|
||||||
|
"response-governance",
|
||||||
|
extra_guardrails={"input-and-output-governance": ["gr-pre"]},
|
||||||
)
|
)
|
||||||
|
|
||||||
await proxy_logging.post_call_success_hook(
|
await proxy_logging.post_call_success_hook(
|
||||||
|
|
@ -1606,8 +1598,11 @@ async def test_pending_background_response_keeps_the_claim_of_a_policy_that_runs
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pending_background_response_keeps_the_claim_of_a_default_on_guardrail_that_ran_pre_call(
|
async def test_pending_background_response_keeps_the_claim_of_a_default_on_guardrail_that_ran_pre_call(
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch, clear_policy_registry
|
proxy_logging: ProxyLogging,
|
||||||
):
|
make_user_api_key_auth: Callable[..., UserAPIKeyAuth],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
clear_policy_registry: None,
|
||||||
|
) -> None:
|
||||||
class DualStageGuardrail(CustomGuardrail):
|
class DualStageGuardrail(CustomGuardrail):
|
||||||
async def async_post_call_success_hook(self, data, user_api_key_dict, response):
|
async def async_post_call_success_hook(self, data, user_api_key_dict, response):
|
||||||
return response
|
return response
|
||||||
|
|
@ -1630,8 +1625,11 @@ async def test_pending_background_response_keeps_the_claim_of_a_default_on_guard
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_retrieved_background_response_keeps_the_policy_claim_once_its_pipeline_ran(
|
async def test_retrieved_background_response_keeps_the_policy_claim_once_its_pipeline_ran(
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch, clear_policy_registry
|
proxy_logging: ProxyLogging,
|
||||||
):
|
make_user_api_key_auth: Callable[..., UserAPIKeyAuth],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
clear_policy_registry: None,
|
||||||
|
) -> None:
|
||||||
monkeypatch.setattr(litellm, "callbacks", _output_passing_callbacks())
|
monkeypatch.setattr(litellm, "callbacks", _output_passing_callbacks())
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
||||||
data = _claimed_post_call_pipeline_data("response-governance")
|
data = _claimed_post_call_pipeline_data("response-governance")
|
||||||
|
|
@ -1647,8 +1645,11 @@ async def test_retrieved_background_response_keeps_the_policy_claim_once_its_pip
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pre_call_hook_stays_quiet_on_background_request_with_post_call_pipeline(
|
async def test_pre_call_hook_stays_quiet_on_background_request_with_post_call_pipeline(
|
||||||
proxy_logging, make_user_api_key_auth, monkeypatch, caplog
|
proxy_logging: ProxyLogging,
|
||||||
):
|
make_user_api_key_auth: Callable[..., UserAPIKeyAuth],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
monkeypatch.setattr(litellm, "callbacks", [])
|
monkeypatch.setattr(litellm, "callbacks", [])
|
||||||
data = _post_call_pipeline_data(background=True)
|
data = _post_call_pipeline_data(background=True)
|
||||||
|
|
||||||
|
|
@ -1709,7 +1710,9 @@ def test_streamable_post_call_pipelines_keeps_supported_and_drops_unsupported(
|
||||||
steps=[PipelineStep(guardrail="gr-post", on_fail="next"), PipelineStep(guardrail="gr-native", on_fail="block")],
|
steps=[PipelineStep(guardrail="gr-post", on_fail="next"), PipelineStep(guardrail="gr-native", on_fail="block")],
|
||||||
)
|
)
|
||||||
pre_call = GuardrailPipeline(mode="pre_call", steps=[PipelineStep(guardrail="gr-native", on_fail="block")])
|
pre_call = GuardrailPipeline(mode="pre_call", steps=[PipelineStep(guardrail="gr-native", on_fail="block")])
|
||||||
data = {"metadata": {"_guardrail_pipelines": [("governed", governed), ("ungoverned", ungoverned), ("req", pre_call)]}}
|
data = {
|
||||||
|
"metadata": {"_guardrail_pipelines": [("governed", governed), ("ungoverned", ungoverned), ("req", pre_call)]}
|
||||||
|
}
|
||||||
|
|
||||||
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
|
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
|
||||||
streamable = _streamable_post_call_pipelines(data, make_user_api_key_auth(request_route="/v1/chat/completions"))
|
streamable = _streamable_post_call_pipelines(data, make_user_api_key_auth(request_route="/v1/chat/completions"))
|
||||||
|
|
@ -2009,7 +2012,9 @@ def _rewriting_stream_guardrail(transform: Callable[[Dict[str, Any]], Dict[str,
|
||||||
async def apply_guardrail(self, inputs, request_data, input_type, logging_obj=None):
|
async def apply_guardrail(self, inputs, request_data, input_type, logging_obj=None):
|
||||||
return {**inputs, **transform(inputs)}
|
return {**inputs, **transform(inputs)}
|
||||||
|
|
||||||
return RewritingStreamGuardrail(guardrail_name="gr-post", event_hook=GuardrailEventHooks.post_call, default_on=False)
|
return RewritingStreamGuardrail(
|
||||||
|
guardrail_name="gr-post", event_hook=GuardrailEventHooks.post_call, default_on=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _tool_call_stream_chunks() -> List[Any]:
|
def _tool_call_stream_chunks() -> List[Any]:
|
||||||
|
|
@ -2188,11 +2193,38 @@ async def test_streaming_iterator_hook_pipeline_releases_originals_on_unresolvab
|
||||||
|
|
||||||
def _anthropic_sse_chunks() -> List[bytes]:
|
def _anthropic_sse_chunks() -> List[bytes]:
|
||||||
events = [
|
events = [
|
||||||
("message_start", {"type": "message_start", "message": {"id": "msg_1", "type": "message", "role": "assistant", "model": "m", "content": [], "stop_reason": None, "usage": {"input_tokens": 1, "output_tokens": 0}}}),
|
(
|
||||||
("content_block_start", {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
|
"message_start",
|
||||||
("content_block_delta", {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "hello world"}}),
|
{
|
||||||
|
"type": "message_start",
|
||||||
|
"message": {
|
||||||
|
"id": "msg_1",
|
||||||
|
"type": "message",
|
||||||
|
"role": "assistant",
|
||||||
|
"model": "m",
|
||||||
|
"content": [],
|
||||||
|
"stop_reason": None,
|
||||||
|
"usage": {"input_tokens": 1, "output_tokens": 0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"content_block_start",
|
||||||
|
{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"content_block_delta",
|
||||||
|
{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "hello world"}},
|
||||||
|
),
|
||||||
("content_block_stop", {"type": "content_block_stop", "index": 0}),
|
("content_block_stop", {"type": "content_block_stop", "index": 0}),
|
||||||
("message_delta", {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": None}, "usage": {"output_tokens": 2}}),
|
(
|
||||||
|
"message_delta",
|
||||||
|
{
|
||||||
|
"type": "message_delta",
|
||||||
|
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
|
||||||
|
"usage": {"output_tokens": 2},
|
||||||
|
},
|
||||||
|
),
|
||||||
("message_stop", {"type": "message_stop"}),
|
("message_stop", {"type": "message_stop"}),
|
||||||
]
|
]
|
||||||
return [f"event: {name}\ndata: {json.dumps(payload)}\n\n".encode() for name, payload in events]
|
return [f"event: {name}\ndata: {json.dumps(payload)}\n\n".encode() for name, payload in events]
|
||||||
|
|
@ -2259,7 +2291,13 @@ async def test_streaming_iterator_hook_pipeline_delivers_text_rewrite_on_anthrop
|
||||||
assert "hello [MASKED]" in raw
|
assert "hello [MASKED]" in raw
|
||||||
assert "hello world" not in raw
|
assert "hello world" not in raw
|
||||||
assert raw.count("event: content_block_delta") == 1
|
assert raw.count("event: content_block_delta") == 1
|
||||||
for expected_event in ("message_start", "content_block_start", "content_block_stop", "message_delta", "message_stop"):
|
for expected_event in (
|
||||||
|
"message_start",
|
||||||
|
"content_block_start",
|
||||||
|
"content_block_stop",
|
||||||
|
"message_delta",
|
||||||
|
"message_stop",
|
||||||
|
):
|
||||||
assert f"event: {expected_event}" in raw
|
assert f"event: {expected_event}" in raw
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2332,9 +2370,7 @@ async def test_per_chunk_streaming_hook_skips_pipeline_managed_guardrail(
|
||||||
managed = UnifiedRecordingGuardrail(
|
managed = UnifiedRecordingGuardrail(
|
||||||
guardrail_name="gr-post", event_hook=GuardrailEventHooks.post_call, default_on=True
|
guardrail_name="gr-post", event_hook=GuardrailEventHooks.post_call, default_on=True
|
||||||
)
|
)
|
||||||
free = RecordingGuardrail(
|
free = RecordingGuardrail(guardrail_name="gr-free", event_hook=GuardrailEventHooks.post_call, default_on=True)
|
||||||
guardrail_name="gr-free", event_hook=GuardrailEventHooks.post_call, default_on=True
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(litellm, "callbacks", [managed, free])
|
monkeypatch.setattr(litellm, "callbacks", [managed, free])
|
||||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
|
||||||
data = _post_call_pipeline_data(stream=True)
|
data = _post_call_pipeline_data(stream=True)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue