mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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:
parent
213799282b
commit
67f90254ed
23 changed files with 2724 additions and 29 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
137
docs/my-website/docs/proxy/guardrails/team_based_guardrails.md
Normal file
137
docs/my-website/docs/proxy/guardrails/team_based_guardrails.md
Normal 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).
|
||||
BIN
docs/my-website/img/admin_team_guardrails.png
Normal file
BIN
docs/my-website/img/admin_team_guardrails.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 523 KiB |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ##
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
92
scripts/create_team_key_and_submit_guardrail.sh
Executable file
92
scripts/create_team_key_and_submit_guardrail.sh
Executable 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}"
|
||||
126
scripts/test_guardrails_register_endpoints.sh
Executable file
126
scripts/test_guardrails_register_endpoints.sh
Executable 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."
|
||||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
15
ui/litellm-dashboard/package-lock.json
generated
15
ui/litellm-dashboard/package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
1081
ui/litellm-dashboard/src/components/guardrails/TeamGuardrailsTab.tsx
Normal file
1081
ui/litellm-dashboard/src/components/guardrails/TeamGuardrailsTab.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue