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
This commit is contained in:
Emerson Gomes
2026-02-10 22:36:03 -08:00
committed by GitHub
parent 713d3022ae
commit 72682f4bd4
2 changed files with 35 additions and 1 deletions
@@ -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
@@ -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"""