mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
119 lines
5 KiB
Python
119 lines
5 KiB
Python
from types import MappingProxyType
|
|
from typing import Final
|
|
|
|
import httpx
|
|
import pytest
|
|
import respx
|
|
from pydantic import JsonValue, TypeAdapter, ValidationError
|
|
|
|
import litellm
|
|
|
|
_REQUEST: Final = TypeAdapter(dict[str, JsonValue])
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _httpx_transport(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("base", ["https://gateway.example", "https://gateway.example/v1/"])
|
|
async def test_apply_returns_native_findings_using_shared_gateway_settings(
|
|
monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter, base: str
|
|
) -> None:
|
|
monkeypatch.setattr(litellm, "api_base", base)
|
|
monkeypatch.setattr(litellm, "api_key", "test-key")
|
|
verdict: Final = {"is_grounded": False, "reason": "The source gives a different height.", "source_ids": []}
|
|
route: Final = respx_mock.post("https://gateway.example/guardrails/apply_guardrail").respond(json=verdict)
|
|
messages: Final = [{"role": "user", "content": "The tower is 330 metres tall."}]
|
|
|
|
result: Final = await litellm.guardrails.apply("grounding", response="450 metres", messages=messages)
|
|
|
|
assert result == verdict
|
|
assert route.calls.last.request.headers["Authorization"] == "Bearer test-key"
|
|
assert _REQUEST.validate_json(route.calls.last.request.content) == {
|
|
"guardrail_name": "grounding",
|
|
"text": "450 metres",
|
|
"input_type": "response",
|
|
"response_format": "native",
|
|
"messages": messages,
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_infers_input_direction_and_preserves_empty_values(
|
|
respx_mock: respx.MockRouter,
|
|
) -> None:
|
|
route: Final = respx_mock.post("https://gateway.example/guardrails/apply_guardrail").respond(json={"safe": True})
|
|
|
|
result: Final = await litellm.guardrails.apply(
|
|
"safety", input="", messages=[], metadata={}, api_base="https://gateway.example", api_key="explicit-key"
|
|
)
|
|
|
|
assert result == {"safe": True}
|
|
assert route.calls.last.request.headers["Authorization"] == "Bearer explicit-key"
|
|
assert _REQUEST.validate_json(route.calls.last.request.content) == {
|
|
"guardrail_name": "safety",
|
|
"text": "",
|
|
"input_type": "request",
|
|
"response_format": "native",
|
|
"messages": [],
|
|
"metadata": {},
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_serializes_read_only_messages_and_metadata(respx_mock: respx.MockRouter) -> None:
|
|
route: Final = respx_mock.post("https://gateway.example/guardrails/apply_guardrail").respond(
|
|
json={"is_grounded": True}
|
|
)
|
|
messages: Final = (MappingProxyType({"role": "user", "content": "The tower is 330 metres tall."}),)
|
|
metadata: Final = MappingProxyType({"trace_id": "demo", "source_ids": ["doc-1"], "options": {"report_only": True}})
|
|
|
|
result: Final = await litellm.guardrails.apply(
|
|
"grounding", response="330 metres", messages=messages, metadata=metadata, api_base="https://gateway.example"
|
|
)
|
|
|
|
assert result == {"is_grounded": True}
|
|
assert _REQUEST.validate_json(route.calls.last.request.content) == {
|
|
"guardrail_name": "grounding",
|
|
"text": "330 metres",
|
|
"input_type": "response",
|
|
"response_format": "native",
|
|
"messages": [{"role": "user", "content": "The tower is 330 metres tall."}],
|
|
"metadata": {"trace_id": "demo", "source_ids": ["doc-1"], "options": {"report_only": True}},
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("texts", [{}, {"input": "prompt", "response": "answer"}])
|
|
async def test_apply_requires_exactly_one_text(texts: dict[str, str], respx_mock: respx.MockRouter) -> None:
|
|
with pytest.raises(ValueError, match="exactly one"):
|
|
await litellm.guardrails.apply(
|
|
"grounding", input=texts.get("input"), response=texts.get("response"), api_base="https://gateway.example"
|
|
)
|
|
assert not respx_mock.calls
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_requires_a_gateway_url(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(litellm, "api_base", None)
|
|
with pytest.raises(ValueError, match="gateway URL"):
|
|
await litellm.guardrails.apply("grounding", response="answer")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_surfaces_guardrail_http_failures(respx_mock: respx.MockRouter) -> None:
|
|
respx_mock.post("https://gateway.example/guardrails/apply_guardrail").respond(
|
|
422, json={"error": {"message": "Guardrail did not report structured native findings."}}
|
|
)
|
|
with pytest.raises(httpx.HTTPStatusError) as failure:
|
|
await litellm.guardrails.apply("grounding", response="answer", api_base="https://gateway.example")
|
|
assert failure.value.response.status_code == 422
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_rejects_an_unstructured_response(respx_mock: respx.MockRouter) -> None:
|
|
respx_mock.post("https://gateway.example/guardrails/apply_guardrail").respond(json="allow")
|
|
with pytest.raises(ValidationError):
|
|
await litellm.guardrails.apply("grounding", response="answer", api_base="https://gateway.example")
|