mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
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:
parent
0dee0143e0
commit
8929445037
2 changed files with 56 additions and 1 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue