fix(spend/daily-activity): stable offset pagination via id tiebreaker (#30164) (#30167)

date alone is not a unique sort key for LiteLLM_DailyUserSpend or
LiteLLM_DailyTeamSpend (many rows per date: api_key x model x
model_group x provider x endpoint). Offset pagination over a
non-unique sort landed on arbitrary boundaries, so a client paging
through all results and summing per-page metrics (the Usage dashboard)
got non-deterministic totals - sometimes inflated, sometimes deflated,
different at different page_size values.

Adding the row's UUID id (present on both tables) as a secondary sort
gives every page a stable cursor. order=[{date desc}, {id asc}].

Fixes #30164
This commit is contained in:
hcl 2026-06-11 18:23:32 +08:00 • committed by GitHub
parent 0dee0143e0
commit 8929445037
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 1 deletions

View file

@ -918,11 +918,21 @@ async def get_daily_activity(
where=where_conditions
)
# Fetch paginated results
# Fetch paginated results.
# ``date`` alone is not a unique sort key -- a busy tenant has many
# rows per date (one per api_key, model, model_group, provider,
# endpoint, ...), so offset pagination over ``date desc`` lands on
# arbitrary boundaries and the same row can be skipped on one page
# and returned on another. A client that pages through and sums the
# per-page metrics (the Usage dashboard) then gets a non-deterministic
# total. Adding ``id`` (the row's UUID primary key, present on both
# LiteLLM_DailyUserSpend and LiteLLM_DailyTeamSpend) as a tiebreaker
# gives every page a stable cursor (#30164).
daily_spend_data = await getattr(prisma_client.db, table_name).find_many(
where=where_conditions,
order=[
{"date": "desc"},
{"id": "asc"},
],
skip=(page - 1) * page_size,
take=page_size,

View file

@ -57,6 +57,51 @@ async def test_get_daily_activity_empty_entity_id_list():
assert where_conditions["team_id"] == {"in": []}
@pytest.mark.asyncio
async def test_get_daily_activity_order_has_id_tiebreaker():
"""Regression for #30164.
``date`` alone is not a unique sort key for either
``LiteLLM_DailyUserSpend`` or ``LiteLLM_DailyTeamSpend`` -- a busy
tenant has many rows per date (one per api_key, model, model_group,
provider, endpoint, ...). Offset pagination over a non-unique sort
landed on arbitrary page boundaries between queries, so summing
per-page totals across pages produced non-deterministic results
(sometimes inflated, sometimes deflated). The tiebreaker on the
UUID primary key pins the row order so a client paging through all
results gets the correct total.
"""
mock_prisma = MagicMock()
mock_prisma.db = MagicMock()
mock_table = MagicMock()
mock_table.count = AsyncMock(return_value=0)
mock_table.find_many = AsyncMock(return_value=[])
mock_prisma.db.litellm_verificationtoken = MagicMock()
mock_prisma.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[])
mock_prisma.db.litellm_dailyspend = mock_table
await get_daily_activity(
prisma_client=mock_prisma,
table_name="litellm_dailyspend",
entity_id_field="team_id",
entity_id="team-1",
entity_metadata_field=None,
start_date="2024-01-01",
end_date="2024-01-02",
model=None,
api_key=None,
page=1,
page_size=10,
)
mock_table.find_many.assert_called_once()
order = mock_table.find_many.call_args[1]["order"]
assert order == [{"date": "desc"}, {"id": "asc"}], (
f"order must include the id tiebreaker after date for stable offset "
f"pagination (see #30164); got {order!r}"
)
def test_is_user_agent_tag():
"""Test _is_user_agent_tag function."""
# Test None and empty string