Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/management_endpoints/gateway_request_endpoints.py: 91%
42 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2GATEWAY REQUEST COUNTS (SGR)
4GET /gateway/daily/activity - successful/failed gateway requests by date and route
6Source of truth is LiteLLM_DailyGatewayRequests, written at the ASGI edge by
7BillableRequestMetricsMiddleware. This counts what the proxy answered, so it is
8independent of whether a request reached litellm's logging callbacks.
10The table carries no key/user/team dimension, so these totals are deployment-wide
11and the endpoint is restricted to proxy admin roles.
12"""
14from collections.abc import Sequence
15from datetime import datetime, timedelta, timezone
16from typing import Annotated, Final
18from fastapi import APIRouter, Depends, HTTPException, Query
19from pydantic import BaseModel, TypeAdapter
21from litellm._logging import verbose_proxy_logger
22from litellm.proxy._types import CommonProxyErrors, LitellmUserRoles, UserAPIKeyAuth
23from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
24from litellm.types.proxy.gateway_requests import (
25 GatewayRequestActivityResponse,
26 GatewayRequestBreakdownEntry,
27 GatewayRequestDailyEntry,
28)
30router: Final = APIRouter()
32_DEFAULT_LOOKBACK_DAYS: Final = 30
34_AGGREGATE_SQL: Final = """
35 SELECT
36 date,
37 category,
38 route,
39 SUM(successful_requests)::bigint AS successful_requests,
40 SUM(failed_requests)::bigint AS failed_requests
41 FROM "LiteLLM_DailyGatewayRequests"
42 WHERE date >= $1 AND date <= $2
43 GROUP BY date, category, route
44"""
47class _AggregateRow(BaseModel):
48 """Validates one query_raw row so the handler works with typed values, not Any."""
50 date: str
51 category: str
52 route: str
53 successful_requests: int
54 failed_requests: int
57_ROWS_ADAPTER: Final = TypeAdapter(tuple[_AggregateRow, ...])
60def _default_range() -> tuple[str, str]:
61 end: Final = datetime.now(timezone.utc)
62 start: Final = end - timedelta(days=_DEFAULT_LOOKBACK_DAYS)
63 return start.strftime("%Y-%m-%d"), end.strftime("%Y-%m-%d")
66def _fold_by_date(rows: Sequence[_AggregateRow]) -> tuple[GatewayRequestDailyEntry, ...]:
67 dates: Final = sorted(frozenset(row.date for row in rows))
68 return tuple(
69 GatewayRequestDailyEntry(
70 date=date,
71 successful_requests=sum(row.successful_requests for row in rows if row.date == date),
72 failed_requests=sum(row.failed_requests for row in rows if row.date == date),
73 )
74 for date in dates
75 )
78def _fold_by_route(rows: Sequence[_AggregateRow]) -> tuple[GatewayRequestBreakdownEntry, ...]:
79 pairs: Final = sorted(frozenset((row.category, row.route) for row in rows))
80 entries: Final = tuple(
81 GatewayRequestBreakdownEntry(
82 category=category,
83 route=route,
84 successful_requests=sum(
85 row.successful_requests for row in rows if row.category == category and row.route == route
86 ),
87 failed_requests=sum(row.failed_requests for row in rows if row.category == category and row.route == route),
88 )
89 for category, route in pairs
90 )
91 return tuple(sorted(entries, key=lambda entry: entry.successful_requests, reverse=True))
94@router.get(
95 "/gateway/daily/activity",
96 tags=["Budget & Spend Tracking"], # mutable-ok: fastapi's decorator signature types tags as a list
97 response_model=GatewayRequestActivityResponse,
98)
99async def get_gateway_daily_activity(
100 user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
101 start_date: str | None = Query(default=None, description="Start date in YYYY-MM-DD format"),
102 end_date: str | None = Query(default=None, description="End date in YYYY-MM-DD format"),
103) -> GatewayRequestActivityResponse:
104 """
105 Successful and failed gateway requests, counted at the ASGI edge.
107 Deployment-wide: the underlying table has no per-key or per-user dimension,
108 so this is admin-only.
109 """
110 from litellm.proxy.proxy_server import prisma_client
112 if user_api_key_dict.user_role not in ( 112 ↛ 116line 112 didn't jump to line 116 because the condition on line 112 was never true
113 LitellmUserRoles.PROXY_ADMIN,
114 LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY,
115 ):
116 raise HTTPException(
117 status_code=403,
118 detail="Only proxy admin roles can view gateway request counts across the deployment",
119 )
121 if prisma_client is None: 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true
122 raise HTTPException(status_code=500, detail=CommonProxyErrors.db_not_connected_error.value)
124 default_start, default_end = _default_range()
125 raw_rows: Final = await prisma_client.db.query_raw( # pyright: ignore[reportAny] # untyped prisma client
126 _AGGREGATE_SQL,
127 start_date or default_start,
128 end_date or default_end,
129 )
130 # Every downstream use is typed: the adapter returns _AggregateRow or raises.
131 rows: Final = _ROWS_ADAPTER.validate_python(raw_rows or ())
132 verbose_proxy_logger.debug("/gateway/daily/activity - aggregated %d rows", len(rows))
134 return GatewayRequestActivityResponse(
135 total_successful_requests=sum(row.successful_requests for row in rows),
136 total_failed_requests=sum(row.failed_requests for row in rows),
137 by_date=_fold_by_date(rows),
138 by_route=_fold_by_route(rows),
139 )