From 7ffaa6f74e531f8fcc50769fdb8100a5f82da44f Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Mon, 9 Mar 2026 18:48:27 -0400 Subject: [PATCH 1/2] fix(tag-usage): remove broken compute_tag_metadata_totals causing cost panel to show 0 The tag daily activity endpoint used compute_tag_metadata_totals which deduplicates by request_id, but LiteLLM_DailyTagSpend stores aggregated daily records where request_id is either NULL or stale. This caused all metadata totals (total_spend, total_requests, etc.) to be 0 in the UI cost panel. Now uses the same standard totals as every other entity type. Co-Authored-By: Claude Opus 4.6 --- .../tag_management_endpoints.py | 3 +- .../test_common_daily_activity.py | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/management_endpoints/tag_management_endpoints.py b/litellm/proxy/management_endpoints/tag_management_endpoints.py index 95b7300992c..f085fa4145f 100644 --- a/litellm/proxy/management_endpoints/tag_management_endpoints.py +++ b/litellm/proxy/management_endpoints/tag_management_endpoints.py @@ -21,7 +21,6 @@ from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.management_endpoints.common_daily_activity import ( SpendAnalyticsPaginatedResponse, - compute_tag_metadata_totals, get_daily_activity, ) from litellm.proxy.management_helpers.utils import handle_budget_for_entity @@ -554,5 +553,5 @@ async def get_tag_daily_activity( api_key=api_key, page=page, page_size=page_size, - metadata_metrics_func=compute_tag_metadata_totals, + metadata_metrics_func=None, ) diff --git a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py index 1e357d2f02e..00a22f9bf7d 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py +++ b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py @@ -405,6 +405,93 @@ async def test_get_api_key_metadata_regenerated_key_uses_most_recent_deleted_rec assert result["old-key-hash"]["team_id"] == "latest-team" +@pytest.mark.asyncio +async def test_tag_daily_activity_metadata_totals_not_zero(): + """Test that tag daily activity returns correct metadata totals. + + Regression test: previously compute_tag_metadata_totals skipped records + with NULL request_id, causing metadata totals (total_spend, etc.) to be 0. + """ + mock_prisma = MagicMock() + mock_prisma.db = MagicMock() + + # Create mock tag spend records (request_id is NULL for aggregated rows) + mock_record_1 = MagicMock() + mock_record_1.request_id = None # NULL in aggregated daily rows + mock_record_1.tag = "production" + mock_record_1.date = "2024-01-01" + mock_record_1.api_key = "key-1" + mock_record_1.model = "gpt-4" + mock_record_1.model_group = "gpt-4" + mock_record_1.custom_llm_provider = "openai" + mock_record_1.mcp_namespaced_tool_name = None + mock_record_1.endpoint = "/chat/completions" + mock_record_1.spend = 25.0 + mock_record_1.prompt_tokens = 500 + mock_record_1.completion_tokens = 200 + mock_record_1.cache_read_input_tokens = 0 + mock_record_1.cache_creation_input_tokens = 0 + mock_record_1.api_requests = 10 + mock_record_1.successful_requests = 9 + mock_record_1.failed_requests = 1 + + mock_record_2 = MagicMock() + mock_record_2.request_id = None + mock_record_2.tag = "staging" + mock_record_2.date = "2024-01-01" + mock_record_2.api_key = "key-2" + mock_record_2.model = "gpt-3.5-turbo" + mock_record_2.model_group = "gpt-3.5-turbo" + mock_record_2.custom_llm_provider = "openai" + mock_record_2.mcp_namespaced_tool_name = None + mock_record_2.endpoint = "/chat/completions" + mock_record_2.spend = 5.0 + mock_record_2.prompt_tokens = 300 + mock_record_2.completion_tokens = 100 + mock_record_2.cache_read_input_tokens = 0 + mock_record_2.cache_creation_input_tokens = 0 + mock_record_2.api_requests = 5 + mock_record_2.successful_requests = 5 + mock_record_2.failed_requests = 0 + + mock_table = MagicMock() + mock_table.count = AsyncMock(return_value=2) + mock_table.find_many = AsyncMock(return_value=[mock_record_1, mock_record_2]) + mock_prisma.db.litellm_dailytagspend = mock_table + mock_prisma.db.litellm_verificationtoken = MagicMock() + mock_prisma.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[]) + + result = await get_daily_activity( + prisma_client=mock_prisma, + table_name="litellm_dailytagspend", + entity_id_field="tag", + entity_id=None, + entity_metadata_field=None, + start_date="2024-01-01", + end_date="2024-01-01", + model=None, + api_key=None, + page=1, + page_size=1000, + metadata_metrics_func=None, # No custom func — matches the fix + ) + + # Metadata totals must reflect actual spend, NOT be zero + assert result.metadata.total_spend == 30.0 # 25.0 + 5.0 + assert result.metadata.total_api_requests == 15 # 10 + 5 + assert result.metadata.total_successful_requests == 14 # 9 + 5 + assert result.metadata.total_failed_requests == 1 + assert result.metadata.total_tokens == 1100 # (500+200) + (300+100) + + # Verify breakdown still works + assert len(result.results) == 1 + daily = result.results[0] + assert "production" in daily.breakdown.entities + assert "staging" in daily.breakdown.entities + assert daily.breakdown.entities["production"].metrics.spend == 25.0 + assert daily.breakdown.entities["staging"].metrics.spend == 5.0 + + @pytest.mark.asyncio async def test_aggregated_activity_preserves_metadata_for_deleted_keys(): """Test that the full aggregation pipeline should preserve metadata for deleted keys.""" From 88d0f9d8348db8928849b4807c13c91de2f0015e Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Tue, 10 Mar 2026 13:56:15 -0400 Subject: [PATCH 2/2] added comments --- .../tag_management_endpoints.py | 7 +++ .../test_common_daily_activity.py | 59 ++----------------- 2 files changed, 12 insertions(+), 54 deletions(-) diff --git a/litellm/proxy/management_endpoints/tag_management_endpoints.py b/litellm/proxy/management_endpoints/tag_management_endpoints.py index f085fa4145f..b7714d3f866 100644 --- a/litellm/proxy/management_endpoints/tag_management_endpoints.py +++ b/litellm/proxy/management_endpoints/tag_management_endpoints.py @@ -553,5 +553,12 @@ async def get_tag_daily_activity( api_key=api_key, page=page, page_size=page_size, + # metadata_metrics_func=None because litellm_dailytagspend rows are + # pre-aggregated per (date, tag, model, …) and have no request_id. + # Deduplication across tags is therefore not possible at this level — + # a request tagged with N tags contributes its spend to N separate rows, + # so passing compute_tag_metadata_totals would double-count spend when + # multiple tags are present. The panel is primarily used to inspect + # individual tags, making this trade-off acceptable. metadata_metrics_func=None, ) diff --git a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py index 00a22f9bf7d..54fbac1264b 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py +++ b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py @@ -10,7 +10,6 @@ sys.path.insert( from litellm.proxy.management_endpoints.common_daily_activity import ( _is_user_agent_tag, - compute_tag_metadata_totals, get_api_key_metadata, get_daily_activity, get_daily_activity_aggregated, @@ -77,57 +76,6 @@ def test_is_user_agent_tag(): assert _is_user_agent_tag("user-agent-tag") is False # no colon -def test_compute_tag_metadata_totals(): - """Test compute_tag_metadata_totals function.""" - # Create mock records - class MockRecord: - def __init__(self, request_id, tag, spend, prompt_tokens=10, completion_tokens=5): - self.request_id = request_id - self.tag = tag - self.spend = spend - self.prompt_tokens = prompt_tokens - self.completion_tokens = completion_tokens - self.total_tokens = prompt_tokens + completion_tokens - self.cache_read_input_tokens = 0 - self.cache_creation_input_tokens = 0 - self.api_requests = 1 - self.successful_requests = 1 - self.failed_requests = 0 - - # Test deduplication by request_id (keeps max spend) - records = [ - MockRecord("req-1", "production", spend=10.0), - MockRecord("req-1", "staging", spend=20.0), # Higher spend, should be kept - MockRecord("req-2", "production", spend=15.0), - ] - result = compute_tag_metadata_totals(records) - assert result.spend == 35.0 # 20.0 + 15.0 (deduplicated req-1) - assert result.prompt_tokens == 20 # 10 + 10 (only deduplicated records) - assert result.completion_tokens == 10 # 5 + 5 (only deduplicated records) - - # Test ignoring user-agent tags - records_with_ua = [ - MockRecord("req-1", "production", spend=10.0), - MockRecord("req-1", "user-agent:chrome", spend=50.0), # Should be ignored - MockRecord("req-2", "staging", spend=15.0), - ] - result = compute_tag_metadata_totals(records_with_ua) - assert result.spend == 25.0 # 10.0 + 15.0 (user-agent ignored) - - # Test ignoring records without request_id - records_no_req_id = [ - MockRecord("req-1", "production", spend=10.0), - MockRecord(None, "staging", spend=20.0), # Should be ignored - ] - result = compute_tag_metadata_totals(records_no_req_id) - assert result.spend == 10.0 - - # Test empty records - result = compute_tag_metadata_totals([]) - assert result.spend == 0.0 - assert result.prompt_tokens == 0 - - @pytest.mark.asyncio async def test_get_daily_activity_aggregated_with_endpoint_breakdown(): """Test that endpoint breakdown is included in aggregated daily activity.""" @@ -409,8 +357,11 @@ async def test_get_api_key_metadata_regenerated_key_uses_most_recent_deleted_rec async def test_tag_daily_activity_metadata_totals_not_zero(): """Test that tag daily activity returns correct metadata totals. - Regression test: previously compute_tag_metadata_totals skipped records - with NULL request_id, causing metadata totals (total_spend, etc.) to be 0. + Regression test: the tag endpoint previously passed metadata_metrics_func= + compute_tag_metadata_totals, which skipped every row whose request_id is + NULL. Rows in litellm_dailytagspend are pre-aggregated and always have + NULL request_id, so the totals panel showed $0. The fix is to pass + metadata_metrics_func=None so the fallback aggregation path is used instead. """ mock_prisma = MagicMock() mock_prisma.db = MagicMock()