feat(daily-activity): read guardrail spend per key from the daily guardrail usage table

Adds DailyActivityRepository.guardrail_spend, which sums LiteLLM_DailyGuardrailUsageUnits.cost by date, guardrail and key. A guardrail row counts only when its key has a spend row on that day under the same scope filters as the rest of the daily activity response, so user, key and date filters apply to guardrail cost too
This commit is contained in:
Mrinal Chanshetty 2026-10-27 18:29:58 -07:00
parent 41336b09e2
commit c864255f33
3 changed files with 30 additions and 0 deletions

View file

@ -19,6 +19,7 @@ from litellm.repositories.daily_activity_sql import (
build_cache_leakage_keys_sql,
build_entity_rollup_sql,
build_export_sql,
build_guardrail_spend_sql,
build_key_page_sql,
build_key_search_sql,
build_model_top_keys_sql,
@ -36,6 +37,7 @@ from litellm.types.repositories.daily_activity import (
ExportRow,
ExportType,
GroupingSetsRow,
GuardrailSpendRow,
KeyMetadataRow,
KeyPage,
KeySpendRow,
@ -100,6 +102,7 @@ class DailyActivityDatabase(Protocol):
_GROUPING_ADAPTER: Final = TypeAdapter(tuple[GroupingSetsRow, ...])
_ENTITY_ADAPTER: Final = TypeAdapter(tuple[EntityRollupRow, ...])
_GUARDRAIL_SPEND_ADAPTER: Final = TypeAdapter(tuple[GuardrailSpendRow, ...])
_KEY_SPEND_ADAPTER: Final = TypeAdapter(tuple[KeySpendRow, ...])
_KEY_PAGE_TOTAL_ADAPTER: Final[TypeAdapter[int]] = TypeAdapter(int)
_USER_SPEND_ADAPTER: Final = TypeAdapter(tuple[UserSpendRow, ...])
@ -191,6 +194,9 @@ class DailyActivityRepository:
distinct_api_keys=distinct_api_keys,
)
async def guardrail_spend(self, scope: DailyActivityScope) -> tuple[GuardrailSpendRow, ...]:
return _GUARDRAIL_SPEND_ADAPTER.validate_python(await self._query(build_guardrail_spend_sql(scope)))
async def search_keys(self, scope: DailyActivityScope, *, search: str, limit: int) -> tuple[str, ...]:
if not 1 <= limit <= constants.USAGE_KEY_SEARCH_MAX:
raise ValueError(f"limit must be between 1 and {constants.USAGE_KEY_SEARCH_MAX}")

View file

@ -261,6 +261,22 @@ def build_entity_rollup_sql(scope: DailyActivityScope, *, api_key_limit: int) ->
return SqlQuery(sql=sql, params=(*where_params, PTU_SENTINEL_API_KEY, api_key_limit))
def build_guardrail_spend_sql(scope: DailyActivityScope) -> SqlQuery:
pg_table: Final = PRISMA_TO_PG_TABLE[scope.table]
where_clause, where_params = build_where_clause(scope)
sql: Final = f"""
SELECT g.date, g.guardrail_id, g.api_key, COALESCE(SUM(g.cost), 0)::float AS spend
FROM "LiteLLM_DailyGuardrailUsageUnits" g
WHERE g.date >= $1 AND g.date <= $2
AND EXISTS (
SELECT 1 FROM "{pg_table}"
WHERE {where_clause} AND "{pg_table}".date = g.date AND "{pg_table}".api_key = g.api_key
)
GROUP BY g.date, g.guardrail_id, g.api_key
"""
return SqlQuery(sql=sql, params=where_params)
def _key_spend_select() -> str:
return """
COALESCE(SUM(spend), 0)::float AS spend,

View file

@ -168,6 +168,14 @@ class EntityRollupRow(RollupMetricsRow):
distinct_api_keys: int | None
@dataclass(frozen=True, slots=True)
class GuardrailSpendRow:
date: str
guardrail_id: str
api_key: str
spend: float
@dataclass(frozen=True, slots=True)
class AggregatedRows:
grouping_rows: tuple[GroupingSetsRow, ...]