chore: initial snapshot for gitea/github upload

This commit is contained in:
Your Name
2026-03-26 16:04:46 +08:00
commit a699a1ac98
3497 changed files with 1586237 additions and 0 deletions

View File

@@ -0,0 +1,834 @@
"""
CRUD ENDPOINTS FOR POLICIES
Provides REST API endpoints for managing policies and policy attachments.
"""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from litellm._logging import verbose_proxy_logger
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,
PolicyCreateRequest,
PolicyDBResponse,
PolicyListDBResponse,
PolicyUpdateRequest,
PolicyVersionCompareResponse,
PolicyVersionCreateRequest,
PolicyVersionListResponse,
PolicyVersionStatusUpdateRequest,
)
router = APIRouter()
# ─────────────────────────────────────────────────────────────────────────────
# Policy CRUD Endpoints
# ─────────────────────────────────────────────────────────────────────────────
@router.get(
"/policies/list",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
response_model=PolicyListDBResponse,
)
async def list_policies(version_status: Optional[str] = None):
"""
List all policies from the database. Optionally filter by version_status.
Query params:
- version_status: Optional. One of "draft", "published", "production".
If omitted, all versions are returned.
Example Request:
```bash
curl -X GET "http://localhost:4000/policies/list" \\
-H "Authorization: Bearer <your_api_key>"
curl -X GET "http://localhost:4000/policies/list?version_status=production" \\
-H "Authorization: Bearer <your_api_key>"
```
Example Response:
```json
{
"policies": [
{
"policy_id": "123e4567-e89b-12d3-a456-426614174000",
"policy_name": "global-baseline",
"version_number": 1,
"version_status": "production",
"inherit": null,
"description": "Base guardrails for all requests",
"guardrails_add": ["pii_masking"],
"guardrails_remove": [],
"condition": null,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
],
"total_count": 1
}
```
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
policies = await get_policy_registry().get_all_policies_from_db(
prisma_client, version_status=version_status
)
return PolicyListDBResponse(policies=policies, total_count=len(policies))
except Exception as e:
verbose_proxy_logger.exception(f"Error listing policies: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/policies",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
response_model=PolicyDBResponse,
)
async def create_policy(
request: PolicyCreateRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Create a new policy.
Example Request:
```bash
curl -X POST "http://localhost:4000/policies" \\
-H "Authorization: Bearer <your_api_key>" \\
-H "Content-Type: application/json" \\
-d '{
"policy_name": "global-baseline",
"description": "Base guardrails for all requests",
"guardrails_add": ["pii_masking", "prompt_injection"],
"guardrails_remove": []
}'
```
Example Response:
```json
{
"policy_id": "123e4567-e89b-12d3-a456-426614174000",
"policy_name": "global-baseline",
"inherit": null,
"description": "Base guardrails for all requests",
"guardrails_add": ["pii_masking", "prompt_injection"],
"guardrails_remove": [],
"condition": null,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
```
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
created_by = user_api_key_dict.user_id
result = await get_policy_registry().add_policy_to_db(
policy_request=request,
prisma_client=prisma_client,
created_by=created_by,
)
return result
except Exception as e:
verbose_proxy_logger.exception(f"Error creating policy: {e}")
if "unique constraint" in str(e).lower():
raise HTTPException(
status_code=400,
detail=f"Policy with name '{request.policy_name}' already exists",
)
raise HTTPException(status_code=500, detail=str(e))
# ─────────────────────────────────────────────────────────────────────────────
# Policy Versioning Endpoints (must be before /policies/{policy_id} to avoid path conflicts)
# ─────────────────────────────────────────────────────────────────────────────
@router.get(
"/policies/name/{policy_name}/versions",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
response_model=PolicyVersionListResponse,
)
async def list_policy_versions(policy_name: str):
"""
List all versions of a policy by name, ordered by version_number descending.
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
return await get_policy_registry().get_versions_by_policy_name(
policy_name=policy_name,
prisma_client=prisma_client,
)
except Exception as e:
verbose_proxy_logger.exception(f"Error listing policy versions: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/policies/name/{policy_name}/versions",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
response_model=PolicyDBResponse,
)
async def create_policy_version(
policy_name: str,
request: PolicyVersionCreateRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Create a new draft version of a policy. Copies all fields from the source.
Source is current production if source_policy_id is not provided.
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
created_by = user_api_key_dict.user_id
return await get_policy_registry().create_new_version(
policy_name=policy_name,
prisma_client=prisma_client,
source_policy_id=request.source_policy_id,
created_by=created_by,
)
except Exception as e:
verbose_proxy_logger.exception(f"Error creating policy version: {e}")
if "not found" in str(e).lower() or "no production" in str(e).lower():
raise HTTPException(status_code=404, detail=str(e))
raise HTTPException(status_code=500, detail=str(e))
@router.put(
"/policies/{policy_id}/status",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
response_model=PolicyDBResponse,
)
async def update_policy_version_status(
policy_id: str,
request: PolicyVersionStatusUpdateRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Update a policy version's status. Valid transitions:
- draft -> published
- published -> production (demotes current production to published)
- production -> published (demotes, policy becomes inactive)
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
updated_by = user_api_key_dict.user_id
return await get_policy_registry().update_version_status(
policy_id=policy_id,
new_status=request.version_status,
prisma_client=prisma_client,
updated_by=updated_by,
)
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(f"Error updating version status: {e}")
if (
"invalid status" in str(e).lower()
or "only draft" in str(e).lower()
or "cannot promote" in str(e).lower()
):
raise HTTPException(status_code=400, detail=str(e))
if "not found" in str(e).lower():
raise HTTPException(status_code=404, detail=str(e))
raise HTTPException(status_code=500, detail=str(e))
@router.get(
"/policies/compare",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
response_model=PolicyVersionCompareResponse,
)
async def compare_policy_versions(
version_a: str,
version_b: str,
):
"""
Compare two policy versions. Query params: version_a, version_b (policy version IDs).
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
return await get_policy_registry().compare_versions(
policy_id_a=version_a,
policy_id_b=version_b,
prisma_client=prisma_client,
)
except Exception as e:
verbose_proxy_logger.exception(f"Error comparing versions: {e}")
if "not found" in str(e).lower():
raise HTTPException(status_code=404, detail=str(e))
raise HTTPException(status_code=500, detail=str(e))
@router.delete(
"/policies/name/{policy_name}/all-versions",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
)
async def delete_all_policy_versions(policy_name: str):
"""
Delete all versions of a policy. Also removes from in-memory registry.
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
return await get_policy_registry().delete_all_versions(
policy_name=policy_name,
prisma_client=prisma_client,
)
except Exception as e:
verbose_proxy_logger.exception(f"Error deleting all versions: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ─────────────────────────────────────────────────────────────────────────────
# Policy CRUD by ID
# ─────────────────────────────────────────────────────────────────────────────
@router.get(
"/policies/{policy_id}",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
response_model=PolicyDBResponse,
)
async def get_policy(policy_id: str):
"""
Get a policy by ID.
Example Request:
```bash
curl -X GET "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\
-H "Authorization: Bearer <your_api_key>"
```
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
result = await get_policy_registry().get_policy_by_id_from_db(
policy_id=policy_id,
prisma_client=prisma_client,
)
if result is None:
raise HTTPException(
status_code=404, detail=f"Policy with ID {policy_id} not found"
)
return result
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(f"Error getting policy: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.put(
"/policies/{policy_id}",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
response_model=PolicyDBResponse,
)
async def update_policy(
policy_id: str,
request: PolicyUpdateRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Update an existing policy.
Example Request:
```bash
curl -X PUT "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\
-H "Authorization: Bearer <your_api_key>" \\
-H "Content-Type: application/json" \\
-d '{
"description": "Updated description",
"guardrails_add": ["pii_masking", "toxicity_filter"]
}'
```
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
# Check if policy exists and is draft (only drafts can be updated)
existing = await get_policy_registry().get_policy_by_id_from_db(
policy_id=policy_id,
prisma_client=prisma_client,
)
if existing is None:
raise HTTPException(
status_code=404, detail=f"Policy with ID {policy_id} not found"
)
if getattr(existing, "version_status", "production") != "draft":
raise HTTPException(
status_code=400,
detail="Only draft versions can be updated. Publish or create a new version to change published/production.",
)
updated_by = user_api_key_dict.user_id
result = await get_policy_registry().update_policy_in_db(
policy_id=policy_id,
policy_request=request,
prisma_client=prisma_client,
updated_by=updated_by,
)
return result
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(f"Error updating policy: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete(
"/policies/{policy_id}",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
)
async def delete_policy(policy_id: str):
"""
Delete a policy.
Example Request:
```bash
curl -X DELETE "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\
-H "Authorization: Bearer <your_api_key>"
```
Example Response:
```json
{
"message": "Policy 123e4567-e89b-12d3-a456-426614174000 deleted successfully"
}
```
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
# Check if policy exists
existing = await get_policy_registry().get_policy_by_id_from_db(
policy_id=policy_id,
prisma_client=prisma_client,
)
if existing is None:
raise HTTPException(
status_code=404, detail=f"Policy with ID {policy_id} not found"
)
result = await get_policy_registry().delete_policy_from_db(
policy_id=policy_id,
prisma_client=prisma_client,
)
# Result may include "warning" if production was deleted
return result
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(f"Error deleting policy: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get(
"/policies/{policy_id}/resolved-guardrails",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
)
async def get_resolved_guardrails(policy_id: str):
"""
Get the resolved guardrails for a policy (including inherited guardrails).
This endpoint resolves the full inheritance chain and returns the final
set of guardrails that would be applied for this policy.
Example Request:
```bash
curl -X GET "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000/resolved-guardrails" \\
-H "Authorization: Bearer <your_api_key>"
```
Example Response:
```json
{
"policy_id": "123e4567-e89b-12d3-a456-426614174000",
"policy_name": "healthcare-compliance",
"resolved_guardrails": ["pii_masking", "prompt_injection", "toxicity_filter"]
}
```
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
# Get the policy
policy = await get_policy_registry().get_policy_by_id_from_db(
policy_id=policy_id,
prisma_client=prisma_client,
)
if policy is None:
raise HTTPException(
status_code=404, detail=f"Policy with ID {policy_id} not found"
)
# Resolve guardrails
resolved = await get_policy_registry().resolve_guardrails_from_db(
policy_name=policy.policy_name,
prisma_client=prisma_client,
)
return {
"policy_id": policy.policy_id,
"policy_name": policy.policy_name,
"resolved_guardrails": resolved,
}
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
verbose_proxy_logger.exception(f"Error resolving guardrails: {e}")
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
# ─────────────────────────────────────────────────────────────────────────────
@router.get(
"/policies/attachments/list",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
response_model=PolicyAttachmentListResponse,
)
async def list_policy_attachments():
"""
List all policy attachments from the database.
Example Request:
```bash
curl -X GET "http://localhost:4000/policies/attachments/list" \\
-H "Authorization: Bearer <your_api_key>"
```
Example Response:
```json
{
"attachments": [
{
"attachment_id": "123e4567-e89b-12d3-a456-426614174000",
"policy_name": "global-baseline",
"scope": "*",
"teams": [],
"keys": [],
"models": [],
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
],
"total_count": 1
}
```
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
attachments = await get_attachment_registry().get_all_attachments_from_db(
prisma_client
)
return PolicyAttachmentListResponse(
attachments=attachments, total_count=len(attachments)
)
except Exception as e:
verbose_proxy_logger.exception(f"Error listing policy attachments: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/policies/attachments",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
response_model=PolicyAttachmentDBResponse,
)
async def create_policy_attachment(
request: PolicyAttachmentCreateRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Create a new policy attachment.
Example Request:
```bash
curl -X POST "http://localhost:4000/policies/attachments" \\
-H "Authorization: Bearer <your_api_key>" \\
-H "Content-Type: application/json" \\
-d '{
"policy_name": "global-baseline",
"scope": "*"
}'
```
Example with team-specific attachment:
```bash
curl -X POST "http://localhost:4000/policies/attachments" \\
-H "Authorization: Bearer <your_api_key>" \\
-H "Content-Type: application/json" \\
-d '{
"policy_name": "healthcare-compliance",
"teams": ["healthcare-team", "medical-research"]
}'
```
Example Response:
```json
{
"attachment_id": "123e4567-e89b-12d3-a456-426614174000",
"policy_name": "global-baseline",
"scope": "*",
"teams": [],
"keys": [],
"models": [],
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
```
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
# Verify the policy has a production version (attachments resolve against production)
policies = await get_policy_registry().get_all_policies_from_db(
prisma_client, version_status="production"
)
policy_names = {p.policy_name for p in policies}
if request.policy_name not in policy_names:
raise HTTPException(
status_code=404,
detail=f"Policy '{request.policy_name}' not found. Create the policy first.",
)
created_by = user_api_key_dict.user_id
result = await get_attachment_registry().add_attachment_to_db(
attachment_request=request,
prisma_client=prisma_client,
created_by=created_by,
)
return result
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(f"Error creating policy attachment: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get(
"/policies/attachments/{attachment_id}",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
response_model=PolicyAttachmentDBResponse,
)
async def get_policy_attachment(attachment_id: str):
"""
Get a policy attachment by ID.
Example Request:
```bash
curl -X GET "http://localhost:4000/policies/attachments/123e4567-e89b-12d3-a456-426614174000" \\
-H "Authorization: Bearer <your_api_key>"
```
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
result = await get_attachment_registry().get_attachment_by_id_from_db(
attachment_id=attachment_id,
prisma_client=prisma_client,
)
if result is None:
raise HTTPException(
status_code=404,
detail=f"Attachment with ID {attachment_id} not found",
)
return result
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(f"Error getting policy attachment: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete(
"/policies/attachments/{attachment_id}",
tags=["Policies"],
dependencies=[Depends(user_api_key_auth)],
)
async def delete_policy_attachment(attachment_id: str):
"""
Delete a policy attachment.
Example Request:
```bash
curl -X DELETE "http://localhost:4000/policies/attachments/123e4567-e89b-12d3-a456-426614174000" \\
-H "Authorization: Bearer <your_api_key>"
```
Example Response:
```json
{
"message": "Attachment 123e4567-e89b-12d3-a456-426614174000 deleted successfully"
}
```
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Database not connected")
try:
# Check if attachment exists
existing = await get_attachment_registry().get_attachment_by_id_from_db(
attachment_id=attachment_id,
prisma_client=prisma_client,
)
if existing is None:
raise HTTPException(
status_code=404,
detail=f"Attachment with ID {attachment_id} not found",
)
result = await get_attachment_registry().delete_attachment_from_db(
attachment_id=attachment_id,
prisma_client=prisma_client,
)
return result
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(f"Error deleting policy attachment: {e}")
raise HTTPException(status_code=500, detail=str(e))