mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
feat(perf): add performance debugging dashboard
New /v1/performance/summary endpoint + UI dashboard for diagnosing proxy overhead. Backend: - Rolling latency ring buffer (last 100 requests) tracking overhead, llm_api, total ms - Per-model breakdown sorted by overhead - Issue detection: debug logging active, under-provisioned workers, high overhead %, high p95, HTTP pool saturation - Overhead histogram bucketed into 8 ranges - Workers/CPU, DB pool, Redis pool, HTTP pool stats UI: - Performance page at /?page=performance-dashboard (admin only) - Issues Detected table with proposed fixes and collapsible fix snippets - Overhead over time line chart with summary stats (total requests, % under 50ms, p95) - Worker provisioning card with ratio bar - Connections card (DB + Redis pool status) - In-flight asyncio tasks chart - HTTP pool utilization chart - Per-model overhead table
This commit is contained in:
parent
0ab1392735
commit
ed913c3a3d
13 changed files with 1158 additions and 0 deletions
|
|
@ -1259,6 +1259,9 @@ RATE_LIMIT_ERROR_MESSAGE_FOR_VIRTUAL_KEY = "LiteLLM Virtual Key user_api_key_has
|
|||
# Format: "gen0,gen1,gen2" e.g., "1000,50,50"
|
||||
PYTHON_GC_THRESHOLD = os.getenv("PYTHON_GC_THRESHOLD")
|
||||
|
||||
# Performance tracker ring buffer size (number of recent requests to keep latency stats for)
|
||||
PERF_TRACKER_RING_BUFFER_SIZE = 100
|
||||
|
||||
# pass through route constansts
|
||||
BEDROCK_AGENT_RUNTIME_PASS_THROUGH_ROUTES = [
|
||||
"agents/",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ from litellm.proxy.common_utils.callback_utils import (
|
|||
get_logging_caching_headers,
|
||||
get_remaining_tokens_and_requests_from_request_data,
|
||||
)
|
||||
from litellm.proxy.performance_endpoints.latency_tracker import record_request_timing
|
||||
from litellm.proxy.route_llm_request import route_request
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
from litellm.router import Router
|
||||
|
|
@ -496,6 +497,11 @@ class ProxyBaseLLMRequestProcessing:
|
|||
if logging_caching_headers:
|
||||
headers.update(logging_caching_headers)
|
||||
|
||||
record_request_timing(
|
||||
hidden_params,
|
||||
model=(request_data or {}).get("model") or None,
|
||||
)
|
||||
|
||||
try:
|
||||
return {
|
||||
key: str(value)
|
||||
|
|
|
|||
0
litellm/proxy/performance_endpoints/__init__.py
Normal file
0
litellm/proxy/performance_endpoints/__init__.py
Normal file
324
litellm/proxy/performance_endpoints/endpoints.py
Normal file
324
litellm/proxy/performance_endpoints/endpoints.py
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.constants import (
|
||||
AIOHTTP_CONNECTOR_LIMIT,
|
||||
LITELLM_DETAILED_TIMING,
|
||||
)
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.performance_endpoints.latency_tracker import (
|
||||
latency_tracker,
|
||||
per_model_tracker,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/v1/performance/summary",
|
||||
tags=["performance"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def get_performance_summary() -> Dict[str, Any]:
|
||||
"""
|
||||
Returns a performance snapshot for diagnosing proxy overhead.
|
||||
|
||||
Sections:
|
||||
- debug_flags: is DEBUG logging or detailed timing enabled
|
||||
- workers: cpu_count, configured num_workers, cpu_percent
|
||||
- connection_pools: in-flight requests, DB pool, Redis pool, HTTP pool
|
||||
- latency: rolling avg/p50/p95 from the last 100 requests
|
||||
- per_model: per-model overhead/llm_api/total stats
|
||||
- issues: ranked list of detected problems with proposed fixes
|
||||
"""
|
||||
# --- Debug flags ---
|
||||
from litellm._logging import verbose_proxy_logger as _proxy_logger
|
||||
from litellm.proxy.proxy_server import general_settings, prisma_client
|
||||
|
||||
is_detailed_debug = _proxy_logger.isEnabledFor(logging.DEBUG)
|
||||
log_level = logging.getLevelName(_proxy_logger.getEffectiveLevel())
|
||||
detailed_timing_enabled = LITELLM_DETAILED_TIMING
|
||||
|
||||
# --- Workers / CPU ---
|
||||
cpu_count = multiprocessing.cpu_count()
|
||||
num_workers = general_settings.get("num_workers", 1)
|
||||
cpu_percent = _get_cpu_percent()
|
||||
|
||||
# --- Connection pools ---
|
||||
in_flight_requests = _get_in_flight_requests()
|
||||
db_pool_info = _get_db_pool_info(general_settings, prisma_client)
|
||||
redis_pool_info = _get_redis_pool_info()
|
||||
http_pool_info = _get_http_pool_info()
|
||||
|
||||
# --- Latency stats ---
|
||||
latency_stats = latency_tracker.stats()
|
||||
overhead_pct = _compute_overhead_pct(latency_stats)
|
||||
|
||||
# --- Per-model stats ---
|
||||
per_model_stats = per_model_tracker.stats()
|
||||
|
||||
# --- Issues ---
|
||||
summary = {
|
||||
"debug_flags": {
|
||||
"is_detailed_debug": is_detailed_debug,
|
||||
"log_level": log_level,
|
||||
"detailed_timing_enabled": detailed_timing_enabled,
|
||||
},
|
||||
"workers": {
|
||||
"cpu_count": cpu_count,
|
||||
"num_workers": num_workers,
|
||||
"cpu_percent": cpu_percent,
|
||||
},
|
||||
"connection_pools": {
|
||||
"in_flight_requests": in_flight_requests,
|
||||
"db": db_pool_info,
|
||||
"redis": redis_pool_info,
|
||||
"http": http_pool_info,
|
||||
},
|
||||
"latency": {
|
||||
**latency_stats,
|
||||
"overhead_pct_of_total": overhead_pct,
|
||||
},
|
||||
"per_model": per_model_stats,
|
||||
}
|
||||
summary["issues"] = _detect_issues(summary)
|
||||
return summary
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Issue detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_SEVERITY_ORDER = {"critical": 0, "warning": 1, "info": 2}
|
||||
|
||||
|
||||
def _detect_issues(summary: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
issues: List[Dict[str, Any]] = []
|
||||
|
||||
debug_flags = summary["debug_flags"]
|
||||
workers = summary["workers"]
|
||||
pools = summary["connection_pools"]
|
||||
latency = summary["latency"]
|
||||
|
||||
# 1. Debug logging active
|
||||
if debug_flags["is_detailed_debug"]:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "warning",
|
||||
"title": "Debug logging is active",
|
||||
"description": (
|
||||
"LITELLM_LOG=DEBUG adds measurable overhead to every request "
|
||||
"and writes verbose output. Disable in production."
|
||||
),
|
||||
"fix": "Set LITELLM_LOG=WARNING (or unset the variable)",
|
||||
"fix_snippet": "export LITELLM_LOG=WARNING",
|
||||
}
|
||||
)
|
||||
|
||||
# 2. Under-provisioned workers
|
||||
cpu_count = workers["cpu_count"]
|
||||
num_workers = workers["num_workers"]
|
||||
if num_workers < cpu_count:
|
||||
recommended = 2 * cpu_count + 1
|
||||
issues.append(
|
||||
{
|
||||
"severity": "warning",
|
||||
"title": f"Under-provisioned: {num_workers} worker{'s' if num_workers != 1 else ''} for {cpu_count} CPU cores",
|
||||
"description": (
|
||||
f"You have {cpu_count} CPU cores but only {num_workers} "
|
||||
f"uvicorn worker{'s' if num_workers != 1 else ''}. "
|
||||
"Additional workers allow more requests to be handled in parallel."
|
||||
),
|
||||
"fix": f"Set num_workers: {recommended} in your config (2× CPU + 1)",
|
||||
"fix_snippet": f"litellm --num_workers {recommended} --config config.yaml",
|
||||
}
|
||||
)
|
||||
|
||||
# 3. High overhead percentage
|
||||
overhead_pct = latency.get("overhead_pct_of_total")
|
||||
overhead = latency.get("overhead")
|
||||
if overhead_pct is not None and overhead_pct > 20 and overhead:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "warning",
|
||||
"title": f"High LiteLLM overhead: {overhead_pct}% of total request time",
|
||||
"description": (
|
||||
f"LiteLLM is adding {overhead['avg_ms']}ms avg overhead "
|
||||
f"(p95: {overhead['p95_ms']}ms). Normal is <5%. "
|
||||
"Common causes: debug logging, DB connection pool exhaustion, or too few workers."
|
||||
),
|
||||
"fix": "Check the other issues on this page — high overhead is usually a symptom",
|
||||
"fix_snippet": None,
|
||||
}
|
||||
)
|
||||
|
||||
# 4. High p95 overhead (even if avg looks okay)
|
||||
if overhead and overhead.get("p95_ms", 0) > 200:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "warning",
|
||||
"title": f"High p95 overhead: {overhead['p95_ms']}ms",
|
||||
"description": (
|
||||
"p95 overhead is elevated even if the average looks acceptable. "
|
||||
"This means 1 in 20 requests experiences significant proxy-added latency. "
|
||||
"Likely cause: occasional DB pool queuing or GC pauses."
|
||||
),
|
||||
"fix": "Consider increasing database_connection_pool_limit and num_workers",
|
||||
"fix_snippet": None,
|
||||
}
|
||||
)
|
||||
|
||||
# 6. HTTP pool near saturation
|
||||
http = pools.get("http", {})
|
||||
http_pct = http.get("aiohttp_pct")
|
||||
if http_pct is not None and http_pct > 80:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "critical",
|
||||
"title": f"HTTP connection pool near capacity: {http_pct}% used",
|
||||
"description": (
|
||||
f"aiohttp connector is at {http_pct}% utilization "
|
||||
f"({http.get('aiohttp_active')} / {http.get('aiohttp_limit')} connections). "
|
||||
"New outbound requests will queue until connections free up."
|
||||
),
|
||||
"fix": "Increase AIOHTTP_CONNECTOR_LIMIT environment variable",
|
||||
"fix_snippet": f"export AIOHTTP_CONNECTOR_LIMIT={http.get('aiohttp_limit', 300) * 2}",
|
||||
}
|
||||
)
|
||||
|
||||
# Sort: critical first, then warning, then info
|
||||
issues.sort(key=lambda i: _SEVERITY_ORDER.get(i["severity"], 99))
|
||||
return issues
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _get_cpu_percent() -> Optional[float]:
|
||||
try:
|
||||
import psutil
|
||||
|
||||
return psutil.cpu_percent(interval=None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_in_flight_requests() -> Optional[int]:
|
||||
try:
|
||||
active = sum(1 for t in asyncio.all_tasks() if not t.done())
|
||||
return active
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_db_pool_info(
|
||||
general_settings: dict, prisma_client: Any
|
||||
) -> Dict[str, Any]:
|
||||
pool_limit = general_settings.get("database_connection_pool_limit", 10)
|
||||
pool_timeout = general_settings.get("database_connection_pool_timeout", 60)
|
||||
connected = prisma_client is not None
|
||||
return {
|
||||
"connected": connected,
|
||||
"pool_limit": pool_limit,
|
||||
"pool_timeout_seconds": pool_timeout,
|
||||
}
|
||||
|
||||
|
||||
def _get_redis_pool_info() -> Dict[str, Any]:
|
||||
try:
|
||||
from litellm.proxy.proxy_server import redis_usage_cache
|
||||
|
||||
if redis_usage_cache is None:
|
||||
return {"enabled": False}
|
||||
result: Dict[str, Any] = {"enabled": True}
|
||||
try:
|
||||
if (
|
||||
hasattr(redis_usage_cache, "redis_client")
|
||||
and redis_usage_cache.redis_client
|
||||
):
|
||||
pool = getattr(
|
||||
redis_usage_cache.redis_client, "connection_pool", None
|
||||
)
|
||||
if pool is not None:
|
||||
result["max_connections"] = getattr(
|
||||
pool, "max_connections", None
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
except Exception:
|
||||
return {"enabled": False}
|
||||
|
||||
|
||||
def _get_http_pool_info() -> Dict[str, Any]:
|
||||
"""
|
||||
Returns aiohttp connector pool stats (configured limit + active connections).
|
||||
Active connections are summed across all cached AsyncHTTPHandler clients.
|
||||
"""
|
||||
result: Dict[str, Any] = {
|
||||
"aiohttp_limit": AIOHTTP_CONNECTOR_LIMIT,
|
||||
"aiohttp_active": None,
|
||||
"aiohttp_pct": None,
|
||||
}
|
||||
try:
|
||||
import litellm
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
|
||||
|
||||
cache = getattr(litellm, "in_memory_llm_clients_cache", None)
|
||||
if cache is None:
|
||||
return result
|
||||
|
||||
# LLMClientCache extends InMemoryCache directly — cache_dict is on cache itself
|
||||
items = getattr(cache, "cache_dict", {})
|
||||
aiohttp_active = 0
|
||||
|
||||
for _key, client in items.items():
|
||||
if not isinstance(client, AsyncHTTPHandler):
|
||||
continue
|
||||
httpx_client = getattr(client, "client", None)
|
||||
if httpx_client is None:
|
||||
continue
|
||||
transport = getattr(httpx_client, "_transport", None)
|
||||
if transport is None:
|
||||
continue
|
||||
# LiteLLMAiohttpTransport stores the aiohttp ClientSession as .client
|
||||
session = getattr(transport, "client", None)
|
||||
if session is None:
|
||||
continue
|
||||
connector = getattr(session, "connector", None)
|
||||
if connector is None:
|
||||
continue
|
||||
acquired = getattr(connector, "_acquired", None)
|
||||
if acquired is not None:
|
||||
aiohttp_active += len(acquired)
|
||||
|
||||
result["aiohttp_active"] = aiohttp_active
|
||||
if AIOHTTP_CONNECTOR_LIMIT > 0:
|
||||
result["aiohttp_pct"] = round(
|
||||
aiohttp_active / AIOHTTP_CONNECTOR_LIMIT * 100, 1
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.debug(f"Error getting HTTP pool info: {e}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _compute_overhead_pct(latency_stats: dict) -> Optional[float]:
|
||||
overhead = latency_stats.get("overhead")
|
||||
total = latency_stats.get("total")
|
||||
if not overhead or not total:
|
||||
return None
|
||||
avg_overhead = overhead.get("avg_ms")
|
||||
avg_total = total.get("avg_ms")
|
||||
if avg_overhead and avg_total and avg_total > 0:
|
||||
return round((avg_overhead / avg_total) * 100, 1)
|
||||
return None
|
||||
186
litellm/proxy/performance_endpoints/latency_tracker.py
Normal file
186
litellm/proxy/performance_endpoints/latency_tracker.py
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import statistics
|
||||
import threading
|
||||
from collections import deque
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from litellm.constants import LITELLM_DETAILED_TIMING, PERF_TRACKER_RING_BUFFER_SIZE
|
||||
|
||||
_HISTOGRAM_BUCKETS = [
|
||||
(0, 5, "0-5"),
|
||||
(5, 10, "5-10"),
|
||||
(10, 25, "10-25"),
|
||||
(25, 50, "25-50"),
|
||||
(50, 100, "50-100"),
|
||||
(100, 200, "100-200"),
|
||||
(200, 500, "200-500"),
|
||||
(500, float("inf"), "500+"),
|
||||
]
|
||||
|
||||
|
||||
def _compute_histogram(values: List[float]) -> List[Dict]:
|
||||
"""Returns bucket counts for an overhead latency histogram (ms)."""
|
||||
counts = {label: 0 for _, _, label in _HISTOGRAM_BUCKETS}
|
||||
for v in values:
|
||||
for lo, hi, label in _HISTOGRAM_BUCKETS:
|
||||
if lo <= v < hi:
|
||||
counts[label] += 1
|
||||
break
|
||||
return [{"bucket": label, "count": counts[label]} for _, _, label in _HISTOGRAM_BUCKETS]
|
||||
|
||||
|
||||
class _LatencyRingBuffer:
|
||||
"""Thread-safe ring buffer storing timing samples from recent requests."""
|
||||
|
||||
def __init__(self, maxlen: int = PERF_TRACKER_RING_BUFFER_SIZE) -> None:
|
||||
self._lock = threading.Lock()
|
||||
self._overhead_ms: deque = deque(maxlen=maxlen)
|
||||
self._llm_api_ms: deque = deque(maxlen=maxlen)
|
||||
self._pre_processing_ms: deque = deque(maxlen=maxlen)
|
||||
self._post_processing_ms: deque = deque(maxlen=maxlen)
|
||||
self._total_ms: deque = deque(maxlen=maxlen)
|
||||
|
||||
def record(
|
||||
self,
|
||||
overhead_ms: Optional[float],
|
||||
llm_api_ms: Optional[float],
|
||||
pre_processing_ms: Optional[float],
|
||||
post_processing_ms: Optional[float],
|
||||
total_ms: Optional[float],
|
||||
) -> None:
|
||||
with self._lock:
|
||||
if overhead_ms is not None:
|
||||
self._overhead_ms.append(overhead_ms)
|
||||
if llm_api_ms is not None:
|
||||
self._llm_api_ms.append(llm_api_ms)
|
||||
if pre_processing_ms is not None:
|
||||
self._pre_processing_ms.append(pre_processing_ms)
|
||||
if post_processing_ms is not None:
|
||||
self._post_processing_ms.append(post_processing_ms)
|
||||
if total_ms is not None:
|
||||
self._total_ms.append(total_ms)
|
||||
|
||||
def stats(self) -> Dict:
|
||||
with self._lock:
|
||||
overhead_list = list(self._overhead_ms)
|
||||
return {
|
||||
"overhead": _compute_stats(overhead_list),
|
||||
"llm_api": _compute_stats(list(self._llm_api_ms)),
|
||||
"pre_processing": _compute_stats(list(self._pre_processing_ms)),
|
||||
"post_processing": _compute_stats(list(self._post_processing_ms)),
|
||||
"total": _compute_stats(list(self._total_ms)),
|
||||
"sample_count": len(self._overhead_ms),
|
||||
"overhead_histogram": _compute_histogram(overhead_list),
|
||||
}
|
||||
|
||||
|
||||
class _PerModelTracker:
|
||||
"""Thread-safe per-model latency tracker using one ring buffer per model."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._lock = threading.Lock()
|
||||
self._models: Dict[str, _LatencyRingBuffer] = {}
|
||||
|
||||
def record(
|
||||
self,
|
||||
model: str,
|
||||
overhead_ms: Optional[float],
|
||||
llm_api_ms: Optional[float],
|
||||
total_ms: Optional[float],
|
||||
) -> None:
|
||||
with self._lock:
|
||||
if model not in self._models:
|
||||
self._models[model] = _LatencyRingBuffer()
|
||||
buf = self._models[model]
|
||||
# record outside the outer lock to minimise contention
|
||||
buf.record(
|
||||
overhead_ms=overhead_ms,
|
||||
llm_api_ms=llm_api_ms,
|
||||
pre_processing_ms=None,
|
||||
post_processing_ms=None,
|
||||
total_ms=total_ms,
|
||||
)
|
||||
|
||||
def stats(self) -> List[Dict]:
|
||||
with self._lock:
|
||||
snapshot = dict(self._models)
|
||||
|
||||
rows = []
|
||||
for model, buf in snapshot.items():
|
||||
s = buf.stats()
|
||||
if s["sample_count"] == 0:
|
||||
continue
|
||||
rows.append(
|
||||
{
|
||||
"model": model,
|
||||
"overhead": s["overhead"],
|
||||
"llm_api": s["llm_api"],
|
||||
"total": s["total"],
|
||||
"sample_count": s["sample_count"],
|
||||
}
|
||||
)
|
||||
# sort by overhead avg descending so worst offenders appear first
|
||||
rows.sort(
|
||||
key=lambda r: (r["overhead"] or {}).get("avg_ms", 0),
|
||||
reverse=True,
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def _compute_stats(values: List[float]) -> Optional[Dict]:
|
||||
if not values:
|
||||
return None
|
||||
sorted_vals = sorted(values)
|
||||
n = len(sorted_vals)
|
||||
return {
|
||||
"avg_ms": round(statistics.mean(values), 1),
|
||||
"p50_ms": round(sorted_vals[n // 2], 1),
|
||||
"p95_ms": round(sorted_vals[min(int(n * 0.95), n - 1)], 1),
|
||||
}
|
||||
|
||||
|
||||
def _to_float(v: object) -> Optional[float]:
|
||||
if v is None:
|
||||
return None
|
||||
try:
|
||||
return float(v) # type: ignore[arg-type]
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
# Module-level singletons — one per worker process
|
||||
latency_tracker = _LatencyRingBuffer()
|
||||
per_model_tracker = _PerModelTracker()
|
||||
|
||||
|
||||
def record_request_timing(hidden_params: dict, model: Optional[str] = None) -> None:
|
||||
"""
|
||||
Call once per completed request with the response's hidden_params.
|
||||
Extracts timing fields and appends to the ring buffers (global + per-model).
|
||||
"""
|
||||
total_ms = _to_float(hidden_params.get("_response_ms"))
|
||||
overhead_ms = _to_float(hidden_params.get("litellm_overhead_time_ms"))
|
||||
|
||||
# Derive llm_api_ms: prefer explicit timing, fall back to total - overhead
|
||||
llm_api_ms = _to_float(hidden_params.get("timing_llm_api_ms"))
|
||||
if llm_api_ms is None and total_ms is not None and overhead_ms is not None:
|
||||
llm_api_ms = max(0.0, total_ms - overhead_ms)
|
||||
|
||||
latency_tracker.record(
|
||||
overhead_ms=overhead_ms,
|
||||
llm_api_ms=llm_api_ms,
|
||||
pre_processing_ms=_to_float(hidden_params.get("timing_pre_processing_ms"))
|
||||
if LITELLM_DETAILED_TIMING
|
||||
else None,
|
||||
post_processing_ms=_to_float(hidden_params.get("timing_post_processing_ms"))
|
||||
if LITELLM_DETAILED_TIMING
|
||||
else None,
|
||||
total_ms=total_ms,
|
||||
)
|
||||
|
||||
if model:
|
||||
per_model_tracker.record(
|
||||
model=model,
|
||||
overhead_ms=overhead_ms,
|
||||
llm_api_ms=llm_api_ms,
|
||||
total_ms=total_ms,
|
||||
)
|
||||
|
|
@ -448,6 +448,7 @@ from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
|||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
router as pass_through_router,
|
||||
)
|
||||
from litellm.proxy.performance_endpoints.endpoints import router as performance_router
|
||||
from litellm.proxy.policy_engine.policy_endpoints import router as policy_crud_router
|
||||
from litellm.proxy.policy_engine.policy_resolve_endpoints import (
|
||||
router as policy_resolve_router,
|
||||
|
|
@ -13027,3 +13028,4 @@ async def dynamic_mcp_route(mcp_server_name: str, request: Request):
|
|||
app.mount(path=BASE_MCP_ROUTE, app=mcp_app)
|
||||
app.include_router(mcp_rest_endpoints_router)
|
||||
app.include_router(mcp_discoverable_endpoints_router)
|
||||
app.include_router(performance_router)
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ const routeFor = (slug: string): string => {
|
|||
return "model-hub";
|
||||
case "logs":
|
||||
return "logs";
|
||||
case "performance-dashboard":
|
||||
return "performance-dashboard";
|
||||
case "guardrails":
|
||||
return "guardrails";
|
||||
case "policies":
|
||||
|
|
@ -198,6 +200,13 @@ const menuItems: MenuItemCfg[] = [
|
|||
icon: <AppstoreOutlined style={{ fontSize: 18 }} />,
|
||||
},
|
||||
{ key: "15", page: "logs", label: "Logs", icon: <LineChartOutlined style={{ fontSize: 18 }} /> },
|
||||
{
|
||||
key: "30",
|
||||
page: "performance-dashboard",
|
||||
label: "Performance",
|
||||
icon: <BarChartOutlined style={{ fontSize: 18 }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "11",
|
||||
page: "guardrails",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||
import { getPerformanceSummaryCall } from "@/components/networking";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export interface LatencyStats {
|
||||
avg_ms: number;
|
||||
p50_ms: number;
|
||||
p95_ms: number;
|
||||
}
|
||||
|
||||
export interface PerModelStats {
|
||||
model: string;
|
||||
overhead: LatencyStats | null;
|
||||
llm_api: LatencyStats | null;
|
||||
total: LatencyStats | null;
|
||||
sample_count: number;
|
||||
}
|
||||
|
||||
export interface PerformanceIssue {
|
||||
severity: "critical" | "warning" | "info";
|
||||
title: string;
|
||||
description: string;
|
||||
fix: string;
|
||||
fix_snippet: string | null;
|
||||
}
|
||||
|
||||
export interface PerformanceSummaryResponse {
|
||||
debug_flags: {
|
||||
is_detailed_debug: boolean;
|
||||
log_level: string;
|
||||
detailed_timing_enabled: boolean;
|
||||
};
|
||||
workers: {
|
||||
cpu_count: number;
|
||||
num_workers: number;
|
||||
cpu_percent: number | null;
|
||||
};
|
||||
connection_pools: {
|
||||
in_flight_requests: number | null;
|
||||
db: {
|
||||
connected: boolean;
|
||||
pool_limit: number;
|
||||
pool_timeout_seconds: number;
|
||||
};
|
||||
redis: {
|
||||
enabled: boolean;
|
||||
max_connections?: number | null;
|
||||
};
|
||||
http: {
|
||||
aiohttp_limit: number;
|
||||
aiohttp_active: number | null;
|
||||
aiohttp_pct: number | null;
|
||||
};
|
||||
};
|
||||
latency: {
|
||||
overhead: LatencyStats | null;
|
||||
llm_api: LatencyStats | null;
|
||||
pre_processing: LatencyStats | null;
|
||||
post_processing: LatencyStats | null;
|
||||
total: LatencyStats | null;
|
||||
overhead_pct_of_total: number | null;
|
||||
sample_count: number;
|
||||
overhead_histogram: { bucket: string; count: number }[];
|
||||
};
|
||||
per_model: PerModelStats[];
|
||||
issues: PerformanceIssue[];
|
||||
}
|
||||
|
||||
export const usePerformanceSummary = (): UseQueryResult<PerformanceSummaryResponse> => {
|
||||
const { accessToken } = useAuthorized();
|
||||
return useQuery<PerformanceSummaryResponse>({
|
||||
queryKey: ["performanceSummary"],
|
||||
queryFn: async () => getPerformanceSummaryCall(accessToken!),
|
||||
enabled: Boolean(accessToken),
|
||||
refetchInterval: 10_000,
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import PerformanceDashboardView from "@/components/PerformanceDashboard/PerformanceDashboardView";
|
||||
|
||||
const PerformanceDashboardPage = () => {
|
||||
useAuthorized();
|
||||
const queryClient = new QueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PerformanceDashboardView />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerformanceDashboardPage;
|
||||
|
|
@ -14,6 +14,7 @@ import LoadingScreen from "@/components/common_components/LoadingScreen";
|
|||
import { CostTrackingSettings } from "@/components/CostTrackingSettings";
|
||||
import GeneralSettings from "@/components/general_settings";
|
||||
import GuardrailsMonitorView from "@/components/GuardrailsMonitor/GuardrailsMonitorView";
|
||||
import PerformanceDashboardView from "@/components/PerformanceDashboard/PerformanceDashboardView";
|
||||
import GuardrailsPanel from "@/components/guardrails";
|
||||
import PoliciesPanel from "@/components/policies";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
|
|
@ -556,6 +557,8 @@ function CreateKeyPageContent() {
|
|||
<ToolPolicies accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "guardrails-monitor" ? (
|
||||
<GuardrailsMonitorView accessToken={accessToken} />
|
||||
) : page == "performance-dashboard" ? (
|
||||
<PerformanceDashboardView />
|
||||
) : page == "new_usage" ? (
|
||||
<NewUsagePage
|
||||
teams={(teams as Team[]) ?? []}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,498 @@
|
|||
"use client";
|
||||
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Collapse, Spin, Tag } from "antd";
|
||||
import { DashboardOutlined, CheckCircleOutlined } from "@ant-design/icons";
|
||||
import { BarChart, LineChart, Card } from "@tremor/react";
|
||||
import { MetricCard } from "@/components/GuardrailsMonitor/MetricCard";
|
||||
import {
|
||||
usePerformanceSummary,
|
||||
PerformanceSummaryResponse,
|
||||
PerformanceIssue,
|
||||
} from "@/app/(dashboard)/hooks/performanceSummary/usePerformanceSummary";
|
||||
|
||||
const MAX_HISTORY = 60; // 60 × 10s = 10 min
|
||||
|
||||
interface HistoryPoint {
|
||||
time: string;
|
||||
"Overhead avg"?: number;
|
||||
"In-flight"?: number;
|
||||
"HTTP pool %"?: number;
|
||||
}
|
||||
|
||||
function formatTime(d: Date): string {
|
||||
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
}
|
||||
|
||||
const SEVERITY_DOT: Record<string, string> = {
|
||||
critical: "bg-red-500",
|
||||
warning: "bg-amber-400",
|
||||
info: "bg-blue-400",
|
||||
};
|
||||
|
||||
export default function PerformanceDashboardView() {
|
||||
const { data, isLoading, error, dataUpdatedAt } = usePerformanceSummary();
|
||||
|
||||
// Client-side ring buffer for time-series charts
|
||||
const overheadHistoryRef = useRef<HistoryPoint[]>([]);
|
||||
const [overheadHistory, setOverheadHistory] = useState<HistoryPoint[]>([]);
|
||||
|
||||
const inflightHistoryRef = useRef<HistoryPoint[]>([]);
|
||||
const [inflightHistory, setInflightHistory] = useState<HistoryPoint[]>([]);
|
||||
|
||||
const httpHistoryRef = useRef<HistoryPoint[]>([]);
|
||||
const [httpHistory, setHttpHistory] = useState<HistoryPoint[]>([]);
|
||||
|
||||
// Live "X seconds ago" counter
|
||||
const [secondsAgo, setSecondsAgo] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!dataUpdatedAt) return;
|
||||
setSecondsAgo(0);
|
||||
const interval = setInterval(() => {
|
||||
setSecondsAgo(Math.floor((Date.now() - dataUpdatedAt) / 1000));
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [dataUpdatedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const now = formatTime(new Date());
|
||||
|
||||
const newOH = [...overheadHistoryRef.current, {
|
||||
time: now,
|
||||
"Overhead avg": data.latency.overhead?.avg_ms ?? undefined,
|
||||
}].slice(-MAX_HISTORY);
|
||||
overheadHistoryRef.current = newOH;
|
||||
setOverheadHistory([...newOH]);
|
||||
|
||||
const newIF = [...inflightHistoryRef.current, {
|
||||
time: now,
|
||||
"In-flight": data.connection_pools.in_flight_requests ?? undefined,
|
||||
}].slice(-MAX_HISTORY);
|
||||
inflightHistoryRef.current = newIF;
|
||||
setInflightHistory([...newIF]);
|
||||
|
||||
const newHTTP = [...httpHistoryRef.current, {
|
||||
time: now,
|
||||
"HTTP pool %": data.connection_pools.http.aiohttp_pct ?? undefined,
|
||||
}].slice(-MAX_HISTORY);
|
||||
httpHistoryRef.current = newHTTP;
|
||||
setHttpHistory([...newHTTP]);
|
||||
}, [dataUpdatedAt]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<Alert
|
||||
type="error"
|
||||
message="Failed to load performance data"
|
||||
description="Check that the proxy is running and your API key has admin access."
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { debug_flags, workers, connection_pools, latency, per_model, issues } = data;
|
||||
const overheadHigh = latency.overhead_pct_of_total != null && latency.overhead_pct_of_total > 20;
|
||||
const workersLow = workers.num_workers < workers.cpu_count;
|
||||
|
||||
// Delta vs ~5 min ago (oldest point in history)
|
||||
const oldestOverhead = overheadHistory.length > 1
|
||||
? overheadHistory[0]["Overhead avg"]
|
||||
: null;
|
||||
const currentOverhead = latency.overhead?.avg_ms ?? null;
|
||||
const overheadDelta = oldestOverhead != null && currentOverhead != null
|
||||
? Math.round((currentOverhead - oldestOverhead) * 10) / 10
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%" }} className="p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between gap-6 mb-6">
|
||||
<div className="flex items-stretch gap-2 min-w-0">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<DashboardOutlined style={{ fontSize: "32px" }} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 ml-1">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-0.5 leading-tight">Performance</h3>
|
||||
<p className="text-xs text-gray-600 leading-tight">LiteLLM Proxy · Latency Diagnostics</p>
|
||||
</div>
|
||||
</div>
|
||||
<Tag color={secondsAgo < 15 ? "green" : "default"} className="text-xs flex-shrink-0">
|
||||
{dataUpdatedAt
|
||||
? secondsAgo === 0 ? "Refreshed just now" : `Refreshed ${secondsAgo}s ago`
|
||||
: "Waiting for data…"}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* ── Issues Detected ── */}
|
||||
<div className="mb-6">
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-sm font-semibold text-gray-700">Issues Detected</p>
|
||||
{issues.length === 0 && latency.sample_count > 0 && (
|
||||
<span className="flex items-center gap-1 text-xs text-green-600">
|
||||
<CheckCircleOutlined /> All clear
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{issues.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">
|
||||
{latency.sample_count === 0 ? "Send traffic to start analysis." : "No issues found — proxy looks healthy."}
|
||||
</p>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-xs text-gray-400 border-b border-gray-100">
|
||||
<th className="pb-2 font-medium w-4"></th>
|
||||
<th className="pb-2 font-medium">Issue</th>
|
||||
<th className="pb-2 font-medium">Suggested Fix</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{issues.map((issue, i) => (
|
||||
<tr key={i} className="border-b border-gray-50 align-top">
|
||||
<td className="py-2 pr-2">
|
||||
<span className={`inline-block w-2 h-2 rounded-full mt-1.5 ${SEVERITY_DOT[issue.severity] ?? "bg-gray-400"}`} />
|
||||
</td>
|
||||
<td className="py-2 pr-4">
|
||||
<p className="font-medium text-gray-800">{issue.title}</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{issue.description}</p>
|
||||
</td>
|
||||
<td className="py-2 min-w-[220px]">
|
||||
{issue.fix_snippet ? (
|
||||
<Collapse ghost size="small">
|
||||
<Collapse.Panel header={<span className="text-xs text-blue-600">{issue.fix}</span>} key="1">
|
||||
<pre className="text-xs font-mono bg-gray-50 border border-gray-200 rounded px-3 py-2 whitespace-pre-wrap">
|
||||
{issue.fix_snippet}
|
||||
</pre>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">{issue.fix}</p>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* ── Top metric cards ── */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<MetricCard
|
||||
label="OVERHEAD AVG"
|
||||
value={latency.overhead ? `${latency.overhead.avg_ms} ms` : "—"}
|
||||
subtitle={
|
||||
latency.overhead
|
||||
? `p50: ${latency.overhead.p50_ms}ms · p95: ${latency.overhead.p95_ms}ms${overheadDelta !== null ? ` · ${overheadDelta >= 0 ? "+" : ""}${overheadDelta}ms vs earlier` : ""}`
|
||||
: "No data yet"
|
||||
}
|
||||
valueColor={overheadHigh ? "text-orange-500" : "text-gray-900"}
|
||||
/>
|
||||
<MetricCard
|
||||
label="OVERHEAD % OF TOTAL"
|
||||
value={latency.overhead_pct_of_total != null ? `${latency.overhead_pct_of_total}%` : "—"}
|
||||
subtitle="LiteLLM share of request time"
|
||||
valueColor={overheadHigh ? "text-orange-500" : "text-green-600"}
|
||||
/>
|
||||
<MetricCard
|
||||
label="LLM API AVG"
|
||||
value={latency.llm_api ? `${latency.llm_api.avg_ms} ms` : "—"}
|
||||
subtitle={latency.llm_api ? `p50: ${latency.llm_api.p50_ms}ms · p95: ${latency.llm_api.p95_ms}ms` : "No data yet"}
|
||||
/>
|
||||
<MetricCard
|
||||
label="TOTAL AVG"
|
||||
value={latency.total ? `${latency.total.avg_ms} ms` : "—"}
|
||||
subtitle={latency.total ? `p95: ${latency.total.p95_ms}ms · n=${latency.sample_count}` : "No data yet"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Main 2-column layout ── */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
{/* Left: Overhead Over Time */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card>
|
||||
<p className="text-sm font-semibold text-gray-700 mb-0.5">Overhead Over Time</p>
|
||||
<p className="text-xs text-gray-400 mb-3">LiteLLM-added latency · last 10 min · 10s resolution</p>
|
||||
|
||||
{/* Plain-English summary */}
|
||||
{latency.sample_count > 0 && latency.overhead_histogram && (() => {
|
||||
const hist = latency.overhead_histogram;
|
||||
const total = hist.reduce((s, b) => s + b.count, 0);
|
||||
const under50 = hist.filter(b => ["0-5","5-10","10-25","25-50"].includes(b.bucket)).reduce((s,b) => s+b.count, 0);
|
||||
const pct = total > 0 ? Math.round(under50 / total * 100) : 0;
|
||||
const worstBucket = [...hist].reverse().find(b => b.count > 0);
|
||||
return (
|
||||
<div className="flex items-center gap-6 mb-3 px-1">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-gray-900">{total}</p>
|
||||
<p className="text-xs text-gray-400">total requests</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className={`text-2xl font-bold ${pct >= 90 ? "text-green-600" : pct >= 70 ? "text-amber-500" : "text-red-500"}`}>{pct}%</p>
|
||||
<p className="text-xs text-gray-400">under 50ms overhead</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-gray-900">{latency.overhead?.p95_ms ?? "—"}ms</p>
|
||||
<p className="text-xs text-gray-400">p95 overhead</p>
|
||||
</div>
|
||||
{worstBucket && worstBucket.bucket !== "0-5" && (
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-orange-500">{worstBucket.count}</p>
|
||||
<p className="text-xs text-gray-400">requests > {worstBucket.bucket.split("-")[0]}ms</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{latency.sample_count === 0 ? (
|
||||
<div className="flex items-center justify-center h-40 text-gray-400 text-sm">
|
||||
No requests yet — send traffic through the proxy to see data.
|
||||
</div>
|
||||
) : (
|
||||
<LineChart
|
||||
data={overheadHistory}
|
||||
index="time"
|
||||
categories={["Overhead avg"]}
|
||||
colors={["blue"]}
|
||||
valueFormatter={(v) => `${v} ms`}
|
||||
yAxisWidth={52}
|
||||
showLegend={false}
|
||||
showAnimation={false}
|
||||
className="h-40"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right: Worker Provisioning + Connections */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-sm font-semibold text-gray-700">WORKER PROVISIONING</p>
|
||||
{workersLow && <Tag color="orange" className="text-xs">Under-provisioned</Tag>}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 mb-3">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">CPU Cores</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{workers.cpu_count}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Workers</p>
|
||||
<p className={`text-2xl font-bold ${workersLow ? "text-orange-500" : "text-gray-900"}`}>
|
||||
{workers.num_workers}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{workers.cpu_percent != null && (
|
||||
<div className="mb-3">
|
||||
<p className="text-xs text-gray-500">CPU Usage</p>
|
||||
<p className={`text-lg font-semibold ${workers.cpu_percent > 80 ? "text-red-500" : workers.cpu_percent > 60 ? "text-orange-500" : "text-green-600"}`}>
|
||||
{workers.cpu_percent}%
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-gray-500 mb-1">
|
||||
Workers / CPU ratio
|
||||
<span className="float-right">{workers.num_workers}/{workers.cpu_count}</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-100 rounded-full h-1.5 mb-2">
|
||||
<div
|
||||
className={`h-1.5 rounded-full ${workersLow ? "bg-orange-400" : "bg-green-500"}`}
|
||||
style={{ width: `${Math.min((workers.num_workers / workers.cpu_count) * 100, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
{workersLow && (
|
||||
<p className="text-xs text-orange-600">Recommended: {2 * workers.cpu_count + 1} workers (2× CPU + 1)</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card className="flex-1">
|
||||
<p className="text-sm font-semibold text-gray-700 mb-3">CONNECTIONS</p>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-0.5">Database</p>
|
||||
<span className={`text-xs font-medium ${connection_pools.db.connected ? "text-green-600" : "text-red-500"}`}>
|
||||
{connection_pools.db.connected ? "● Connected" : "● Disconnected"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-500 mb-0.5">Pool limit</p>
|
||||
<p className="text-lg font-bold text-gray-900">{connection_pools.db.pool_limit}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 -mt-1">
|
||||
Timeout: {connection_pools.db.pool_timeout_seconds}s
|
||||
</div>
|
||||
<div className="border-t border-gray-100 pt-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-0.5">Redis</p>
|
||||
<span className={`text-xs font-medium ${connection_pools.redis.enabled ? "text-green-600" : "text-gray-400"}`}>
|
||||
{connection_pools.redis.enabled ? "● Connected" : "● Not configured"}
|
||||
</span>
|
||||
</div>
|
||||
{connection_pools.redis.enabled && (
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-500 mb-0.5">Max connections</p>
|
||||
<p className="text-lg font-bold text-gray-900">{connection_pools.redis.max_connections ?? "∞"}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Bottom row: In-Flight + HTTP pool ── */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<p className="text-sm font-semibold text-gray-700">In-Flight Requests</p>
|
||||
<span className="text-sm font-bold text-gray-700">
|
||||
{connection_pools.in_flight_requests ?? "—"}
|
||||
<span className="text-xs font-normal text-gray-400"> asyncio tasks</span>
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mb-4">
|
||||
Concurrent asyncio tasks (proxy-wide) · last 10 min
|
||||
</p>
|
||||
<LineChart
|
||||
data={inflightHistory}
|
||||
index="time"
|
||||
categories={["In-flight"]}
|
||||
colors={["blue"]}
|
||||
yAxisWidth={40}
|
||||
showLegend={false}
|
||||
showAnimation={false}
|
||||
className="h-40"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<p className="text-sm font-semibold text-gray-700">HTTP Client Pool Utilization</p>
|
||||
<span className="text-sm font-bold text-gray-700">
|
||||
{connection_pools.http.aiohttp_active ?? "—"}
|
||||
<span className="text-xs font-normal text-gray-400">
|
||||
{" "}/ {connection_pools.http.aiohttp_limit} limit
|
||||
{connection_pools.http.aiohttp_pct != null && ` · ${connection_pools.http.aiohttp_pct}%`}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mb-4">
|
||||
aiohttp active connections ÷ pool limit · amber line = 80% · last 10 min
|
||||
</p>
|
||||
<LineChart
|
||||
data={httpHistory}
|
||||
index="time"
|
||||
categories={["HTTP pool %"]}
|
||||
colors={["blue"]}
|
||||
valueFormatter={(v) => `${v}%`}
|
||||
yAxisWidth={44}
|
||||
showLegend={false}
|
||||
showAnimation={false}
|
||||
className="h-40"
|
||||
referenceLine={{ value: 80, label: "80%", color: "amber" }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* ── Per-model breakdown ── */}
|
||||
{per_model.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<Card>
|
||||
<p className="text-sm font-semibold text-gray-700 mb-0.5">Per-Model Overhead</p>
|
||||
<p className="text-xs text-gray-400 mb-4">Sorted by overhead avg · last {latency.sample_count} requests</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-xs text-gray-400 border-b border-gray-100">
|
||||
<th className="pb-2 font-medium">Model</th>
|
||||
<th className="pb-2 font-medium text-right">Overhead avg</th>
|
||||
<th className="pb-2 font-medium text-right">Overhead p95</th>
|
||||
<th className="pb-2 font-medium text-right">LLM API avg</th>
|
||||
<th className="pb-2 font-medium text-right">Total avg</th>
|
||||
<th className="pb-2 font-medium text-right">Requests</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{per_model.map((row) => {
|
||||
const overheadPct = row.overhead && row.total
|
||||
? Math.round((row.overhead.avg_ms / row.total.avg_ms) * 100)
|
||||
: null;
|
||||
return (
|
||||
<tr key={row.model} className="border-b border-gray-50 hover:bg-gray-50">
|
||||
<td className="py-2 font-mono text-xs text-gray-700 max-w-[200px] truncate pr-4">
|
||||
{row.model}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
<span className={overheadPct != null && overheadPct > 20 ? "text-orange-500 font-semibold" : "text-gray-700"}>
|
||||
{row.overhead ? `${row.overhead.avg_ms}ms` : "—"}
|
||||
</span>
|
||||
{overheadPct != null && (
|
||||
<span className="text-xs text-gray-400 ml-1">({overheadPct}%)</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 text-right text-gray-600">
|
||||
{row.overhead ? `${row.overhead.p95_ms}ms` : "—"}
|
||||
</td>
|
||||
<td className="py-2 text-right text-gray-600">
|
||||
{row.llm_api ? `${row.llm_api.avg_ms}ms` : "—"}
|
||||
</td>
|
||||
<td className="py-2 text-right text-gray-600">
|
||||
{row.total ? `${row.total.avg_ms}ms` : "—"}
|
||||
</td>
|
||||
<td className="py-2 text-right text-gray-400 text-xs">{row.sample_count}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Config summary ── */}
|
||||
<div className="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Configuration Summary</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-xs text-gray-400 mb-1">Log Level</p>
|
||||
<Tag color={debug_flags.is_detailed_debug ? "red" : "green"}>{debug_flags.log_level}</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-400 mb-1">Detailed Timing</p>
|
||||
<Tag color={debug_flags.detailed_timing_enabled ? "green" : "default"}>
|
||||
{debug_flags.detailed_timing_enabled ? "Enabled" : "Disabled"}
|
||||
</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-400 mb-1">Workers / CPU</p>
|
||||
<span className="font-semibold text-gray-700">{workers.num_workers} / {workers.cpu_count}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-400 mb-1">Sample Count</p>
|
||||
<span className="font-semibold text-gray-700">{latency.sample_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -168,6 +168,13 @@ const menuGroups: MenuGroup[] = [
|
|||
icon: <SafetyOutlined />,
|
||||
roles: [...all_admin_roles, ...internalUserRoles],
|
||||
},
|
||||
{
|
||||
key: "performance-dashboard",
|
||||
page: "performance-dashboard",
|
||||
label: "Performance",
|
||||
icon: <BarChartOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10035,3 +10035,29 @@ export const updateToolPolicy = async (
|
|||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
|
||||
export const getPerformanceSummaryCall = async (accessToken: string) => {
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/v1/performance/summary`
|
||||
: `/v1/performance/summary`;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Failed to get performance summary:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue