From 17c22bcbbcc773df220985a4771bd8a3daa6d55d Mon Sep 17 00:00:00 2001 From: yassin Date: Mon, 28 Sep 2026 21:27:25 +0000 Subject: [PATCH] Revert "Merge pull request #41293 from BerriAI/litellm_usage_key_free_aggregate_split" --- litellm/constants.py | 1 - litellm/proxy/_lazy_openapi_snapshot.json | 24 -- .../common_daily_activity.py | 224 ++++++++---------- .../common_daily_activity.py | 10 - .../test_common_daily_activity.py | 174 ++++---------- .../_components/CacheLeakageCard.test.tsx | 24 -- .../_components/CacheLeakageCard.tsx | 9 +- .../useDailyActivityRange.test.tsx | 17 +- .../_components/useDailyActivityRange.ts | 3 - .../EntityUsage/EntityUsage.test.tsx | 17 -- .../components/EntityUsage/EntityUsage.tsx | 15 +- .../_components/components/UsagePageView.tsx | 8 +- .../exportBlockedReason.test.ts | 26 +- .../EntityUsageExport/exportBlockedReason.ts | 20 +- .../components/KeyActivityPanel.test.tsx | 10 - .../UsagePage/components/KeyActivityPanel.tsx | 14 +- ui/litellm-dashboard/src/lib/http/schema.d.ts | 10 - 17 files changed, 160 insertions(+), 446 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 24ff9a0cc96e..fd9812e2cf59 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -2140,7 +2140,6 @@ PTU_SENTINEL_API_KEY: Final[str] = "__ptu_flat_cost__" PTU_ROLLUP_JOB_ID: Final[str] = "ptu_flat_cost_rollup_job" PTU_ROLLUP_LOCK_TTL_SECONDS: Final[int] = 900 -USAGE_TOP_API_KEYS_LIMIT: Final[int] = int(os.getenv("USAGE_TOP_API_KEYS_LIMIT", "100")) # Furthest back the catch-up pass looks for unpriced PTU days when a deployment # declares no ptu_effective_from, bounding the scan for an open-ended window. PTU_ROLLUP_MAX_BACKFILL_DAYS: Final[int] = 90 diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index db57aa4f0460..c4ff4b415dd4 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -3313,18 +3313,6 @@ }, "DailySpendMetadata": { "properties": { - "api_key_limit": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "When set, api_keys and every api_key_breakdown list at most this many keys, ranked by spend. Totals and the model, provider, mcp and endpoint rollups still cover every key.", - "title": "Api Key Limit" - }, "has_more": { "default": false, "title": "Has More", @@ -3335,18 +3323,6 @@ "title": "Page", "type": "integer" }, - "total_api_keys": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Distinct API keys matching the filters. When this exceeds api_key_limit, the per-key lists are truncated to the highest-spend keys.", - "title": "Total Api Keys" - }, "total_api_requests": { "default": 0, "title": "Total Api Requests", diff --git a/litellm/proxy/management_endpoints/common_daily_activity.py b/litellm/proxy/management_endpoints/common_daily_activity.py index 7b07c397a0b8..cecf3e50f5cc 100644 --- a/litellm/proxy/management_endpoints/common_daily_activity.py +++ b/litellm/proxy/management_endpoints/common_daily_activity.py @@ -9,7 +9,7 @@ from typing_extensions import ReadOnly, TypedDict from litellm._logging import verbose_proxy_logger -from litellm.constants import PTU_SENTINEL_API_KEY, USAGE_TOP_API_KEYS_LIMIT +from litellm.constants import PTU_SENTINEL_API_KEY from litellm.proxy._types import CommonProxyErrors from litellm.proxy.spend_tracking.key_metadata_recovery import ( attach_user_details, @@ -150,9 +150,15 @@ class _AggregatedSpendData(TypedDict): totals: SpendMetrics -class _RollupMetricsRow(SimpleNamespace): +class _GroupingSetsRow(SimpleNamespace): date: str api_key: str | None + model: str | None + model_group: str | None + custom_llm_provider: str | None + mcp_namespaced_tool_name: str | None + endpoint: str | None + group_level: int spend: float | None prompt_tokens: int | None completion_tokens: int | None @@ -170,46 +176,12 @@ class _RollupMetricsRow(SimpleNamespace): timed_requests: int | None -class _GroupingSetsRow(_RollupMetricsRow): - model: str | None - model_group: str | None - custom_llm_provider: str | None - mcp_namespaced_tool_name: str | None - endpoint: str | None - group_level: int - distinct_api_keys: int | None - - -class _EntityRollupRow(_RollupMetricsRow): +class _EntityRollupRow(_GroupingSetsRow): entity_id: str | None api_key_rolled: int -class _AggregatedQueryKwargs(TypedDict): - table_name: ReadOnly[str] - entity_id_field: ReadOnly[str] - entity_id: ReadOnly[str | list[str] | None] - start_date: ReadOnly[str] - end_date: ReadOnly[str] - model: ReadOnly[str | None] - api_key: ReadOnly[str | list[str] | None] - exclude_entity_ids: ReadOnly[list[str] | None] - timezone_offset_minutes: ReadOnly[int | None] - include_current_utc_day: ReadOnly[bool] - - -_SqlQuery = tuple[str, list[str]] - - -async def _query_raw_optional( - prisma_client: PrismaClient, query: _SqlQuery | None -) -> list[dict[str, object]] | None: # mutable-ok: prisma query_raw return shape - if query is None: - return None - return await prisma_client.db.query_raw(query[0], *query[1]) - - -def _reported_flat_cost(record: DailySpendRecord | _RollupMetricsRow) -> float: +def _reported_flat_cost(record: DailySpendRecord | _GroupingSetsRow) -> float: """Flat cost a daily row reports, which is zero unless PTU cost attribution is enabled. Both read paths funnel through here: the paginated path reads the ``ptu_flat_cost`` @@ -733,29 +705,6 @@ def _ptu_flat_cost_select(table_name: str) -> str: return "0::float AS ptu_flat_cost" -def _rollup_metric_select(table_name: str) -> str: - return f""" - SUM(spend)::float AS spend, - {_ptu_flat_cost_select(table_name)}, - SUM(prompt_tokens)::bigint AS prompt_tokens, - SUM(completion_tokens)::bigint AS completion_tokens, - SUM(cache_read_input_tokens)::bigint AS cache_read_input_tokens, - SUM(cache_creation_input_tokens)::bigint AS cache_creation_input_tokens, - SUM(compression_saved_tokens)::bigint AS compression_saved_tokens, - SUM(compression_savings_spend)::float AS compression_savings_spend, - SUM(prompt_caching_savings_spend)::float AS prompt_caching_savings_spend, - SUM(gateway_injected_caching_savings_spend)::float AS gateway_injected_caching_savings_spend, - SUM(autorouter_savings_spend)::float AS autorouter_savings_spend, - SUM(api_requests)::bigint AS api_requests, - SUM(successful_requests)::bigint AS successful_requests, - SUM(failed_requests)::bigint AS failed_requests, - SUM(total_response_time_ms)::bigint AS total_response_time_ms, - SUM(timed_requests)::bigint AS timed_requests""" - - -_MODEL_GROUP_EXPR: Final = "COALESCE(NULLIF(model_group, ''), model)" - - def _build_aggregated_sql_query( *, table_name: str, @@ -769,7 +718,12 @@ def _build_aggregated_sql_query( timezone_offset_minutes: int | None = None, include_current_utc_day: bool = False, ) -> tuple[str, list[str]]: # mutable-ok: SQL text plus its ordered $N params - """Build the GROUPING SETS query for aggregated daily activity. + """Build a parameterized SQL GROUP BY query for aggregated daily activity. + + Groups by (date, api_key, model, model_group, custom_llm_provider, + mcp_namespaced_tool_name, endpoint) with SUMs on all metric columns. + The entity_id column is intentionally omitted from GROUP BY to collapse + rows across entities — this is where the biggest row reduction comes from. Returns: Tuple of (sql_query, params_list) ready for prisma_client.db.query_raw(). @@ -782,7 +736,7 @@ def _build_aggregated_sql_query( start_date, end_date, timezone_offset_minutes, include_current_utc_day ) - where_clause, where_params = _build_aggregated_where_clause( + where_clause, sql_params = _build_aggregated_where_clause( entity_id_field=entity_id_field, entity_id=entity_id, adjusted_start=adjusted_start, @@ -791,72 +745,67 @@ def _build_aggregated_sql_query( api_key=api_key, exclude_entity_ids=exclude_entity_ids, ) - sentinel_param: Final = f"${len(where_params) + 1}" - metric_select: Final = _rollup_metric_select(table_name) + # Postgres computes every rollup level the response needs — per-date + # totals, per-(date, model), per-(date, model, api_key), per-provider, + # etc. — in a single pass via GROUPING SETS. The GROUPING() bitmask + # encodes which level a row belongs to so Python can dispatch rows + # straight into their buckets without re-summing. The leaf grouping + # is omitted on purpose: nothing in the response shape needs it once + # all the rollups are present. + # # TODO: drop the successful_requests/failed_requests aggregates (and the # total_successful_requests metadata they feed) once the admin UI reads SGR # only from LiteLLM_DailyGatewayRequests. The remaining spend, token and # api_requests rollups are still served from here. sql_query: Final = f""" - (SELECT - date, - NULL::text AS api_key, - model, - {_MODEL_GROUP_EXPR} AS model_group, - custom_llm_provider, - mcp_namespaced_tool_name, - endpoint, - (GROUPING(date) << 6) | {_API_KEY_ROLLED_UP_BIT} - | GROUPING(model, {_MODEL_GROUP_EXPR}, - custom_llm_provider, mcp_namespaced_tool_name, - endpoint) AS group_level, - NULL::bigint AS distinct_api_keys,{metric_select} - FROM "{pg_table}" - WHERE {where_clause} - GROUP BY GROUPING SETS ( - (date), - (date, model), - (date, {_MODEL_GROUP_EXPR}), - (date, custom_llm_provider), - (date, mcp_namespaced_tool_name), - (date, endpoint), - () - )) - UNION ALL - (WITH top_api_keys AS ( - SELECT api_key, COUNT(*) OVER () AS distinct_api_keys - FROM "{pg_table}" - WHERE {where_clause} AND api_key <> {sentinel_param} - GROUP BY api_key - ORDER BY SUM(spend) DESC, api_key - LIMIT {USAGE_TOP_API_KEYS_LIMIT} - ) SELECT date, api_key, model, - {_MODEL_GROUP_EXPR} AS model_group, + COALESCE(NULLIF(model_group, ''), model) AS model_group, custom_llm_provider, mcp_namespaced_tool_name, endpoint, - GROUPING(date, api_key, model, {_MODEL_GROUP_EXPR}, + GROUPING(date, api_key, model, COALESCE(NULLIF(model_group, ''), model), custom_llm_provider, mcp_namespaced_tool_name, endpoint) AS group_level, - MAX(top_api_keys.distinct_api_keys) AS distinct_api_keys,{metric_select} - FROM "{pg_table}" JOIN top_api_keys USING (api_key) + SUM(spend)::float AS spend, + {_ptu_flat_cost_select(table_name)}, + SUM(prompt_tokens)::bigint AS prompt_tokens, + SUM(completion_tokens)::bigint AS completion_tokens, + SUM(cache_read_input_tokens)::bigint AS cache_read_input_tokens, + SUM(cache_creation_input_tokens)::bigint AS cache_creation_input_tokens, + SUM(compression_saved_tokens)::bigint AS compression_saved_tokens, + SUM(compression_savings_spend)::float AS compression_savings_spend, + SUM(prompt_caching_savings_spend)::float AS prompt_caching_savings_spend, + SUM(gateway_injected_caching_savings_spend)::float AS gateway_injected_caching_savings_spend, + SUM(autorouter_savings_spend)::float AS autorouter_savings_spend, + SUM(api_requests)::bigint AS api_requests, + SUM(successful_requests)::bigint AS successful_requests, + SUM(failed_requests)::bigint AS failed_requests, + SUM(total_response_time_ms)::bigint AS total_response_time_ms, + SUM(timed_requests)::bigint AS timed_requests + FROM "{pg_table}" WHERE {where_clause} GROUP BY GROUPING SETS ( + (date), (date, api_key), + (date, model), (date, model, api_key), - (date, {_MODEL_GROUP_EXPR}, api_key), + (date, COALESCE(NULLIF(model_group, ''), model)), + (date, COALESCE(NULLIF(model_group, ''), model), api_key), + (date, custom_llm_provider), (date, custom_llm_provider, api_key), + (date, mcp_namespaced_tool_name), (date, mcp_namespaced_tool_name, api_key), - (date, endpoint, api_key) - )) + (date, endpoint), + (date, endpoint, api_key), + () + ) """ - return sql_query, [*where_params, PTU_SENTINEL_API_KEY] + return sql_query, sql_params def _build_entity_rollup_sql_query( @@ -901,7 +850,23 @@ def _build_entity_rollup_sql_query( "{entity_id_field}" AS entity_id, date, api_key, - GROUPING(api_key) AS api_key_rolled,{_rollup_metric_select(table_name)} + GROUPING(api_key) AS api_key_rolled, + SUM(spend)::float AS spend, + {_ptu_flat_cost_select(table_name)}, + SUM(prompt_tokens)::bigint AS prompt_tokens, + SUM(completion_tokens)::bigint AS completion_tokens, + SUM(cache_read_input_tokens)::bigint AS cache_read_input_tokens, + SUM(cache_creation_input_tokens)::bigint AS cache_creation_input_tokens, + SUM(compression_saved_tokens)::bigint AS compression_saved_tokens, + SUM(compression_savings_spend)::float AS compression_savings_spend, + SUM(prompt_caching_savings_spend)::float AS prompt_caching_savings_spend, + SUM(gateway_injected_caching_savings_spend)::float AS gateway_injected_caching_savings_spend, + SUM(autorouter_savings_spend)::float AS autorouter_savings_spend, + SUM(api_requests)::bigint AS api_requests, + SUM(successful_requests)::bigint AS successful_requests, + SUM(failed_requests)::bigint AS failed_requests, + SUM(total_response_time_ms)::bigint AS total_response_time_ms, + SUM(timed_requests)::bigint AS timed_requests FROM "{pg_table}" WHERE {where_clause} GROUP BY GROUPING SETS ( @@ -1003,7 +968,6 @@ async def _aggregate_spend_records( # current grouping set's key), 0 when the column is part of the key. _GROUP_GRAND_TOTAL: Final = 127 # 0b1111111 — all rolled up _GROUP_DATE: Final = 63 # 0b0111111 — only date kept -_API_KEY_ROLLED_UP_BIT: Final = 32 # 0b0100000 _GROUP_DATE_API_KEY: Final = 31 # 0b0011111 _GROUP_DATE_MODEL: Final = 47 # 0b0101111 _GROUP_DATE_MODEL_API_KEY: Final = 15 # 0b0001111 @@ -1017,7 +981,7 @@ async def _aggregate_spend_records( _GROUP_DATE_ENDPOINT_API_KEY: Final = 30 # 0b0011110 -def _record_to_spend_metrics(record: _RollupMetricsRow) -> SpendMetrics: +def _record_to_spend_metrics(record: _GroupingSetsRow) -> SpendMetrics: """Build a SpendMetrics directly from one already-aggregated rollup row. SUM() over zero rows is SQL NULL, so rollup rows (notably the grand-total @@ -1373,6 +1337,10 @@ async def get_daily_activity_aggregated( ) -> SpendAnalyticsPaginatedResponse: """Aggregated variant that returns the full result set (no pagination). + Uses SQL GROUP BY to aggregate rows in the database rather than fetching + all individual rows into Python. This collapses rows across entities + (users/teams/orgs), reducing ~150k rows to ~2-3k grouped rows. + include_entity_breakdown runs a small companion rollup query and folds `breakdown.entities` onto the response, as entity-scoped views like Team Usage need. @@ -1391,7 +1359,7 @@ async def get_daily_activity_aggregated( ) try: - query_kwargs: Final = _AggregatedQueryKwargs( + sql_query, sql_params = _build_aggregated_sql_query( table_name=table_name, entity_id_field=entity_id_field, entity_id=entity_id, @@ -1403,16 +1371,36 @@ async def get_daily_activity_aggregated( timezone_offset_minutes=timezone_offset_minutes, include_current_utc_day=include_current_utc_day, ) - sql_query, sql_params = _build_aggregated_sql_query(**query_kwargs) - entity_query: Final = _build_entity_rollup_sql_query(**query_kwargs) if include_entity_breakdown else None - raw_rows, raw_entity_rows = await asyncio.gather( - prisma_client.db.query_raw(sql_query, *sql_params), - _query_raw_optional(prisma_client, entity_query), + entity_query: Final = ( + _build_entity_rollup_sql_query( + table_name=table_name, + entity_id_field=entity_id_field, + entity_id=entity_id, + start_date=start_date, + end_date=end_date, + model=model, + api_key=api_key, + exclude_entity_ids=exclude_entity_ids, + timezone_offset_minutes=timezone_offset_minutes, + include_current_utc_day=include_current_utc_day, + ) + if include_entity_breakdown + else None + ) + + # Execute the GROUPING SETS query (one row per rollup level), alongside + # the per-entity companion rollup when the caller wants entities. + raw_rows, raw_entity_rows = ( + await asyncio.gather( + prisma_client.db.query_raw(sql_query, *sql_params), + prisma_client.db.query_raw(entity_query[0], *entity_query[1]), + ) + if entity_query is not None + else (await prisma_client.db.query_raw(sql_query, *sql_params), None) ) - records: Final = [_GroupingSetsRow(**row) for row in (raw_rows or ())] - total_api_keys: Final = next((r.distinct_api_keys for r in records if r.distinct_api_keys is not None), 0) + records: Final = [_GroupingSetsRow(**row) for row in (raw_rows or [])] # The grouping-sets dispatcher places each row directly in its bucket # using the row's GROUPING() bitmask. No Python-side summing needed. @@ -1466,8 +1454,6 @@ async def get_daily_activity_aggregated( page=1, total_pages=1, has_more=False, - api_key_limit=USAGE_TOP_API_KEYS_LIMIT, - total_api_keys=total_api_keys, ), ) diff --git a/litellm/types/proxy/management_endpoints/common_daily_activity.py b/litellm/types/proxy/management_endpoints/common_daily_activity.py index 2a4f6b2944a6..28488ba7de6b 100644 --- a/litellm/types/proxy/management_endpoints/common_daily_activity.py +++ b/litellm/types/proxy/management_endpoints/common_daily_activity.py @@ -101,16 +101,6 @@ class DailySpendMetadata(BaseModel): page: int = Field(default=1) total_pages: int = Field(default=1) has_more: bool = Field(default=False) - api_key_limit: int | None = Field( - default=None, - description="When set, api_keys and every api_key_breakdown list at most this many keys, " - "ranked by spend. Totals and the model, provider, mcp and endpoint rollups still cover every key.", - ) - total_api_keys: int | None = Field( - default=None, - description="Distinct API keys matching the filters. When this exceeds api_key_limit, the per-key " - "lists are truncated to the highest-spend keys.", - ) class SpendAnalyticsPaginatedResponse(BaseModel): diff --git a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py index 32a8b7392855..32856a3bee9f 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py +++ b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py @@ -10,7 +10,7 @@ from psycopg.rows import dict_row from pytest_postgresql import factories -from litellm.constants import PTU_SENTINEL_API_KEY, USAGE_TOP_API_KEYS_LIMIT +from litellm.constants import PTU_SENTINEL_API_KEY from litellm.proxy.management_endpoints.common_daily_activity import ( _adjust_dates_for_timezone, _build_aggregated_sql_query, @@ -173,7 +173,6 @@ async def test_get_daily_activity_aggregated_with_endpoint_breakdown(): "endpoint": "/v1/chat/completions", "api_key": None, "group_level": 62, - "distinct_api_keys": None, "spend": 15.0, "prompt_tokens": 150, "completion_tokens": 75, @@ -186,7 +185,31 @@ async def test_get_daily_activity_aggregated_with_endpoint_breakdown(): "endpoint": "/v1/embeddings", "api_key": None, "group_level": 62, - "distinct_api_keys": None, + "spend": 3.0, + "prompt_tokens": 30, + "completion_tokens": 0, + "api_requests": 1, + "successful_requests": 1, + }, + # (date, endpoint, api_key) — populates the per-key sub-bucket + { + **base, + "date": "2024-01-01", + "endpoint": "/v1/chat/completions", + "api_key": "key-1", + "group_level": 30, + "spend": 15.0, + "prompt_tokens": 150, + "completion_tokens": 75, + "api_requests": 2, + "successful_requests": 2, + }, + { + **base, + "date": "2024-01-01", + "endpoint": "/v1/embeddings", + "api_key": "key-2", + "group_level": 30, "spend": 3.0, "prompt_tokens": 30, "completion_tokens": 0, @@ -200,7 +223,6 @@ async def test_get_daily_activity_aggregated_with_endpoint_breakdown(): "endpoint": None, "api_key": None, "group_level": 63, - "distinct_api_keys": None, "spend": 18.0, "prompt_tokens": 180, "completion_tokens": 75, @@ -214,40 +236,12 @@ async def test_get_daily_activity_aggregated_with_endpoint_breakdown(): "endpoint": None, "api_key": None, "group_level": 127, - "distinct_api_keys": None, "spend": 18.0, "prompt_tokens": 180, "completion_tokens": 75, "api_requests": 3, "successful_requests": 3, }, - # (date, endpoint, api_key) — populates the per-key sub-bucket - { - **base, - "date": "2024-01-01", - "endpoint": "/v1/chat/completions", - "api_key": "key-1", - "group_level": 30, - "distinct_api_keys": 2, - "spend": 15.0, - "prompt_tokens": 150, - "completion_tokens": 75, - "api_requests": 2, - "successful_requests": 2, - }, - { - **base, - "date": "2024-01-01", - "endpoint": "/v1/embeddings", - "api_key": "key-2", - "group_level": 30, - "distinct_api_keys": 2, - "spend": 3.0, - "prompt_tokens": 30, - "completion_tokens": 0, - "api_requests": 1, - "successful_requests": 1, - }, ] mock_prisma.db.query_raw = AsyncMock(return_value=mock_rows) @@ -861,7 +855,6 @@ async def test_aggregated_activity_preserves_metadata_for_deleted_keys(): "endpoint": "/v1/chat/completions", "api_key": None, "group_level": 62, - "distinct_api_keys": None, "spend": 10.0, "prompt_tokens": 100, "completion_tokens": 50, @@ -874,7 +867,6 @@ async def test_aggregated_activity_preserves_metadata_for_deleted_keys(): "endpoint": "/v1/chat/completions", "api_key": "deleted-key-hash", "group_level": 30, - "distinct_api_keys": 1, "spend": 10.0, "prompt_tokens": 100, "completion_tokens": 50, @@ -1319,7 +1311,6 @@ def test_optional_filters_appear_in_params_in_order(self): "user-1", "bedrock/global.anthropic.claude-opus-4-8", "sk-test", - PTU_SENTINEL_API_KEY, ] assert "model = $4" in sql assert "api_key = $5" in sql @@ -1343,8 +1334,7 @@ def test_empty_entity_list_emits_no_degenerate_in_clause(self, build): normalized = " ".join(sql.split()) assert "IN ()" not in normalized assert '"team_id" IN' not in normalized - sentinel_params = [PTU_SENTINEL_API_KEY] if build is _build_aggregated_sql_query else [] - assert params == ["2026-08-01", "2026-08-19", *sentinel_params] + assert params == ["2026-08-01", "2026-08-19"] @pytest.mark.parametrize("build", _BUILDERS) def test_empty_entity_list_matches_nothing_rather_than_everything(self, build): @@ -1375,8 +1365,7 @@ def test_populated_entity_list_still_filters_on_its_ids(self, build): normalized = " ".join(sql.split()) assert '"team_id" IN ($3, $4)' in normalized assert "FALSE" not in normalized - sentinel_params = [PTU_SENTINEL_API_KEY] if build is _build_aggregated_sql_query else [] - assert params == ["2026-08-01", "2026-08-19", "team-alpha", "team-beta", *sentinel_params] + assert params == ["2026-08-01", "2026-08-19", "team-alpha", "team-beta"] @pytest.mark.asyncio @@ -1401,7 +1390,6 @@ async def test_get_daily_activity_aggregated_empty_result_set(): "mcp_namespaced_tool_name": None, "endpoint": None, "group_level": 127, - "distinct_api_keys": None, "spend": None, "prompt_tokens": None, "completion_tokens": None, @@ -1512,18 +1500,10 @@ async def query_raw(sql: str, *params: str) -> list[dict[str, object]]: @pytest.mark.asyncio -async def test_get_daily_activity_aggregated_bounds_api_key_rollups( +async def test_get_daily_activity_aggregated_returns_every_api_key( _aggregated_postgresql: psycopg.Connection, ): - """Run the GROUPING SETS statement against real Postgres with more keys than the cap. - - key-004 and key-005 tie on spend exactly at the USAGE_TOP_API_KEYS_LIMIT - cutoff; the api_key tiebreaker must keep key-004 and drop key-005. The PTU - sentinel outspends every key but must not take a slot. Excluded keys and the - sentinel still count toward the totals and the model rollup, which come from - the key-free arm. - """ - n_keys: Final = USAGE_TOP_API_KEYS_LIMIT + 5 + n_keys: Final = 105 key_rows: Final = [ ( f"row-{i:03d}", @@ -1557,11 +1537,11 @@ async def test_get_daily_activity_aggregated_bounds_api_key_rollups( ) _seed_daily_user_spend(_aggregated_postgresql, [*key_rows, sentinel_row]) key_spend: Final = sum(6.0 if i == 4 else float(i + 1) for i in range(n_keys)) + expected_api_keys: Final = {f"key-{i:03d}" for i in range(n_keys)} - row_counts: Final[list[int]] = [] # mutable-ok: out-param for the query_raw shim mock_prisma = MagicMock() mock_prisma.db = MagicMock() - mock_prisma.db.query_raw = _psycopg_query_raw(_aggregated_postgresql, row_counts) + mock_prisma.db.query_raw = _psycopg_query_raw(_aggregated_postgresql, []) mock_prisma.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[]) mock_prisma.db.litellm_deletedverificationtoken.find_many = AsyncMock(return_value=[]) @@ -1577,37 +1557,24 @@ async def test_get_daily_activity_aggregated_bounds_api_key_rollups( api_key=None, ) - # Key-free arm: (), (date), (date, model), (date, model_group), two providers, - # one mcp NULL bucket, endpoint plus its NULL bucket = 9 rows regardless of key count. - # Per-key arm: six per-key grouping sets, each capped at the limit. - assert row_counts == [9 + 6 * USAGE_TOP_API_KEYS_LIMIT] - assert result.metadata.total_spend == pytest.approx(key_spend + 1000.0) - assert result.metadata.total_api_requests == n_keys - assert result.metadata.api_key_limit == USAGE_TOP_API_KEYS_LIMIT - assert result.metadata.total_api_keys == n_keys - - expected_top: Final = {f"key-{i:03d}" for i in range(6, n_keys)} | {"key-004"} + assert result.metadata.total_api_requests == 105 day: Final = result.results[0] assert day.metrics.spend == pytest.approx(key_spend + 1000.0) - assert set(day.breakdown.api_keys) == expected_top - assert day.breakdown.api_keys["key-004"].metrics.spend == 6.0 - assert "key-005" not in day.breakdown.api_keys + assert set(day.breakdown.api_keys) == expected_api_keys assert PTU_SENTINEL_API_KEY not in day.breakdown.api_keys - assert day.breakdown.models["gpt-5"].metrics.spend == pytest.approx(key_spend + 1000.0) - assert set(day.breakdown.models["gpt-5"].api_key_breakdown) == expected_top + assert set(day.breakdown.models["gpt-5"].api_key_breakdown) == expected_api_keys assert day.breakdown.providers["openai"].metrics.spend == pytest.approx(key_spend) - assert set(day.breakdown.providers["openai"].api_key_breakdown) == expected_top - assert day.breakdown.endpoints["/v1/chat/completions"].metrics.api_requests == n_keys + assert set(day.breakdown.providers["openai"].api_key_breakdown) == expected_api_keys + assert day.breakdown.endpoints["/v1/chat/completions"].metrics.api_requests == 105 @pytest.mark.asyncio -async def test_get_daily_activity_aggregated_explicit_api_key_filter_scopes_both_arms( +async def test_get_daily_activity_aggregated_explicit_api_key_filter_scopes_results( _aggregated_postgresql: psycopg.Connection, ): - """An explicit api_key filter must scope the key-free totals and the per-key - rollups to that key alone, so the two arms never disagree.""" + """An explicit api_key filter must scope the results to that key alone.""" rows: Final = [ ( f"row-{i}", @@ -1627,10 +1594,9 @@ async def test_get_daily_activity_aggregated_explicit_api_key_filter_scopes_both ] _seed_daily_user_spend(_aggregated_postgresql, rows) - row_counts: Final[list[int]] = [] # mutable-ok: out-param for the query_raw shim mock_prisma = MagicMock() mock_prisma.db = MagicMock() - mock_prisma.db.query_raw = _psycopg_query_raw(_aggregated_postgresql, row_counts) + mock_prisma.db.query_raw = _psycopg_query_raw(_aggregated_postgresql, []) mock_prisma.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[]) mock_prisma.db.litellm_deletedverificationtoken.find_many = AsyncMock(return_value=[]) @@ -1647,7 +1613,6 @@ async def test_get_daily_activity_aggregated_explicit_api_key_filter_scopes_both ) assert result.metadata.total_spend == 2.0 - assert result.metadata.total_api_keys == 1 day: Final = result.results[0] assert set(day.breakdown.api_keys) == {"key-1"} assert day.breakdown.api_keys["key-1"].metrics.spend == 2.0 @@ -1655,55 +1620,6 @@ async def test_get_daily_activity_aggregated_explicit_api_key_filter_scopes_both assert set(day.breakdown.models["gpt-5"].api_key_breakdown) == {"key-1"} -@pytest.mark.asyncio -async def test_get_daily_activity_aggregated_reports_exact_limit_key_count_as_complete( - _aggregated_postgresql: psycopg.Connection, -): - """With exactly USAGE_TOP_API_KEYS_LIMIT keys nothing is dropped, and the - response must say so: total_api_keys equals the limit rather than exceeding it.""" - rows: Final = [ - ( - f"row-{i:03d}", - f"user-{i:03d}", - "2026-06-01", - f"key-{i:03d}", - "gpt-5", - "", - "openai", - "/v1/chat/completions", - 10, - float(i + 1), - 1, - 1, - ) - for i in range(USAGE_TOP_API_KEYS_LIMIT) - ] - _seed_daily_user_spend(_aggregated_postgresql, rows) - - row_counts: Final[list[int]] = [] # mutable-ok: out-param for the query_raw shim - mock_prisma = MagicMock() - mock_prisma.db = MagicMock() - mock_prisma.db.query_raw = _psycopg_query_raw(_aggregated_postgresql, row_counts) - mock_prisma.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[]) - mock_prisma.db.litellm_deletedverificationtoken.find_many = AsyncMock(return_value=[]) - - result = await get_daily_activity_aggregated( - prisma_client=mock_prisma, - table_name="litellm_dailyuserspend", - entity_id_field="user_id", - entity_id=None, - entity_metadata_field=None, - start_date="2026-06-01", - end_date="2026-06-01", - model=None, - api_key=None, - ) - - assert result.metadata.total_api_keys == USAGE_TOP_API_KEYS_LIMIT - assert result.metadata.api_key_limit == USAGE_TOP_API_KEYS_LIMIT - assert len(result.results[0].breakdown.api_keys) == USAGE_TOP_API_KEYS_LIMIT - - @pytest.mark.asyncio async def test_get_daily_activity_aggregated_model_group_rollups_fall_back_to_model_name( _aggregated_postgresql: psycopg.Connection, @@ -2543,7 +2459,7 @@ def test_entity_rollup_sql_query_and_api_key_list_filter(): api_key=[], ) assert "FALSE" in empty_sql - assert empty_params == ["2024-01-01", "2024-01-31", PTU_SENTINEL_API_KEY] + assert empty_params == ["2024-01-01", "2024-01-31"] @pytest.mark.asyncio @@ -2577,10 +2493,10 @@ async def test_get_daily_activity_aggregated_with_entity_breakdown(): "successful_requests": 0, } main_rows = [ - {**base, "date": None, "group_level": 127, "distinct_api_keys": None, "spend": 18.0}, - {**base, "date": "2024-01-01", "group_level": 63, "distinct_api_keys": None, "spend": 18.0}, - {**base, "date": "2024-01-01", "model": "gpt-4o", "group_level": 47, "distinct_api_keys": None, "spend": 18.0}, - {**base, "date": "2024-01-01", "api_key": "key-1", "group_level": 31, "distinct_api_keys": 1, "spend": 12.0}, + {**base, "date": None, "group_level": 127, "spend": 18.0}, + {**base, "date": "2024-01-01", "group_level": 63, "spend": 18.0}, + {**base, "date": "2024-01-01", "model": "gpt-4o", "group_level": 47, "spend": 18.0}, + {**base, "date": "2024-01-01", "api_key": "key-1", "group_level": 31, "spend": 12.0}, ] entity_base = { key: value diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.test.tsx index b06986d01f07..8d328c4c329d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.test.tsx @@ -178,28 +178,4 @@ describe("CacheLeakageCard", () => { screen.queryByText("Data is still loading; rows and totals will update as the rest of the range arrives."), ).not.toBeInTheDocument(); }); - - it("says which keys are missing from the key ranking when the proxy capped the per-key lists", () => { - const day = dayWithKeys("2026-07-12", { - "hash-leaky": key("leaky-key", { prompt_tokens: 10000, cache_read_input_tokens: 0 }), - }); - renderWith([day], { apiKeyTruncation: { limit: 100, total: 3000 } }); - - expect(screen.getByRole("note")).toHaveTextContent( - "Only the 100 highest-spend keys of 3,000 are loaded, so a lower-spend key that leaks more is not listed here.", - ); - - fireEvent.click(screen.getByRole("tab", { name: "By model" })); - - expect(screen.queryByRole("note")).not.toBeInTheDocument(); - }); - - it("keeps the key ranking note off when every key was loaded", () => { - const day = dayWithKeys("2026-07-12", { - "hash-leaky": key("leaky-key", { prompt_tokens: 10000, cache_read_input_tokens: 0 }), - }); - renderWith([day]); - - expect(screen.queryByRole("note")).not.toBeInTheDocument(); - }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.tsx index f5b71a00061c..cfac77788b7a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.tsx @@ -80,7 +80,7 @@ const SortableHead = ({ }; const CacheLeakageCard: React.FC = ({ activity }) => { - const { results, loading, isFetchingMore, apiKeyTruncation } = activity; + const { results, loading, isFetchingMore } = activity; const [dimension, setDimension] = useState("key"); const [sort, setSort] = useState({ column: "potentialSavings", dir: "desc" }); const leakage = useMemo(() => computeCacheLeakage(results, dimension), [results, dimension]); @@ -119,13 +119,6 @@ const CacheLeakageCard: React.FC = ({ activity }) => { - {dimension === "key" && apiKeyTruncation !== undefined && ( -

- Only the {apiKeyTruncation.limit.toLocaleString()} highest-spend keys of{" "} - {apiKeyTruncation.total.toLocaleString()} are loaded, so a lower-spend key that leaks more is not listed - here. Raise USAGE_TOP_API_KEYS_LIMIT on the proxy to load more keys. -

- )} {rows.length > 0 && isFetchingMore && (

Data is still loading; rows and totals will update as the rest of the range arrives. diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx index e501cf00b905..b94fa45ecb70 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx @@ -4,13 +4,12 @@ import { describe, expect, it, vi } from "vitest"; const mockUsePaginatedDailyActivity = vi.fn(); const mockCancel = vi.fn(); -let mockMetadata: Record = {}; vi.mock("@/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity", () => ({ usePaginatedDailyActivity: (args: unknown) => { mockUsePaginatedDailyActivity(args); return { - data: { results: [], metadata: mockMetadata }, + data: { results: [] }, loading: false, isFetchingMore: false, progress: { currentPage: 4, totalPages: 9 }, @@ -83,18 +82,4 @@ describe("useDailyActivityRange", () => { expect(mockUsePaginatedDailyActivity).toHaveBeenLastCalledWith(expect.objectContaining({ enabled: false })); }); - - it("reports how many keys the proxy left out of the per-key lists", () => { - mockMetadata = { api_key_limit: 100, total_api_keys: 3000 }; - const { result } = renderHook(() => useDailyActivityRange("test-token", "u1", "proxy_admin")); - - expect(result.current.apiKeyTruncation).toEqual({ limit: 100, total: 3000 }); - }); - - it("reports no key truncation when every key fit under the proxy limit", () => { - mockMetadata = { api_key_limit: 100, total_api_keys: 100 }; - const { result } = renderHook(() => useDailyActivityRange("test-token", "u1", "proxy_admin")); - - expect(result.current.apiKeyTruncation).toBeUndefined(); - }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts index 4eb9f257d30b..605926132e95 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts @@ -1,7 +1,6 @@ import { useMemo, useState } from "react"; import { userDailyActivityAggregatedCall, userDailyActivityCall } from "@/components/networking"; -import { ApiKeyTruncation, getApiKeyTruncation } from "@/components/EntityUsageExport/exportBlockedReason"; import { DailyData } from "@/components/UsagePage/types"; import { spendScopeUserId } from "@/utils/roles"; import { usePaginatedDailyActivity } from "@/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity"; @@ -23,7 +22,6 @@ export interface DailyActivityRange { cancelled: boolean; failed: boolean; cancel: () => void; - apiKeyTruncation?: ApiKeyTruncation; } /** @@ -82,7 +80,6 @@ export const useScopedDailyActivityRange = ( cancelled, failed, cancel, - apiKeyTruncation: getApiKeyTruncation(data.metadata?.api_key_limit, data.metadata?.total_api_keys), }; }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx index 6bd16095351a..8f5bb9455d0d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx @@ -650,23 +650,6 @@ describe("EntityUsage", () => { expect(screen.getAllByText("Activity Metrics")[1]).toBeInTheDocument(); }); - it("tells the team view how many keys the proxy left out of the per-key lists", async () => { - mockTeamDailyActivityAggregatedCall.mockResolvedValue({ - ...mockSpendData, - metadata: { ...mockSpendData.metadata, api_key_limit: 100, total_api_keys: 3000 }, - }); - render(); - - await waitFor(() => { - expect(mockTeamDailyActivityAggregatedCall).toHaveBeenCalled(); - }); - act(() => { - fireEvent.click(screen.getByText("Key Activity")); - }); - - expect(await screen.findByRole("note")).toHaveTextContent("Only the 100 highest-spend keys of 3,000 are loaded"); - }); - // An inactive tab panel is marked aria-selected="false" by one tab library and hidden by the // other, so treat either as "not on screen" and the assertion holds whichever one is rendering. const isShowing = (element: HTMLElement): boolean => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx index 27460b211086..6687bd4df038 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx @@ -25,7 +25,7 @@ import TeamMultiSelect from "@/components/common_components/team_multi_select"; import UserDropdown from "@/components/common_components/UserDropdown"; import { ActivityMetrics, processActivityData } from "@/components/activity_metrics"; import { UsageExportHeader } from "@/components/EntityUsageExport"; -import { getApiKeyTruncation, getExportBlockedReason } from "@/components/EntityUsageExport/exportBlockedReason"; +import { getExportBlockedReason } from "@/components/EntityUsageExport/exportBlockedReason"; import type { EntityType } from "@/components/EntityUsageExport/types"; import { agentDailyActivityCall, @@ -71,8 +71,6 @@ interface EntitySpendData { total_successful_requests: number; total_failed_requests: number; total_tokens: number; - api_key_limit?: number | null; - total_api_keys?: number | null; }; } @@ -162,7 +160,6 @@ const EntityUsage: React.FC = ({ }); const spendData = spendDataRaw as unknown as EntitySpendData; - const apiKeyTruncation = getApiKeyTruncation(spendData.metadata?.api_key_limit, spendData.metadata?.total_api_keys); const { data: agentSpendDataRaw, @@ -662,18 +659,12 @@ const EntityUsage: React.FC = ({ { key: "keys", label: "Key Activity", - content: ( - - ), + content: , }, { key: "endpoints", label: "Endpoint Activity", content: }, ]; - const spendFetchState = { coversRange, cancelled, failed, apiKeyTruncation }; + const spendFetchState = { coversRange, cancelled, failed }; return (

diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx index 228d8acf1460..018dfc847408 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx @@ -30,7 +30,7 @@ import { ActivityMetrics, processActivityData } from "@/components/activity_metr import CloudZeroExportModal from "@/components/cloudzero_export_modal"; import UserDropdown from "@/components/common_components/UserDropdown"; import EntityUsageExportModal from "@/components/EntityUsageExport"; -import { getApiKeyTruncation, getExportBlockedReason } from "@/components/EntityUsageExport/exportBlockedReason"; +import { getExportBlockedReason } from "@/components/EntityUsageExport/exportBlockedReason"; import KeyActivityPanel from "@/components/UsagePage/components/KeyActivityPanel"; import { Team } from "@/components/key_team_helpers/key_list"; import { @@ -256,10 +256,6 @@ const UsagePage: React.FC = ({ teams, organizations }) => { coversRange: activeAggregated !== null || paginatedResult.coversRange, cancelled: paginatedResult.cancelled, failed: paginatedResult.failed, - apiKeyTruncation: getApiKeyTruncation( - userSpendData.metadata?.api_key_limit, - userSpendData.metadata?.total_api_keys, - ), }; const exportBlockedReason = getExportBlockedReason(spendFetchState); @@ -865,7 +861,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { - + diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/exportBlockedReason.test.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/exportBlockedReason.test.ts index 8491b31f5f98..e39b01a5dea0 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/exportBlockedReason.test.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/exportBlockedReason.test.ts @@ -1,12 +1,11 @@ import { describe, expect, it } from "vitest"; -import { getApiKeyTruncation, getExportBlockedReason, type UsageFetchState } from "./exportBlockedReason"; +import { getExportBlockedReason, type UsageFetchState } from "./exportBlockedReason"; const state = (overrides: Partial = {}): UsageFetchState => ({ coversRange: true, cancelled: false, failed: false, - apiKeyTruncation: undefined, ...overrides, }); @@ -32,27 +31,4 @@ describe("getExportBlockedReason", () => { expect(reason).toMatch(/failed to load/i); expect(reason).not.toMatch(/stopped/i); }); - - it("blocks when the aggregated endpoint dropped keys, since a per-team CSV would miss them", () => { - const reason = getExportBlockedReason(state({ apiKeyTruncation: { limit: 100, total: 3000 } })); - - expect(reason).toMatch(/100 highest-spend keys of 3000/); - expect(reason).toMatch(/USAGE_TOP_API_KEYS_LIMIT/); - }); -}); - -describe("getApiKeyTruncation", () => { - it("reports truncation once the proxy saw more keys than it returned", () => { - expect(getApiKeyTruncation(100, 101)).toEqual({ limit: 100, total: 101 }); - }); - - it("stays quiet when exactly the cap exists, since every key is on screen", () => { - expect(getApiKeyTruncation(100, 100)).toBeUndefined(); - expect(getApiKeyTruncation(100, 7)).toBeUndefined(); - }); - - it("stays quiet when the response carries no cap, as the paginated fallback does", () => { - expect(getApiKeyTruncation(undefined, undefined)).toBeUndefined(); - expect(getApiKeyTruncation(100, null)).toBeUndefined(); - }); }); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/exportBlockedReason.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/exportBlockedReason.ts index 6c5a5f83231c..71408ba8f3ff 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/exportBlockedReason.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/exportBlockedReason.ts @@ -1,31 +1,13 @@ -export interface ApiKeyTruncation { - limit: number; - total: number; -} - export interface UsageFetchState { coversRange: boolean; cancelled: boolean; failed: boolean; - apiKeyTruncation: ApiKeyTruncation | undefined; } -export const getApiKeyTruncation = (apiKeyLimit: unknown, totalApiKeys: unknown): ApiKeyTruncation | undefined => { - if (typeof apiKeyLimit !== "number" || typeof totalApiKeys !== "number") return undefined; - return totalApiKeys > apiKeyLimit ? { limit: apiKeyLimit, total: totalApiKeys } : undefined; -}; - -export const getExportBlockedReason = ({ - coversRange, - cancelled, - failed, - apiKeyTruncation, -}: UsageFetchState): string | undefined => { +export const getExportBlockedReason = ({ coversRange, cancelled, failed }: UsageFetchState): string | undefined => { if (failed) return "Some spend data failed to load, so an export would under-report. Reload the page to try again."; if (cancelled) return "Loading was stopped before the whole range arrived, so an export would under-report. Reload the page to load it all."; if (!coversRange) return "Spend data is still loading, so an export would under-report. Wait for it to finish."; - if (apiKeyTruncation !== undefined) - return `Only the ${apiKeyTruncation.limit} highest-spend keys of ${apiKeyTruncation.total} were loaded, so a per-team export would under-report. Raise USAGE_TOP_API_KEYS_LIMIT on the proxy to load more keys.`; return undefined; }; diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/KeyActivityPanel.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/KeyActivityPanel.test.tsx index 830139143e94..693ac20a3601 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/KeyActivityPanel.test.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/KeyActivityPanel.test.tsx @@ -68,14 +68,4 @@ describe("KeyActivityPanel", () => { expect(screen.getByLabelText("Search keys")).toHaveValue(""); expect(screen.getByTestId("rendered-keys")).toHaveTextContent("hash-alicehash-bob"); }); - - it("says how many keys the proxy left out when only the top spenders were loaded", () => { - render(); - expect(screen.getByRole("note")).toHaveTextContent("Only the 2 highest-spend keys of 3,000 are loaded"); - }); - - it("shows no truncation note when every key is loaded", () => { - render(); - expect(screen.queryByRole("note")).not.toBeInTheDocument(); - }); }); diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/KeyActivityPanel.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/KeyActivityPanel.tsx index 8b2141f85285..8287a04d0c78 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/KeyActivityPanel.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/KeyActivityPanel.tsx @@ -2,7 +2,6 @@ import { Search, X } from "lucide-react"; import React, { useMemo, useState } from "react"; import { ActivityMetrics } from "@/components/activity_metrics"; -import type { ApiKeyTruncation } from "@/components/EntityUsageExport/exportBlockedReason"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group"; import { filterKeyActivity } from "../keyActivityFilter"; @@ -11,14 +10,9 @@ import type { ModelActivityData } from "../types"; interface KeyActivityPanelProps { keyMetrics: Record; hidePromptCachingMetrics?: boolean; - apiKeyTruncation?: ApiKeyTruncation; } -const KeyActivityPanel: React.FC = ({ - keyMetrics, - hidePromptCachingMetrics = false, - apiKeyTruncation, -}) => { +const KeyActivityPanel: React.FC = ({ keyMetrics, hidePromptCachingMetrics = false }) => { const [query, setQuery] = useState(""); const filtered = useMemo(() => filterKeyActivity(keyMetrics, query), [keyMetrics, query]); const totalKeys = Object.keys(keyMetrics).length; @@ -49,12 +43,6 @@ const KeyActivityPanel: React.FC = ({ Showing {shownKeys.toLocaleString()} of {totalKeys.toLocaleString()} keys - {apiKeyTruncation !== undefined && ( - - Only the {apiKeyTruncation.limit.toLocaleString()} highest-spend keys of{" "} - {apiKeyTruncation.total.toLocaleString()} are loaded - - )}
{isFiltering && totalKeys > 0 && shownKeys === 0 ? (

diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 717dcd957e85..4e64a98dd658 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -29438,11 +29438,6 @@ export interface components { }; /** DailySpendMetadata */ DailySpendMetadata: { - /** - * Api Key Limit - * @description When set, api_keys and every api_key_breakdown list at most this many keys, ranked by spend. Totals and the model, provider, mcp and endpoint rollups still cover every key. - */ - api_key_limit?: number | null; /** * Has More * @default false @@ -29453,11 +29448,6 @@ export interface components { * @default 1 */ page: number; - /** - * Total Api Keys - * @description Distinct API keys matching the filters. When this exceeds api_key_limit, the per-key lists are truncated to the highest-spend keys. - */ - total_api_keys?: number | null; /** * Total Api Requests * @default 0