Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/db/db_transaction_queue/spend_update_queue.py: 81%
90 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1import asyncio
2from typing import Final
4from litellm._logging import verbose_proxy_logger
5from litellm.constants import LITELLM_ASYNCIO_QUEUE_MAXSIZE
6from litellm.proxy._types import (
7 DBSpendUpdateTransactions,
8 Litellm_EntityType,
9 SpendUpdateQueueItem,
10)
11from litellm.proxy.db.db_transaction_queue.base_update_queue import (
12 BaseUpdateQueue,
13 service_logger_obj,
14)
15from litellm.types.services import ServiceTypes
18class SpendUpdateQueue(BaseUpdateQueue):
19 """
20 In memory buffer for spend updates that should be committed to the database
21 """
23 def __init__(self):
24 super().__init__()
25 self.update_queue: asyncio.Queue[SpendUpdateQueueItem] = asyncio.Queue(maxsize=LITELLM_ASYNCIO_QUEUE_MAXSIZE)
27 async def flush_and_get_aggregated_db_spend_update_transactions(
28 self,
29 ) -> DBSpendUpdateTransactions:
30 """Flush all updates from the queue and return all updates aggregated by entity type."""
31 updates: Final = await self.flush_all_updates_from_in_memory_queue()
32 if len(updates) > 0:
33 verbose_proxy_logger.info(
34 "Spend tracking - flushed %d spend update items from in-memory queue",
35 len(updates),
36 )
37 verbose_proxy_logger.debug("Aggregating updates by entity type: %s", updates)
38 return self.get_aggregated_db_spend_update_transactions(updates)
40 async def add_update(self, update: SpendUpdateQueueItem):
41 """Enqueue an update to the spend update queue"""
42 verbose_proxy_logger.debug("Adding update to queue: %s", update)
43 await self.update_queue.put(update)
45 # if the queue is full, aggregate the updates
46 if self.update_queue.qsize() >= self.MAX_SIZE_IN_MEMORY_QUEUE:
47 verbose_proxy_logger.warning(
48 "Spend update queue is full. Aggregating all entries in queue to concatenate entries."
49 )
50 await self.aggregate_queue_updates()
52 async def aggregate_queue_updates(self):
53 """Concatenate all updates in the queue to reduce the size of in-memory queue"""
54 updates: Final[list[SpendUpdateQueueItem]] = await self.flush_all_updates_from_in_memory_queue()
55 aggregated_updates: Final = self._get_aggregated_spend_update_queue_item(updates)
56 for update in aggregated_updates:
57 await self.update_queue.put(update)
59 def _get_aggregated_spend_update_queue_item(
60 self, updates: list[SpendUpdateQueueItem]
61 ) -> list[SpendUpdateQueueItem]:
62 """
63 This is used to reduce the size of the in-memory queue by aggregating updates by entity type + id
66 Aggregate updates by entity type + id
68 eg.
70 ```
71 [
72 {
73 "entity_type": "user",
74 "entity_id": "123",
75 "response_cost": 100
76 },
77 {
78 "entity_type": "user",
79 "entity_id": "123",
80 "response_cost": 200
81 }
82 ]
84 ```
86 becomes
88 ```
90 [
91 {
92 "entity_type": "user",
93 "entity_id": "123",
94 "response_cost": 300
95 }
96 ]
98 ```
99 """
100 verbose_proxy_logger.debug(
101 "Aggregating spend updates, current queue size: %s",
102 self.update_queue.qsize(),
103 )
104 aggregated_spend_updates: Final[list[SpendUpdateQueueItem]] = []
106 _in_memory_map: Final[dict[str, SpendUpdateQueueItem]] = {}
107 """
108 Used for combining several updates into a single update
109 Key=entity_type:entity_id
110 Value=SpendUpdateQueueItem
111 """
112 for update in updates:
113 _key = f"{update.get('entity_type')}:{update.get('entity_id')}"
114 if _key not in _in_memory_map:
115 # avoid mutating caller-owned dicts while aggregating queue entries
116 _in_memory_map[_key] = update.copy()
117 else:
118 current_cost = _in_memory_map[_key].get("response_cost", 0) or 0
119 update_cost = update.get("response_cost", 0) or 0
120 _in_memory_map[_key]["response_cost"] = current_cost + update_cost
122 for _key, update in _in_memory_map.items():
123 aggregated_spend_updates.append(update)
125 verbose_proxy_logger.debug("Aggregated spend updates: %s", aggregated_spend_updates)
126 return aggregated_spend_updates
128 def get_aggregated_db_spend_update_transactions(
129 self, updates: list[SpendUpdateQueueItem]
130 ) -> DBSpendUpdateTransactions:
131 """Aggregate updates by entity type."""
132 # Initialize all transaction lists as empty dicts
133 db_spend_update_transactions: Final = DBSpendUpdateTransactions(
134 user_list_transactions={},
135 end_user_list_transactions={},
136 key_list_transactions={},
137 team_list_transactions={},
138 team_member_list_transactions={},
139 org_list_transactions={},
140 org_member_list_transactions={},
141 project_list_transactions={},
142 tag_list_transactions={},
143 agent_list_transactions={},
144 model_access_group_list_transactions={},
145 )
147 # Map entity types to their corresponding transaction dictionary keys
148 entity_type_to_dict_key: Final = {
149 Litellm_EntityType.USER: "user_list_transactions",
150 Litellm_EntityType.END_USER: "end_user_list_transactions",
151 Litellm_EntityType.KEY: "key_list_transactions",
152 Litellm_EntityType.TEAM: "team_list_transactions",
153 Litellm_EntityType.TEAM_MEMBER: "team_member_list_transactions",
154 Litellm_EntityType.ORGANIZATION: "org_list_transactions",
155 Litellm_EntityType.ORGANIZATION_MEMBER: "org_member_list_transactions",
156 Litellm_EntityType.PROJECT: "project_list_transactions",
157 Litellm_EntityType.TAG: "tag_list_transactions",
158 Litellm_EntityType.AGENT: "agent_list_transactions",
159 Litellm_EntityType.MODEL_ACCESS_GROUP: "model_access_group_list_transactions",
160 }
162 for update in updates:
163 entity_type = update.get("entity_type")
164 entity_id = update.get("entity_id") or ""
165 response_cost = update.get("response_cost") or 0
167 if entity_type is None: 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true
168 verbose_proxy_logger.debug(
169 "Skipping update spend for update: %s, because entity_type is None",
170 update,
171 )
172 continue
174 dict_key = entity_type_to_dict_key.get(entity_type)
175 if dict_key is None: 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true
176 verbose_proxy_logger.debug(
177 "Skipping update spend for update: %s, because entity_type is not in entity_type_to_dict_key",
178 update,
179 )
180 continue # Skip unknown entity types
182 # Type-safe access using if/elif statements
183 if dict_key == "user_list_transactions":
184 transactions_dict = db_spend_update_transactions["user_list_transactions"]
185 elif dict_key == "end_user_list_transactions":
186 transactions_dict = db_spend_update_transactions["end_user_list_transactions"]
187 elif dict_key == "key_list_transactions":
188 transactions_dict = db_spend_update_transactions["key_list_transactions"]
189 elif dict_key == "team_list_transactions": 189 ↛ 190line 189 didn't jump to line 190 because the condition on line 189 was never true
190 transactions_dict = db_spend_update_transactions["team_list_transactions"]
191 elif dict_key == "team_member_list_transactions": 191 ↛ 192line 191 didn't jump to line 192 because the condition on line 191 was never true
192 transactions_dict = db_spend_update_transactions["team_member_list_transactions"]
193 elif dict_key == "org_list_transactions": 193 ↛ 194line 193 didn't jump to line 194 because the condition on line 193 was never true
194 transactions_dict = db_spend_update_transactions["org_list_transactions"]
195 elif dict_key == "org_member_list_transactions": 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true
196 transactions_dict = db_spend_update_transactions["org_member_list_transactions"]
197 elif dict_key == "project_list_transactions": 197 ↛ 198line 197 didn't jump to line 198 because the condition on line 197 was never true
198 transactions_dict = db_spend_update_transactions["project_list_transactions"]
199 elif dict_key == "tag_list_transactions":
200 transactions_dict = db_spend_update_transactions["tag_list_transactions"]
201 elif dict_key == "agent_list_transactions": 201 ↛ 203line 201 didn't jump to line 203 because the condition on line 201 was always true
202 transactions_dict = db_spend_update_transactions["agent_list_transactions"]
203 elif dict_key == "model_access_group_list_transactions":
204 transactions_dict = db_spend_update_transactions["model_access_group_list_transactions"]
205 else:
206 continue
208 if transactions_dict is None: 208 ↛ 209line 208 didn't jump to line 209 because the condition on line 208 was never true
209 transactions_dict = {}
211 # type ignore: dict_key is guaranteed to be one of "one of ("user_list_transactions", "end_user_list_transactions", "key_list_transactions", "team_list_transactions", "team_member_list_transactions", "org_list_transactions")"
212 db_spend_update_transactions[dict_key] = transactions_dict
214 if entity_id not in transactions_dict:
215 transactions_dict[entity_id] = 0
217 transactions_dict[entity_id] += response_cost or 0
219 return db_spend_update_transactions
221 async def _emit_new_item_added_to_queue_event(
222 self,
223 queue_size: int | None = None,
224 ):
225 asyncio.create_task(
226 service_logger_obj.async_service_success_hook(
227 service=ServiceTypes.IN_MEMORY_SPEND_UPDATE_QUEUE,
228 duration=0,
229 call_type="_emit_new_item_added_to_queue_event",
230 event_metadata={
231 "gauge_labels": ServiceTypes.IN_MEMORY_SPEND_UPDATE_QUEUE,
232 "gauge_value": queue_size,
233 },
234 )
235 )