From 72682f4bd4e8433cb2dcf06f7395c7aaf904434a Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Wed, 11 Feb 2026 00:36:03 -0600 Subject: [PATCH] fix(proxy): avoid in-place mutation in SpendUpdateQueue aggregation (#20876) * fix(proxy): prevent spend queue aggregation from mutating input updates * test(proxy): avoid order-dependent spend queue aggregation assertion --- .../spend_update_queue.py | 3 +- .../test_spend_update_queue.py | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/db/db_transaction_queue/spend_update_queue.py b/litellm/proxy/db/db_transaction_queue/spend_update_queue.py index 9b0449bb9a..2ed3f151a9 100644 --- a/litellm/proxy/db/db_transaction_queue/spend_update_queue.py +++ b/litellm/proxy/db/db_transaction_queue/spend_update_queue.py @@ -109,7 +109,8 @@ class SpendUpdateQueue(BaseUpdateQueue): for update in updates: _key = f"{update.get('entity_type')}:{update.get('entity_id')}" if _key not in _in_memory_map: - _in_memory_map[_key] = update + # avoid mutating caller-owned dicts while aggregating queue entries + _in_memory_map[_key] = update.copy() else: current_cost = _in_memory_map[_key].get("response_cost", 0) or 0 update_cost = update.get("response_cost", 0) or 0 diff --git a/tests/test_litellm/proxy/db/db_transaction_queue/test_spend_update_queue.py b/tests/test_litellm/proxy/db/db_transaction_queue/test_spend_update_queue.py index 9993b25dfd..0ed5940dd7 100644 --- a/tests/test_litellm/proxy/db/db_transaction_queue/test_spend_update_queue.py +++ b/tests/test_litellm/proxy/db/db_transaction_queue/test_spend_update_queue.py @@ -225,6 +225,39 @@ async def test_aggregate_queue_updates_accuracy(spend_queue): assert aggregated["team_list_transactions"]["team1"] == 5.0 +def test_get_aggregated_spend_update_queue_item_does_not_mutate_original_updates( + spend_queue, +): + original_update: SpendUpdateQueueItem = { + "entity_type": Litellm_EntityType.USER, + "entity_id": "user1", + "response_cost": 10.0, + } + duplicate_key_update: SpendUpdateQueueItem = { + "entity_type": Litellm_EntityType.USER, + "entity_id": "user1", + "response_cost": 20.0, + } + + aggregated_updates = spend_queue._get_aggregated_spend_update_queue_item( + [original_update, duplicate_key_update] + ) + user1_aggregated_update = next( + ( + update + for update in aggregated_updates + if update.get("entity_type") == Litellm_EntityType.USER + and update.get("entity_id") == "user1" + ), + None, + ) + + assert original_update["response_cost"] == 10.0 + assert user1_aggregated_update is not None + assert user1_aggregated_update["response_cost"] == 30.0 + assert user1_aggregated_update is not original_update + + @pytest.mark.asyncio async def test_queue_size_reduction_with_large_volume(monkeypatch, spend_queue): """Test that queue size is actually reduced when dealing with many items"""