This commit is contained in:
Timofei Efimov 2026-11-04 00:26:36 +00:00 • committed by GitHub
commit fa23a8fe3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 243 additions and 9 deletions

View file

@ -1113,6 +1113,15 @@ async def _guardrail_spend_or_empty(
return ()
async def _user_entity_metadata(
repository: DailyActivityRepository, entity_rows: Sequence[EntityRollupRow]
) -> Mapping[str, dict[str, object]]: # mutable-ok: entity_metadata_field shape
users: Final = await repository.user_metadata(frozenset(row.entity_id for row in entity_rows if row.entity_id))
return MappingProxyType(
{user_id: {"user_email": user.user_email, "user_alias": user.user_alias} for user_id, user in users.items()}
)
async def get_daily_activity_aggregated(
repository: DailyActivityRepository,
scope: DailyActivityScope,
@ -1150,19 +1159,22 @@ async def get_daily_activity_aggregated(
entity_api_keys: Final = frozenset(
row.api_key for row in entity_records if row.api_key and row.api_key != PTU_SENTINEL_API_KEY
)
entity_key_metadata: Final[Mapping[str, KeyMetadataRow]] = (
await repository.key_metadata(
entity_key_metadata, rollup_entity_metadata = await asyncio.gather(
repository.key_metadata(
entity_api_keys, spend_logs_window(frozenset(row.date for row in entity_records))
)
if entity_api_keys
else MappingProxyType({})
else asyncio.sleep(0, result=MappingProxyType({})),
_user_entity_metadata(repository, entity_records)
if scope.table is DailyActivityTable.USER
else asyncio.sleep(0, result=MappingProxyType({})),
)
await asyncio.to_thread(
_fold_entity_rollups_sync,
results=aggregated["results"],
entity_rows=entity_records,
api_key_metadata=entity_key_metadata,
entity_metadata_field=entity_metadata_field,
entity_metadata_field=MappingProxyType({**rollup_entity_metadata, **(entity_metadata_field or {})}),
)
results: Final = (
list(await asyncio.to_thread(_with_guardrail_breakdown, aggregated["results"], guardrail_rows))

View file

@ -363,7 +363,7 @@ USER_RESOLVER = EntityScopeResolver(
tags=("Budget & Spend Tracking", "Internal User management"),
query=_user_query,
resolve=_resolve_user,
include_entity_breakdown=False,
include_entity_breakdown=True,
include_guardrail_breakdown=True,
operation_names=MappingProxyType(
{

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
{
"metadata": {"entity_total_api_keys":null,
"metadata": {"entity_total_api_keys":{"user-1": 5},
"api_key_limit": 100,
"has_more": false,
"page": 1,
@ -346,7 +346,181 @@
}
}
},
"entities": {},
"entities": {
"user-1": {
"api_key_breakdown": {
"key-a": {
"metadata": {
"key_alias": "alias-a",
"key_exists": true,
"team_id": "team-1",
"user_email": "user@example.com",
"user_id": "user-1"
},
"metrics": {
"api_requests": 1,
"autorouter_savings_spend": 0.0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 1,
"completion_tokens": 2,
"compression_saved_tokens": 0,
"compression_savings_spend": 0.0,
"failed_requests": 0,
"flat_cost": 0.0,
"gateway_injected_caching_savings_spend": 0.0,
"prompt_caching_savings_spend": 0.0,
"prompt_tokens": 2,
"ptu_hours": 0.0,
"spend": 100.0,
"successful_requests": 1,
"timed_requests": 0,
"total_response_time_ms": 0,
"total_tokens": 4
}
},
"key-b": {
"metadata": {
"key_alias": "alias-b",
"key_exists": true,
"team_id": "team-1",
"user_email": "user@example.com",
"user_id": "user-1"
},
"metrics": {
"api_requests": 1,
"autorouter_savings_spend": 0.0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 1,
"completion_tokens": 2,
"compression_saved_tokens": 0,
"compression_savings_spend": 0.0,
"failed_requests": 0,
"flat_cost": 0.0,
"gateway_injected_caching_savings_spend": 0.0,
"prompt_caching_savings_spend": 0.0,
"prompt_tokens": 3,
"ptu_hours": 0.0,
"spend": 90.0,
"successful_requests": 1,
"timed_requests": 0,
"total_response_time_ms": 0,
"total_tokens": 5
}
},
"key-c": {
"metadata": {
"key_alias": "alias-c",
"key_exists": true,
"team_id": "team-1",
"user_email": "user@example.com",
"user_id": "user-1"
},
"metrics": {
"api_requests": 1,
"autorouter_savings_spend": 0.0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 1,
"completion_tokens": 2,
"compression_saved_tokens": 0,
"compression_savings_spend": 0.0,
"failed_requests": 0,
"flat_cost": 0.0,
"gateway_injected_caching_savings_spend": 0.0,
"prompt_caching_savings_spend": 0.0,
"prompt_tokens": 4,
"ptu_hours": 0.0,
"spend": 80.0,
"successful_requests": 1,
"timed_requests": 0,
"total_response_time_ms": 0,
"total_tokens": 6
}
},
"key-cache": {
"metadata": {
"key_alias": "alias-cache",
"key_exists": true,
"team_id": "team-1",
"user_email": "user@example.com",
"user_id": "user-1"
},
"metrics": {
"api_requests": 1,
"autorouter_savings_spend": 0.0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 1,
"completion_tokens": 2,
"compression_saved_tokens": 0,
"compression_savings_spend": 0.0,
"failed_requests": 0,
"flat_cost": 0.0,
"gateway_injected_caching_savings_spend": 0.0,
"prompt_caching_savings_spend": 0.0,
"prompt_tokens": 1000,
"ptu_hours": 0.0,
"spend": 2.0,
"successful_requests": 1,
"timed_requests": 0,
"total_response_time_ms": 0,
"total_tokens": 1002
}
},
"key-target": {
"metadata": {
"key_alias": "deleted-target",
"key_exists": false,
"team_id": "team-1",
"user_email": "user@example.com",
"user_id": "user-1"
},
"metrics": {
"api_requests": 1,
"autorouter_savings_spend": 0.0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 2,
"completion_tokens": 2,
"compression_saved_tokens": 0,
"compression_savings_spend": 0.0,
"failed_requests": 0,
"flat_cost": 0.0,
"gateway_injected_caching_savings_spend": 0.0,
"prompt_caching_savings_spend": 0.0,
"prompt_tokens": 5,
"ptu_hours": 0.0,
"spend": 1.0,
"successful_requests": 1,
"timed_requests": 0,
"total_response_time_ms": 0,
"total_tokens": 7
}
}
},
"metadata": {
"user_alias": null,
"user_email": "user@example.com"
},
"metrics": {
"api_requests": 5,
"autorouter_savings_spend": 0.0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 6,
"completion_tokens": 10,
"compression_saved_tokens": 0,
"compression_savings_spend": 0.0,
"failed_requests": 0,
"flat_cost": 0.0,
"gateway_injected_caching_savings_spend": 0.0,
"prompt_caching_savings_spend": 0.0,
"prompt_tokens": 1014,
"ptu_hours": 0.0,
"spend": 1273.0,
"successful_requests": 5,
"timed_requests": 0,
"total_response_time_ms": 0,
"total_tokens": 1024
}
}
},
"mcp_servers": {},
"model_groups": {
"model-cache": {

View file

@ -23,7 +23,11 @@ from litellm.proxy.management_endpoints.daily_activity_routes import (
router,
)
from litellm.router import Router
from litellm.types.proxy.management_endpoints.common_daily_activity import KeySpendMetrics, SpendMetrics
from litellm.types.proxy.management_endpoints.common_daily_activity import (
KeySpendMetrics,
SpendAnalyticsPaginatedResponse,
SpendMetrics,
)
from litellm.types.repositories.daily_activity import (
AggregatedRows,
DailyActivityScope,
@ -910,6 +914,50 @@ def test_key_page_route_rejects_invalid_bounds(
repository.key_page.assert_not_awaited()
def _entity_spend_by_day(body: SpendAnalyticsPaginatedResponse) -> dict[str, dict[str, float]]:
return {
day.date.isoformat(): {entity_id: entity.metrics.spend for entity_id, entity in day.breakdown.entities.items()}
for day in body.results
}
def test_admin_user_aggregate_breaks_spend_down_per_user_with_email_and_alias(
daily_activity_client: tuple[TestClient, _FakeRepository],
) -> None:
client, _ = daily_activity_client
response: Final = client.get("/user/daily/activity/aggregated", params=_DATE_PARAMS)
assert response.status_code == 200, response.text
body: Final = SpendAnalyticsPaginatedResponse.model_validate_json(response.content)
spend_by_day: Final = _entity_spend_by_day(body)
assert spend_by_day == {
"2025-01-01": {"user-a": 12.5, "user-b": 100.0},
"2025-01-02": {"user-a": 2.0},
}, response.text
assert sum(spend for day in spend_by_day.values() for spend in day.values()) == body.metadata.total_spend
entities: Final = next(day for day in body.results if day.date.isoformat() == "2025-01-01").breakdown.entities
assert entities["user-a"].metadata == {"user_email": "user@example.test", "user_alias": "Alias A"}
assert entities["user-b"].metadata == {"user_email": "other-user@example.test", "user_alias": None}
def test_non_admin_user_aggregate_breaks_down_only_their_own_spend(
daily_activity_client: tuple[TestClient, _FakeRepository],
) -> None:
client, _ = daily_activity_client
response: Final = client.get(
"/user/daily/activity/aggregated",
params=_DATE_PARAMS,
headers={"x-user-role": LitellmUserRoles.INTERNAL_USER.value, "x-user-id": "user-a"},
)
assert response.status_code == 200, response.text
assert _entity_spend_by_day(SpendAnalyticsPaginatedResponse.model_validate_json(response.content)) == {
"2025-01-01": {"user-a": 12.5},
"2025-01-02": {"user-a": 2.0},
}, response.text
assert "other-user@example.test" not in response.text
def test_user_page_route_ranks_users_by_spend_and_attaches_metadata(
daily_activity_client: tuple[TestClient, _FakeRepository],
) -> None: