mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
Merge d1b23030d1 into 570f41daad
This commit is contained in:
commit
fa23a8fe3e
5 changed files with 243 additions and 9 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue