Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5f3d21c
Add pipeline type definitions for guardrail pipelines
ishaan-jaff Feb 14, 2026
396064a
Export pipeline types from policy_engine types package
ishaan-jaff Feb 14, 2026
968b4cf
Add optional pipeline field to Policy model
ishaan-jaff Feb 14, 2026
df263aa
Add pipeline executor for sequential guardrail execution
ishaan-jaff Feb 14, 2026
03b0c3f
Parse pipeline config in policy registry
ishaan-jaff Feb 14, 2026
9c073ce
Add pipeline validation in policy validator
ishaan-jaff Feb 14, 2026
0a19fbc
Add pipeline resolution and managed guardrail tracking
ishaan-jaff Feb 14, 2026
ba39275
Resolve pipelines and exclude managed guardrails in pre-call
ishaan-jaff Feb 14, 2026
c940745
Integrate pipeline execution into proxy pre_call_hook
ishaan-jaff Feb 14, 2026
a8d3c05
Add test guardrails for pipeline E2E testing
ishaan-jaff Feb 14, 2026
36763b8
Add example pipeline config YAML
ishaan-jaff Feb 14, 2026
cad2003
Add unit tests for pipeline type definitions
ishaan-jaff Feb 14, 2026
31e3e61
Add unit tests for pipeline executor
ishaan-jaff Feb 14, 2026
bba891e
Add pipeline column to LiteLLM_PolicyTable schema
ishaan-jaff Feb 14, 2026
1cbd795
Add pipeline field to policy CRUD request/response types
ishaan-jaff Feb 14, 2026
d51b3de
Add pipeline support to policy DB CRUD operations
ishaan-jaff Feb 14, 2026
17a5cb4
Add PipelineStep and GuardrailPipeline TypeScript types
ishaan-jaff Feb 14, 2026
d4b8c01
Add Zapier-style pipeline flow builder UI component
ishaan-jaff Feb 14, 2026
7f9e857
Integrate pipeline flow builder with mode toggle in policy form
ishaan-jaff Feb 14, 2026
3b5e074
Add pipeline display section to policy info view
ishaan-jaff Feb 14, 2026
53c6236
Add unit tests for pipeline in policy CRUD types
ishaan-jaff Feb 14, 2026
51bf7da
Refactor policy form to show mode picker first with icon cards
ishaan-jaff Feb 14, 2026
d3b479e
Add full-screen FlowBuilderPage component for pipeline editing
ishaan-jaff Feb 14, 2026
4341aed
Wire up full-screen flow builder in PoliciesPanel with edit routing
ishaan-jaff Feb 14, 2026
aac69c8
Restyle flow builder to match dev-tool UI aesthetic
ishaan-jaff Feb 14, 2026
e6ef15e
Restyle flow builder cards to match reference design
ishaan-jaff Feb 14, 2026
fb52b5c
Update step card to expanded layout with stacked ON PASS / ON FAIL se…
ishaan-jaff Feb 14, 2026
ba2d697
Add end card to flow builder showing return to normal control flow
ishaan-jaff Feb 14, 2026
d960fec
Add PipelineTestRequest type for test-pipeline endpoint
ishaan-jaff Feb 14, 2026
9593b56
Export PipelineTestRequest from policy_engine types
ishaan-jaff Feb 14, 2026
1faa473
Add POST /policies/test-pipeline endpoint
ishaan-jaff Feb 14, 2026
0bbf4fa
Add testPipelineCall networking function
ishaan-jaff Feb 14, 2026
7a7a54e
Add PipelineStepResult and PipelineTestResult types
ishaan-jaff Feb 14, 2026
d74542a
Add test pipeline panel to flow builder with run button and results d…
ishaan-jaff Feb 14, 2026
7269d9a
Fix pipeline executor: inject guardrail name into metadata so should_…
ishaan-jaff Feb 14, 2026
fceabee
Update litellm/proxy/policy_engine/pipeline_executor.py
ishaan-jaff Feb 14, 2026
96f480a
Update litellm/proxy/utils.py
ishaan-jaff Feb 14, 2026
252d7bf
Update litellm/proxy/policy_engine/policy_endpoints.py
ishaan-jaff Feb 14, 2026
9ec29a8
Update litellm/proxy/policy_engine/pipeline_executor.py
ishaan-jaff Feb 14, 2026
7686564
Merge branch 'main' into litellm_guardrail_pipelines_ui
ishaan-jaff Feb 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion litellm/proxy/policy_engine/pipeline_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ async def execute_steps(
PipelineExecutionResult with terminal action and step results
"""
step_results: List[PipelineStepResult] = []
working_data = copy.deepcopy(data)
working_data = data.copy()
if "metadata" in working_data:
working_data["metadata"] = working_data["metadata"].copy()

for i, step in enumerate(steps):
start_time = time.perf_counter()
Expand Down Expand Up @@ -148,6 +150,12 @@ async def _run_step(
return ("error", None, f"Guardrail '{step.guardrail}' not found")

try:
# Inject guardrail name into metadata so should_run_guardrail() allows it
if "metadata" not in data:
data["metadata"] = {}
original_guardrails = data["metadata"].get("guardrails")
data["metadata"]["guardrails"] = [step.guardrail]

# Use unified_guardrail path if callback implements apply_guardrail
target = callback
use_unified = "apply_guardrail" in type(callback).__dict__
Expand Down
66 changes: 66 additions & 0 deletions litellm/proxy/policy_engine/policy_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.policy_engine.attachment_registry import get_attachment_registry
from litellm.proxy.policy_engine.pipeline_executor import PipelineExecutor
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
from litellm.types.proxy.policy_engine import (
GuardrailPipeline,
PipelineTestRequest,
PolicyAttachmentCreateRequest,
PolicyAttachmentDBResponse,
PolicyAttachmentListResponse,
Expand Down Expand Up @@ -349,6 +352,69 @@ async def get_resolved_guardrails(policy_id: str):
raise HTTPException(status_code=500, detail=str(e))


# ─────────────────────────────────────────────────────────────────────────────
# Pipeline Test Endpoint
# ─────────────────────────────────────────────────────────────────────────────


@router.post(
"/policies/test-pipeline",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
)
async def test_pipeline(
request: PipelineTestRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Test a guardrail pipeline with sample messages.

Executes the pipeline steps against the provided test messages and returns
step-by-step results showing which guardrails passed/failed, actions taken,
and timing information.

Example Request:
```bash
curl -X POST "http://localhost:4000/policies/test-pipeline" \\
-H "Authorization: Bearer <your_api_key>" \\
-H "Content-Type: application/json" \\
-d '{
"pipeline": {
"mode": "pre_call",
"steps": [
{"guardrail": "pii-guard", "on_pass": "next", "on_fail": "block"}
]
},
"test_messages": [{"role": "user", "content": "My SSN is 123-45-6789"}]
}'
```
"""
try:
validated_pipeline = GuardrailPipeline(**request.pipeline)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid pipeline: {e}")

data = {
"messages": request.test_messages,
"model": "test",
"metadata": {},
}

try:
result = await PipelineExecutor.execute_steps(
steps=validated_pipeline.steps,
mode=validated_pipeline.mode,
data=data,
user_api_key_dict=user_api_key_dict,
call_type="completion",
policy_name="test-pipeline",
)
return result.model_dump()
except Exception as e:
verbose_proxy_logger.exception(f"Error testing pipeline: {e}")
raise HTTPException(status_code=500, detail=str(e))


# ─────────────────────────────────────────────────────────────────────────────
# Policy Attachment CRUD Endpoints
# ─────────────────────────────────────────────────────────────────────────────
Expand Down
20 changes: 18 additions & 2 deletions litellm/proxy/policy_engine/policy_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Dict, List, Optional

from prisma import Json as PrismaJson

from litellm._logging import verbose_proxy_logger
from litellm.types.proxy.policy_engine import (
GuardrailPipeline,
Expand Down Expand Up @@ -248,7 +250,10 @@ async def add_policy_to_db(
data["created_by"] = created_by
data["updated_by"] = created_by
if policy_request.condition is not None:
data["condition"] = policy_request.condition.model_dump()
data["condition"] = PrismaJson(policy_request.condition.model_dump())
if policy_request.pipeline is not None:
validated_pipeline = GuardrailPipeline(**policy_request.pipeline)
data["pipeline"] = PrismaJson(validated_pipeline.model_dump())

created_policy = await prisma_client.db.litellm_policytable.create(
data=data
Expand All @@ -267,6 +272,7 @@ async def add_policy_to_db(
"condition": policy_request.condition.model_dump()
if policy_request.condition
else None,
"pipeline": policy_request.pipeline,
},
)
self.add_policy(policy_request.policy_name, policy)
Expand All @@ -279,6 +285,7 @@ async def add_policy_to_db(
guardrails_add=created_policy.guardrails_add or [],
guardrails_remove=created_policy.guardrails_remove or [],
condition=created_policy.condition,
pipeline=created_policy.pipeline,
created_at=created_policy.created_at,
updated_at=created_policy.updated_at,
created_by=created_policy.created_by,
Expand Down Expand Up @@ -325,7 +332,10 @@ async def update_policy_in_db(
if policy_request.guardrails_remove is not None:
update_data["guardrails_remove"] = policy_request.guardrails_remove
if policy_request.condition is not None:
update_data["condition"] = policy_request.condition.model_dump()
update_data["condition"] = PrismaJson(policy_request.condition.model_dump())
if policy_request.pipeline is not None:
validated_pipeline = GuardrailPipeline(**policy_request.pipeline)
update_data["pipeline"] = PrismaJson(validated_pipeline.model_dump())

updated_policy = await prisma_client.db.litellm_policytable.update(
where={"policy_id": policy_id},
Expand All @@ -343,6 +353,7 @@ async def update_policy_in_db(
"remove": updated_policy.guardrails_remove,
},
"condition": updated_policy.condition,
"pipeline": updated_policy.pipeline,
},
)
self.add_policy(updated_policy.policy_name, policy)
Expand All @@ -355,6 +366,7 @@ async def update_policy_in_db(
guardrails_add=updated_policy.guardrails_add or [],
guardrails_remove=updated_policy.guardrails_remove or [],
condition=updated_policy.condition,
pipeline=updated_policy.pipeline,
created_at=updated_policy.created_at,
updated_at=updated_policy.updated_at,
created_by=updated_policy.created_by,
Expand Down Expand Up @@ -432,6 +444,7 @@ async def get_policy_by_id_from_db(
guardrails_add=policy.guardrails_add or [],
guardrails_remove=policy.guardrails_remove or [],
condition=policy.condition,
pipeline=policy.pipeline,
created_at=policy.created_at,
updated_at=policy.updated_at,
created_by=policy.created_by,
Expand Down Expand Up @@ -468,6 +481,7 @@ async def get_all_policies_from_db(
guardrails_add=p.guardrails_add or [],
guardrails_remove=p.guardrails_remove or [],
condition=p.condition,
pipeline=p.pipeline,
created_at=p.created_at,
updated_at=p.updated_at,
created_by=p.created_by,
Expand Down Expand Up @@ -503,6 +517,7 @@ async def sync_policies_from_db(
"remove": policy_response.guardrails_remove,
},
"condition": policy_response.condition,
"pipeline": policy_response.pipeline,
},
)
self.add_policy(policy_response.policy_name, policy)
Expand Down Expand Up @@ -551,6 +566,7 @@ async def resolve_guardrails_from_db(
"remove": policy_response.guardrails_remove,
},
"condition": policy_response.condition,
"pipeline": policy_response.pipeline,
},
)
temp_policies[policy_response.policy_name] = policy
Expand Down
1 change: 1 addition & 0 deletions litellm/proxy/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,7 @@ model LiteLLM_PolicyTable {
guardrails_add String[] @default([])
guardrails_remove String[] @default([])
condition Json? @default("{}") // Policy conditions (e.g., model matching)
pipeline Json? // Optional guardrail pipeline (mode + steps[])
created_at DateTime @default(now())
created_by String?
updated_at DateTime @default(now()) @updatedAt
Expand Down
8 changes: 1 addition & 7 deletions litellm/proxy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1222,10 +1222,7 @@ def _handle_pipeline_result(
},
}
}
if HTTPException is not None:
raise HTTPException(status_code=400, detail=error_detail)
else:
raise Exception(str(error_detail))
raise HTTPException(status_code=400, detail=error_detail)

if result.terminal_action == "modify_response":
raise ModifyResponseException(
Expand All @@ -1236,9 +1233,6 @@ def _handle_pipeline_result(
detection_info=None,
)

verbose_proxy_logger.warning(
f"Pipeline '{policy_name}': unrecognized terminal_action '{result.terminal_action}', defaulting to allow"
)
return data

# The actual implementation of the function
Expand Down
3 changes: 3 additions & 0 deletions litellm/types/proxy/policy_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
)
from litellm.types.proxy.policy_engine.resolver_types import (
AttachmentImpactResponse,
PipelineTestRequest,
PolicyAttachmentCreateRequest,
PolicyAttachmentDBResponse,
PolicyAttachmentListResponse,
Expand Down Expand Up @@ -90,6 +91,8 @@
"PolicyAttachmentCreateRequest",
"PolicyAttachmentDBResponse",
"PolicyAttachmentListResponse",
# Pipeline test types
"PipelineTestRequest",
# Resolve types
"PolicyResolveRequest",
"PolicyResolveResponse",
Expand Down
22 changes: 22 additions & 0 deletions litellm/types/proxy/policy_engine/resolver_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ class PolicyCreateRequest(BaseModel):
default=None,
description="Condition for when this policy applies.",
)
pipeline: Optional[Dict[str, Any]] = Field(
default=None,
description="Optional guardrail pipeline for ordered execution. Contains 'mode' and 'steps'.",
)


class PolicyUpdateRequest(BaseModel):
Expand Down Expand Up @@ -183,6 +187,10 @@ class PolicyUpdateRequest(BaseModel):
default=None,
description="Condition for when this policy applies.",
)
pipeline: Optional[Dict[str, Any]] = Field(
default=None,
description="Optional guardrail pipeline for ordered execution. Contains 'mode' and 'steps'.",
)


class PolicyDBResponse(BaseModel):
Expand All @@ -201,6 +209,9 @@ class PolicyDBResponse(BaseModel):
condition: Optional[Dict[str, Any]] = Field(
default=None, description="Policy condition."
)
pipeline: Optional[Dict[str, Any]] = Field(
default=None, description="Optional guardrail pipeline."
)
created_at: Optional[datetime] = Field(
default=None, description="When the policy was created."
)
Expand Down Expand Up @@ -291,6 +302,17 @@ class PolicyAttachmentListResponse(BaseModel):
# ─────────────────────────────────────────────────────────────────────────────


class PipelineTestRequest(BaseModel):
"""Request body for testing a guardrail pipeline with sample messages."""

pipeline: Dict[str, Any] = Field(
description="Pipeline definition with 'mode' and 'steps'.",
)
test_messages: List[Dict[str, str]] = Field(
description="Test messages to run through the pipeline, e.g. [{'role': 'user', 'content': '...'}].",
)


class PolicyResolveRequest(BaseModel):
"""Request body for resolving effective policies/guardrails for a context."""

Expand Down
Loading
Loading