feat(guardrails): team-based guardrail registration and approval workflow (#22459)

* feat(guardrails): team-based guardrail registration and approval workflow

Add team-based guardrail submission system where teams can register
Generic Guardrail API guardrails for admin review. Includes:

- POST /guardrails/register endpoint for team-scoped submissions
- Admin review endpoints (list/get/approve/reject submissions)
- Team Guardrails tab in the UI dashboard
- extra_headers support for forwarding client headers to guardrail APIs
- Prisma schema migration for status, submitted_at, reviewed_at fields
- Documentation for team-based guardrails and static/dynamic headers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(guardrails): address review feedback - SSRF, silent failure, redundant query

- Validate api_base URL scheme (http/https only) and hostname in
  register_guardrail to prevent SSRF via team submissions
- Return warning field in approve response when in-memory initialization
  fails so admins know the guardrail won't work until next sync cycle
- Eliminate redundant DB query in list_guardrail_submissions by fetching
  all team guardrails once and deriving both filtered list and summary
  counts from the single result set

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(guardrails): add pending_review status guard to reject endpoint

Prevent rejecting already-active or already-rejected guardrails, which
would create a DB/memory inconsistency (active in memory but rejected
in DB). Now mirrors the approve endpoint's status check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Krish Dholakia 2026-03-02 22:06:49 -08:00 committed by GitHub
parent 213799282b
commit 67f90254ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 2724 additions and 29 deletions

View file

@ -244,6 +244,35 @@ litellm_settings:
language: "en"
```
### Static and dynamic headers
You can send two kinds of headers to your guardrail endpoint:
- **Static headers** (`headers`): A key/value map sent with **every** request to your guardrail. Use this for fixed values (e.g. API keys, `X-Service-Name`). Configure in `litellm_params`:
```yaml
litellm_params:
guardrail: generic_guardrail_api
api_base: https://your-guardrail-api.com
headers:
X-Service-Name: "my-app"
X-API-Key: "secret"
```
- **Dynamic headers** (`extra_headers`): A list of **header names** that are forwarded from the **client request** to your guardrail. Only headers in this list (plus a small default allowlist such as `x-litellm-*`) have their values sent; others are sent as `[present]`. Use this to pass through client-provided headers (e.g. `x-request-id`, `x-correlation-id`). Configure in `litellm_params`:
```yaml
litellm_params:
guardrail: generic_guardrail_api
api_base: https://your-guardrail-api.com
extra_headers:
- x-request-id
- x-correlation-id
- x-custom-auth
```
This mirrors the [MCP static and extra headers](/docs/mcp#forwarding-custom-headers-to-mcp-servers) behavior.
### Example: Pillar Security
[Pillar Security](https://pillar.security) uses the Generic Guardrail API to provide comprehensive AI security scanning including prompt injection protection, PII/PCI detection, secret detection, and content moderation.

View file

@ -73,6 +73,7 @@ guardrails:
plr_scanners: true
```
For generic guardrail APIs you can also set **static headers** (`headers`: key/value sent on every request) and **dynamic headers** (`extra_headers`: list of client header names to forward). See [Generic Guardrail API - Static and dynamic headers](/docs/adding_provider/generic_guardrail_api#static-and-dynamic-headers).
### Supported values for `mode` (Event Hooks)

View file

@ -0,0 +1,137 @@
import Image from '@theme/IdealImage';
# Team-Based Guardrails
Team-based guardrails let **developers** register a guardrail for their team via the API; an **admin** then reviews and approves or rejects it in the LiteLLM UI. Only [Generic Guardrail API](/docs/adding_provider/generic_guardrail_api) guardrails can be registered this way.
## Overview
- **Developer flow:** Use a **team-scoped API key** to `POST /guardrails/register` with your guardrail config. The submission is stored with status `pending_review`.
- **Admin flow:** In the proxy UI, open **Guardrails → Team Guardrails**, review pending submissions, and **Approve** or **Reject**. Approved guardrails become active and are initialized in memory.
---
## Developer flow: Register a guardrail
### Prerequisites
- A **team-scoped** API key (the key must be associated with a team). Keys without a team cannot register guardrails.
- Your guardrail must follow the [Generic Guardrail API](/docs/adding_provider/generic_guardrail_api) contract and config.
### Request
**Endpoint:** `POST /guardrails/register`
**Headers:** `Authorization: Bearer <team_scoped_api_key>`
**Body:** JSON matching the Generic Guardrail API config.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `guardrail_name` | string | Yes | Unique name for the guardrail. |
| `litellm_params` | object | Yes | Must include `guardrail: "generic_guardrail_api"`, `mode` (e.g. `pre_call`, `post_call`), and `api_base`. See [Generic Guardrail API](/docs/adding_provider/generic_guardrail_api#litellm-configuration). |
| `guardrail_info` | object | No | Optional metadata (e.g. `description`). |
### Requirements for `litellm_params`
- `guardrail` must be exactly `"generic_guardrail_api"`.
- `api_base` is required (your guardrail API base URL).
- `mode` is required (e.g. `pre_call`, `post_call`, `during_call`).
### Example
```bash
curl -X POST "http://localhost:4000/guardrails/register" \
-H "Authorization: Bearer <your_team_scoped_api_key>" \
-H "Content-Type: application/json" \
-d '{
"guardrail_name": "my-team-guard",
"litellm_params": {
"guardrail": "generic_guardrail_api",
"mode": "pre_call",
"api_base": "https://your-guardrail-api.com",
"api_key": "optional-api-key",
"unreachable_fallback": "fail_closed",
"forward_api_key": true
},
"guardrail_info": {
"description": "Team content moderation guardrail"
}
}'
```
### Example response
```json
{
"guardrail_id": "123e4567-e89b-12d3-a456-426614174000",
"guardrail_name": "my-team-guard",
"status": "pending_review",
"submitted_at": "2025-02-28T12:00:00.000Z"
}
```
### Errors
- **400** Missing or invalid body (e.g. `guardrail` not `generic_guardrail_api`, missing `api_base` or `mode`), or a guardrail with the same `guardrail_name` already exists.
- **400** "Registration requires an API key associated with a team. Use a team-scoped key." → Use an API key that has a team.
- **500** Server/database error.
After a successful register, the guardrail stays in `pending_review` until an admin approves or rejects it.
---
## Admin flow: Approve or reject in the UI
Admins review and approve or reject team guardrail submissions in the LiteLLM proxy UI.
### 1. Open the Guardrails page
In the proxy dashboard, go to **Guardrails** (sidebar or navigation).
### 2. Open the Team Guardrails tab
Switch to the **Team Guardrails** tab. This tab lists all team-submitted guardrails and their status.
<Image img={require('../../../img/admin_team_guardrails.png')} alt="Team Guardrails admin view: status summary (Total, Pending Review, Active, Rejected), guardrail list with Pending Review tag, and detail panel with Approve/Reject buttons and configuration options." style={{ width: '100%', maxWidth: '900px', height: 'auto' }} />
### 3. Review submissions
The table shows:
- **Name**, **Team**, **Endpoint** (api_base), **Status** (Pending Review / Active / Rejected), **Submitted** date, **Submitted by** (user/email), and other config details.
Summary cards show counts for **Total**, **Pending Review**, **Active**, and **Rejected**.
<!-- Optional: screenshot of the Team Guardrails table and summary -->
### 4. Approve or reject
- **Pending Review:** Use **Approve** to activate the guardrail. The proxy sets its status to `active` and initializes it in memory so it can be used on requests.
- Use **Reject** to decline the submission (status becomes `rejected`).
Approval triggers the same initialization as adding a guardrail via config or the admin guardrail API; rejection only updates the status and does not load the guardrail.
<!-- Optional: screenshot of Approve/Reject actions or confirmation dialog -->
### API equivalent (admin only)
Admins can also use the REST API:
- **List submissions:** `GET /guardrails/submissions` (optional query: `status`, `team_id`, `search`)
- **Get one:** `GET /guardrails/submissions/{guardrail_id}`
- **Approve:** `POST /guardrails/submissions/{guardrail_id}/approve`
- **Reject:** `POST /guardrails/submissions/{guardrail_id}/reject`
These endpoints require **admin** (e.g. `PROXY_ADMIN`) authentication.
---
## Summary
| Role | Action |
|------|--------|
| **Developer** | Call `POST /guardrails/register` with a team-scoped key and a `generic_guardrail_api` config. Submission enters `pending_review`. |
| **Admin** | Open **Guardrails → Team Guardrails** in the UI (or use the submissions API), then **Approve** or **Reject** each submission. Approved guardrails become active. |
Only guardrails with `litellm_params.guardrail: "generic_guardrail_api"` are accepted for registration. For the full contract and config options, see [Generic Guardrail API](/docs/adding_provider/generic_guardrail_api).

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

View file

@ -42,6 +42,7 @@ const sidebars = {
label: "Guardrails",
items: [
"proxy/guardrails/quick_start",
"proxy/guardrails/team_based_guardrails",
"proxy/guardrails/guardrail_load_balancing",
"proxy/guardrails/test_playground",
"proxy/guardrails/litellm_content_filter",

View file

@ -0,0 +1,8 @@
-- AlterTable
ALTER TABLE "LiteLLM_GuardrailsTable" ADD COLUMN "reviewed_at" TIMESTAMP(3),
ADD COLUMN "status" TEXT NOT NULL DEFAULT 'active',
ADD COLUMN "submitted_at" TIMESTAMP(3);
-- CreateIndex
CREATE INDEX "LiteLLM_GuardrailsTable_status_idx" ON "LiteLLM_GuardrailsTable"("status");

View file

@ -871,6 +871,13 @@ model LiteLLM_GuardrailsTable {
team_id String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
// Submission lifecycle. Possible values: pending_review (team-registered, awaiting approval), active (approved), rejected
status String @default("active")
submitted_at DateTime?
reviewed_at DateTime?
// submitted_by_user_id and submitted_by_email live in guardrail_info JSON
@@index([status])
}
// Daily guardrail metrics for usage dashboard (one row per guardrail per day)

View file

@ -646,6 +646,8 @@ class LiteLLMRoutes(enum.Enum):
# Invitation routes - org/team admins checked in endpoint via _user_has_admin_privileges
"/invitation/new",
"/invitation/delete",
# Team guardrail submission - requires team-scoped key; endpoint enforces team_id
"/guardrails/register",
] # routes that manage their own allowed/disallowed logic
## Org Admin Routes ##

View file

@ -4,7 +4,10 @@ CRUD ENDPOINTS FOR GUARDRAILS
import concurrent.futures
import inspect
import json
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast
from urllib.parse import urlparse
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
@ -12,6 +15,7 @@ from pydantic import BaseModel
from litellm._logging import verbose_proxy_logger
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.guardrails.guardrail_hooks.custom_code.code_validator import (
@ -525,6 +529,456 @@ async def delete_guardrail(
raise HTTPException(status_code=500, detail=str(e))
# --- Team guardrail registration (Generic Guardrail API spec) ---
GENERIC_GUARDRAIL_API = "generic_guardrail_api"
class RegisterGuardrailRequest(BaseModel):
"""Request body for POST /guardrails/register. Follows Generic Guardrail API config."""
guardrail_name: str
litellm_params: Dict[
str, Any
] # guardrail, mode, api_base required; api_key, headers, etc. optional
guardrail_info: Optional[Dict[str, Any]] = None
def get_litellm_params_dict(self) -> Dict[str, Any]:
return dict(self.litellm_params)
class RegisterGuardrailResponse(BaseModel):
guardrail_id: str
guardrail_name: str
status: str
submitted_at: Optional[datetime] = None
class GuardrailSubmissionSummary(BaseModel):
total: int
pending_review: int
active: int
rejected: int
class GuardrailSubmissionItem(BaseModel):
guardrail_id: str
guardrail_name: str
status: str # pending_review | active | rejected
team_id: Optional[str] = None
team_guardrail: bool = (
False # True when submitted via team (team_id set); use to distinguish team vs regular guardrails
)
litellm_params: Optional[Dict[str, Any]] = None
guardrail_info: Optional[Dict[str, Any]] = None
submitted_by_user_id: Optional[str] = None
submitted_by_email: Optional[str] = None
submitted_at: Optional[datetime] = None
reviewed_at: Optional[datetime] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class ListGuardrailSubmissionsResponse(BaseModel):
submissions: List[GuardrailSubmissionItem]
summary: GuardrailSubmissionSummary
@router.post(
"/guardrails/register",
tags=["Guardrails"],
response_model=RegisterGuardrailResponse,
)
async def register_guardrail(
request: RegisterGuardrailRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Register a guardrail for onboarding (team submission).
Accepts a guardrail config in the
[Generic Guardrail API](https://docs.litellm.ai/docs/adding_provider/generic_guardrail_api) format.
The submission is stored with status `pending_review` until an admin approves it.
"""
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
raise HTTPException(status_code=500, detail="Prisma client not initialized")
if not user_api_key_dict.team_id:
raise HTTPException(
status_code=400,
detail="Registration requires an API key associated with a team. Use a team-scoped key.",
)
params = request.get_litellm_params_dict()
if params.get("guardrail") != GENERIC_GUARDRAIL_API:
raise HTTPException(
status_code=400,
detail=f"Only guardrails with litellm_params.guardrail={GENERIC_GUARDRAIL_API!r} are accepted for registration",
)
api_base = params.get("api_base")
if not api_base:
raise HTTPException(
status_code=400,
detail="litellm_params.api_base is required for generic_guardrail_api",
)
parsed = urlparse(api_base)
if parsed.scheme not in ("http", "https"):
raise HTTPException(
status_code=400,
detail="litellm_params.api_base must use http or https scheme",
)
if not parsed.hostname:
raise HTTPException(
status_code=400,
detail="litellm_params.api_base must contain a valid hostname",
)
mode = params.get("mode")
if mode is None:
raise HTTPException(
status_code=400,
detail="litellm_params.mode is required (e.g. pre_call, post_call)",
)
try:
existing = await prisma_client.db.litellm_guardrailstable.find_unique(
where={"guardrail_name": request.guardrail_name}
)
if existing is not None:
raise HTTPException(
status_code=400,
detail=f"Guardrail with name {request.guardrail_name!r} already exists",
)
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(
"Error checking guardrail name uniqueness: %s", e
)
raise HTTPException(status_code=500, detail=str(e))
now = datetime.now(timezone.utc)
litellm_params_str = safe_dumps(params)
guardrail_info = dict(request.guardrail_info or {})
guardrail_info["submitted_by_user_id"] = user_api_key_dict.user_id
guardrail_info["submitted_by_email"] = user_api_key_dict.user_email
guardrail_info["team_guardrail"] = (
True # Mark as team submission for filtering/display
)
guardrail_info_str = safe_dumps(guardrail_info)
try:
created = await prisma_client.db.litellm_guardrailstable.create(
data={
"guardrail_name": request.guardrail_name,
"litellm_params": litellm_params_str,
"guardrail_info": guardrail_info_str,
"status": "pending_review",
"team_id": user_api_key_dict.team_id,
"submitted_at": now,
"created_at": now,
"updated_at": now,
}
)
return RegisterGuardrailResponse(
guardrail_id=created.guardrail_id,
guardrail_name=created.guardrail_name,
status=created.status,
submitted_at=created.submitted_at,
)
except Exception as e:
verbose_proxy_logger.exception("Error registering guardrail: %s", e)
raise HTTPException(status_code=500, detail=str(e))
def _parse_json_field(value: Any) -> Optional[Dict[str, Any]]:
if value is None:
return None
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
return json.loads(value)
except Exception:
return None
return None
def _row_to_submission_item(row: Any) -> GuardrailSubmissionItem:
guardrail_info = _parse_json_field(row.guardrail_info) or {}
team_guardrail = row.team_id is not None
return GuardrailSubmissionItem(
guardrail_id=row.guardrail_id,
guardrail_name=row.guardrail_name,
status=row.status or "active",
team_id=row.team_id,
team_guardrail=team_guardrail,
litellm_params=_parse_json_field(row.litellm_params),
guardrail_info=guardrail_info,
submitted_by_user_id=guardrail_info.get("submitted_by_user_id"),
submitted_by_email=guardrail_info.get("submitted_by_email"),
submitted_at=getattr(row, "submitted_at", None),
reviewed_at=getattr(row, "reviewed_at", None),
created_at=row.created_at,
updated_at=row.updated_at,
)
@router.get(
"/guardrails/submissions",
tags=["Guardrails"],
response_model=ListGuardrailSubmissionsResponse,
)
async def list_guardrail_submissions(
status: Optional[str] = None,
team_id: Optional[str] = None,
search: Optional[str] = None,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
List team guardrail submissions (admin only). Returns only guardrails with a team_id.
Status values: pending_review (team-registered, awaiting approval), active (approved), rejected.
Optional filters:
- status: pending_review | active | rejected
- team_id: filter by specific team
- search: name/description
"""
from litellm.proxy.proxy_server import prisma_client
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(status_code=403, detail="Admin access required")
if prisma_client is None:
raise HTTPException(status_code=500, detail="Prisma client not initialized")
try:
# Single query: fetch all team guardrails (team_id is not null)
all_team_rows = await prisma_client.db.litellm_guardrailstable.find_many(
where={"team_id": {"not": None}},
order={"created_at": "desc"},
)
# Derive summary counts from the full result set
total = len(all_team_rows)
pending_review = sum(
1 for r in all_team_rows if (r.status or "active") == "pending_review"
)
active_count = sum(
1 for r in all_team_rows if (r.status or "active") == "active"
)
rejected = sum(
1 for r in all_team_rows if (r.status or "active") == "rejected"
)
# Apply filters to get the submissions list
rows = all_team_rows
if status:
rows = [r for r in rows if r.status == status]
if team_id:
rows = [r for r in rows if r.team_id == team_id]
if search:
search_lower = search.lower()
rows = [
r
for r in rows
if search_lower in (r.guardrail_name or "").lower()
or (
isinstance(r.guardrail_info, dict)
and search_lower
in str((r.guardrail_info or {}).get("description", "")).lower()
)
or (
isinstance(r.guardrail_info, str)
and search_lower in r.guardrail_info.lower()
)
]
items = [_row_to_submission_item(r) for r in rows]
return ListGuardrailSubmissionsResponse(
submissions=items,
summary=GuardrailSubmissionSummary(
total=total,
pending_review=pending_review,
active=active_count,
rejected=rejected,
),
)
except Exception as e:
verbose_proxy_logger.exception("Error listing guardrail submissions: %s", e)
raise HTTPException(status_code=500, detail=str(e))
@router.get(
"/guardrails/submissions/{guardrail_id}",
tags=["Guardrails"],
response_model=GuardrailSubmissionItem,
)
async def get_guardrail_submission(
guardrail_id: str,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""Get a single guardrail submission by id (admin only)."""
from litellm.proxy.proxy_server import prisma_client
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(status_code=403, detail="Admin access required")
if prisma_client is None:
raise HTTPException(status_code=500, detail="Prisma client not initialized")
try:
row = await prisma_client.db.litellm_guardrailstable.find_unique(
where={"guardrail_id": guardrail_id}
)
if row is None:
raise HTTPException(
status_code=404, detail="Guardrail submission not found"
)
return _row_to_submission_item(row)
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception("Error getting guardrail submission: %s", e)
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/guardrails/submissions/{guardrail_id}/approve",
tags=["Guardrails"],
)
async def approve_guardrail_submission(
guardrail_id: str,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""Approve a pending guardrail submission: set status to active and initialize in memory (admin only)."""
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(status_code=403, detail="Admin access required")
if prisma_client is None:
raise HTTPException(status_code=500, detail="Prisma client not initialized")
try:
row = await prisma_client.db.litellm_guardrailstable.find_unique(
where={"guardrail_id": guardrail_id}
)
if row is None:
raise HTTPException(
status_code=404, detail="Guardrail submission not found"
)
if row.status != "pending_review":
raise HTTPException(
status_code=400,
detail=f"Guardrail is not pending review (status={row.status})",
)
now = datetime.now(timezone.utc)
await prisma_client.db.litellm_guardrailstable.update(
where={"guardrail_id": guardrail_id},
data={"status": "active", "reviewed_at": now, "updated_at": now},
)
litellm_params = _parse_json_field(row.litellm_params)
guardrail_info = _parse_json_field(row.guardrail_info)
if not litellm_params:
raise HTTPException(
status_code=500,
detail="Guardrail litellm_params is missing or invalid",
)
guardrail_dict = {
"guardrail_id": row.guardrail_id,
"guardrail_name": row.guardrail_name,
"litellm_params": litellm_params,
"guardrail_info": guardrail_info or {},
}
try:
IN_MEMORY_GUARDRAIL_HANDLER.initialize_guardrail(
guardrail=cast(Guardrail, guardrail_dict)
)
verbose_proxy_logger.info(
"Approved guardrail %s (ID: %s) and initialized in memory",
row.guardrail_name,
guardrail_id,
)
except Exception as init_err:
verbose_proxy_logger.warning(
"Failed to initialize approved guardrail %s in memory: %s",
guardrail_id,
init_err,
)
return {
"guardrail_id": guardrail_id,
"status": "active",
"message": "Guardrail approved",
"warning": f"Guardrail was marked active but failed to initialize in memory: {init_err}. "
"It will be picked up on the next sync cycle.",
}
return {
"guardrail_id": guardrail_id,
"status": "active",
"message": "Guardrail approved",
}
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception("Error approving guardrail submission: %s", e)
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/guardrails/submissions/{guardrail_id}/reject",
tags=["Guardrails"],
)
async def reject_guardrail_submission(
guardrail_id: str,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""Reject a guardrail submission (admin only)."""
from litellm.proxy.proxy_server import prisma_client
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(status_code=403, detail="Admin access required")
if prisma_client is None:
raise HTTPException(status_code=500, detail="Prisma client not initialized")
try:
row = await prisma_client.db.litellm_guardrailstable.find_unique(
where={"guardrail_id": guardrail_id}
)
if row is None:
raise HTTPException(
status_code=404, detail="Guardrail submission not found"
)
if row.status != "pending_review":
raise HTTPException(
status_code=400,
detail=f"Guardrail is not pending review (status={row.status})",
)
now = datetime.now(timezone.utc)
await prisma_client.db.litellm_guardrailstable.update(
where={"guardrail_id": guardrail_id},
data={"status": "rejected", "reviewed_at": now, "updated_at": now},
)
return {
"guardrail_id": guardrail_id,
"status": "rejected",
"message": "Guardrail rejected",
}
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception("Error rejecting guardrail submission: %s", e)
raise HTTPException(status_code=500, detail=str(e))
@router.patch(
"/guardrails/{guardrail_id}",
tags=["Guardrails"],
@ -1356,9 +1810,9 @@ async def get_provider_specific_params():
lakera_v2_fields = _get_fields_from_model(LakeraV2GuardrailConfigModel)
tool_permission_fields = _get_fields_from_model(ToolPermissionGuardrailConfigModel)
tool_permission_fields[
"ui_friendly_name"
] = ToolPermissionGuardrailConfigModel.ui_friendly_name()
tool_permission_fields["ui_friendly_name"] = (
ToolPermissionGuardrailConfigModel.ui_friendly_name()
)
# Return the provider-specific parameters
provider_params = {
@ -1497,7 +1951,6 @@ async def test_custom_code_guardrail(
```
"""
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(
status_code=403,
@ -1632,10 +2085,10 @@ async def apply_guardrail(
from litellm.proxy.utils import handle_exception_on_proxy
try:
active_guardrail: Optional[
CustomGuardrail
] = GUARDRAIL_REGISTRY.get_initialized_guardrail_callback(
guardrail_name=request.guardrail_name
active_guardrail: Optional[CustomGuardrail] = (
GUARDRAIL_REGISTRY.get_initialized_guardrail_callback(
guardrail_name=request.guardrail_name
)
)
if active_guardrail is None:
raise HTTPException(

View file

@ -21,6 +21,7 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail"
unreachable_fallback=getattr(
litellm_params, "unreachable_fallback", "fail_closed"
),
extra_headers=getattr(litellm_params, "extra_headers", None),
guardrail_name=guardrail.get("guardrail_name", ""),
event_hook=litellm_params.mode,
default_on=litellm_params.default_on,

View file

@ -7,7 +7,7 @@
import fnmatch
import os
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Set
import httpx
@ -54,22 +54,30 @@ _HEADER_VALUE_ALLOWLIST = frozenset(
_HEADER_PRESENT_PLACEHOLDER = "[present]"
def _header_value_allowed(header_name: str) -> bool:
"""Return True if this header's value may be forwarded (allowlist, including globs)."""
def _header_value_allowed(
header_name: str,
extra_allowlist: Optional[Set[str]] = None,
) -> bool:
"""Return True if this header's value may be forwarded (allowlist, including globs and extra_headers)."""
lower = header_name.lower()
if lower in _HEADER_VALUE_ALLOWLIST:
return True
for pattern in _HEADER_VALUE_ALLOWLIST:
if "*" in pattern and fnmatch.fnmatch(lower, pattern):
return True
if extra_allowlist and lower in extra_allowlist:
return True
return False
def _sanitize_inbound_headers(headers: Any) -> Optional[Dict[str, str]]:
def _sanitize_inbound_headers(
headers: Any,
extra_allowlist: Optional[Set[str]] = None,
) -> Optional[Dict[str, str]]:
"""
Sanitize inbound headers before passing them to a 3rd party guardrail service.
- Allowlist: only headers in the allowlist have their values forwarded (exact + glob: x-stainless-*, x-litellm-*).
- Allowlist: default allowlist + extra_allowlist (from litellm_params.extra_headers); only these have values forwarded.
- All other headers are included with value "[present]" so the guardrail knows the header existed.
- Coerces values to str (for JSON serialization).
"""
@ -81,7 +89,7 @@ def _sanitize_inbound_headers(headers: Any) -> Optional[Dict[str, str]]:
if k is None:
continue
key = str(k)
if _header_value_allowed(key):
if _header_value_allowed(key, extra_allowlist=extra_allowlist):
try:
sanitized[key] = str(v)
except Exception:
@ -93,7 +101,9 @@ def _sanitize_inbound_headers(headers: Any) -> Optional[Dict[str, str]]:
def _extract_inbound_headers(
request_data: dict, logging_obj: Optional["LiteLLMLoggingObj"]
request_data: dict,
logging_obj: Optional["LiteLLMLoggingObj"],
extra_allowlist: Optional[Set[str]] = None,
) -> Optional[Dict[str, str]]:
"""
Extract inbound headers from available request context.
@ -107,23 +117,27 @@ def _extract_inbound_headers(
# 1) Most common path (proxy): full request context in proxy_server_request
headers = request_data.get("proxy_server_request", {}).get("headers")
if headers:
return _sanitize_inbound_headers(headers)
return _sanitize_inbound_headers(headers, extra_allowlist=extra_allowlist)
# 2) Some guardrails pass proxy_server_request as request_data itself
headers = request_data.get("headers")
if headers:
return _sanitize_inbound_headers(headers)
return _sanitize_inbound_headers(headers, extra_allowlist=extra_allowlist)
# 3) Pre-call: headers stored in request metadata
metadata_headers = (request_data.get("metadata") or {}).get("headers")
if metadata_headers:
return _sanitize_inbound_headers(metadata_headers)
return _sanitize_inbound_headers(
metadata_headers, extra_allowlist=extra_allowlist
)
litellm_metadata_headers = (request_data.get("litellm_metadata") or {}).get(
"headers"
)
if litellm_metadata_headers:
return _sanitize_inbound_headers(litellm_metadata_headers)
return _sanitize_inbound_headers(
litellm_metadata_headers, extra_allowlist=extra_allowlist
)
# 4) Post-call: headers not present on response; fallback to logging object
if logging_obj and getattr(logging_obj, "model_call_details", None):
@ -135,7 +149,9 @@ def _extract_inbound_headers(
.get("headers", None)
)
if headers:
return _sanitize_inbound_headers(headers)
return _sanitize_inbound_headers(
headers, extra_allowlist=extra_allowlist
)
except Exception:
pass
@ -171,12 +187,14 @@ class GenericGuardrailAPI(CustomGuardrail):
api_key: Optional[str] = None,
additional_provider_specific_params: Optional[Dict[str, Any]] = None,
unreachable_fallback: Literal["fail_closed", "fail_open"] = "fail_closed",
extra_headers: Optional[list] = None,
**kwargs,
):
self.async_handler = get_async_httpx_client(
llm_provider=httpxSpecialProvider.GuardrailCallback
)
self.headers = headers or {}
self.extra_headers = extra_headers or []
# If api_key is provided, add it as x-api-key header
if api_key:
@ -370,8 +388,15 @@ class GenericGuardrailAPI(CustomGuardrail):
# Extract user API key metadata
user_metadata = self._extract_user_api_key_metadata(request_data)
extra_allowlist = (
{h.lower() for h in self.extra_headers if isinstance(h, str)}
if self.extra_headers
else None
)
inbound_headers = _extract_inbound_headers(
request_data=request_data, logging_obj=logging_obj
request_data=request_data,
logging_obj=logging_obj,
extra_allowlist=extra_allowlist,
)
# Create request payload

View file

@ -11,8 +11,12 @@ from litellm._logging import verbose_proxy_logger
from litellm._uuid import uuid
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.proxy.utils import PrismaClient
from litellm.proxy.guardrails.guardrail_hooks.grayswan import GraySwanGuardrail
from litellm.proxy.guardrails.guardrail_hooks.grayswan import (
initialize_guardrail as initialize_grayswan,
)
from litellm.proxy.types_utils.utils import get_instance_fn
from litellm.proxy.utils import PrismaClient
from litellm.secret_managers.main import get_secret
from litellm.types.guardrails import (
Guardrail,
@ -21,10 +25,6 @@ from litellm.types.guardrails import (
LitellmParams,
SupportedGuardrailIntegrations,
)
from litellm.proxy.guardrails.guardrail_hooks.grayswan import (
GraySwanGuardrail,
initialize_guardrail as initialize_grayswan,
)
from .guardrail_initializers import (
initialize_bedrock,
@ -327,11 +327,13 @@ class GuardrailRegistry:
prisma_client: PrismaClient,
) -> List[Guardrail]:
"""
Get all guardrails from the database
Get all active guardrails from the database.
Only rows with status == "active" are returned (pending_review and rejected are excluded).
"""
try:
guardrails_from_db = (
await prisma_client.db.litellm_guardrailstable.find_many(
where={"status": "active"},
order={"created_at": "desc"},
)
)

View file

@ -871,6 +871,13 @@ model LiteLLM_GuardrailsTable {
team_id String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
// Submission lifecycle. Possible values: pending_review (team-registered, awaiting approval), active (approved), rejected
status String @default("active")
submitted_at DateTime?
reviewed_at DateTime?
// submitted_by_user_id and submitted_by_email live in guardrail_info JSON
@@index([status])
}
// Daily guardrail metrics for usage dashboard (one row per guardrail per day)

View file

@ -698,6 +698,15 @@ class BaseLitellmParams(
),
)
extra_headers: Optional[List[str]] = Field(
default=None,
description=(
"Header names to forward from the client request to the guardrail (e.g. x-request-id). "
"Only these headers' values are sent; others may be omitted or sent as [present]. "
"Used by generic_guardrail_api (similar to MCP extra_headers)."
),
)
# Custom code guardrail params
custom_code: Optional[str] = Field(
default=None,

View file

@ -871,6 +871,13 @@ model LiteLLM_GuardrailsTable {
team_id String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
// Submission lifecycle. Possible values: pending_review (team-registered, awaiting approval), active (approved), rejected
status String @default("active")
submitted_at DateTime?
reviewed_at DateTime?
// submitted_by_user_id and submitted_by_email live in guardrail_info JSON
@@index([status])
}
// Daily guardrail metrics for usage dashboard (one row per guardrail per day)

View file

@ -0,0 +1,92 @@
#!/usr/bin/env bash
#
# Creates a team, generates a team key, and submits a test guardrail with it.
# Requires: curl, jq
#
# Usage:
# ADMIN_KEY=sk-your-admin-key ./scripts/create_team_key_and_submit_guardrail.sh
# BASE_URL=http://localhost:4000 ADMIN_KEY=sk-your-admin-key ./scripts/create_team_key_and_submit_guardrail.sh
set -e
BASE_URL="${BASE_URL:-http://localhost:4000}"
BASE_URL="${BASE_URL%/}"
if [ -z "${ADMIN_KEY}" ]; then
echo "Error: ADMIN_KEY is required (admin API key for the proxy)."
echo "Usage: ADMIN_KEY=sk-your-admin-key $0"
exit 1
fi
AUTH_HEADER="Authorization: Bearer ${ADMIN_KEY}"
echo "Using BASE_URL=${BASE_URL}"
echo "Creating team..."
TEAM_RESP=$(curl -s -X POST "${BASE_URL}/team/new" \
-H "${AUTH_HEADER}" \
-H "Content-Type: application/json" \
-d '{
"team_alias": "guardrail-test-team"
}')
if ! echo "$TEAM_RESP" | jq -e .team_id >/dev/null 2>&1; then
echo "Failed to create team. Response:"
echo "$TEAM_RESP" | jq . 2>/dev/null || echo "$TEAM_RESP"
exit 1
fi
TEAM_ID=$(echo "$TEAM_RESP" | jq -r .team_id)
echo "Created team_id: ${TEAM_ID}"
echo "Creating key for team..."
KEY_RESP=$(curl -s -X POST "${BASE_URL}/key/generate" \
-H "${AUTH_HEADER}" \
-H "Content-Type: application/json" \
-d "{
\"team_id\": \"${TEAM_ID}\"
}")
if ! echo "$KEY_RESP" | jq -e .key >/dev/null 2>&1; then
echo "Failed to create key. Response:"
echo "$KEY_RESP" | jq . 2>/dev/null || echo "$KEY_RESP"
exit 1
fi
TEAM_KEY=$(echo "$KEY_RESP" | jq -r .key)
echo "Created team key: ${TEAM_KEY}"
GUARDRAIL_NAME="test-guardrail-$(date +%s)"
echo "Submitting guardrail: ${GUARDRAIL_NAME}"
REGISTER_RESP=$(curl -s -X POST "${BASE_URL}/guardrails/register" \
-H "Authorization: Bearer ${TEAM_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"guardrail_name\": \"${GUARDRAIL_NAME}\",
\"litellm_params\": {
\"guardrail\": \"generic_guardrail_api\",
\"mode\": \"pre_call\",
\"api_base\": \"https://example.com/guardrail\"
},
\"guardrail_info\": {
\"description\": \"Test guardrail submitted via team key\"
}
}")
if ! echo "$REGISTER_RESP" | jq -e .guardrail_id >/dev/null 2>&1; then
echo "Failed to register guardrail. Response:"
echo "$REGISTER_RESP" | jq . 2>/dev/null || echo "$REGISTER_RESP"
exit 1
fi
GUARDRAIL_ID=$(echo "$REGISTER_RESP" | jq -r .guardrail_id)
echo "Registered guardrail_id: ${GUARDRAIL_ID}"
echo ""
echo "Done."
echo " team_id: ${TEAM_ID}"
echo " team_key: ${TEAM_KEY}"
echo " guardrail_id: ${GUARDRAIL_ID}"
echo " guardrail_name: ${GUARDRAIL_NAME}"

View file

@ -0,0 +1,126 @@
#!/usr/bin/env bash
#
# Test guardrail register and submissions endpoints.
# Requires: proxy running with DB (migrations applied), valid admin API key.
#
# Usage:
# export LITELLM_API_KEY="sk-..." # required, use an admin key
# ./scripts/test_guardrails_register_endpoints.sh
# BASE_URL=http://localhost:4000 LITELLM_API_KEY="sk-..." ./scripts/test_guardrails_register_endpoints.sh
#
set -euo pipefail
BASE_URL="${BASE_URL:-http://localhost:4000}"
API_KEY="${LITELLM_API_KEY:-}"
if ! command -v jq &>/dev/null; then
echo "Error: jq is required. Install with: brew install jq (macOS) or apt-get install jq (Linux)"
exit 1
fi
if [[ -z "$API_KEY" ]]; then
echo "Error: LITELLM_API_KEY is not set. Use an admin key to test list/approve/reject."
exit 1
fi
AUTH_HEADER="Authorization: Bearer $API_KEY"
TIMESTAMP=$(date +%s)
NAME_APPROVE="test-guardrail-approve-$TIMESTAMP"
NAME_REJECT="test-guardrail-reject-$TIMESTAMP"
echo "BASE_URL=$BASE_URL"
echo "Testing guardrail register and submissions endpoints..."
echo ""
# --- 1. Register a guardrail (will approve later) ---
echo "[1/6] POST /guardrails/register (guardrail: $NAME_APPROVE)"
REGISTER_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/guardrails/register" \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "{
\"guardrail_name\": \"$NAME_APPROVE\",
\"litellm_params\": {
\"guardrail\": \"generic_guardrail_api\",
\"mode\": \"pre_call\",
\"api_base\": \"https://guardrails.example.com/validate\"
},
\"guardrail_info\": { \"description\": \"Test guardrail for approve flow\" }
}")
REGISTER_HTTP=$(echo "$REGISTER_RESPONSE" | tail -n1)
REGISTER_BODY=$(echo "$REGISTER_RESPONSE" | sed '$d')
if [[ "$REGISTER_HTTP" -ne 200 ]]; then
echo " FAIL: expected 200, got $REGISTER_HTTP"
echo "$REGISTER_BODY" | jq . 2>/dev/null || echo "$REGISTER_BODY"
exit 1
fi
GUARDRAIL_ID_APPROVE=$(echo "$REGISTER_BODY" | jq -r '.guardrail_id')
echo " OK (201/200) guardrail_id=$GUARDRAIL_ID_APPROVE"
# --- 2. Register a second guardrail (will reject later) ---
echo "[2/6] POST /guardrails/register (guardrail: $NAME_REJECT)"
REJECT_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/guardrails/register" \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "{
\"guardrail_name\": \"$NAME_REJECT\",
\"litellm_params\": {
\"guardrail\": \"generic_guardrail_api\",
\"mode\": \"post_call\",
\"api_base\": \"https://guardrails.example.com/reject-test\"
},
\"guardrail_info\": { \"description\": \"Test guardrail for reject flow\" }
}")
REJECT_HTTP=$(echo "$REJECT_RESPONSE" | tail -n1)
if [[ "$REJECT_HTTP" -ne 200 ]]; then
echo " FAIL: expected 200, got $REJECT_HTTP"
echo "$REJECT_RESPONSE" | sed '$d' | jq . 2>/dev/null || echo "$REJECT_RESPONSE"
exit 1
fi
GUARDRAIL_ID_REJECT=$(echo "$REJECT_RESPONSE" | sed '$d' | jq -r '.guardrail_id')
echo " OK guardrail_id=$GUARDRAIL_ID_REJECT"
# --- 3. List submissions (admin) ---
echo "[3/6] GET /guardrails/submissions"
LIST_RESPONSE=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/guardrails/submissions" -H "$AUTH_HEADER")
LIST_HTTP=$(echo "$LIST_RESPONSE" | tail -n1)
LIST_BODY=$(echo "$LIST_RESPONSE" | sed '$d')
if [[ "$LIST_HTTP" -ne 200 ]]; then
echo " FAIL: expected 200, got $LIST_HTTP"
echo "$LIST_BODY" | jq . 2>/dev/null || echo "$LIST_BODY"
exit 1
fi
echo " OK summary: $(echo "$LIST_BODY" | jq -c '.summary' 2>/dev/null || echo "N/A")"
# --- 4. Get one submission by id ---
echo "[4/6] GET /guardrails/submissions/$GUARDRAIL_ID_APPROVE"
GET_RESPONSE=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/guardrails/submissions/$GUARDRAIL_ID_APPROVE" -H "$AUTH_HEADER")
GET_HTTP=$(echo "$GET_RESPONSE" | tail -n1)
if [[ "$GET_HTTP" -ne 200 ]]; then
echo " FAIL: expected 200, got $GET_HTTP"
exit 1
fi
echo " OK status=$(echo "$GET_RESPONSE" | sed '$d' | jq -r '.status')"
# --- 5. Approve first submission ---
echo "[5/6] POST /guardrails/submissions/$GUARDRAIL_ID_APPROVE/approve"
APPROVE_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/guardrails/submissions/$GUARDRAIL_ID_APPROVE/approve" -H "$AUTH_HEADER")
APPROVE_HTTP=$(echo "$APPROVE_RESPONSE" | tail -n1)
if [[ "$APPROVE_HTTP" -ne 200 ]]; then
echo " FAIL: expected 200, got $APPROVE_HTTP"
echo "$APPROVE_RESPONSE" | sed '$d' | jq . 2>/dev/null || echo "$APPROVE_RESPONSE"
exit 1
fi
echo " OK $(echo "$APPROVE_RESPONSE" | sed '$d' | jq -c '.' 2>/dev/null)"
# --- 6. Reject second submission ---
echo "[6/6] POST /guardrails/submissions/$GUARDRAIL_ID_REJECT/reject"
REJECT_POST_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/guardrails/submissions/$GUARDRAIL_ID_REJECT/reject" -H "$AUTH_HEADER")
REJECT_POST_HTTP=$(echo "$REJECT_POST_RESPONSE" | tail -n1)
if [[ "$REJECT_POST_HTTP" -ne 200 ]]; then
echo " FAIL: expected 200, got $REJECT_POST_HTTP"
exit 1
fi
echo " OK $(echo "$REJECT_POST_RESPONSE" | sed '$d' | jq -c '.' 2>/dev/null)"
echo ""
echo "All 6 requests succeeded. Guardrail register and submissions endpoints are working."

View file

@ -13,8 +13,8 @@ import pytest
import litellm
from litellm import ModelResponse
from litellm.exceptions import GuardrailRaisedException, Timeout
from litellm._version import version as litellm_version
from litellm.exceptions import GuardrailRaisedException, Timeout
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.guardrails.guardrail_hooks.generic_guardrail_api import (
GenericGuardrailAPI,
@ -188,6 +188,97 @@ class TestGenericGuardrailAPIConfiguration:
)
assert "x-api-key" not in guardrail.headers
def test_init_with_extra_headers(self):
"""Test that extra_headers is stored for forwarding client headers to the guardrail"""
guardrail = GenericGuardrailAPI(
api_base="https://api.test.guardrail.com",
extra_headers=["x-request-id", "x-custom-auth"],
)
assert guardrail.extra_headers == ["x-request-id", "x-custom-auth"]
class TestExtraHeadersForwarding:
"""Test extra_headers: client headers allowed to be forwarded to the guardrail"""
@pytest.mark.asyncio
async def test_extra_headers_values_forwarded_to_guardrail(self):
"""When extra_headers is set, those client header values are sent to the guardrail."""
guardrail = GenericGuardrailAPI(
api_base="https://api.test.guardrail.com",
extra_headers=["x-my-header", "x-request-id"],
)
request_data = {
"proxy_server_request": {
"headers": {
"x-my-header": "my-value",
"x-request-id": "req-123",
"x-private": "secret",
},
},
}
mock_response = MagicMock()
mock_response.json.return_value = {
"action": "NONE",
"texts": ["test"],
}
mock_response.raise_for_status = MagicMock()
with patch.object(
guardrail.async_handler, "post", return_value=mock_response
) as mock_post:
await guardrail.apply_guardrail(
inputs={"texts": ["test"]},
request_data=request_data,
input_type="request",
)
call_args = mock_post.call_args
json_payload = call_args.kwargs["json"]
request_headers = json_payload.get("request_headers") or {}
# Headers in extra_headers have their values forwarded
assert request_headers.get("x-my-header") == "my-value"
assert request_headers.get("x-request-id") == "req-123"
# Headers not in allowlist are sent as placeholder
assert request_headers.get("x-private") == _HEADER_PRESENT_PLACEHOLDER
@pytest.mark.asyncio
async def test_without_extra_headers_custom_header_value_not_forwarded(self):
"""Without extra_headers, a custom client header is sent as [present] only."""
guardrail = GenericGuardrailAPI(
api_base="https://api.test.guardrail.com",
# no extra_headers
)
request_data = {
"proxy_server_request": {
"headers": {
"x-custom-auth": "bearer secret-token",
},
},
}
mock_response = MagicMock()
mock_response.json.return_value = {
"action": "NONE",
"texts": ["test"],
}
mock_response.raise_for_status = MagicMock()
with patch.object(
guardrail.async_handler, "post", return_value=mock_response
) as mock_post:
await guardrail.apply_guardrail(
inputs={"texts": ["test"]},
request_data=request_data,
input_type="request",
)
call_args = mock_post.call_args
json_payload = call_args.kwargs["json"]
request_headers = json_payload.get("request_headers") or {}
# x-custom-auth is not in default allowlist nor extra_headers, so value is not forwarded
assert request_headers.get("x-custom-auth") == _HEADER_PRESENT_PLACEHOLDER
class TestMetadataExtraction:
"""Test metadata extraction from request data"""

View file

@ -17,13 +17,19 @@ from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.guardrails.guardrail_endpoints import (
CreateGuardrailRequest,
PatchGuardrailRequest,
RegisterGuardrailRequest,
UpdateGuardrailRequest,
apply_guardrail,
approve_guardrail_submission,
create_guardrail,
delete_guardrail,
get_guardrail_info,
get_guardrail_submission,
list_guardrail_submissions,
list_guardrails_v2,
patch_guardrail,
register_guardrail,
reject_guardrail_submission,
update_guardrail,
)
@ -1103,4 +1109,466 @@ async def test_get_guardrail_info_endpoint_db_guardrail(mocker):
assert isinstance(result, GuardrailInfoResponse)
assert result.guardrail_id == "test-db-guardrail"
assert result.guardrail_name == "Test DB Guardrail"
assert result.guardrail_definition_location == "db"
assert result.guardrail_definition_location == "db"
# --- Team guardrail registration (register / submissions) ---
MOCK_REGISTER_REQUEST = RegisterGuardrailRequest(
guardrail_name="team-prompt-guard",
litellm_params={
"guardrail": "generic_guardrail_api",
"mode": "pre_call",
"api_base": "https://guardrails.example.com/validate",
},
guardrail_info={"description": "Team prompt injection detector"},
)
@pytest.mark.asyncio
async def test_register_guardrail_success(mocker):
"""Register creates a row with status pending_review and returns guardrail_id."""
mock_prisma = mocker.Mock()
mock_prisma.db.litellm_guardrailstable.find_unique = AsyncMock(return_value=None)
created_row = mocker.Mock(
guardrail_id="reg-123",
guardrail_name=MOCK_REGISTER_REQUEST.guardrail_name,
status="pending_review",
submitted_at=datetime.now(),
)
mock_prisma.db.litellm_guardrailstable.create = AsyncMock(return_value=created_row)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
user = UserAPIKeyAuth(user_id="u1", user_email="alice@co.com", team_id="team-1")
result = await register_guardrail(MOCK_REGISTER_REQUEST, user)
assert result.guardrail_id == "reg-123"
assert result.guardrail_name == MOCK_REGISTER_REQUEST.guardrail_name
assert result.status == "pending_review"
mock_prisma.db.litellm_guardrailstable.create.assert_called_once()
call_data = mock_prisma.db.litellm_guardrailstable.create.call_args[1]["data"]
assert call_data["status"] == "pending_review"
assert call_data["guardrail_name"] == MOCK_REGISTER_REQUEST.guardrail_name
@pytest.mark.asyncio
async def test_register_guardrail_rejects_non_generic_api(mocker):
"""Register returns 400 when litellm_params.guardrail is not generic_guardrail_api."""
mocker.patch("litellm.proxy.proxy_server.prisma_client", mocker.Mock())
req = RegisterGuardrailRequest(
guardrail_name="other-guard",
litellm_params={"guardrail": "bedrock", "mode": "pre_call", "api_base": "https://x.com"},
)
user = UserAPIKeyAuth(user_id="u1", user_email="a@b.com", team_id="team-1")
with pytest.raises(HTTPException) as exc_info:
await register_guardrail(req, user)
assert exc_info.value.status_code == 400
assert "generic_guardrail_api" in exc_info.value.detail
@pytest.mark.asyncio
async def test_register_guardrail_requires_team_id(mocker):
"""Register returns 400 when API key has no associated team_id."""
mocker.patch("litellm.proxy.proxy_server.prisma_client", mocker.Mock())
user = UserAPIKeyAuth(user_id="u1", user_email="a@b.com", team_id=None)
with pytest.raises(HTTPException) as exc_info:
await register_guardrail(MOCK_REGISTER_REQUEST, user)
assert exc_info.value.status_code == 400
assert "team" in exc_info.value.detail.lower()
@pytest.mark.asyncio
async def test_register_guardrail_duplicate_name(mocker):
"""Register returns 400 when guardrail_name already exists."""
mock_prisma = mocker.Mock()
mock_prisma.db.litellm_guardrailstable.find_unique = AsyncMock(
return_value={"guardrail_name": MOCK_REGISTER_REQUEST.guardrail_name}
)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
user = UserAPIKeyAuth(user_id="u1", user_email="a@b.com", team_id="team-1")
with pytest.raises(HTTPException) as exc_info:
await register_guardrail(MOCK_REGISTER_REQUEST, user)
assert exc_info.value.status_code == 400
assert "already exists" in exc_info.value.detail
@pytest.mark.asyncio
async def test_list_guardrail_submissions_requires_admin(mocker):
"""List submissions returns 403 when user is not admin."""
mocker.patch("litellm.proxy.proxy_server.prisma_client", mocker.Mock())
user = UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER)
with pytest.raises(HTTPException) as exc_info:
await list_guardrail_submissions(user_api_key_dict=user)
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_list_guardrail_submissions_success(mocker):
"""List submissions returns list and summary for admin."""
mock_prisma = mocker.Mock()
row = mocker.Mock(
guardrail_id="sub-1",
guardrail_name="pending-guard",
status="pending_review",
team_id="t1",
litellm_params={"guardrail": "generic_guardrail_api", "api_base": "https://x.com"},
guardrail_info={
"description": "A guard",
"submitted_by_user_id": "u1",
"submitted_by_email": "alice@co.com",
},
submitted_at=datetime.now(),
reviewed_at=None,
created_at=datetime.now(),
updated_at=datetime.now(),
)
mock_prisma.db.litellm_guardrailstable.find_many = AsyncMock(return_value=[row])
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
result = await list_guardrail_submissions(user_api_key_dict=user)
assert len(result.submissions) == 1
assert result.submissions[0].guardrail_id == "sub-1"
assert result.submissions[0].status == "pending_review"
assert result.submissions[0].team_guardrail is True # team_id is set
assert result.summary.total >= 1
assert result.summary.pending_review >= 1
@pytest.mark.asyncio
async def test_list_guardrail_submissions_returns_only_team_guardrails(mocker):
"""List submissions only returns team guardrails (team_id not null)."""
mock_prisma = mocker.Mock()
find_many = AsyncMock(return_value=[])
mock_prisma.db.litellm_guardrailstable.find_many = find_many
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
await list_guardrail_submissions(user_api_key_dict=user)
calls = find_many.call_args_list
assert len(calls) >= 1
first_where = calls[0].kwargs.get("where", {})
assert first_where.get("team_id") == {"not": None}
@pytest.mark.asyncio
async def test_list_guardrail_submissions_team_id_filter(mocker):
"""List submissions with team_id filter returns only that team's guardrails."""
mock_prisma = mocker.Mock()
row_abc = mocker.Mock(
guardrail_id="team-1",
guardrail_name="team-guard",
status="active",
team_id="team-abc",
litellm_params={},
guardrail_info={},
submitted_at=None,
reviewed_at=None,
created_at=datetime.now(),
updated_at=datetime.now(),
)
row_other = mocker.Mock(
guardrail_id="team-2",
guardrail_name="other-guard",
status="active",
team_id="team-xyz",
litellm_params={},
guardrail_info={},
submitted_at=None,
reviewed_at=None,
created_at=datetime.now(),
updated_at=datetime.now(),
)
find_many = AsyncMock(return_value=[row_abc, row_other])
mock_prisma.db.litellm_guardrailstable.find_many = find_many
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
result = await list_guardrail_submissions(
user_api_key_dict=user, team_id="team-abc"
)
assert len(result.submissions) == 1
assert result.submissions[0].guardrail_id == "team-1"
assert result.submissions[0].team_guardrail is True
assert result.summary.total == 2 # summary counts all team guardrails
@pytest.mark.asyncio
async def test_get_guardrail_submission_not_found(mocker):
"""Get submission returns 404 when guardrail_id does not exist."""
mock_prisma = mocker.Mock()
mock_prisma.db.litellm_guardrailstable.find_unique = AsyncMock(return_value=None)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
with pytest.raises(HTTPException) as exc_info:
await get_guardrail_submission("nonexistent-id", user)
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_approve_guardrail_submission_success(mocker):
"""Approve sets status to active and initializes guardrail in memory."""
mock_prisma = mocker.Mock()
row = mocker.Mock(
guardrail_id="approve-me",
guardrail_name="my-guard",
status="pending_review",
litellm_params={"guardrail": "generic_guardrail_api", "mode": "pre_call", "api_base": "https://g.com"},
guardrail_info={},
)
mock_prisma.db.litellm_guardrailstable.find_unique = AsyncMock(return_value=row)
mock_prisma.db.litellm_guardrailstable.update = AsyncMock()
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
mock_handler = mocker.Mock()
mock_handler.initialize_guardrail = mocker.Mock()
mocker.patch(
"litellm.proxy.guardrails.guardrail_registry.IN_MEMORY_GUARDRAIL_HANDLER",
mock_handler,
)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
result = await approve_guardrail_submission("approve-me", user)
assert result["status"] == "active"
assert result["guardrail_id"] == "approve-me"
mock_prisma.db.litellm_guardrailstable.update.assert_called_once()
call_data = mock_prisma.db.litellm_guardrailstable.update.call_args[1]["data"]
assert call_data["status"] == "active"
@pytest.mark.asyncio
async def test_approve_guardrail_submission_not_pending(mocker):
"""Approve returns 400 when status is not pending_review."""
mock_prisma = mocker.Mock()
row = mocker.Mock(guardrail_id="x", guardrail_name="y", status="active")
mock_prisma.db.litellm_guardrailstable.find_unique = AsyncMock(return_value=row)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
with pytest.raises(HTTPException) as exc_info:
await approve_guardrail_submission("x", user)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_reject_guardrail_submission_success(mocker):
"""Reject sets status to rejected."""
mock_prisma = mocker.Mock()
row = mocker.Mock(guardrail_id="rej-1", guardrail_name="r", status="pending_review")
mock_prisma.db.litellm_guardrailstable.find_unique = AsyncMock(return_value=row)
mock_prisma.db.litellm_guardrailstable.update = AsyncMock()
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
result = await reject_guardrail_submission("rej-1", user)
assert result["status"] == "rejected"
mock_prisma.db.litellm_guardrailstable.update.assert_called_once()
call_data = mock_prisma.db.litellm_guardrailstable.update.call_args[1]["data"]
assert call_data["status"] == "rejected"
@pytest.mark.asyncio
async def test_reject_guardrail_submission_not_pending(mocker):
"""Reject returns 400 when status is not pending_review (e.g. already active)."""
mock_prisma = mocker.Mock()
row = mocker.Mock(guardrail_id="already-active", guardrail_name="g", status="active")
mock_prisma.db.litellm_guardrailstable.find_unique = AsyncMock(return_value=row)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
with pytest.raises(HTTPException) as exc_info:
await reject_guardrail_submission("already-active", user)
assert exc_info.value.status_code == 400
assert "not pending review" in exc_info.value.detail.lower()
# --- Tests for review fixes ---
@pytest.mark.asyncio
@pytest.mark.parametrize(
"api_base,expected_detail",
[
("file:///etc/passwd", "http or https scheme"),
("ftp://internal.host/data", "http or https scheme"),
("javascript:alert(1)", "http or https scheme"),
("://missing-scheme", "http or https scheme"),
("https://", "valid hostname"),
],
ids=[
"file_scheme",
"ftp_scheme",
"javascript_scheme",
"no_scheme",
"no_hostname",
],
)
async def test_register_guardrail_rejects_bad_api_base(mocker, api_base, expected_detail):
"""Register returns 400 when api_base has invalid scheme or missing hostname."""
mocker.patch("litellm.proxy.proxy_server.prisma_client", mocker.Mock())
req = RegisterGuardrailRequest(
guardrail_name="bad-url-guard",
litellm_params={
"guardrail": "generic_guardrail_api",
"mode": "pre_call",
"api_base": api_base,
},
)
user = UserAPIKeyAuth(user_id="u1", user_email="a@b.com", team_id="team-1")
with pytest.raises(HTTPException) as exc_info:
await register_guardrail(req, user)
assert exc_info.value.status_code == 400
assert expected_detail in exc_info.value.detail
@pytest.mark.asyncio
async def test_register_guardrail_accepts_valid_https_url(mocker):
"""Register accepts valid https api_base URLs."""
mock_prisma = mocker.Mock()
mock_prisma.db.litellm_guardrailstable.find_unique = AsyncMock(return_value=None)
created_row = mocker.Mock(
guardrail_id="valid-url-123",
guardrail_name="valid-guard",
status="pending_review",
submitted_at=datetime.now(),
)
mock_prisma.db.litellm_guardrailstable.create = AsyncMock(return_value=created_row)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
req = RegisterGuardrailRequest(
guardrail_name="valid-guard",
litellm_params={
"guardrail": "generic_guardrail_api",
"mode": "pre_call",
"api_base": "https://guardrails.example.com/v1/check",
},
)
user = UserAPIKeyAuth(user_id="u1", user_email="a@b.com", team_id="team-1")
result = await register_guardrail(req, user)
assert result.guardrail_id == "valid-url-123"
assert result.status == "pending_review"
@pytest.mark.asyncio
async def test_approve_guardrail_init_failure_returns_warning(mocker):
"""Approve returns a warning field when in-memory initialization fails."""
mock_prisma = mocker.Mock()
row = mocker.Mock(
guardrail_id="warn-me",
guardrail_name="fragile-guard",
status="pending_review",
litellm_params={
"guardrail": "generic_guardrail_api",
"mode": "pre_call",
"api_base": "https://g.com",
},
guardrail_info={},
)
mock_prisma.db.litellm_guardrailstable.find_unique = AsyncMock(return_value=row)
mock_prisma.db.litellm_guardrailstable.update = AsyncMock()
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
mock_handler = mocker.Mock()
mock_handler.initialize_guardrail = mocker.Mock(
side_effect=Exception("missing dependency")
)
mocker.patch(
"litellm.proxy.guardrails.guardrail_registry.IN_MEMORY_GUARDRAIL_HANDLER",
mock_handler,
)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
result = await approve_guardrail_submission("warn-me", user)
assert result["status"] == "active"
assert "warning" in result
assert "failed to initialize" in result["warning"].lower()
assert "missing dependency" in result["warning"]
@pytest.mark.asyncio
async def test_approve_guardrail_no_warning_on_success(mocker):
"""Approve does NOT include a warning field when init succeeds."""
mock_prisma = mocker.Mock()
row = mocker.Mock(
guardrail_id="ok-guard",
guardrail_name="good-guard",
status="pending_review",
litellm_params={
"guardrail": "generic_guardrail_api",
"mode": "pre_call",
"api_base": "https://g.com",
},
guardrail_info={},
)
mock_prisma.db.litellm_guardrailstable.find_unique = AsyncMock(return_value=row)
mock_prisma.db.litellm_guardrailstable.update = AsyncMock()
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
mock_handler = mocker.Mock()
mock_handler.initialize_guardrail = mocker.Mock() # no exception
mocker.patch(
"litellm.proxy.guardrails.guardrail_registry.IN_MEMORY_GUARDRAIL_HANDLER",
mock_handler,
)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
result = await approve_guardrail_submission("ok-guard", user)
assert result["status"] == "active"
assert "warning" not in result
@pytest.mark.asyncio
async def test_list_submissions_single_db_query(mocker):
"""List submissions makes exactly one find_many call (no redundant query)."""
mock_prisma = mocker.Mock()
find_many = AsyncMock(return_value=[])
mock_prisma.db.litellm_guardrailstable.find_many = find_many
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
await list_guardrail_submissions(user_api_key_dict=user)
assert find_many.call_count == 1
@pytest.mark.asyncio
async def test_list_submissions_summary_counts_unaffected_by_filters(mocker):
"""Summary counts reflect all team guardrails regardless of status filter."""
mock_prisma = mocker.Mock()
pending_row = mocker.Mock(
guardrail_id="p1", guardrail_name="p", status="pending_review",
team_id="t1", litellm_params={}, guardrail_info={},
submitted_at=None, reviewed_at=None,
created_at=datetime.now(), updated_at=datetime.now(),
)
active_row = mocker.Mock(
guardrail_id="a1", guardrail_name="a", status="active",
team_id="t1", litellm_params={}, guardrail_info={},
submitted_at=None, reviewed_at=None,
created_at=datetime.now(), updated_at=datetime.now(),
)
all_rows = [pending_row, active_row]
mock_prisma.db.litellm_guardrailstable.find_many = AsyncMock(return_value=all_rows)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma)
user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
# Filter to only pending, but summary should still show both
result = await list_guardrail_submissions(status="pending_review", user_api_key_dict=user)
assert len(result.submissions) == 1 # filtered
assert result.summary.total == 2 # unfiltered
assert result.summary.pending_review == 1
assert result.summary.active == 1

View file

@ -12984,6 +12984,21 @@
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}

View file

@ -14,6 +14,7 @@ import DeleteResourceModal from "./common_components/DeleteResourceModal";
import { getGuardrailLogoAndName } from "./guardrails/guardrail_info_helpers";
import { CustomCodeModal } from "./guardrails/custom_code";
import GuardrailGarden from "./guardrails/guardrail_garden";
import { TeamGuardrailsTab } from "./guardrails/TeamGuardrailsTab";
interface GuardrailsPanelProps {
accessToken: string | null;
@ -139,6 +140,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
<Tab>Guardrail Garden</Tab>
<Tab>Guardrails</Tab>
<Tab disabled={!accessToken || guardrailsList.length === 0}>Test Playground</Tab>
<Tab>Team Guardrails</Tab>
</TabList>
<TabPanels>
@ -242,6 +244,11 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
onClose={() => setActiveTab(0)}
/>
</TabPanel>
{/* Team Guardrails Tab */}
<TabPanel>
<TeamGuardrailsTab accessToken={accessToken} />
</TabPanel>
</TabPanels>
</TabGroup>
</div>

File diff suppressed because it is too large Load diff

View file

@ -5484,6 +5484,131 @@ export const getGuardrailsList = async (accessToken: string) => {
}
};
// Team guardrail submissions (admin)
export interface GuardrailSubmissionItem {
guardrail_id: string;
guardrail_name: string;
status: string; // "pending_review" | "active" | "rejected"
team_id?: string | null;
team_guardrail?: boolean; // true when submitted via team (team_id set)
litellm_params?: Record<string, unknown> | null;
guardrail_info?: Record<string, unknown> | null;
submitted_by_user_id?: string | null;
submitted_by_email?: string | null;
submitted_at?: string | null;
reviewed_at?: string | null;
created_at?: string | null;
updated_at?: string | null;
}
export interface GuardrailSubmissionSummary {
total: number;
pending_review: number;
active: number;
rejected: number;
}
export interface ListGuardrailSubmissionsResponse {
submissions: GuardrailSubmissionItem[];
summary: GuardrailSubmissionSummary;
}
export const listGuardrailSubmissions = async (
accessToken: string,
params?: { status?: string; team_id?: string; team_guardrail?: boolean; search?: string }
): Promise<ListGuardrailSubmissionsResponse> => {
const url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/submissions` : `/guardrails/submissions`;
const searchParams = new URLSearchParams();
if (params?.status) searchParams.set("status", params.status);
if (params?.team_id) searchParams.set("team_id", params.team_id);
if (params?.team_guardrail !== undefined) searchParams.set("team_guardrail", String(params.team_guardrail));
if (params?.search) searchParams.set("search", params.search);
const fullUrl = searchParams.toString() ? `${url}?${searchParams.toString()}` : url;
const response = await fetch(fullUrl, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
return response.json();
};
export const getGuardrailSubmission = async (
accessToken: string,
guardrailId: string
): Promise<GuardrailSubmissionItem> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/guardrails/submissions/${encodeURIComponent(guardrailId)}`
: `/guardrails/submissions/${encodeURIComponent(guardrailId)}`;
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
return response.json();
};
export const approveGuardrailSubmission = async (
accessToken: string,
guardrailId: string
): Promise<{ guardrail_id: string; status: string; message: string }> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/guardrails/submissions/${encodeURIComponent(guardrailId)}/approve`
: `/guardrails/submissions/${encodeURIComponent(guardrailId)}/approve`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
return response.json();
};
export const rejectGuardrailSubmission = async (
accessToken: string,
guardrailId: string
): Promise<{ guardrail_id: string; status: string; message: string }> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/guardrails/submissions/${encodeURIComponent(guardrailId)}/reject`
: `/guardrails/submissions/${encodeURIComponent(guardrailId)}/reject`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
return response.json();
};
// Guardrails / Policies usage (dashboard)
export const getGuardrailsUsageOverview = async (
accessToken: string,
@ -8364,6 +8489,7 @@ export const updateGuardrailCall = async (
guardrail_name?: string;
default_on?: boolean;
guardrail_info?: Record<string, any>;
litellm_params?: Record<string, any>;
},
) => {
try {