From 94da5c076c3b27b4341f5cb82fc2e5011ef0c5d6 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 12 Nov 2025 18:22:26 -0800 Subject: [PATCH 01/18] Add Daily Org Spend Table, read path, and write path --- .../litellm_proxy_extras/schema.prisma | 29 +++ litellm/constants.py | 1 + litellm/proxy/_types.py | 4 + litellm/proxy/db/db_spend_update_writer.py | 104 ++++++++++- .../redis_update_buffer.py | 36 ++++ .../organization_endpoints.py | 99 ++++++++++ litellm/proxy/schema.prisma | 29 +++ schema.prisma | 29 +++ .../proxy/db/test_db_spend_update_writer.py | 84 ++++++++- .../test_organization_endpoints.py | 175 ++++++++++++++++++ 10 files changed, 585 insertions(+), 5 deletions(-) diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 88904561129..739ae8cc60f 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -419,6 +419,35 @@ model LiteLLM_DailyUserSpend { @@index([mcp_namespaced_tool_name]) } +// Track daily organization spend metrics per model and key +model LiteLLM_DailyOrganizationSpend { + id String @id @default(uuid()) + organization_id String? + date String + api_key String + model String? + model_group String? + custom_llm_provider String? + mcp_namespaced_tool_name String? + prompt_tokens BigInt @default(0) + completion_tokens BigInt @default(0) + cache_read_input_tokens BigInt @default(0) + cache_creation_input_tokens BigInt @default(0) + spend Float @default(0.0) + api_requests BigInt @default(0) + successful_requests BigInt @default(0) + failed_requests BigInt @default(0) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + @@unique([organization_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name]) + @@index([date]) + @@index([organization_id]) + @@index([api_key]) + @@index([model]) + @@index([mcp_namespaced_tool_name]) +} + // Track daily team spend metrics per model and key model LiteLLM_DailyTeamSpend { id String @id @default(uuid()) diff --git a/litellm/constants.py b/litellm/constants.py index 220e425068b..48be1c1fbf2 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -131,6 +131,7 @@ DEFAULT_SSL_CIPHERS = os.getenv( REDIS_UPDATE_BUFFER_KEY = "litellm_spend_update_buffer" REDIS_DAILY_SPEND_UPDATE_BUFFER_KEY = "litellm_daily_spend_update_buffer" REDIS_DAILY_TEAM_SPEND_UPDATE_BUFFER_KEY = "litellm_daily_team_spend_update_buffer" +REDIS_DAILY_ORG_SPEND_UPDATE_BUFFER_KEY = "litellm_daily_org_spend_update_buffer" REDIS_DAILY_TAG_SPEND_UPDATE_BUFFER_KEY = "litellm_daily_tag_spend_update_buffer" MAX_REDIS_BUFFER_DEQUEUE_COUNT = int(os.getenv("MAX_REDIS_BUFFER_DEQUEUE_COUNT", 100)) MAX_SIZE_IN_MEMORY_QUEUE = int(os.getenv("MAX_SIZE_IN_MEMORY_QUEUE", 10000)) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index a212eab076c..48307fc6a2e 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3536,6 +3536,10 @@ class DailyTeamSpendTransaction(BaseDailySpendTransaction): team_id: str +class DailyOrganizationSpendTransaction(BaseDailySpendTransaction): + organization_id: str + + class DailyUserSpendTransaction(BaseDailySpendTransaction): user_id: str diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index 06b5301424e..8b276c583dd 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -23,6 +23,7 @@ from litellm.proxy._types import ( DB_CONNECTION_ERROR_TYPES, BaseDailySpendTransaction, DailyTagSpendTransaction, + DailyOrganizationSpendTransaction, DailyTeamSpendTransaction, DailyUserSpendTransaction, DBSpendUpdateTransactions, @@ -64,6 +65,7 @@ class DBSpendUpdateWriter: self.spend_update_queue = SpendUpdateQueue() self.daily_spend_update_queue = DailySpendUpdateQueue() self.daily_team_spend_update_queue = DailySpendUpdateQueue() + self.daily_org_spend_update_queue = DailySpendUpdateQueue() self.daily_tag_spend_update_queue = DailySpendUpdateQueue() async def update_database( @@ -180,7 +182,13 @@ class DBSpendUpdateWriter: prisma_client=prisma_client, ) ) - + asyncio.create_task( + self.add_spend_log_transaction_to_daily_org_transaction( + payload=payload, + org_id=org_id, + prisma_client=prisma_client, + ) + ) asyncio.create_task( self.add_spend_log_transaction_to_daily_tag_transaction( payload=payload, @@ -460,6 +468,7 @@ class DBSpendUpdateWriter: spend_update_queue=self.spend_update_queue, daily_spend_update_queue=self.daily_spend_update_queue, daily_team_spend_update_queue=self.daily_team_spend_update_queue, + daily_org_spend_update_queue=self.daily_org_spend_update_queue, daily_tag_spend_update_queue=self.daily_tag_spend_update_queue, ) @@ -502,6 +511,17 @@ class DBSpendUpdateWriter: daily_spend_transactions=daily_team_spend_update_transactions, ) + daily_org_spend_update_transactions = ( + await self.redis_update_buffer.get_all_daily_org_spend_update_transactions_from_redis_buffer() + ) + if daily_org_spend_update_transactions is not None: + await DBSpendUpdateWriter.update_daily_org_spend( + n_retry_times=n_retry_times, + prisma_client=prisma_client, + proxy_logging_obj=proxy_logging_obj, + daily_spend_transactions=daily_org_spend_update_transactions, + ) + daily_tag_spend_update_transactions = ( await self.redis_update_buffer.get_all_daily_tag_spend_update_transactions_from_redis_buffer() ) @@ -573,6 +593,20 @@ class DBSpendUpdateWriter: daily_spend_transactions=daily_team_spend_update_transactions, ) + ################## Daily Organization Spend Update Transactions ################## + # Aggregate all in memory daily org spend transactions and commit to db + daily_org_spend_update_transactions = cast( + Dict[str, DailyOrganizationSpendTransaction], + await self.daily_org_spend_update_queue.flush_and_get_aggregated_daily_spend_update_transactions(), + ) + + await DBSpendUpdateWriter.update_daily_org_spend( + n_retry_times=n_retry_times, + prisma_client=prisma_client, + proxy_logging_obj=proxy_logging_obj, + daily_spend_transactions=daily_org_spend_update_transactions, + ) + ################## Daily Tag Spend Update Transactions ################## # Aggregate all in memory daily tag spend transactions and commit to db daily_tag_spend_update_transactions = cast( @@ -962,14 +996,15 @@ class DBSpendUpdateWriter: Dict[str, DailyUserSpendTransaction], Dict[str, DailyTeamSpendTransaction], Dict[str, DailyTagSpendTransaction], + Dict[str, DailyOrganizationSpendTransaction], ], - entity_type: Literal["user", "team", "tag"], + entity_type: Literal["user", "team", "org", "tag"], entity_id_field: str, table_name: str, unique_constraint_name: str, ) -> None: """ - Generic function to update daily spend for any entity type (user, team, tag) + Generic function to update daily spend for any entity type (user, team, org, tag) """ from litellm.proxy.utils import _raise_failed_update_spend_exception @@ -1191,6 +1226,27 @@ class DBSpendUpdateWriter: unique_constraint_name="team_id_date_api_key_model_custom_llm_provider_mcp_namespaced_tool_name", ) + @staticmethod + async def update_daily_org_spend( + n_retry_times: int, + prisma_client: PrismaClient, + proxy_logging_obj: ProxyLogging, + daily_spend_transactions: Dict[str, DailyOrganizationSpendTransaction], + ): + """ + Batch job to update LiteLLM_DailyOrganizationSpend table using in-memory daily_spend_transactions + """ + await DBSpendUpdateWriter._update_daily_spend( + n_retry_times=n_retry_times, + prisma_client=prisma_client, + proxy_logging_obj=proxy_logging_obj, + daily_spend_transactions=daily_spend_transactions, + entity_type="org", + entity_id_field="organization_id", + table_name="litellm_dailyorganizationspend", + unique_constraint_name="organization_id_date_api_key_model_custom_llm_provider_mcp_namespaced_tool_name", + ) + @staticmethod async def update_daily_tag_spend( n_retry_times: int, @@ -1216,13 +1272,15 @@ class DBSpendUpdateWriter: self, payload: Union[dict, SpendLogsPayload], prisma_client: PrismaClient, - type: Literal["user", "team", "request_tags"] = "user", + type: Literal["user", "team", "org", "request_tags"] = "user", ) -> Optional[BaseDailySpendTransaction]: common_expected_keys = ["startTime", "api_key"] if type == "user": expected_keys = ["user", *common_expected_keys] elif type == "team": expected_keys = ["team_id", *common_expected_keys] + elif type == "org": + expected_keys = ["organization_id", *common_expected_keys] elif type == "request_tags": expected_keys = ["request_tags", *common_expected_keys] else: @@ -1354,6 +1412,44 @@ class DBSpendUpdateWriter: update={daily_transaction_key: daily_transaction} ) + async def add_spend_log_transaction_to_daily_org_transaction( + self, + payload: SpendLogsPayload, + prisma_client: Optional[PrismaClient] = None, + org_id: Optional[str] = None, + ) -> None: + if prisma_client is None: + verbose_proxy_logger.debug( + "prisma_client is None. Skipping writing spend logs to db." + ) + return + + if org_id is None: + verbose_proxy_logger.debug( + "organization_id is None for request. Skipping incrementing organization spend." + ) + return + + # Inject org_id for daily aggregation check + payload_with_org: SpendLogsPayload = dict(payload) + payload_with_org["organization_id"] = org_id + + base_daily_transaction = ( + await self._common_add_spend_log_transaction_to_daily_transaction( + payload_with_org, prisma_client, "org" + ) + ) + if base_daily_transaction is None: + return + + daily_transaction_key = f"{org_id}_{base_daily_transaction['date']}_{payload_with_org['api_key']}_{payload_with_org['model']}_{payload_with_org['custom_llm_provider']}" + daily_transaction = DailyOrganizationSpendTransaction( + organization_id=org_id, **base_daily_transaction + ) + await self.daily_org_spend_update_queue.add_update( + update={daily_transaction_key: daily_transaction} + ) + async def add_spend_log_transaction_to_daily_tag_transaction( self, payload: SpendLogsPayload, diff --git a/litellm/proxy/db/db_transaction_queue/redis_update_buffer.py b/litellm/proxy/db/db_transaction_queue/redis_update_buffer.py index 91d0bee1d3a..921fd9701bd 100644 --- a/litellm/proxy/db/db_transaction_queue/redis_update_buffer.py +++ b/litellm/proxy/db/db_transaction_queue/redis_update_buffer.py @@ -15,6 +15,7 @@ from litellm.constants import ( REDIS_DAILY_SPEND_UPDATE_BUFFER_KEY, REDIS_DAILY_TAG_SPEND_UPDATE_BUFFER_KEY, REDIS_DAILY_TEAM_SPEND_UPDATE_BUFFER_KEY, + REDIS_DAILY_ORG_SPEND_UPDATE_BUFFER_KEY, REDIS_UPDATE_BUFFER_KEY, ) from litellm.litellm_core_utils.safe_json_dumps import safe_dumps @@ -22,6 +23,7 @@ from litellm.proxy._types import ( DailyTagSpendTransaction, DailyTeamSpendTransaction, DailyUserSpendTransaction, + DailyOrganizationSpendTransaction, DBSpendUpdateTransactions, ) from litellm.proxy.db.db_transaction_queue.base_update_queue import service_logger_obj @@ -104,6 +106,7 @@ class RedisUpdateBuffer: spend_update_queue: SpendUpdateQueue, daily_spend_update_queue: DailySpendUpdateQueue, daily_team_spend_update_queue: DailySpendUpdateQueue, + daily_org_spend_update_queue: DailySpendUpdateQueue, daily_tag_spend_update_queue: DailySpendUpdateQueue, ): """ @@ -166,6 +169,9 @@ class RedisUpdateBuffer: daily_team_spend_update_transactions = ( await daily_team_spend_update_queue.flush_and_get_aggregated_daily_spend_update_transactions() ) + daily_org_spend_update_transactions = ( + await daily_org_spend_update_queue.flush_and_get_aggregated_daily_spend_update_transactions() + ) daily_tag_spend_update_transactions = ( await daily_tag_spend_update_queue.flush_and_get_aggregated_daily_spend_update_transactions() ) @@ -195,6 +201,12 @@ class RedisUpdateBuffer: service_type=ServiceTypes.REDIS_DAILY_TEAM_SPEND_UPDATE_QUEUE, ) + await self._store_transactions_in_redis( + transactions=daily_org_spend_update_transactions, + redis_key=REDIS_DAILY_ORG_SPEND_UPDATE_BUFFER_KEY, + service_type=ServiceTypes.REDIS_DAILY_SPEND_UPDATE_QUEUE, + ) + await self._store_transactions_in_redis( transactions=daily_tag_spend_update_transactions, redis_key=REDIS_DAILY_TAG_SPEND_UPDATE_BUFFER_KEY, @@ -329,6 +341,30 @@ class RedisUpdateBuffer: ), ) + async def get_all_daily_org_spend_update_transactions_from_redis_buffer( + self, + ) -> Optional[Dict[str, DailyOrganizationSpendTransaction]]: + """ + Gets all the daily organization spend update transactions from Redis + """ + if self.redis_cache is None: + return None + list_of_transactions = await self.redis_cache.async_lpop( + key=REDIS_DAILY_ORG_SPEND_UPDATE_BUFFER_KEY, + count=MAX_REDIS_BUFFER_DEQUEUE_COUNT, + ) + if list_of_transactions is None: + return None + list_of_daily_spend_update_transactions = [ + json.loads(transaction) for transaction in list_of_transactions + ] + return cast( + Dict[str, DailyOrganizationSpendTransaction], + DailySpendUpdateQueue.get_aggregated_daily_spend_update_transactions( + list_of_daily_spend_update_transactions + ), + ) + async def get_all_daily_tag_spend_update_transactions_from_redis_buffer( self, ) -> Optional[Dict[str, DailyTagSpendTransaction]]: diff --git a/litellm/proxy/management_endpoints/organization_endpoints.py b/litellm/proxy/management_endpoints/organization_endpoints.py index 402dffaaa7d..65764b3b957 100644 --- a/litellm/proxy/management_endpoints/organization_endpoints.py +++ b/litellm/proxy/management_endpoints/organization_endpoints.py @@ -25,6 +25,7 @@ from litellm.proxy.management_endpoints.budget_management_endpoints import ( update_budget, ) from litellm.proxy.management_endpoints.common_utils import _set_object_metadata_field +from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view from litellm.proxy.management_helpers.object_permission_utils import ( handle_update_object_permission_common, ) @@ -34,6 +35,10 @@ from litellm.proxy.management_helpers.utils import ( ) from litellm.proxy.utils import PrismaClient from litellm.utils import _update_dictionary +from litellm.types.proxy.management_endpoints.common_daily_activity import ( + SpendAnalyticsPaginatedResponse, +) +from litellm.proxy.management_endpoints.common_daily_activity import get_daily_activity router = APIRouter() @@ -255,6 +260,100 @@ async def new_organization( return response +@router.get( + "/organization/daily/activity", + response_model=SpendAnalyticsPaginatedResponse, + tags=["organization management"], +) +async def get_organization_daily_activity( + organization_ids: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + model: Optional[str] = None, + api_key: Optional[str] = None, + page: int = 1, + page_size: int = 10, + exclude_organization_ids: Optional[str] = None, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Get daily activity for specific organizations or all accessible organizations. + """ + from litellm.proxy.proxy_server import ( + prisma_client, + proxy_logging_obj, + user_api_key_cache, + ) + + if prisma_client is None: + raise HTTPException( + status_code=500, + detail={"error": CommonProxyErrors.db_not_connected_error.value}, + ) + + # Parse comma-separated ids + org_ids_list = organization_ids.split(",") if organization_ids else None + exclude_org_ids_list: Optional[List[str]] = None + if exclude_organization_ids: + exclude_org_ids_list = ( + exclude_organization_ids.split(",") if exclude_organization_ids else None + ) + + # Restrict non-proxy-admins to only organizations where they are org_admin + if not _user_has_admin_view(user_api_key_dict): + memberships = await prisma_client.db.litellm_organizationmembership.find_many( + where={"user_id": user_api_key_dict.user_id} + ) + admin_org_ids = [ + m.organization_id + for m in memberships + if m.user_role == LitellmUserRoles.ORG_ADMIN.value + ] + if org_ids_list is None: + # Default to orgs where user is org_admin + org_ids_list = admin_org_ids + else: + # Ensure user is org_admin for all requested orgs + for org_id in org_ids_list: + if org_id not in admin_org_ids: + raise HTTPException( + status_code=403, + detail={ + "error": "User is not org_admin for Organization= {}.".format( + org_id + ) + }, + ) + + # Fetch organization aliases for metadata + where_condition = {} + if org_ids_list: + where_condition["organization_id"] = {"in": list(org_ids_list)} + org_aliases = await prisma_client.db.litellm_organizationtable.find_many( + where=where_condition + ) + org_alias_metadata = { + o.organization_id: {"organization_alias": o.organization_alias} + for o in org_aliases + } + + # Query daily activity for organizations + return await get_daily_activity( + prisma_client=prisma_client, + table_name="litellm_dailyorganizationspend", + entity_id_field="organization_id", + entity_id=org_ids_list, + entity_metadata_field=org_alias_metadata, + exclude_entity_ids=exclude_org_ids_list, + start_date=start_date, + end_date=end_date, + model=model, + api_key=api_key, + page=page, + page_size=page_size, + ) + + async def _set_object_permission( data: NewOrganizationRequest, prisma_client: Optional[PrismaClient], diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 51e6ea94540..4ec9662c325 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -419,6 +419,35 @@ model LiteLLM_DailyUserSpend { @@index([mcp_namespaced_tool_name]) } +// Track daily organization spend metrics per model and key +model LiteLLM_DailyOrganizationSpend { + id String @id @default(uuid()) + organization_id String? + date String + api_key String + model String? + model_group String? + custom_llm_provider String? + mcp_namespaced_tool_name String? + prompt_tokens BigInt @default(0) + completion_tokens BigInt @default(0) + cache_read_input_tokens BigInt @default(0) + cache_creation_input_tokens BigInt @default(0) + spend Float @default(0.0) + api_requests BigInt @default(0) + successful_requests BigInt @default(0) + failed_requests BigInt @default(0) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + @@unique([organization_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name]) + @@index([date]) + @@index([organization_id]) + @@index([api_key]) + @@index([model]) + @@index([mcp_namespaced_tool_name]) +} + // Track daily team spend metrics per model and key model LiteLLM_DailyTeamSpend { id String @id @default(uuid()) diff --git a/schema.prisma b/schema.prisma index 51e6ea94540..4ec9662c325 100644 --- a/schema.prisma +++ b/schema.prisma @@ -419,6 +419,35 @@ model LiteLLM_DailyUserSpend { @@index([mcp_namespaced_tool_name]) } +// Track daily organization spend metrics per model and key +model LiteLLM_DailyOrganizationSpend { + id String @id @default(uuid()) + organization_id String? + date String + api_key String + model String? + model_group String? + custom_llm_provider String? + mcp_namespaced_tool_name String? + prompt_tokens BigInt @default(0) + completion_tokens BigInt @default(0) + cache_read_input_tokens BigInt @default(0) + cache_creation_input_tokens BigInt @default(0) + spend Float @default(0.0) + api_requests BigInt @default(0) + successful_requests BigInt @default(0) + failed_requests BigInt @default(0) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + @@unique([organization_id, date, api_key, model, custom_llm_provider, mcp_namespaced_tool_name]) + @@index([date]) + @@index([organization_id]) + @@index([api_key]) + @@index([model]) + @@index([mcp_namespaced_tool_name]) +} + // Track daily team spend metrics per model and key model LiteLLM_DailyTeamSpend { id String @id @default(uuid()) diff --git a/tests/test_litellm/proxy/db/test_db_spend_update_writer.py b/tests/test_litellm/proxy/db/test_db_spend_update_writer.py index 6dbbbdd7442..d29ddffa36e 100644 --- a/tests/test_litellm/proxy/db/test_db_spend_update_writer.py +++ b/tests/test_litellm/proxy/db/test_db_spend_update_writer.py @@ -387,4 +387,86 @@ async def test_add_spend_log_transaction_to_daily_tag_transaction_with_request_i transaction_dict = call[1]["update"] # Each transaction should have one key with the format tag_date_api_key_model_provider for key, transaction in transaction_dict.items(): - assert transaction["request_id"] == request_id, f"request_id should be {request_id} but got {transaction.get('request_id')}" \ No newline at end of file + assert transaction["request_id"] == request_id, f"request_id should be {request_id} but got {transaction.get('request_id')}" + + +@pytest.mark.asyncio +async def test_add_spend_log_transaction_to_daily_org_transaction_injects_org_id_and_queues_update(): + """ + Verify org_id is injected into payload for daily aggregation and the update is queued. + """ + writer = DBSpendUpdateWriter() + mock_prisma = MagicMock() + mock_prisma.get_request_status = MagicMock(return_value="success") + + org_id = "org-xyz" + payload = { + "request_id": "req-1", + "user": "test-user", + "startTime": "2024-01-01T12:00:00", + "api_key": "test-key", + "model": "gpt-4", + "custom_llm_provider": "openai", + "model_group": "gpt-4-group", + "prompt_tokens": 10, + "completion_tokens": 5, + "spend": 0.2, + "metadata": '{"usage_object": {}}', + } + + writer.daily_org_spend_update_queue.add_update = AsyncMock() + + await writer.add_spend_log_transaction_to_daily_org_transaction( + payload=payload, + prisma_client=mock_prisma, + org_id=org_id, + ) + + # Should enqueue one org spend update + writer.daily_org_spend_update_queue.add_update.assert_called_once() + + # Validate key and injected fields + call_args = writer.daily_org_spend_update_queue.add_update.call_args[1] + update_dict = call_args["update"] + assert len(update_dict) == 1 + for key, transaction in update_dict.items(): + assert key == f"{org_id}_2024-01-01_test-key_gpt-4_openai" + assert transaction["organization_id"] == org_id + assert transaction["date"] == "2024-01-01" + assert transaction["api_key"] == "test-key" + assert transaction["model"] == "gpt-4" + assert transaction["custom_llm_provider"] == "openai" + + +@pytest.mark.asyncio +async def test_add_spend_log_transaction_to_daily_org_transaction_skips_when_org_id_missing(): + """ + Ensure no update is queued when org_id is None. + """ + writer = DBSpendUpdateWriter() + mock_prisma = MagicMock() + mock_prisma.get_request_status = MagicMock(return_value="success") + + payload = { + "request_id": "req-2", + "user": "test-user", + "startTime": "2024-01-01T12:00:00", + "api_key": "test-key", + "model": "gpt-4", + "custom_llm_provider": "openai", + "model_group": "gpt-4-group", + "prompt_tokens": 10, + "completion_tokens": 5, + "spend": 0.2, + "metadata": '{"usage_object": {}}', + } + + writer.daily_org_spend_update_queue.add_update = AsyncMock() + + await writer.add_spend_log_transaction_to_daily_org_transaction( + payload=payload, + prisma_client=mock_prisma, + org_id=None, + ) + + writer.daily_org_spend_update_queue.add_update.assert_not_called() \ No newline at end of file diff --git a/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py index 981c26d6db9..c02db727bf0 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py @@ -94,6 +94,181 @@ async def test_organization_update_object_permissions_existing_permission(monkey mock_prisma_client.db.litellm_objectpermissiontable.upsert.assert_called_once() +@pytest.mark.asyncio +async def test_get_organization_daily_activity_admin_param_passing(monkeypatch): + """ + As admin, ensure parsed params are forwarded to get_daily_activity with correct values. + """ + from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth + from litellm.proxy.management_endpoints import organization_endpoints + from litellm.proxy.management_endpoints.organization_endpoints import ( + get_organization_daily_activity, + ) + + # Mock prisma client + mock_prisma_client = AsyncMock() + mock_prisma_client.db.litellm_organizationtable.find_many = AsyncMock( + return_value=[] + ) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + # Admin view -> skip membership restriction + monkeypatch.setattr( + "litellm.proxy.management_endpoints.organization_endpoints._user_has_admin_view", + lambda _: True, + ) + + # Patch downstream common function and verify call args + mocked_response = MagicMock(name="SpendAnalyticsPaginatedResponse") + get_daily_activity_mock = AsyncMock(return_value=mocked_response) + monkeypatch.setattr( + organization_endpoints, "get_daily_activity", get_daily_activity_mock + ) + + auth = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin1") + result = await get_organization_daily_activity( + organization_ids="org1,org2", + start_date="2024-01-01", + end_date="2024-01-31", + model="gpt-4", + api_key="test-key", + page=2, + page_size=5, + exclude_organization_ids="org3", + user_api_key_dict=auth, + ) + + # Ensure passthrough to common method with correct args + get_daily_activity_mock.assert_awaited_once() + kwargs = get_daily_activity_mock.call_args.kwargs + assert kwargs["table_name"] == "litellm_dailyorganizationspend" + assert kwargs["entity_id_field"] == "organization_id" + assert kwargs["entity_id"] == ["org1", "org2"] + assert kwargs["exclude_entity_ids"] == ["org3"] + assert kwargs["start_date"] == "2024-01-01" + assert kwargs["end_date"] == "2024-01-31" + assert kwargs["model"] == "gpt-4" + assert kwargs["api_key"] == "test-key" + assert kwargs["page"] == 2 + assert kwargs["page_size"] == 5 + + assert result is mocked_response + + +@pytest.mark.asyncio +async def test_get_organization_daily_activity_non_admin_defaults_to_admin_orgs(monkeypatch): + """ + Non-admin with no explicit organization_ids should default to orgs they are ORG_ADMIN of. + """ + from types import SimpleNamespace + + from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth + from litellm.proxy.management_endpoints import organization_endpoints + from litellm.proxy.management_endpoints.organization_endpoints import ( + get_organization_daily_activity, + ) + + # Mock prisma client and memberships + mock_prisma_client = AsyncMock() + mock_prisma_client.db.litellm_organizationtable.find_many = AsyncMock( + return_value=[] + ) + mock_prisma_client.db.litellm_organizationmembership.find_many = AsyncMock( + return_value=[ + SimpleNamespace( + organization_id="orgA", user_role=LitellmUserRoles.ORG_ADMIN.value + ), + SimpleNamespace( + organization_id="orgB", user_role=LitellmUserRoles.ORG_ADMIN.value + ), + ] + ) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + # Non-admin view + monkeypatch.setattr( + "litellm.proxy.management_endpoints.organization_endpoints._user_has_admin_view", + lambda _: False, + ) + + # Patch downstream aggregator + mocked_response = MagicMock(name="SpendAnalyticsPaginatedResponse") + get_daily_activity_mock = AsyncMock(return_value=mocked_response) + monkeypatch.setattr( + organization_endpoints, "get_daily_activity", get_daily_activity_mock + ) + + auth = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, user_id="regular-user" + ) + await get_organization_daily_activity( + organization_ids=None, + start_date="2024-02-01", + end_date="2024-02-28", + model=None, + api_key=None, + page=1, + page_size=10, + exclude_organization_ids=None, + user_api_key_dict=auth, + ) + + kwargs = get_daily_activity_mock.call_args.kwargs + assert kwargs["entity_id"] == ["orgA", "orgB"] + assert kwargs["start_date"] == "2024-02-01" + assert kwargs["end_date"] == "2024-02-28" + + +@pytest.mark.asyncio +async def test_get_organization_daily_activity_non_admin_unauthorized_org_raises(monkeypatch): + """ + Non-admin requesting an org they aren't ORG_ADMIN for should raise 403. + """ + from types import SimpleNamespace + + from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth + from litellm.proxy.management_endpoints.organization_endpoints import ( + get_organization_daily_activity, + ) + + # Mock prisma client and memberships (only orgA is admin) + mock_prisma_client = AsyncMock() + mock_prisma_client.db.litellm_organizationmembership.find_many = AsyncMock( + return_value=[ + SimpleNamespace( + organization_id="orgA", user_role=LitellmUserRoles.ORG_ADMIN.value + ) + ] + ) + mock_prisma_client.db.litellm_organizationtable.find_many = AsyncMock( + return_value=[] + ) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + # Non-admin view + monkeypatch.setattr( + "litellm.proxy.management_endpoints.organization_endpoints._user_has_admin_view", + lambda _: False, + ) + + auth = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, user_id="regular-user" + ) + + with pytest.raises(HTTPException) as exc: + await get_organization_daily_activity( + organization_ids="orgA,orgX", # orgX is unauthorized + start_date="2024-03-01", + end_date="2024-03-31", + model=None, + api_key=None, + page=1, + page_size=10, + exclude_organization_ids=None, + user_api_key_dict=auth, + ) + assert exc.value.status_code == 403 + @pytest.mark.asyncio async def test_organization_update_object_permissions_no_existing_permission( monkeypatch, From 09afe14a5f104cbef09c7f29142c630bdd506ffa Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 13 Nov 2025 15:49:35 -0800 Subject: [PATCH 02/18] Addressing linting issues --- litellm/proxy/_types.py | 1 + litellm/proxy/db/db_spend_update_writer.py | 27 ++++++++++++++++--- .../organization_endpoints.py | 4 +-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 48307fc6a2e..81c5a2e920e 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2600,6 +2600,7 @@ class SpendLogsPayload(TypedDict): cache_key: str request_tags: str # json str team_id: Optional[str] + organization_id: Optional[str] end_user: Optional[str] requester_ip_address: Optional[str] custom_llm_provider: Optional[str] diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index 8b276c583dd..647a815071e 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -972,6 +972,20 @@ class DBSpendUpdateWriter: ) -> None: ... + @overload + @staticmethod + async def _update_daily_spend( + n_retry_times: int, + prisma_client: PrismaClient, + proxy_logging_obj: ProxyLogging, + daily_spend_transactions: Dict[str, DailyOrganizationSpendTransaction], + entity_type: Literal["org"], + entity_id_field: str, + table_name: str, + unique_constraint_name: str, + ) -> None: + ... + @overload @staticmethod async def _update_daily_spend( @@ -1430,9 +1444,16 @@ class DBSpendUpdateWriter: ) return - # Inject org_id for daily aggregation check - payload_with_org: SpendLogsPayload = dict(payload) - payload_with_org["organization_id"] = org_id + print("org_id", org_id) + print("payload", payload) + + payload_with_org = cast( + SpendLogsPayload, + { + **payload, + "organization_id": org_id, + }, + ) base_daily_transaction = ( await self._common_add_spend_log_transaction_to_daily_transaction( diff --git a/litellm/proxy/management_endpoints/organization_endpoints.py b/litellm/proxy/management_endpoints/organization_endpoints.py index 65764b3b957..99b37c765a8 100644 --- a/litellm/proxy/management_endpoints/organization_endpoints.py +++ b/litellm/proxy/management_endpoints/organization_endpoints.py @@ -281,10 +281,8 @@ async def get_organization_daily_activity( """ from litellm.proxy.proxy_server import ( prisma_client, - proxy_logging_obj, - user_api_key_cache, ) - + if prisma_client is None: raise HTTPException( status_code=500, From cfc44ea2794b9634bd7533d565fdde9fec46d568 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 13 Nov 2025 15:51:49 -0800 Subject: [PATCH 03/18] Remove debug statements --- litellm/proxy/db/db_spend_update_writer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index 647a815071e..1d998f457b5 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -1444,9 +1444,6 @@ class DBSpendUpdateWriter: ) return - print("org_id", org_id) - print("payload", payload) - payload_with_org = cast( SpendLogsPayload, { From ae4dfb53dd4f03658d0850fb258590477685c1d3 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 14 Nov 2025 18:06:49 -0800 Subject: [PATCH 04/18] Add migration --- .../migration.sql | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20251114180624_Add_org_usage_table/migration.sql diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20251114180624_Add_org_usage_table/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20251114180624_Add_org_usage_table/migration.sql new file mode 100644 index 00000000000..74e0eea3134 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20251114180624_Add_org_usage_table/migration.sql @@ -0,0 +1,42 @@ +-- CreateTable +CREATE TABLE "LiteLLM_DailyOrganizationSpend" ( + "id" TEXT NOT NULL, + "organization_id" TEXT, + "date" TEXT NOT NULL, + "api_key" TEXT NOT NULL, + "model" TEXT, + "model_group" TEXT, + "custom_llm_provider" TEXT, + "mcp_namespaced_tool_name" TEXT, + "prompt_tokens" BIGINT NOT NULL DEFAULT 0, + "completion_tokens" BIGINT NOT NULL DEFAULT 0, + "cache_read_input_tokens" BIGINT NOT NULL DEFAULT 0, + "cache_creation_input_tokens" BIGINT NOT NULL DEFAULT 0, + "spend" DOUBLE PRECISION NOT NULL DEFAULT 0.0, + "api_requests" BIGINT NOT NULL DEFAULT 0, + "successful_requests" BIGINT NOT NULL DEFAULT 0, + "failed_requests" BIGINT NOT NULL DEFAULT 0, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "LiteLLM_DailyOrganizationSpend_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "LiteLLM_DailyOrganizationSpend_date_idx" ON "LiteLLM_DailyOrganizationSpend"("date"); + +-- CreateIndex +CREATE INDEX "LiteLLM_DailyOrganizationSpend_organization_id_idx" ON "LiteLLM_DailyOrganizationSpend"("organization_id"); + +-- CreateIndex +CREATE INDEX "LiteLLM_DailyOrganizationSpend_api_key_idx" ON "LiteLLM_DailyOrganizationSpend"("api_key"); + +-- CreateIndex +CREATE INDEX "LiteLLM_DailyOrganizationSpend_model_idx" ON "LiteLLM_DailyOrganizationSpend"("model"); + +-- CreateIndex +CREATE INDEX "LiteLLM_DailyOrganizationSpend_mcp_namespaced_tool_name_idx" ON "LiteLLM_DailyOrganizationSpend"("mcp_namespaced_tool_name"); + +-- CreateIndex +CREATE UNIQUE INDEX "LiteLLM_DailyOrganizationSpend_organization_id_date_api_key_key" ON "LiteLLM_DailyOrganizationSpend"("organization_id", "date", "api_key", "model", "custom_llm_provider", "mcp_namespaced_tool_name"); + From 24c23c62ba0f3e9caca2986fd4d8fc3a8864ae47 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 14 Nov 2025 18:14:22 -0800 Subject: [PATCH 05/18] Linting --- litellm/proxy/spend_tracking/spend_tracking_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index 32d9c4b1f21..10dae1deed7 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -381,6 +381,7 @@ def get_logging_payload( # noqa: PLR0915 model=kwargs.get("model", "") or "", user=metadata.get("user_api_key_user_id", "") or "", team_id=metadata.get("user_api_key_team_id", "") or "", + organization_id=metadata.get("user_api_key_org_id") or "", metadata=safe_dumps(clean_metadata), cache_key=cache_key, spend=kwargs.get("response_cost", 0), From 3d2552be9fd87b7cf57cb3065affe7977e6f1715 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sat, 15 Nov 2025 16:31:37 -0800 Subject: [PATCH 06/18] Fixed failing tests --- .../proxy/spend_tracking/test_spend_management_endpoints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py index 65b245b9e7e..33715eb461a 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py @@ -196,6 +196,7 @@ ignored_keys = [ "endTime", "completionStartTime", "endTime", + "organization_id", "metadata.model_map_information", "metadata.usage_object", "metadata.cold_storage_object_key", From 24f90679f8bf765e8eec9878dd6a22c7e2750b9e Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sat, 22 Nov 2025 10:21:52 -0800 Subject: [PATCH 07/18] Changes for CI/CD Tests --- .../gcs_pub_sub_body/spend_logs_payload.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/logging_callback_tests/gcs_pub_sub_body/spend_logs_payload.json b/tests/logging_callback_tests/gcs_pub_sub_body/spend_logs_payload.json index b25080df0d0..8b2941672b3 100644 --- a/tests/logging_callback_tests/gcs_pub_sub_body/spend_logs_payload.json +++ b/tests/logging_callback_tests/gcs_pub_sub_body/spend_logs_payload.json @@ -10,6 +10,7 @@ "model": "gpt-4o", "user": "", "team_id": "", + "organization_id": "", "metadata": "{\"applied_guardrails\": [], \"batch_models\": null, \"mcp_tool_call_metadata\": null, \"vector_store_request_metadata\": null, \"guardrail_information\": null, \"usage_object\": {\"completion_tokens\": 20, \"prompt_tokens\": 10, \"total_tokens\": 30, \"completion_tokens_details\": null, \"prompt_tokens_details\": null}, \"model_map_information\": {\"model_map_key\": \"gpt-4o\", \"model_map_value\": {\"key\": \"gpt-4o\", \"max_tokens\": 16384, \"max_input_tokens\": 128000, \"max_output_tokens\": 16384, \"input_cost_per_token\": 2.5e-06, \"cache_creation_input_token_cost\": null, \"cache_read_input_token_cost\": 1.25e-06, \"input_cost_per_character\": null, \"input_cost_per_token_above_128k_tokens\": null, \"input_cost_per_token_above_200k_tokens\": null, \"input_cost_per_query\": null, \"input_cost_per_second\": null, \"input_cost_per_audio_token\": null, \"input_cost_per_token_batches\": 1.25e-06, \"output_cost_per_token_batches\": 5e-06, \"output_cost_per_token\": 1e-05, \"output_cost_per_audio_token\": null, \"output_cost_per_character\": null, \"output_cost_per_token_above_128k_tokens\": null, \"output_cost_per_character_above_128k_tokens\": null, \"output_cost_per_token_above_200k_tokens\": null, \"output_cost_per_second\": null, \"output_cost_per_image\": null, \"output_vector_size\": null, \"litellm_provider\": \"openai\", \"mode\": \"chat\", \"supports_system_messages\": true, \"supports_response_schema\": true, \"supports_vision\": true, \"supports_function_calling\": true, \"supports_tool_choice\": true, \"supports_assistant_prefill\": false, \"supports_prompt_caching\": true, \"supports_audio_input\": false, \"supports_audio_output\": false, \"supports_pdf_input\": false, \"supports_embedding_image_input\": false, \"supports_native_streaming\": null, \"supports_web_search\": true, \"supports_reasoning\": false, \"search_context_cost_per_query\": {\"search_context_size_low\": 0.03, \"search_context_size_medium\": 0.035, \"search_context_size_high\": 0.05}, \"tpm\": null, \"rpm\": null, \"supported_openai_params\": [\"frequency_penalty\", \"logit_bias\", \"logprobs\", \"top_logprobs\", \"max_tokens\", \"max_completion_tokens\", \"modalities\", \"prediction\", \"n\", \"presence_penalty\", \"seed\", \"stop\", \"stream\", \"stream_options\", \"temperature\", \"top_p\", \"tools\", \"tool_choice\", \"function_call\", \"functions\", \"max_retries\", \"extra_headers\", \"parallel_tool_calls\", \"audio\", \"response_format\", \"user\"]}}, \"additional_usage_values\": {\"completion_tokens_details\": null, \"prompt_tokens_details\": null}}", "cache_key": "Cache OFF", "spend": 0.00022500000000000002, From dd72ff3abf5b080907103435f4b52dbbc0d57e96 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sat, 22 Nov 2025 12:54:49 -0800 Subject: [PATCH 08/18] Add organization_id to spend logs table --- .../migration.sql | 3 +++ litellm-proxy-extras/litellm_proxy_extras/schema.prisma | 1 + litellm/proxy/schema.prisma | 1 + schema.prisma | 1 + 4 files changed, 6 insertions(+) create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20251122125322_Add organization_id to spend logs/migration.sql diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20251122125322_Add organization_id to spend logs/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20251122125322_Add organization_id to spend logs/migration.sql new file mode 100644 index 00000000000..4ea082f2750 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20251122125322_Add organization_id to spend logs/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "LiteLLM_SpendLogs" ADD COLUMN "organization_id" TEXT; + diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index e11dca1531c..2883dfc4b82 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -304,6 +304,7 @@ model LiteLLM_SpendLogs { cache_key String? @default("") request_tags Json? @default("[]") team_id String? + organization_id String? end_user String? requester_ip_address String? messages Json? @default("{}") diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index e11dca1531c..2883dfc4b82 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -304,6 +304,7 @@ model LiteLLM_SpendLogs { cache_key String? @default("") request_tags Json? @default("[]") team_id String? + organization_id String? end_user String? requester_ip_address String? messages Json? @default("{}") diff --git a/schema.prisma b/schema.prisma index e11dca1531c..2883dfc4b82 100644 --- a/schema.prisma +++ b/schema.prisma @@ -304,6 +304,7 @@ model LiteLLM_SpendLogs { cache_key String? @default("") request_tags Json? @default("[]") team_id String? + organization_id String? end_user String? requester_ip_address String? messages Json? @default("{}") From 031bb3c5d90e429bf65210302ecae80e46cc505c Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 24 Nov 2025 22:40:46 -0800 Subject: [PATCH 09/18] Test prisma changes for Gemini tests --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index a518628afb9..d756755c552 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3047,6 +3047,7 @@ jobs: docker run -d \ -p 4000:4000 \ -e DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/circle_test \ + -e USE_PRISMA_MIGRATE=True \ -e LITELLM_MASTER_KEY="sk-1234" \ -e OPENAI_API_KEY=$OPENAI_API_KEY \ -e GEMINI_API_KEY=$GEMINI_API_KEY \ From 42fa19a152c40bc6faf8bcc07dd82a404029f604 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 25 Nov 2025 11:02:45 -0800 Subject: [PATCH 10/18] Revert "Test prisma changes for Gemini tests" This reverts commit 031bb3c5d90e429bf65210302ecae80e46cc505c. --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d756755c552..a518628afb9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3047,7 +3047,6 @@ jobs: docker run -d \ -p 4000:4000 \ -e DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/circle_test \ - -e USE_PRISMA_MIGRATE=True \ -e LITELLM_MASTER_KEY="sk-1234" \ -e OPENAI_API_KEY=$OPENAI_API_KEY \ -e GEMINI_API_KEY=$GEMINI_API_KEY \ From 359025554ec8265b9531fe75347352e4493eb658 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 25 Nov 2025 11:05:49 -0800 Subject: [PATCH 11/18] =?UTF-8?q?bump:=20version=200.4.6=20=E2=86=92=200.4?= =?UTF-8?q?.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- litellm-proxy-extras/pyproject.toml | 4 ++-- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/litellm-proxy-extras/pyproject.toml b/litellm-proxy-extras/pyproject.toml index 78e34ccd01a..a22e3ca330c 100644 --- a/litellm-proxy-extras/pyproject.toml +++ b/litellm-proxy-extras/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm-proxy-extras" -version = "0.4.6" +version = "0.4.7" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." authors = ["BerriAI"] readme = "README.md" @@ -22,7 +22,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "0.4.6" +version = "0.4.7" version_files = [ "pyproject.toml:version", "../requirements.txt:litellm-proxy-extras==", diff --git a/pyproject.toml b/pyproject.toml index d485772b36e..15094667a53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ websockets = {version = "^13.1.0", optional = true} boto3 = {version = "1.36.0", optional = true} redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"} mcp = {version = "^1.21.2", optional = true, python = ">=3.10"} -litellm-proxy-extras = {version = "0.4.6", optional = true} +litellm-proxy-extras = {version = "0.4.7", optional = true} rich = {version = "13.7.1", optional = true} litellm-enterprise = {version = "0.1.22", optional = true} diskcache = {version = "^5.6.1", optional = true} diff --git a/requirements.txt b/requirements.txt index 3a426d83e31..25b15b9b0fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ sentry_sdk==2.21.0 # for sentry error handling detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests cryptography==44.0.1 tzdata==2025.1 # IANA time zone database -litellm-proxy-extras==0.4.6 # for proxy extras - e.g. prisma migrations +litellm-proxy-extras==0.4.7 # for proxy extras - e.g. prisma migrations ### LITELLM PACKAGE DEPENDENCIES python-dotenv==1.0.1 # for env tiktoken==0.8.0 # for calculating usage From 2794153012b8039f736174e81c68a2abb5b7a80b Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 25 Nov 2025 21:04:05 -0800 Subject: [PATCH 12/18] bump proxy extras --- .../litellm_proxy_extras-0.4.7-py3-none-any.whl | Bin 0 -> 39608 bytes .../dist/litellm_proxy_extras-0.4.7.tar.gz | Bin 0 -> 18629 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.7-py3-none-any.whl create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.7.tar.gz diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.7-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.7-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..376c1e0d0704e203a7ce2573c175c3a15d111281 GIT binary patch literal 39608 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 T5D@%NpK!zw5NY0j`{};`yttH4 literal 0 HcmV?d00001 diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.7.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.7.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..0bb0fd9c74a9439c761807fff66689bbf214509b GIT binary patch literal 18629 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 literal 0 HcmV?d00001 From 5b645d5f2f7dfa6d5b398048e171749f4fee77a8 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 26 Nov 2025 10:32:49 -0800 Subject: [PATCH 13/18] Update spend logs writer to add organization_id --- litellm/proxy/db/db_spend_update_writer.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index f404c640a08..b6f98393700 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -118,6 +118,21 @@ class DBSpendUpdateWriter: payload["startTime"] = payload["startTime"].isoformat() if isinstance(payload["endTime"], datetime): payload["endTime"] = payload["endTime"].isoformat() + + # Override organization_id and team_id from parameters if provided + # This ensures the correct values are set even if metadata is missing them + # Convert empty strings to None for optional fields to avoid database issues + if org_id is not None and org_id != "": + payload["organization_id"] = org_id + elif payload.get("organization_id") == "": + # Convert empty string to None for optional field + payload["organization_id"] = None + + if team_id is not None and team_id != "": + payload["team_id"] = team_id + elif payload.get("team_id") == "": + # Convert empty string to None for optional field + payload["team_id"] = None asyncio.create_task( self._update_user_db( From 40c21c4081106ebe705eb5d1b314bef3972439bf Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 26 Nov 2025 11:14:27 -0800 Subject: [PATCH 14/18] test fixes --- litellm/proxy/spend_tracking/spend_tracking_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index 2dde15c4cce..e3d774190bf 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -380,7 +380,7 @@ def get_logging_payload( # noqa: PLR0915 model=kwargs.get("model", "") or "", user=metadata.get("user_api_key_user_id", "") or "", team_id=metadata.get("user_api_key_team_id", "") or "", - organization_id=metadata.get("user_api_key_org_id") or "", + organization_id=metadata.get("user_api_key_org_id") or None, metadata=safe_dumps(clean_metadata), cache_key=cache_key, spend=kwargs.get("response_cost", 0), From 13848530b7683b879204b51662f2facd5acfa7a6 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 26 Nov 2025 11:27:06 -0800 Subject: [PATCH 15/18] Revert "test fixes" This reverts commit 40c21c4081106ebe705eb5d1b314bef3972439bf. --- litellm/proxy/spend_tracking/spend_tracking_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index e3d774190bf..2dde15c4cce 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -380,7 +380,7 @@ def get_logging_payload( # noqa: PLR0915 model=kwargs.get("model", "") or "", user=metadata.get("user_api_key_user_id", "") or "", team_id=metadata.get("user_api_key_team_id", "") or "", - organization_id=metadata.get("user_api_key_org_id") or None, + organization_id=metadata.get("user_api_key_org_id") or "", metadata=safe_dumps(clean_metadata), cache_key=cache_key, spend=kwargs.get("response_cost", 0), From b11a803dd36e5c45c4814cfc70ad43d5c2c68046 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 26 Nov 2025 12:39:04 -0800 Subject: [PATCH 16/18] Revert "Update spend logs writer to add organization_id" This reverts commit 5b645d5f2f7dfa6d5b398048e171749f4fee77a8. --- litellm/proxy/db/db_spend_update_writer.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index b6f98393700..f404c640a08 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -118,21 +118,6 @@ class DBSpendUpdateWriter: payload["startTime"] = payload["startTime"].isoformat() if isinstance(payload["endTime"], datetime): payload["endTime"] = payload["endTime"].isoformat() - - # Override organization_id and team_id from parameters if provided - # This ensures the correct values are set even if metadata is missing them - # Convert empty strings to None for optional fields to avoid database issues - if org_id is not None and org_id != "": - payload["organization_id"] = org_id - elif payload.get("organization_id") == "": - # Convert empty string to None for optional field - payload["organization_id"] = None - - if team_id is not None and team_id != "": - payload["team_id"] = team_id - elif payload.get("team_id") == "": - # Convert empty string to None for optional field - payload["team_id"] = None asyncio.create_task( self._update_user_db( From 02e99f22f8ba6b818c6ad631449983b1e38c2c41 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 26 Nov 2025 13:29:27 -0800 Subject: [PATCH 17/18] Revert "Revert "Update spend logs writer to add organization_id"" This reverts commit b11a803dd36e5c45c4814cfc70ad43d5c2c68046. --- litellm/proxy/db/db_spend_update_writer.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index f404c640a08..b6f98393700 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -118,6 +118,21 @@ class DBSpendUpdateWriter: payload["startTime"] = payload["startTime"].isoformat() if isinstance(payload["endTime"], datetime): payload["endTime"] = payload["endTime"].isoformat() + + # Override organization_id and team_id from parameters if provided + # This ensures the correct values are set even if metadata is missing them + # Convert empty strings to None for optional fields to avoid database issues + if org_id is not None and org_id != "": + payload["organization_id"] = org_id + elif payload.get("organization_id") == "": + # Convert empty string to None for optional field + payload["organization_id"] = None + + if team_id is not None and team_id != "": + payload["team_id"] = team_id + elif payload.get("team_id") == "": + # Convert empty string to None for optional field + payload["team_id"] = None asyncio.create_task( self._update_user_db( From ce9474a03913e63fc6edc26e91fa173484c56244 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 26 Nov 2025 13:31:04 -0800 Subject: [PATCH 18/18] Updating spend writer --- litellm/proxy/db/db_spend_update_writer.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index b6f98393700..6c9289e3ff6 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -119,20 +119,11 @@ class DBSpendUpdateWriter: if isinstance(payload["endTime"], datetime): payload["endTime"] = payload["endTime"].isoformat() - # Override organization_id and team_id from parameters if provided - # This ensures the correct values are set even if metadata is missing them - # Convert empty strings to None for optional fields to avoid database issues if org_id is not None and org_id != "": payload["organization_id"] = org_id - elif payload.get("organization_id") == "": - # Convert empty string to None for optional field - payload["organization_id"] = None - + if team_id is not None and team_id != "": payload["team_id"] = team_id - elif payload.get("team_id") == "": - # Convert empty string to None for optional field - payload["team_id"] = None asyncio.create_task( self._update_user_db(