mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
feat: implement guardrails usage dashboard backend
Add backend infrastructure for guardrails performance monitoring dashboard:
Database Schema:
- Add LiteLLM_DailyGuardrailMetrics table for daily aggregated metrics
- Track total_requests, success/intervened/failed/not_run counts per guardrail
- Store aggregated latency metrics in milliseconds
- Unique constraint on [guardrail_name, provider, mode, date, api_key]
Data Collection & Aggregation:
- Add DailyGuardrailMetricsTransaction type for queue transactions
- Implement guardrail metrics extraction from spend log metadata
- Add batch upsert logic with retry handling (60s commit interval)
- Process each guardrail separately with status-based counting
API Endpoints:
- GET /guardrail/metrics - List all guardrails with aggregated metrics
- GET /guardrail/{name}/metrics - Detail view with daily time-series
- GET /guardrail/{name}/logs - Request logs with status filtering
Type Definitions:
- Add Pydantic models for API request/response validation
- GuardrailSummary, GuardrailDetailMetrics, GuardrailLogsResponse
Key Features:
- Fail rate = (intervened_count / total_requests) * 100
- Avg latency measures guardrail execution overhead only
- Reuses LiteLLM_SpendLogs for per-request drill-down
- Follows existing daily spend tracking patterns
Testing Required:
- Run: poetry run prisma migrate dev --name add_guardrail_metrics
- Frontend implementation pending (Phase 5)
- See IMPLEMENTATION_STATUS.md for details
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2c8fcf854a
commit
e394867fc9
7 changed files with 901 additions and 19 deletions
196
IMPLEMENTATION_STATUS.md
Normal file
196
IMPLEMENTATION_STATUS.md
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
# Guardrails Usage Dashboard - Implementation Status
|
||||
|
||||
## Completed: Backend Implementation (Phases 1-4)
|
||||
|
||||
### ✅ Phase 1: Database Schema
|
||||
- **File**: `litellm/proxy/schema.prisma`
|
||||
- Added `LiteLLM_DailyGuardrailMetrics` table with:
|
||||
- Unique constraint on `[guardrail_name, guardrail_provider, guardrail_mode, date, api_key]`
|
||||
- Indexes on `date`, `guardrail_name`, `guardrail_provider`, `api_key`
|
||||
- Fields for tracking total_requests, success/intervened/failed/not_run counts
|
||||
- Aggregated latency metrics in milliseconds
|
||||
- **Action Required**: Run `poetry run prisma migrate dev --name add_guardrail_metrics` to create the table
|
||||
|
||||
### ✅ Phase 2: Data Collection & Aggregation
|
||||
- **File**: `litellm/proxy/_types.py`
|
||||
- Added `DailyGuardrailMetricsTransaction` TypedDict
|
||||
|
||||
- **File**: `litellm/proxy/db/db_spend_update_writer.py`
|
||||
- Added `daily_guardrail_metrics_update_queue` to `__init__`
|
||||
- Implemented `add_spend_log_transaction_to_daily_guardrail_transaction()`:
|
||||
- Extracts guardrail_information from metadata
|
||||
- Creates separate transaction per guardrail
|
||||
- Calculates status counts and latency in milliseconds
|
||||
- Implemented `update_daily_guardrail_metrics()` static method:
|
||||
- Batch upserts to database with retry logic
|
||||
- Increments counters on conflict
|
||||
- Added guardrail transaction call to `update_database()` flow
|
||||
- Added commit logic to `_commit_spend_updates_to_db_without_redis_buffer()`
|
||||
|
||||
### ✅ Phase 3: Type Definitions
|
||||
- **File**: `litellm/types/proxy/management_endpoints/guardrail_metrics.py`
|
||||
- Created Pydantic models for:
|
||||
- `GuardrailMetrics` - Aggregated metrics
|
||||
- `GuardrailSummary` - Table view
|
||||
- `GuardrailMetricsResponse` - List endpoint response
|
||||
- `GuardrailDailyMetrics` - Time-series data
|
||||
- `GuardrailDetailMetrics` - Detail view with daily metrics
|
||||
- `GuardrailLogEntry` - Individual request log
|
||||
- `GuardrailLogsResponse` - Logs endpoint response
|
||||
|
||||
### ✅ Phase 4: API Endpoints
|
||||
- **File**: `litellm/proxy/management_endpoints/guardrail_metrics_endpoints.py`
|
||||
- Implemented 3 endpoints:
|
||||
1. `GET /guardrail/metrics` - List guardrails with aggregated metrics
|
||||
- Query params: start_date, end_date, guardrail_name, provider, page, page_size
|
||||
- Returns sorted by fail_rate descending
|
||||
2. `GET /guardrail/{guardrail_name}/metrics` - Detail view metrics
|
||||
- Returns overview stats + daily time-series
|
||||
3. `GET /guardrail/{guardrail_name}/logs` - Request logs
|
||||
- Query params: start_date, end_date, status_filter, page, page_size
|
||||
- Filters LiteLLM_SpendLogs by guardrail_information
|
||||
|
||||
- **File**: `litellm/proxy/proxy_server.py`
|
||||
- Added import for `guardrail_metrics_router`
|
||||
- Registered router with `app.include_router(guardrail_metrics_router)`
|
||||
|
||||
## 🔲 Pending: Frontend Implementation (Phase 5)
|
||||
|
||||
### TypeScript Types
|
||||
- **File to create**: `ui/litellm-dashboard/src/components/GuardrailsPage/types.ts`
|
||||
- Defines interfaces matching backend Pydantic models
|
||||
|
||||
### Networking Functions
|
||||
- **File to update**: `ui/litellm-dashboard/src/components/networking.tsx`
|
||||
- Add API call functions:
|
||||
- `guardrailMetricsCall()`
|
||||
- `guardrailDetailMetricsCall()`
|
||||
- `guardrailLogsCall()`
|
||||
|
||||
### Components
|
||||
1. **Table View**: `ui/litellm-dashboard/src/components/GuardrailsPage/GuardrailsTableView.tsx`
|
||||
- Displays list of guardrails with metrics
|
||||
- Clickable rows navigate to detail view
|
||||
|
||||
2. **Detail View**: `ui/litellm-dashboard/src/components/GuardrailsPage/GuardrailDetailView.tsx`
|
||||
- Metric cards (Requests, Fail Rate, Latency, Blocked)
|
||||
- Tabs for Overview and Logs
|
||||
- Area chart for fail rate trend
|
||||
|
||||
3. **Logs Tab**: `ui/litellm-dashboard/src/components/GuardrailsPage/GuardrailLogsTab.tsx`
|
||||
- Filterable table (All, Blocked, Passed)
|
||||
- Expandable rows for guardrail response details
|
||||
|
||||
### Pages
|
||||
1. **Main Page**: `ui/litellm-dashboard/src/app/(dashboard)/guardrails/page.tsx`
|
||||
- Date range picker
|
||||
- Renders GuardrailsTableView
|
||||
|
||||
2. **Detail Page**: `ui/litellm-dashboard/src/app/(dashboard)/guardrails/[name]/page.tsx`
|
||||
- Back button to overview
|
||||
- Date range picker
|
||||
- Renders GuardrailDetailView
|
||||
|
||||
## Testing & Verification
|
||||
|
||||
### Backend Testing Steps
|
||||
1. Run Prisma migration:
|
||||
```bash
|
||||
cd /Users/krrishdholakia/Documents/litellm-guardrails-dashboard
|
||||
poetry run prisma migrate dev --name add_guardrail_metrics --schema=litellm/proxy/schema.prisma
|
||||
```
|
||||
|
||||
2. Start the proxy server with guardrails configured
|
||||
|
||||
3. Send requests that trigger guardrails (both pass and fail cases)
|
||||
|
||||
4. Wait 60s for batch commit to database
|
||||
|
||||
5. Test API endpoints:
|
||||
```bash
|
||||
# List guardrails
|
||||
curl -X GET "http://localhost:4000/guardrail/metrics?start_date=2026-02-01&end_date=2026-02-19" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
|
||||
# Get guardrail details
|
||||
curl -X GET "http://localhost:4000/guardrail/my-guardrail/metrics?start_date=2026-02-01&end_date=2026-02-19" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
|
||||
# Get logs
|
||||
curl -X GET "http://localhost:4000/guardrail/my-guardrail/logs?start_date=2026-02-01&end_date=2026-02-19" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
6. Verify data in database:
|
||||
```sql
|
||||
SELECT * FROM "LiteLLM_DailyGuardrailMetrics" LIMIT 10;
|
||||
```
|
||||
|
||||
### Frontend Testing Steps (Once Implemented)
|
||||
1. Navigate to `/guardrails` page
|
||||
2. Verify table loads with metrics
|
||||
3. Click guardrail row → navigate to detail page
|
||||
4. Verify Overview tab shows metrics and chart
|
||||
5. Switch to Logs tab → verify logs display
|
||||
6. Test status filter (All, Blocked, Passed)
|
||||
7. Test date range filtering
|
||||
8. Test pagination
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Fail Rate Calculation
|
||||
- **Formula**: `(intervened_count / total_requests) * 100`
|
||||
- Only counts `guardrail_intervened` as failures (policy violations)
|
||||
- Excludes `guardrail_failed_to_respond` (infrastructure errors)
|
||||
|
||||
### Average Latency
|
||||
- **Formula**: `total_latency_ms / total_requests`
|
||||
- Measures **guardrail execution overhead** (not total request latency)
|
||||
- Captured from `StandardLoggingGuardrailInformation.duration` field
|
||||
- Stored in milliseconds for better precision
|
||||
|
||||
### Per-Request Logs
|
||||
- No new table needed
|
||||
- Existing `LiteLLM_SpendLogs.metadata.guardrail_information` used
|
||||
- Query optimized with date filtering and over-fetching strategy
|
||||
|
||||
### Aggregation Strategy
|
||||
- Daily aggregation reduces full table scans on large datasets
|
||||
- Batch commits every 60s reduce database load
|
||||
- Indexed queries for fast filtering
|
||||
- Pagination limits memory usage
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Backend
|
||||
1. `litellm/proxy/schema.prisma` - Added table
|
||||
2. `litellm/proxy/_types.py` - Added transaction type
|
||||
3. `litellm/proxy/db/db_spend_update_writer.py` - Data collection & commit
|
||||
4. `litellm/types/proxy/management_endpoints/guardrail_metrics.py` - New file
|
||||
5. `litellm/proxy/management_endpoints/guardrail_metrics_endpoints.py` - New file
|
||||
6. `litellm/proxy/proxy_server.py` - Router registration
|
||||
|
||||
### Frontend (Pending - 7 files)
|
||||
1. `ui/litellm-dashboard/src/components/GuardrailsPage/types.ts`
|
||||
2. `ui/litellm-dashboard/src/components/networking.tsx`
|
||||
3. `ui/litellm-dashboard/src/components/GuardrailsPage/GuardrailsTableView.tsx`
|
||||
4. `ui/litellm-dashboard/src/components/GuardrailsPage/GuardrailDetailView.tsx`
|
||||
5. `ui/litellm-dashboard/src/components/GuardrailsPage/GuardrailLogsTab.tsx`
|
||||
6. `ui/litellm-dashboard/src/app/(dashboard)/guardrails/page.tsx`
|
||||
7. `ui/litellm-dashboard/src/app/(dashboard)/guardrails/[name]/page.tsx`
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Run Prisma Migration** to create the database table
|
||||
2. **Test Backend** with curl requests after sending guardrail traffic
|
||||
3. **Implement Frontend** following Phase 5 specifications
|
||||
4. **Run `make test-unit`** to ensure no regressions
|
||||
5. **Test Integration** with high request volume (1000+ requests)
|
||||
6. **Create Pull Request** with proper tests and documentation
|
||||
|
||||
## Notes
|
||||
|
||||
- Migration needs to be run in an environment with proper Prisma setup
|
||||
- Some Pyright diagnostics appeared but are pre-existing in codebase
|
||||
- Frontend implementation follows existing LiteLLM dashboard patterns (Tremor UI components)
|
||||
- All backend code follows existing patterns from daily spend tracking
|
||||
|
|
@ -4079,6 +4079,21 @@ class DailyAgentSpendTransaction(BaseDailySpendTransaction):
|
|||
agent_id: str
|
||||
|
||||
|
||||
class DailyGuardrailMetricsTransaction(TypedDict):
|
||||
"""Transaction for daily guardrail metrics aggregation."""
|
||||
guardrail_name: str
|
||||
guardrail_provider: str
|
||||
guardrail_mode: str
|
||||
date: str # YYYY-MM-DD
|
||||
api_key: str
|
||||
total_requests: int
|
||||
success_count: int
|
||||
intervened_count: int
|
||||
failed_count: int
|
||||
not_run_count: int
|
||||
total_latency_ms: float
|
||||
|
||||
|
||||
class DBSpendUpdateTransactions(TypedDict):
|
||||
"""
|
||||
Internal Data Structure for buffering spend updates in Redis or in memory before committing them to the database
|
||||
|
|
|
|||
|
|
@ -13,7 +13,17 @@ import random
|
|||
import time
|
||||
import traceback
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, cast, overload
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
|
|
@ -23,12 +33,13 @@ from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
|
|||
from litellm.proxy._types import (
|
||||
DB_CONNECTION_ERROR_TYPES,
|
||||
BaseDailySpendTransaction,
|
||||
DailyTagSpendTransaction,
|
||||
DailyOrganizationSpendTransaction,
|
||||
DailyTeamSpendTransaction,
|
||||
DailyEndUserSpendTransaction,
|
||||
DailyUserSpendTransaction,
|
||||
DailyAgentSpendTransaction,
|
||||
DailyEndUserSpendTransaction,
|
||||
DailyGuardrailMetricsTransaction,
|
||||
DailyOrganizationSpendTransaction,
|
||||
DailyTagSpendTransaction,
|
||||
DailyTeamSpendTransaction,
|
||||
DailyUserSpendTransaction,
|
||||
DBSpendUpdateTransactions,
|
||||
Litellm_EntityType,
|
||||
LiteLLM_UserTable,
|
||||
|
|
@ -73,6 +84,7 @@ class DBSpendUpdateWriter:
|
|||
self.daily_agent_spend_update_queue = DailySpendUpdateQueue()
|
||||
self.daily_org_spend_update_queue = DailySpendUpdateQueue()
|
||||
self.daily_tag_spend_update_queue = DailySpendUpdateQueue()
|
||||
self.daily_guardrail_metrics_update_queue = DailySpendUpdateQueue()
|
||||
|
||||
async def update_database(
|
||||
# LiteLLM management object fields
|
||||
|
|
@ -221,6 +233,12 @@ class DBSpendUpdateWriter:
|
|||
prisma_client=prisma_client,
|
||||
)
|
||||
)
|
||||
asyncio.create_task(
|
||||
self.add_spend_log_transaction_to_daily_guardrail_transaction(
|
||||
payload=copy.deepcopy(payload),
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
)
|
||||
|
||||
verbose_proxy_logger.debug("Runs spend update on all tables")
|
||||
except Exception:
|
||||
|
|
@ -699,6 +717,20 @@ class DBSpendUpdateWriter:
|
|||
daily_spend_transactions=daily_agent_spend_update_transactions,
|
||||
)
|
||||
|
||||
################## Daily Guardrail Metrics Update Transactions ##################
|
||||
# Aggregate all in memory daily guardrail metrics transactions and commit to db
|
||||
daily_guardrail_metrics_transactions = cast(
|
||||
Dict[str, DailyGuardrailMetricsTransaction],
|
||||
await self.daily_guardrail_metrics_update_queue.flush_and_get_aggregated_daily_spend_update_transactions(),
|
||||
)
|
||||
|
||||
await DBSpendUpdateWriter.update_daily_guardrail_metrics(
|
||||
n_retry_times=n_retry_times,
|
||||
prisma_client=prisma_client,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
daily_metrics_transactions=daily_guardrail_metrics_transactions,
|
||||
)
|
||||
|
||||
async def _commit_spend_updates_to_db( # noqa: PLR0915
|
||||
self,
|
||||
prisma_client: PrismaClient,
|
||||
|
|
@ -1475,6 +1507,134 @@ class DBSpendUpdateWriter:
|
|||
unique_constraint_name="tag_date_api_key_model_custom_llm_provider_mcp_namespaced_tool_name_endpoint",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def update_daily_guardrail_metrics(
|
||||
n_retry_times: int,
|
||||
prisma_client: PrismaClient,
|
||||
proxy_logging_obj: ProxyLogging,
|
||||
daily_metrics_transactions: Dict[str, DailyGuardrailMetricsTransaction],
|
||||
):
|
||||
"""
|
||||
Batch upsert daily guardrail metrics to database.
|
||||
|
||||
Uses same pattern as daily spend tables with upsert on conflict.
|
||||
"""
|
||||
if not daily_metrics_transactions:
|
||||
return
|
||||
|
||||
from litellm.proxy.utils import _raise_failed_update_spend_exception
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
f"Daily Guardrail Metrics transactions: {len(daily_metrics_transactions)}"
|
||||
)
|
||||
BATCH_SIZE = 100
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
for i in range(n_retry_times + 1):
|
||||
try:
|
||||
# Sort transactions to minimize deadlocks
|
||||
transactions_to_process = dict(
|
||||
sorted(
|
||||
daily_metrics_transactions.items(),
|
||||
key=lambda x: (
|
||||
x[1].get("date") or "",
|
||||
x[1].get("guardrail_name") or "",
|
||||
x[1].get("api_key") or "",
|
||||
),
|
||||
)[:BATCH_SIZE]
|
||||
)
|
||||
|
||||
if len(transactions_to_process) == 0:
|
||||
verbose_proxy_logger.debug(
|
||||
"No new transactions to process for daily guardrail metrics update"
|
||||
)
|
||||
break
|
||||
|
||||
try:
|
||||
async with prisma_client.db.batch_() as batcher:
|
||||
for _, transaction in transactions_to_process.items():
|
||||
where_clause = {
|
||||
"guardrail_daily_unique": {
|
||||
"guardrail_name": transaction["guardrail_name"],
|
||||
"guardrail_provider": transaction.get("guardrail_provider", "unknown"),
|
||||
"guardrail_mode": transaction.get("guardrail_mode", "unknown"),
|
||||
"date": transaction["date"],
|
||||
"api_key": transaction["api_key"],
|
||||
}
|
||||
}
|
||||
|
||||
common_data = {
|
||||
"guardrail_name": transaction["guardrail_name"],
|
||||
"guardrail_provider": transaction.get("guardrail_provider"),
|
||||
"guardrail_mode": transaction.get("guardrail_mode"),
|
||||
"date": transaction["date"],
|
||||
"api_key": transaction["api_key"],
|
||||
"total_requests": transaction["total_requests"],
|
||||
"success_count": transaction["success_count"],
|
||||
"intervened_count": transaction["intervened_count"],
|
||||
"failed_count": transaction["failed_count"],
|
||||
"not_run_count": transaction["not_run_count"],
|
||||
"total_latency_ms": transaction["total_latency_ms"],
|
||||
}
|
||||
|
||||
update_data = {
|
||||
"total_requests": {"increment": transaction["total_requests"]},
|
||||
"success_count": {"increment": transaction["success_count"]},
|
||||
"intervened_count": {"increment": transaction["intervened_count"]},
|
||||
"failed_count": {"increment": transaction["failed_count"]},
|
||||
"not_run_count": {"increment": transaction["not_run_count"]},
|
||||
"total_latency_ms": {"increment": transaction["total_latency_ms"]},
|
||||
}
|
||||
|
||||
batcher.litellm_dailyguardrailmetrics.upsert(
|
||||
where=where_clause,
|
||||
data={
|
||||
"create": common_data,
|
||||
"update": update_data,
|
||||
},
|
||||
)
|
||||
except Exception as batch_error:
|
||||
verbose_proxy_logger.exception(
|
||||
f"Daily guardrail metrics batch upsert failed. "
|
||||
f"Batch size: {len(transactions_to_process)}, "
|
||||
f"Error: {str(batch_error)}"
|
||||
)
|
||||
raise
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
f"Processed {len(transactions_to_process)} daily guardrail metrics transactions in {time.time() - start_time:.2f}s"
|
||||
)
|
||||
|
||||
# Remove processed transactions
|
||||
for key in transactions_to_process.keys():
|
||||
daily_metrics_transactions.pop(key, None)
|
||||
|
||||
break
|
||||
|
||||
except DB_CONNECTION_ERROR_TYPES as e:
|
||||
if i >= n_retry_times:
|
||||
_raise_failed_update_spend_exception(
|
||||
e=e,
|
||||
start_time=start_time,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
verbose_proxy_logger.debug(
|
||||
f"Error updating daily guardrail metrics, retrying. {i + 1}/{n_retry_times + 1}"
|
||||
)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.error(f"Error updating daily guardrail metrics: {e}")
|
||||
if n_retry_times > 0:
|
||||
await asyncio.sleep(0.5)
|
||||
await DBSpendUpdateWriter.update_daily_guardrail_metrics(
|
||||
n_retry_times - 1,
|
||||
prisma_client,
|
||||
proxy_logging_obj,
|
||||
daily_metrics_transactions,
|
||||
)
|
||||
|
||||
async def _common_add_spend_log_transaction_to_daily_transaction(
|
||||
self,
|
||||
payload: Union[dict, SpendLogsPayload],
|
||||
|
|
@ -1797,3 +1957,95 @@ class DBSpendUpdateWriter:
|
|||
await self.daily_tag_spend_update_queue.add_update(
|
||||
update={daily_transaction_key: daily_transaction}
|
||||
)
|
||||
|
||||
async def add_spend_log_transaction_to_daily_guardrail_transaction(
|
||||
self,
|
||||
payload: Union[dict, SpendLogsPayload],
|
||||
prisma_client: Optional[PrismaClient] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Extract guardrail information from payload and queue for daily aggregation.
|
||||
|
||||
Creates separate transaction for each guardrail in the request.
|
||||
"""
|
||||
if prisma_client is None:
|
||||
return
|
||||
|
||||
try:
|
||||
# Parse metadata
|
||||
metadata_str = payload.get("metadata")
|
||||
if isinstance(metadata_str, str):
|
||||
_metadata = json.loads(metadata_str)
|
||||
else:
|
||||
_metadata = metadata_str or {}
|
||||
|
||||
guardrail_information = _metadata.get("guardrail_information")
|
||||
|
||||
if not guardrail_information or not isinstance(guardrail_information, list):
|
||||
return
|
||||
|
||||
# Get date from startTime
|
||||
start_time = payload.get("startTime")
|
||||
if isinstance(start_time, datetime):
|
||||
date = start_time.date().isoformat()
|
||||
elif isinstance(start_time, str):
|
||||
date = start_time.split("T")[0]
|
||||
else:
|
||||
return
|
||||
|
||||
api_key = payload.get("api_key", "")
|
||||
|
||||
# Process each guardrail separately
|
||||
for guardrail in guardrail_information:
|
||||
guardrail_name = guardrail.get("guardrail_name")
|
||||
if not guardrail_name:
|
||||
continue
|
||||
|
||||
guardrail_provider = guardrail.get("guardrail_provider") or "unknown"
|
||||
guardrail_mode = self._serialize_guardrail_mode(guardrail.get("guardrail_mode"))
|
||||
guardrail_status = guardrail.get("guardrail_status", "not_run")
|
||||
|
||||
# Calculate status counts
|
||||
success_count = 1 if guardrail_status == "success" else 0
|
||||
intervened_count = 1 if guardrail_status == "guardrail_intervened" else 0
|
||||
failed_count = 1 if guardrail_status == "guardrail_failed_to_respond" else 0
|
||||
not_run_count = 1 if guardrail_status == "not_run" else 0
|
||||
|
||||
# Get guardrail execution latency in milliseconds
|
||||
# duration = time from guardrail start_time to end_time (guardrail overhead only)
|
||||
duration = guardrail.get("duration") or 0
|
||||
duration_ms = float(duration) * 1000 # convert seconds to ms
|
||||
|
||||
# Create unique transaction key
|
||||
transaction_key = f"{guardrail_name}_{guardrail_provider}_{guardrail_mode}_{date}_{api_key}"
|
||||
|
||||
daily_transaction = DailyGuardrailMetricsTransaction(
|
||||
guardrail_name=guardrail_name,
|
||||
guardrail_provider=guardrail_provider,
|
||||
guardrail_mode=guardrail_mode,
|
||||
date=date,
|
||||
api_key=api_key,
|
||||
total_requests=1,
|
||||
success_count=success_count,
|
||||
intervened_count=intervened_count,
|
||||
failed_count=failed_count,
|
||||
not_run_count=not_run_count,
|
||||
total_latency_ms=duration_ms,
|
||||
)
|
||||
|
||||
await self.daily_guardrail_metrics_update_queue.add_update(
|
||||
update={transaction_key: daily_transaction} # type: ignore
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.error(f"Error adding guardrail transaction: {e}")
|
||||
|
||||
@staticmethod
|
||||
def _serialize_guardrail_mode(mode) -> str:
|
||||
"""Convert guardrail_mode to string for storage."""
|
||||
if isinstance(mode, str):
|
||||
return mode
|
||||
elif isinstance(mode, list):
|
||||
return json.dumps(mode)
|
||||
else:
|
||||
return str(mode) if mode else "unknown"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,317 @@
|
|||
import json
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from litellm.proxy._types import CommonProxyErrors
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.types.proxy.management_endpoints.guardrail_metrics import (
|
||||
GuardrailDailyMetrics,
|
||||
GuardrailDetailMetrics,
|
||||
GuardrailLogEntry,
|
||||
GuardrailLogsResponse,
|
||||
GuardrailMetricsResponse,
|
||||
GuardrailSummary,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/guardrail/metrics",
|
||||
tags=["guardrails"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=GuardrailMetricsResponse,
|
||||
)
|
||||
async def get_guardrail_metrics(
|
||||
start_date: str = Query(..., description="Start date YYYY-MM-DD"),
|
||||
end_date: str = Query(..., description="End date YYYY-MM-DD"),
|
||||
guardrail_name: Optional[str] = Query(None),
|
||||
provider: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=500),
|
||||
):
|
||||
"""
|
||||
Get aggregated guardrail metrics for dashboard table view.
|
||||
|
||||
Returns list of guardrails with total requests, fail rate, avg latency.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||
)
|
||||
|
||||
# Build query filters
|
||||
where_conditions: Dict[str, Any] = {
|
||||
"date": {
|
||||
"gte": start_date,
|
||||
"lte": end_date,
|
||||
}
|
||||
}
|
||||
|
||||
if guardrail_name:
|
||||
where_conditions["guardrail_name"] = guardrail_name
|
||||
|
||||
if provider:
|
||||
where_conditions["guardrail_provider"] = provider
|
||||
|
||||
# Query daily metrics
|
||||
daily_metrics = await prisma_client.db.litellm_dailyguardrailmetrics.find_many(
|
||||
where=where_conditions,
|
||||
order=[{"date": "desc"}],
|
||||
)
|
||||
|
||||
# Aggregate by guardrail_name across all dates
|
||||
guardrail_aggregates = {}
|
||||
for record in daily_metrics:
|
||||
name = record.guardrail_name
|
||||
if name not in guardrail_aggregates:
|
||||
guardrail_aggregates[name] = {
|
||||
"provider": record.guardrail_provider or "unknown",
|
||||
"total_requests": 0,
|
||||
"intervened_count": 0,
|
||||
"total_latency_ms": 0.0,
|
||||
}
|
||||
|
||||
agg = guardrail_aggregates[name]
|
||||
agg["total_requests"] += int(record.total_requests)
|
||||
agg["intervened_count"] += int(record.intervened_count)
|
||||
agg["total_latency_ms"] += float(record.total_latency_ms)
|
||||
|
||||
# Calculate fail rate and avg latency
|
||||
results = []
|
||||
for name, agg in guardrail_aggregates.items():
|
||||
fail_rate = (
|
||||
(agg["intervened_count"] / agg["total_requests"] * 100)
|
||||
if agg["total_requests"] > 0
|
||||
else 0.0
|
||||
)
|
||||
avg_latency = (
|
||||
agg["total_latency_ms"] / agg["total_requests"]
|
||||
if agg["total_requests"] > 0
|
||||
else 0.0
|
||||
)
|
||||
|
||||
results.append(
|
||||
GuardrailSummary(
|
||||
guardrail_name=name,
|
||||
provider=agg["provider"],
|
||||
total_requests=agg["total_requests"],
|
||||
fail_rate=round(fail_rate, 2),
|
||||
avg_latency_ms=round(avg_latency, 2),
|
||||
)
|
||||
)
|
||||
|
||||
# Sort by fail rate descending
|
||||
results.sort(key=lambda x: x.fail_rate, reverse=True)
|
||||
|
||||
# Pagination
|
||||
start_idx = (page - 1) * page_size
|
||||
end_idx = start_idx + page_size
|
||||
paginated_results = results[start_idx:end_idx]
|
||||
|
||||
total_count = len(results)
|
||||
|
||||
return GuardrailMetricsResponse(
|
||||
results=paginated_results,
|
||||
metadata={
|
||||
"page": page,
|
||||
"total_pages": (total_count + page_size - 1) // page_size,
|
||||
"has_more": end_idx < total_count,
|
||||
"total_count": total_count,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/guardrail/{guardrail_name}/metrics",
|
||||
tags=["guardrails"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=GuardrailDetailMetrics,
|
||||
)
|
||||
async def get_guardrail_detail_metrics(
|
||||
guardrail_name: str,
|
||||
start_date: str = Query(..., description="Start date YYYY-MM-DD"),
|
||||
end_date: str = Query(..., description="End date YYYY-MM-DD"),
|
||||
):
|
||||
"""
|
||||
Get detailed metrics for a specific guardrail (for overview tab).
|
||||
|
||||
Returns aggregated metrics plus daily time-series data.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||
)
|
||||
|
||||
# Query daily metrics for this guardrail
|
||||
daily_records = await prisma_client.db.litellm_dailyguardrailmetrics.find_many(
|
||||
where={
|
||||
"guardrail_name": guardrail_name,
|
||||
"date": {
|
||||
"gte": start_date,
|
||||
"lte": end_date,
|
||||
},
|
||||
},
|
||||
order=[{"date": "asc"}],
|
||||
)
|
||||
|
||||
if not daily_records:
|
||||
return GuardrailDetailMetrics(
|
||||
requests_evaluated=0,
|
||||
fail_rate=0.0,
|
||||
avg_latency_ms=0.0,
|
||||
blocked_count=0,
|
||||
daily_metrics=[],
|
||||
)
|
||||
|
||||
# Calculate totals
|
||||
total_requests = sum(int(r.total_requests) for r in daily_records)
|
||||
total_intervened = sum(int(r.intervened_count) for r in daily_records)
|
||||
total_latency_ms = sum(float(r.total_latency_ms) for r in daily_records)
|
||||
|
||||
fail_rate = (total_intervened / total_requests * 100) if total_requests > 0 else 0.0
|
||||
avg_latency_ms = total_latency_ms / total_requests if total_requests > 0 else 0.0
|
||||
|
||||
# Build daily time-series
|
||||
daily_metrics = []
|
||||
for record in daily_records:
|
||||
requests = int(record.total_requests)
|
||||
intervened = int(record.intervened_count)
|
||||
latency = float(record.total_latency_ms)
|
||||
|
||||
daily_fail_rate = (intervened / requests * 100) if requests > 0 else 0.0
|
||||
daily_avg_latency = latency / requests if requests > 0 else 0.0
|
||||
|
||||
daily_metrics.append(
|
||||
GuardrailDailyMetrics(
|
||||
date=record.date,
|
||||
total_requests=requests,
|
||||
intervened_count=intervened,
|
||||
success_count=int(record.success_count),
|
||||
fail_rate=round(daily_fail_rate, 2),
|
||||
avg_latency_ms=round(daily_avg_latency, 2),
|
||||
)
|
||||
)
|
||||
|
||||
return GuardrailDetailMetrics(
|
||||
requests_evaluated=total_requests,
|
||||
fail_rate=round(fail_rate, 2),
|
||||
avg_latency_ms=round(avg_latency_ms, 2),
|
||||
blocked_count=total_intervened,
|
||||
daily_metrics=daily_metrics,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/guardrail/{guardrail_name}/logs",
|
||||
tags=["guardrails"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=GuardrailLogsResponse,
|
||||
)
|
||||
async def get_guardrail_logs(
|
||||
guardrail_name: str,
|
||||
start_date: str = Query(..., description="Start date YYYY-MM-DD"),
|
||||
end_date: str = Query(..., description="End date YYYY-MM-DD"),
|
||||
status_filter: Optional[str] = Query(None, description="'blocked' or 'passed'"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=100),
|
||||
):
|
||||
"""
|
||||
Get individual request logs for a guardrail (for logs tab).
|
||||
|
||||
Queries LiteLLM_SpendLogs and filters by guardrail_information in metadata.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||
)
|
||||
|
||||
# Convert dates to datetime for filtering
|
||||
start_datetime = datetime.fromisoformat(start_date).isoformat()
|
||||
end_datetime = datetime.fromisoformat(end_date + "T23:59:59").isoformat()
|
||||
|
||||
# Query spend logs with metadata containing guardrail_information
|
||||
# Note: This is a simplified approach - may need optimization for large datasets
|
||||
spend_logs = await prisma_client.db.litellm_spendlogs.find_many(
|
||||
where={
|
||||
"startTime": {
|
||||
"gte": start_datetime,
|
||||
"lte": end_datetime,
|
||||
},
|
||||
},
|
||||
order=[{"startTime": "desc"}],
|
||||
skip=(page - 1) * page_size,
|
||||
take=page_size * 3, # Over-fetch to account for filtering
|
||||
)
|
||||
|
||||
# Parse and filter logs
|
||||
filtered_logs = []
|
||||
for log in spend_logs:
|
||||
try:
|
||||
metadata = json.loads(log.metadata) if isinstance(log.metadata, str) else log.metadata
|
||||
guardrail_info = metadata.get("guardrail_information", [])
|
||||
|
||||
# Find matching guardrail in list
|
||||
for g in guardrail_info:
|
||||
if g.get("guardrail_name") == guardrail_name:
|
||||
status = g.get("guardrail_status", "")
|
||||
|
||||
# Map status to blocked/passed
|
||||
if status == "guardrail_intervened":
|
||||
log_status = "blocked"
|
||||
elif status == "success":
|
||||
log_status = "passed"
|
||||
else:
|
||||
continue # Skip other statuses
|
||||
|
||||
# Apply status filter
|
||||
if status_filter and log_status != status_filter:
|
||||
continue
|
||||
|
||||
# Extract request content
|
||||
request_content = None
|
||||
messages = metadata.get("messages", [])
|
||||
if messages and isinstance(messages, list) and len(messages) > 0:
|
||||
last_msg = messages[-1]
|
||||
if isinstance(last_msg, dict):
|
||||
request_content = last_msg.get("content", "")
|
||||
|
||||
filtered_logs.append(
|
||||
GuardrailLogEntry(
|
||||
request_id=log.request_id,
|
||||
timestamp=log.startTime.isoformat() if log.startTime else "",
|
||||
model=log.model or "unknown",
|
||||
status=log_status,
|
||||
guardrail_response=g.get("guardrail_response"),
|
||||
request_content=request_content,
|
||||
latency_ms=round((g.get("duration") or 0) * 1000, 2),
|
||||
)
|
||||
)
|
||||
|
||||
if len(filtered_logs) >= page_size:
|
||||
break
|
||||
|
||||
if len(filtered_logs) >= page_size:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
return GuardrailLogsResponse(
|
||||
logs=filtered_logs[:page_size],
|
||||
total_count=len(filtered_logs), # Approximate
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
|
@ -357,12 +357,13 @@ from litellm.proxy.management_endpoints.customer_endpoints import (
|
|||
from litellm.proxy.management_endpoints.fallback_management_endpoints import (
|
||||
router as fallback_management_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.guardrail_metrics_endpoints import (
|
||||
router as guardrail_metrics_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.internal_user_endpoints import (
|
||||
router as internal_user_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.internal_user_endpoints import (
|
||||
user_update,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.internal_user_endpoints import user_update
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
||||
delete_verification_tokens,
|
||||
duration_in_seconds,
|
||||
|
|
@ -388,10 +389,10 @@ from litellm.proxy.management_endpoints.model_management_endpoints import (
|
|||
from litellm.proxy.management_endpoints.organization_endpoints import (
|
||||
router as organization_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.policy_endpoints import router as policy_router
|
||||
from litellm.proxy.management_endpoints.project_endpoints import (
|
||||
router as project_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.policy_endpoints import router as policy_router
|
||||
from litellm.proxy.management_endpoints.router_settings_endpoints import (
|
||||
router as router_settings_router,
|
||||
)
|
||||
|
|
@ -421,9 +422,7 @@ from litellm.proxy.openai_evals_endpoints.endpoints import router as evals_route
|
|||
from litellm.proxy.openai_files_endpoints.files_endpoints import (
|
||||
router as openai_files_router,
|
||||
)
|
||||
from litellm.proxy.openai_files_endpoints.files_endpoints import (
|
||||
set_files_config,
|
||||
)
|
||||
from litellm.proxy.openai_files_endpoints.files_endpoints import set_files_config
|
||||
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
|
||||
passthrough_endpoint_router,
|
||||
)
|
||||
|
|
@ -522,9 +521,7 @@ from litellm.types.proxy.management_endpoints.ui_sso import (
|
|||
LiteLLM_UpperboundKeyGenerateParams,
|
||||
)
|
||||
from litellm.types.realtime import RealtimeQueryParams
|
||||
from litellm.types.router import (
|
||||
DeploymentTypedDict,
|
||||
)
|
||||
from litellm.types.router import DeploymentTypedDict
|
||||
from litellm.types.router import ModelInfo as RouterModelInfo
|
||||
from litellm.types.router import (
|
||||
RouterGeneralSettings,
|
||||
|
|
@ -12489,6 +12486,7 @@ app.include_router(cloudzero_router)
|
|||
app.include_router(caching_router)
|
||||
app.include_router(analytics_router)
|
||||
app.include_router(guardrails_router)
|
||||
app.include_router(guardrail_metrics_router)
|
||||
app.include_router(policy_router)
|
||||
app.include_router(policy_crud_router)
|
||||
app.include_router(policy_resolve_router)
|
||||
|
|
|
|||
|
|
@ -747,11 +747,11 @@ model LiteLLM_DailyTeamSpend {
|
|||
model LiteLLM_DailyTagSpend {
|
||||
id String @id @default(uuid())
|
||||
request_id String?
|
||||
tag String?
|
||||
tag String?
|
||||
date String
|
||||
api_key String
|
||||
api_key String
|
||||
model String?
|
||||
model_group String?
|
||||
model_group String?
|
||||
custom_llm_provider String?
|
||||
mcp_namespaced_tool_name String?
|
||||
endpoint String?
|
||||
|
|
@ -775,6 +775,35 @@ model LiteLLM_DailyTagSpend {
|
|||
@@index([endpoint])
|
||||
}
|
||||
|
||||
// Track daily guardrail metrics per guardrail
|
||||
model LiteLLM_DailyGuardrailMetrics {
|
||||
id String @id @default(uuid())
|
||||
guardrail_name String
|
||||
guardrail_provider String?
|
||||
guardrail_mode String? // pre_call, post_call, during_call
|
||||
date String // YYYY-MM-DD format
|
||||
api_key String // for per-key breakdowns
|
||||
|
||||
// Aggregated counts
|
||||
total_requests BigInt @default(0)
|
||||
success_count BigInt @default(0) // guardrail_status = "success"
|
||||
intervened_count BigInt @default(0) // guardrail_status = "guardrail_intervened"
|
||||
failed_count BigInt @default(0) // guardrail_status = "guardrail_failed_to_respond"
|
||||
not_run_count BigInt @default(0) // guardrail_status = "not_run"
|
||||
|
||||
// Aggregated latency (guardrail execution overhead only)
|
||||
total_latency_ms Float @default(0.0) // sum of guardrail durations in milliseconds
|
||||
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
// Unique constraint: one row per guardrail per day per api_key
|
||||
@@unique([guardrail_name, guardrail_provider, guardrail_mode, date, api_key], name: "guardrail_daily_unique")
|
||||
@@index([date])
|
||||
@@index([guardrail_name])
|
||||
@@index([guardrail_provider])
|
||||
@@index([api_key])
|
||||
}
|
||||
|
||||
// Track the status of cron jobs running. Only allow one pod to run the job at a time
|
||||
model LiteLLM_CronJob {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
from datetime import date
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class GuardrailMetrics(BaseModel):
|
||||
"""Aggregated metrics for a guardrail."""
|
||||
|
||||
total_requests: int = 0
|
||||
success_count: int = 0
|
||||
intervened_count: int = 0
|
||||
failed_count: int = 0
|
||||
not_run_count: int = 0
|
||||
fail_rate: float = 0.0 # percentage
|
||||
avg_latency_ms: float = 0.0
|
||||
|
||||
|
||||
class GuardrailSummary(BaseModel):
|
||||
"""Summary view for guardrails table."""
|
||||
|
||||
guardrail_name: str
|
||||
provider: str
|
||||
total_requests: int
|
||||
fail_rate: float # percentage
|
||||
avg_latency_ms: float
|
||||
|
||||
|
||||
class GuardrailMetricsResponse(BaseModel):
|
||||
"""Response for /guardrail/metrics endpoint."""
|
||||
|
||||
results: List[GuardrailSummary]
|
||||
metadata: Dict[str, Any]
|
||||
|
||||
|
||||
class GuardrailDailyMetrics(BaseModel):
|
||||
"""Daily time-series data for a guardrail."""
|
||||
|
||||
date: str
|
||||
total_requests: int
|
||||
intervened_count: int
|
||||
success_count: int
|
||||
fail_rate: float
|
||||
avg_latency_ms: float
|
||||
|
||||
|
||||
class GuardrailDetailMetrics(BaseModel):
|
||||
"""Detailed metrics for guardrail overview page."""
|
||||
|
||||
requests_evaluated: int
|
||||
fail_rate: float
|
||||
avg_latency_ms: float
|
||||
blocked_count: int # intervened in selected period
|
||||
daily_metrics: List[GuardrailDailyMetrics]
|
||||
|
||||
|
||||
class GuardrailLogEntry(BaseModel):
|
||||
"""Individual request log entry."""
|
||||
|
||||
request_id: str
|
||||
timestamp: str
|
||||
model: str
|
||||
status: str # "blocked" or "passed"
|
||||
guardrail_response: Optional[dict] = None
|
||||
request_content: Optional[str] = None
|
||||
latency_ms: float = 0.0
|
||||
|
||||
|
||||
class GuardrailLogsResponse(BaseModel):
|
||||
"""Response for logs tab."""
|
||||
|
||||
logs: List[GuardrailLogEntry]
|
||||
total_count: int
|
||||
page: int
|
||||
page_size: int
|
||||
Loading…
Add table
Reference in a new issue