mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
* test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy): stub HIBP through respx by disabling the aiohttp transport Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy): share the httpx transport fixture across proxy unit tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy): restore proxy globals without a missing-value sentinel Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy): package moved dirs and stub the login breach check at the HTTP boundary Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy): isolate the mcp server manager per test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: yuneng <yuneng@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
255 lines
8.9 KiB
Python
255 lines
8.9 KiB
Python
"""
|
|
Unit tests for PolicyResolver - tests guardrail resolution.
|
|
|
|
Tests:
|
|
- Inheritance chain resolution
|
|
- Inheritance with add/remove
|
|
- Model conditions
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from litellm.proxy.policy_engine.policy_resolver import PolicyResolver
|
|
from litellm.types.proxy.policy_engine import (
|
|
GuardrailPipeline,
|
|
PipelineStep,
|
|
Policy,
|
|
PolicyCondition,
|
|
PolicyGuardrails,
|
|
PolicyMatchContext,
|
|
)
|
|
|
|
|
|
class TestPolicyResolverInheritance:
|
|
"""Test resolve_policy_guardrails - inheritance and add/remove."""
|
|
|
|
def test_resolve_simple_policy(self):
|
|
"""Test resolving guardrails for a simple policy."""
|
|
policies = {
|
|
"global": Policy(
|
|
guardrails=PolicyGuardrails(add=["pii_blocker", "toxicity_filter"]),
|
|
),
|
|
}
|
|
|
|
resolved = PolicyResolver.resolve_policy_guardrails(
|
|
policy_name="global", policies=policies
|
|
)
|
|
|
|
assert set(resolved.guardrails) == {"pii_blocker", "toxicity_filter"}
|
|
assert resolved.inheritance_chain == ["global"]
|
|
|
|
def test_resolve_with_inheritance(self):
|
|
"""Test child policy inherits and adds guardrails from parent."""
|
|
policies = {
|
|
"base": Policy(
|
|
guardrails=PolicyGuardrails(add=["pii_blocker"]),
|
|
),
|
|
"healthcare": Policy(
|
|
inherit="base",
|
|
guardrails=PolicyGuardrails(add=["hipaa_audit"]),
|
|
),
|
|
}
|
|
|
|
resolved = PolicyResolver.resolve_policy_guardrails(
|
|
policy_name="healthcare", policies=policies
|
|
)
|
|
|
|
# Healthcare inherits pii_blocker from base and adds hipaa_audit
|
|
assert set(resolved.guardrails) == {"pii_blocker", "hipaa_audit"}
|
|
assert resolved.inheritance_chain == ["base", "healthcare"]
|
|
|
|
def test_resolve_with_remove(self):
|
|
"""Test child policy can remove guardrails from parent."""
|
|
policies = {
|
|
"base": Policy(
|
|
guardrails=PolicyGuardrails(add=["pii_blocker", "phi_blocker"]),
|
|
),
|
|
"dev": Policy(
|
|
inherit="base",
|
|
guardrails=PolicyGuardrails(
|
|
add=["toxicity_filter"], remove=["phi_blocker"]
|
|
),
|
|
),
|
|
}
|
|
|
|
resolved = PolicyResolver.resolve_policy_guardrails(
|
|
policy_name="dev", policies=policies
|
|
)
|
|
|
|
# dev inherits pii_blocker from base, adds toxicity_filter, removes phi_blocker
|
|
assert "pii_blocker" in resolved.guardrails
|
|
assert "toxicity_filter" in resolved.guardrails
|
|
assert "phi_blocker" not in resolved.guardrails
|
|
|
|
def test_resolve_deep_inheritance_chain(self):
|
|
"""Test multi-level inheritance chain."""
|
|
policies = {
|
|
"root": Policy(
|
|
guardrails=PolicyGuardrails(add=["root_guardrail"]),
|
|
),
|
|
"middle": Policy(
|
|
inherit="root",
|
|
guardrails=PolicyGuardrails(add=["middle_guardrail"]),
|
|
),
|
|
"leaf": Policy(
|
|
inherit="middle",
|
|
guardrails=PolicyGuardrails(add=["leaf_guardrail"]),
|
|
),
|
|
}
|
|
|
|
resolved = PolicyResolver.resolve_policy_guardrails(
|
|
policy_name="leaf", policies=policies
|
|
)
|
|
|
|
assert set(resolved.guardrails) == {
|
|
"root_guardrail",
|
|
"middle_guardrail",
|
|
"leaf_guardrail",
|
|
}
|
|
assert resolved.inheritance_chain == ["root", "middle", "leaf"]
|
|
|
|
|
|
class TestPolicyResolverWithConditions:
|
|
"""Test resolve_policy_guardrails with model conditions."""
|
|
|
|
def test_condition_matches(self):
|
|
"""Test guardrails are added when condition matches."""
|
|
policies = {
|
|
"gpt4-policy": Policy(
|
|
guardrails=PolicyGuardrails(add=["toxicity_filter"]),
|
|
condition=PolicyCondition(model="gpt-4.*"),
|
|
),
|
|
}
|
|
|
|
# GPT-4 should get guardrails
|
|
context = PolicyMatchContext(team_alias="team", key_alias="k", model="gpt-4")
|
|
resolved = PolicyResolver.resolve_policy_guardrails(
|
|
policy_name="gpt4-policy",
|
|
policies=policies,
|
|
context=context,
|
|
)
|
|
|
|
assert "toxicity_filter" in resolved.guardrails
|
|
|
|
def test_condition_does_not_match(self):
|
|
"""Test guardrails are NOT added when condition doesn't match."""
|
|
policies = {
|
|
"gpt4-policy": Policy(
|
|
guardrails=PolicyGuardrails(add=["toxicity_filter"]),
|
|
condition=PolicyCondition(model="gpt-4.*"),
|
|
),
|
|
}
|
|
|
|
# GPT-3.5 should NOT get guardrails
|
|
context = PolicyMatchContext(team_alias="team", key_alias="k", model="gpt-3.5")
|
|
resolved = PolicyResolver.resolve_policy_guardrails(
|
|
policy_name="gpt4-policy",
|
|
policies=policies,
|
|
context=context,
|
|
)
|
|
|
|
assert "toxicity_filter" not in resolved.guardrails
|
|
|
|
def test_no_condition_always_applies(self):
|
|
"""Test policy without condition always applies."""
|
|
policies = {
|
|
"global": Policy(
|
|
guardrails=PolicyGuardrails(add=["pii_blocker"]),
|
|
),
|
|
}
|
|
|
|
context = PolicyMatchContext(team_alias="any", key_alias="any", model="any")
|
|
resolved = PolicyResolver.resolve_policy_guardrails(
|
|
policy_name="global",
|
|
policies=policies,
|
|
context=context,
|
|
)
|
|
|
|
assert "pii_blocker" in resolved.guardrails
|
|
|
|
def test_inheritance_with_condition(self):
|
|
"""Test inheritance works with conditions."""
|
|
policies = {
|
|
"base": Policy(
|
|
guardrails=PolicyGuardrails(add=["pii_blocker"]),
|
|
),
|
|
"child": Policy(
|
|
inherit="base",
|
|
guardrails=PolicyGuardrails(add=["child_guardrail"]),
|
|
condition=PolicyCondition(model="gpt-4"),
|
|
),
|
|
}
|
|
|
|
# GPT-4 should get both base and child guardrails
|
|
context_gpt4 = PolicyMatchContext(team_alias="t", key_alias="k", model="gpt-4")
|
|
resolved_gpt4 = PolicyResolver.resolve_policy_guardrails(
|
|
policy_name="child",
|
|
policies=policies,
|
|
context=context_gpt4,
|
|
)
|
|
assert "pii_blocker" in resolved_gpt4.guardrails
|
|
assert "child_guardrail" in resolved_gpt4.guardrails
|
|
|
|
# GPT-3.5 should only get base guardrails (child condition doesn't match)
|
|
context_gpt35 = PolicyMatchContext(
|
|
team_alias="t", key_alias="k", model="gpt-3.5"
|
|
)
|
|
resolved_gpt35 = PolicyResolver.resolve_policy_guardrails(
|
|
policy_name="child",
|
|
policies=policies,
|
|
context=context_gpt35,
|
|
)
|
|
assert "pii_blocker" in resolved_gpt35.guardrails
|
|
assert "child_guardrail" not in resolved_gpt35.guardrails
|
|
|
|
def test_resolve_guardrails_for_context_with_condition_missing_child_keeps_inherited_parent(self):
|
|
"""Test a matched child whose condition misses still contributes unconditional parent guardrails."""
|
|
policies = {
|
|
"parent": Policy(
|
|
guardrails=PolicyGuardrails(add=["y"]),
|
|
),
|
|
"child": Policy(
|
|
inherit="parent",
|
|
guardrails=PolicyGuardrails(add=["x"]),
|
|
condition=PolicyCondition(model="claude.*"),
|
|
),
|
|
}
|
|
|
|
context_miss = PolicyMatchContext(team_alias="t", key_alias="k", model="gpt-5.5")
|
|
assert PolicyResolver.resolve_guardrails_for_context(
|
|
context=context_miss, policies=policies, policy_names=["child"]
|
|
) == ["y"]
|
|
|
|
context_hit = PolicyMatchContext(team_alias="t", key_alias="k", model="claude-haiku")
|
|
assert set(
|
|
PolicyResolver.resolve_guardrails_for_context(
|
|
context=context_hit, policies=policies, policy_names=["child"]
|
|
)
|
|
) == {"x", "y"}
|
|
|
|
def test_resolve_pipelines_for_context_skips_pipeline_when_own_condition_misses(self):
|
|
"""Test a matched child whose own condition misses does not run its pipeline."""
|
|
pipeline = GuardrailPipeline(mode="pre_call", steps=[PipelineStep(guardrail="child-guard")])
|
|
policies = {
|
|
"parent": Policy(
|
|
guardrails=PolicyGuardrails(add=["y"]),
|
|
),
|
|
"child": Policy(
|
|
inherit="parent",
|
|
pipeline=pipeline,
|
|
condition=PolicyCondition(model="gpt-5.5"),
|
|
),
|
|
}
|
|
|
|
context_miss = PolicyMatchContext(team_alias="t", key_alias="k", model="gpt-4o")
|
|
assert (
|
|
PolicyResolver.resolve_pipelines_for_context(
|
|
context=context_miss, policies=policies, policy_names=["child"]
|
|
)
|
|
== []
|
|
)
|
|
|
|
context_hit = PolicyMatchContext(team_alias="t", key_alias="k", model="gpt-5.5")
|
|
assert PolicyResolver.resolve_pipelines_for_context(
|
|
context=context_hit, policies=policies, policy_names=["child"]
|
|
) == [("child", pipeline)]
|