mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(usage): return user_email on key activity metadata
Recovery already resolved the owner email for double-hashed spend keys, then Key Activity dropped it. The Usage payload now carries user_email and the key label falls back to that email before key-hash-... Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
f610d20543
commit
e76a18ca32
8 changed files with 60 additions and 11 deletions
|
|
@ -6,12 +6,13 @@ from types import MappingProxyType, SimpleNamespace
|
|||
from typing import TYPE_CHECKING, Final, Protocol
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from typing_extensions import TypedDict
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
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_emails,
|
||||
recover_double_hashed_key_metadata,
|
||||
)
|
||||
from litellm.proxy.spend_tracking.ptu_feature_flag import is_ptu_cost_attribution_enabled
|
||||
|
|
@ -116,6 +117,8 @@ class DailySpendRecord(Protocol):
|
|||
class _KeyMetadataDict(TypedDict, total=False):
|
||||
key_alias: str | None
|
||||
team_id: str | None
|
||||
user_id: ReadOnly[str | None]
|
||||
user_email: ReadOnly[str | None]
|
||||
|
||||
|
||||
_WhereValue = str | dict[str, object]
|
||||
|
|
@ -456,7 +459,12 @@ async def get_api_key_metadata(
|
|||
where={"token": {"in": list(api_keys)}}
|
||||
)
|
||||
result: Final[dict[str, _KeyMetadataDict]] = {
|
||||
k.token: {"key_alias": k.key_alias, "team_id": k.team_id} for k in key_records
|
||||
k.token: {
|
||||
"key_alias": k.key_alias,
|
||||
"team_id": k.team_id,
|
||||
"user_id": getattr(k, "user_id", None),
|
||||
}
|
||||
for k in key_records
|
||||
}
|
||||
|
||||
# For any keys not found in the active table, check the deleted keys table
|
||||
|
|
@ -475,6 +483,7 @@ async def get_api_key_metadata(
|
|||
result[k.token] = {
|
||||
"key_alias": k.key_alias,
|
||||
"team_id": k.team_id,
|
||||
"user_id": getattr(k, "user_id", None),
|
||||
}
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.warning(
|
||||
|
|
@ -484,10 +493,12 @@ async def get_api_key_metadata(
|
|||
)
|
||||
|
||||
still_missing: Final = api_keys - frozenset(result)
|
||||
if not still_missing:
|
||||
return result
|
||||
recovered: Final = await recover_double_hashed_key_metadata(prisma_client, still_missing)
|
||||
return MappingProxyType({**result, **recovered})
|
||||
combined: Final = (
|
||||
result
|
||||
if not still_missing
|
||||
else MappingProxyType({**result, **(await recover_double_hashed_key_metadata(prisma_client, still_missing))})
|
||||
)
|
||||
return await attach_user_emails(prisma_client, combined)
|
||||
|
||||
|
||||
def _adjust_dates_for_timezone(
|
||||
|
|
@ -961,7 +972,11 @@ def _record_to_spend_metrics(record: _GroupingSetsRow) -> SpendMetrics:
|
|||
|
||||
def _key_metadata(api_key_metadata: Mapping[str, _KeyMetadataDict], api_key: str) -> KeyMetadata:
|
||||
meta: Final = api_key_metadata.get(api_key, {})
|
||||
return KeyMetadata(key_alias=meta.get("key_alias"), team_id=meta.get("team_id"))
|
||||
return KeyMetadata(
|
||||
key_alias=meta.get("key_alias"),
|
||||
team_id=meta.get("team_id"),
|
||||
user_email=meta.get("user_email"),
|
||||
)
|
||||
|
||||
|
||||
def _aggregate_grouping_sets_records_sync(
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ def _meta_with_email(meta: KeyMetadataDict, emails: Mapping[str, str]) -> KeyMet
|
|||
return updated
|
||||
|
||||
|
||||
async def _with_user_emails(
|
||||
async def attach_user_emails(
|
||||
prisma_client: PrismaClient,
|
||||
recovered: Mapping[str, KeyMetadataDict],
|
||||
) -> Mapping[str, KeyMetadataDict]:
|
||||
|
|
@ -249,7 +249,7 @@ async def recover_double_hashed_key_metadata(
|
|||
if not still_missing
|
||||
else MappingProxyType({**from_tokens, **(await _spend_logs_key_metadata(prisma_client, still_missing))})
|
||||
)
|
||||
return await _with_user_emails(prisma_client, recovered)
|
||||
return await attach_user_emails(prisma_client, recovered)
|
||||
|
||||
|
||||
def _row_with_recovered_fields(
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class KeyMetadata(BaseModel):
|
|||
|
||||
key_alias: str | None = None
|
||||
team_id: str | None = None
|
||||
user_email: str | None = None
|
||||
|
||||
|
||||
class KeyMetricWithMetadata(MetricBase):
|
||||
|
|
|
|||
|
|
@ -516,6 +516,7 @@ async def test_get_api_key_metadata_recovers_double_hashed_key_via_spend_logs():
|
|||
}
|
||||
]
|
||||
)
|
||||
mock_prisma.db.litellm_usertable.find_many = AsyncMock(return_value=[])
|
||||
|
||||
result = await get_api_key_metadata(
|
||||
prisma_client=mock_prisma,
|
||||
|
|
@ -527,6 +528,24 @@ async def test_get_api_key_metadata_recovers_double_hashed_key_via_spend_logs():
|
|||
mock_prisma.db.query_raw.assert_called_once()
|
||||
|
||||
|
||||
def test_key_metadata_includes_recovered_user_email():
|
||||
from litellm.proxy.management_endpoints.common_daily_activity import _key_metadata
|
||||
|
||||
meta = _key_metadata(
|
||||
{
|
||||
"dirty-key": {
|
||||
"key_alias": "batch-worker",
|
||||
"team_id": "team-1",
|
||||
"user_email": "alice@example.com",
|
||||
}
|
||||
},
|
||||
"dirty-key",
|
||||
)
|
||||
|
||||
assert meta.key_alias == "batch-worker"
|
||||
assert meta.user_email == "alice@example.com"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tag_daily_activity_metadata_totals_not_zero():
|
||||
"""Test that tag daily activity returns correct metadata totals.
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export interface KeyMetricWithMetadata {
|
|||
export interface KeyMetadata {
|
||||
key_alias: string | null;
|
||||
team_id: string | null;
|
||||
user_email?: string | null;
|
||||
tags?: { tag: string; usage: number }[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ const createMockDailyData = (
|
|||
});
|
||||
|
||||
const createMockKeyMetricWithMetadata = (
|
||||
metadata: { key_alias: string | null; team_id: string | null },
|
||||
metadata: { key_alias: string | null; team_id: string | null; user_email?: string | null },
|
||||
metrics: typeof EMPTY_SPEND_METRICS = EMPTY_SPEND_METRICS,
|
||||
): KeyMetricWithMetadata => ({
|
||||
metrics,
|
||||
|
|
@ -1450,6 +1450,17 @@ describe("formatKeyLabel", () => {
|
|||
expect(result).toBe("key-hash-actual-key (team: Test Team 1)");
|
||||
});
|
||||
|
||||
it("should use user_email when key_alias is null", () => {
|
||||
const modelData = createMockKeyMetricWithMetadata({
|
||||
key_alias: null,
|
||||
team_id: "team1",
|
||||
user_email: "alice@example.com",
|
||||
});
|
||||
|
||||
const result = formatKeyLabel(modelData, "actual-key", MOCK_TEAMS);
|
||||
expect(result).toBe("alice@example.com (team: Test Team 1)");
|
||||
});
|
||||
|
||||
it("should return key_alias with team_id when teams array is empty", () => {
|
||||
const modelData = createMockKeyMetricWithMetadata({
|
||||
key_alias: "my-key",
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics,
|
|||
|
||||
// Helper function to format key label
|
||||
export const formatKeyLabel = (modelData: KeyMetricWithMetadata, model: string, teams: Team[]): string => {
|
||||
const keyAlias = modelData.metadata.key_alias || `key-hash-${model}`;
|
||||
const keyAlias = modelData.metadata.key_alias || modelData.metadata.user_email || `key-hash-${model}`;
|
||||
const teamId = modelData.metadata.team_id;
|
||||
if (teamId) {
|
||||
const teamAlias = resolveTeamAliasFromTeamID(teamId, teams);
|
||||
|
|
|
|||
2
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
2
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -27994,6 +27994,8 @@ export interface components {
|
|||
key_alias?: string | null;
|
||||
/** Team Id */
|
||||
team_id?: string | null;
|
||||
/** User Email */
|
||||
user_email?: string | null;
|
||||
};
|
||||
/**
|
||||
* KeyMetricWithMetadata
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue