From 33815682bc20ec5b32d9b363995608fc29ee357a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 01:38:26 +0000 Subject: [PATCH] fix(spend): keep CloudZero and Focus spend-user email when filling alias Export rows already join user_email from DailyUserSpend.user_id. Recovered key-owner email must not replace that when only the alias join missed. Co-authored-by: Mateo Wang --- .../spend_tracking/key_metadata_recovery.py | 2 +- .../test_key_metadata_recovery.py | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/spend_tracking/key_metadata_recovery.py b/litellm/proxy/spend_tracking/key_metadata_recovery.py index d524b158c9c..2940c9a8337 100644 --- a/litellm/proxy/spend_tracking/key_metadata_recovery.py +++ b/litellm/proxy/spend_tracking/key_metadata_recovery.py @@ -186,7 +186,7 @@ def _row_with_recovered_fields( **row, alias_field: meta.get("key_alias") or row.get(alias_field), team_id_field: meta.get("team_id") or row.get(team_id_field), - user_email_field: meta.get("user_email") or row.get(user_email_field), + user_email_field: row.get(user_email_field) or meta.get("user_email"), } ) diff --git a/tests/test_litellm/proxy/spend_tracking/test_key_metadata_recovery.py b/tests/test_litellm/proxy/spend_tracking/test_key_metadata_recovery.py index 43f7d20cf13..d76d355ec0e 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_key_metadata_recovery.py +++ b/tests/test_litellm/proxy/spend_tracking/test_key_metadata_recovery.py @@ -170,3 +170,32 @@ async def test_fill_missing_api_key_aliases_leaves_rows_untouched_when_nothing_i assert filled == rows mock_prisma.db.query_raw.assert_not_called() + + +@pytest.mark.asyncio +async def test_fill_missing_api_key_aliases_keeps_spend_user_email_when_alias_is_missing(): + double_hashed = hash_token("f" * 64) + mock_prisma = MagicMock() + mock_prisma.db.query_raw = _query_raw_by_table( + active_rows=[_digest_row(double_hashed, "team-key", "team-9", "key-owner")], + deleted_rows=[], + ) + mock_prisma.db.litellm_usertable.find_many = AsyncMock( + return_value=[SimpleNamespace(user_id="key-owner", user_email="owner@example.com")] + ) + + rows = ( + { + "api_key": double_hashed, + "api_key_alias": None, + "team_id": None, + "user_email": "spender@example.com", + "spend": 4.0, + }, + ) + + filled = await fill_missing_api_key_aliases(mock_prisma, rows) + + assert filled[0]["api_key_alias"] == "team-key" + assert filled[0]["team_id"] == "team-9" + assert filled[0]["user_email"] == "spender@example.com"