mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
Merge pull request #23202 from mubashir1osmani/fix/tag-usage-cost-panel-zero
fix: tag usage cost panel zero
This commit is contained in:
commit
26febf11a7
2 changed files with 98 additions and 54 deletions
|
|
@ -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,12 @@ 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 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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
@ -405,6 +353,96 @@ 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: 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()
|
||||
|
||||
# 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."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue