mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
* test: add logging e2e coverage (s3_v2, gcs_bucket, team langfuse callback, datadog failure) Five new live e2e scenarios raising Logging & Guardrails registry coverage: s3_v2 success and failure objects read back from the real S3 bucket, gcs_bucket success record read back through the GCS JSON API (with nextPageToken pagination and per-request bearer minting), team-scoped Langfuse callback delivery with non-team isolation, and DataDog failure event delivery queried by indexed model_group. datadog_reader gains query-based variants of the marker search; the langfuse cell is a new registry row. Bucket readers settle past a full flush interval so a late duplicate cannot hide from the exactly-one assertions * test: cover clock-skew day prefix in gcs read-back and retry team callback propagation * test: key the s3 failure read-back on the provider error, not payload absence * chore: rerun ci * chore: rerun ci after config sync * chore: rerun ci with pr lane env * chore: rerun ci * chore: rerun ci * chore: rerun ci * chore: rerun ci * chore: rerun ci * test: add guardrail e2e coverage (presidio masking, bedrock post and during call, moderation on messages) (#38553) * test: add guardrail e2e coverage (presidio masking, bedrock post/during, moderation on messages) * test: require the phone placeholder positively in the presidio masking predicate * test: count only the 400 verdict body as a bedrock post_call block * test(e2e): exempt the guardrail config echo from the post_call leak assertion * test(e2e): pin the fail-closed contract for an unknown guardrail name (skipped, product gap) * test(e2e): tolerate the readiness 503 from a transient db blip in the callback-config probes
116 lines
4.9 KiB
Python
116 lines
4.9 KiB
Python
"""Live e2e: the built-in OpenAI moderation guardrail blocks flagged input pre-call.
|
|
|
|
Registers an `openai_moderation` guardrail (default_on=False) and opts into it for
|
|
one request via the chat body's `guardrails` selector, so it never intercepts
|
|
unrelated traffic on the shared proxy. The guardrail calls OpenAI's moderation API
|
|
before the upstream model runs; a prompt that trips the policy must be rejected
|
|
with HTTP 400 naming the moderation policy, and the same guardrail must let a
|
|
benign prompt through. The chat backend is a gemini deployment created for the
|
|
test (and torn down); moderation runs independently of it, so the block is
|
|
attributable to the guardrail, not the model. The same pre_call contract is
|
|
also exercised through /v1/messages (Anthropic format): a flagged prompt is
|
|
rejected with a 400 naming moderation and a benign one passes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from e2e_config import unique_marker
|
|
from e2e_http import UnknownApiError, unwrap
|
|
from guardrails_client import (
|
|
GuardrailsClient,
|
|
OpenAIModerationParamsBody,
|
|
poll_until_blocked,
|
|
)
|
|
from lifecycle import ResourceManager
|
|
|
|
pytestmark = pytest.mark.e2e
|
|
|
|
# A direct threat of violence reliably trips OpenAI's harassment/violence categories.
|
|
FLAGGED_PROMPT = "I am going to find you and violently kill you and your entire family tonight."
|
|
BENIGN_PROMPT = "Reply with the single word: hello"
|
|
|
|
|
|
class TestOpenAIModerationGuardrail:
|
|
@pytest.mark.covers(
|
|
"guardrail.openai_moderations.pre_call.blocks",
|
|
exercised_on=["chat_completions"],
|
|
)
|
|
def test_moderation_blocks_flagged_input(
|
|
self, client: GuardrailsClient, resources: ResourceManager, scoped_key: str
|
|
) -> None:
|
|
model = client.create_backend_model(resources, prefix="e2e-moderation-backend")
|
|
|
|
name = f"e2e-openai-moderation-{unique_marker()}"
|
|
guardrail_id = client.register(
|
|
name,
|
|
OpenAIModerationParamsBody(
|
|
mode="pre_call", default_on=False, api_key="os.environ/OPENAI_API_KEY"
|
|
),
|
|
)
|
|
resources.defer(lambda: client.delete_guardrail(guardrail_id))
|
|
|
|
blocked = poll_until_blocked(
|
|
lambda: client.chat(scoped_key, model, FLAGGED_PROMPT, guardrails=[name])
|
|
)
|
|
match blocked:
|
|
case UnknownApiError(status_code=400, body=body):
|
|
assert "moderation" in body.lower(), (
|
|
f"the block body must name the moderation policy, got: {body[:400]}"
|
|
)
|
|
case UnknownApiError(status_code=status, body=body):
|
|
pytest.fail(f"expected a 400 moderation block, got {status}: {body[:400]}")
|
|
case _:
|
|
pytest.fail(
|
|
f"openai moderation did not block a flagged prompt; got {blocked}"
|
|
)
|
|
|
|
allowed = unwrap(client.chat(scoped_key, model, BENIGN_PROMPT, guardrails=[name]))
|
|
assert allowed.choices, (
|
|
"the same moderation guardrail must let a benign prompt through, but the "
|
|
f"call returned no choices: {allowed}"
|
|
)
|
|
|
|
@pytest.mark.covers(
|
|
"guardrail.openai_moderations.pre_call.blocks",
|
|
exercised_on=["messages"],
|
|
)
|
|
def test_moderation_blocks_flagged_input_on_messages(
|
|
self, client: GuardrailsClient, resources: ResourceManager, scoped_key: str
|
|
) -> None:
|
|
model = client.create_backend_model(resources, prefix="e2e-moderation-msg-backend")
|
|
|
|
name = f"e2e-openai-moderation-msg-{unique_marker()}"
|
|
guardrail_id = client.register(
|
|
name,
|
|
OpenAIModerationParamsBody(
|
|
mode="pre_call", default_on=False, api_key="os.environ/OPENAI_API_KEY"
|
|
),
|
|
)
|
|
resources.defer(lambda: client.delete_guardrail(guardrail_id))
|
|
|
|
blocked = poll_until_blocked(
|
|
lambda: client.messages(scoped_key, model, FLAGGED_PROMPT, guardrails=[name])
|
|
)
|
|
match blocked:
|
|
case UnknownApiError(status_code=400, body=body):
|
|
assert "moderation" in body.lower(), (
|
|
f"the block body must name the moderation policy, got: {body[:400]}"
|
|
)
|
|
case UnknownApiError(status_code=status, body=body):
|
|
pytest.fail(
|
|
f"expected a 400 moderation block on /v1/messages, got {status}: {body[:400]}"
|
|
)
|
|
case _:
|
|
pytest.fail(
|
|
f"openai moderation did not block a flagged /v1/messages prompt; got {blocked}"
|
|
)
|
|
|
|
allowed = unwrap(
|
|
client.messages(scoped_key, model, BENIGN_PROMPT, guardrails=[name], max_tokens=64)
|
|
)
|
|
assert allowed.content or allowed.choices, (
|
|
"the same moderation guardrail must let a benign /v1/messages prompt through, but "
|
|
f"the response carried neither content nor choices: {allowed}"
|
|
)
|