From a40009eaafe0084a03e49d750aededafe2a15c56 Mon Sep 17 00:00:00 2001 From: Harshith Gujjeti <153299927+Harshxth@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:17:24 -0400 Subject: [PATCH] fix: coalesce null aggregates in update_metrics for no-spend keys (#29945) --- .../common_daily_activity.py | 29 ++++++++---- .../test_common_daily_activity.py | 46 +++++++++++++++++++ 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/management_endpoints/common_daily_activity.py b/litellm/proxy/management_endpoints/common_daily_activity.py index e6b040ef2ee..341a8767db0 100644 --- a/litellm/proxy/management_endpoints/common_daily_activity.py +++ b/litellm/proxy/management_endpoints/common_daily_activity.py @@ -35,16 +35,25 @@ _PRISMA_TO_PG_TABLE: Dict[str, str] = { def update_metrics(existing_metrics: SpendMetrics, record: Any) -> SpendMetrics: - """Update metrics with new record data.""" - existing_metrics.spend += record.spend - existing_metrics.prompt_tokens += record.prompt_tokens - existing_metrics.completion_tokens += record.completion_tokens - existing_metrics.total_tokens += record.prompt_tokens + record.completion_tokens - existing_metrics.cache_read_input_tokens += record.cache_read_input_tokens - existing_metrics.cache_creation_input_tokens += record.cache_creation_input_tokens - existing_metrics.api_requests += record.api_requests - existing_metrics.successful_requests += record.successful_requests - existing_metrics.failed_requests += record.failed_requests + """Update metrics with new record data. + + Rollup rows can carry None for numeric fields when SUM() spans zero rows + (e.g. a key with no spend), so coalesce to 0 before accumulating to avoid + a TypeError. Mirrors the handling in ``_record_to_spend_metrics``. + """ + prompt_tokens = record.prompt_tokens or 0 + completion_tokens = record.completion_tokens or 0 + existing_metrics.spend += record.spend or 0.0 + existing_metrics.prompt_tokens += prompt_tokens + existing_metrics.completion_tokens += completion_tokens + existing_metrics.total_tokens += prompt_tokens + completion_tokens + existing_metrics.cache_read_input_tokens += record.cache_read_input_tokens or 0 + existing_metrics.cache_creation_input_tokens += ( + record.cache_creation_input_tokens or 0 + ) + existing_metrics.api_requests += record.api_requests or 0 + existing_metrics.successful_requests += record.successful_requests or 0 + existing_metrics.failed_requests += record.failed_requests or 0 return existing_metrics 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 bf507cb065d..2d2c18bb46e 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 @@ -1,5 +1,6 @@ import os import sys +from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock import pytest @@ -12,10 +13,13 @@ from litellm.proxy.management_endpoints.common_daily_activity import ( _adjust_dates_for_timezone, _build_aggregated_sql_query, _is_user_agent_tag, + _record_to_spend_metrics, get_api_key_metadata, get_daily_activity, get_daily_activity_aggregated, + update_metrics, ) +from litellm.types.proxy.management_endpoints.common_daily_activity import SpendMetrics @pytest.mark.asyncio @@ -810,3 +814,45 @@ async def test_get_daily_activity_aggregated_empty_result_set(): assert result.metadata.total_failed_requests == 0 assert result.metadata.total_cache_read_input_tokens == 0 assert result.metadata.total_cache_creation_input_tokens == 0 + + +def _no_spend_record(): + """A rollup row for a key with no spend, where SUM() returns NULL (None).""" + return SimpleNamespace( + spend=None, + prompt_tokens=None, + completion_tokens=None, + cache_read_input_tokens=None, + cache_creation_input_tokens=None, + api_requests=None, + successful_requests=None, + failed_requests=None, + ) + + +def test_record_to_spend_metrics_handles_none_values(): + """Keys with no spend produce NULL aggregates; treat them as zero, not a crash.""" + metrics = _record_to_spend_metrics(_no_spend_record()) + assert metrics.spend == 0 + assert metrics.prompt_tokens == 0 + assert metrics.completion_tokens == 0 + assert metrics.total_tokens == 0 + assert metrics.api_requests == 0 + assert metrics.successful_requests == 0 + assert metrics.failed_requests == 0 + assert metrics.cache_read_input_tokens == 0 + assert metrics.cache_creation_input_tokens == 0 + + +def test_update_metrics_handles_none_values(): + """update_metrics should coalesce NULL aggregates instead of raising TypeError.""" + metrics = update_metrics(SpendMetrics(), _no_spend_record()) + assert metrics.spend == 0 + assert metrics.prompt_tokens == 0 + assert metrics.completion_tokens == 0 + assert metrics.total_tokens == 0 + assert metrics.api_requests == 0 + assert metrics.successful_requests == 0 + assert metrics.failed_requests == 0 + assert metrics.cache_read_input_tokens == 0 + assert metrics.cache_creation_input_tokens == 0