mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(proxy): end the cursorless grouped log page at the capped total
A page size that does not divide SPEND_LOGS_PAGINATION_COUNT_CAP left the last page starting inside the capped window and reading past it, so the rows disagreed with the total reported next to them. The page limit now stops at the end of that window, and has_more plus next_session_cursor still hand back a cursor for walking further. Claude-Session: https://claude.ai/code/session_01ESi9JwaXDww1vP3Qsrr4Mz
This commit is contained in:
parent
4295bf823a
commit
cb434742c6
2 changed files with 48 additions and 9 deletions
|
|
@ -2947,9 +2947,9 @@ async def _ui_session_grouped_spend_logs(
|
|||
page depth does not degrade the query plan. A request for ``page > 1``
|
||||
without a cursor (the UI jumping straight to the last page, or back to a
|
||||
page it never walked through) falls back to ``OFFSET (page - 1) *
|
||||
page_size``; a page starting at or past ``SPEND_LOGS_PAGINATION_COUNT_CAP``
|
||||
lies outside the capped total the client is given, so it returns no rows
|
||||
without running the query and the sort bound stays capped. Each session is represented
|
||||
page_size``, trimmed to the end of the ``SPEND_LOGS_PAGINATION_COUNT_CAP``
|
||||
window the capped ``total`` promises, so a page never runs past that total
|
||||
and one starting at or past it returns no rows without a query. Each session is represented
|
||||
by its newest non-MCP row, enriched by ``_build_ui_spend_logs_response``
|
||||
exactly like the flat listing, and the response carries
|
||||
``next_session_cursor`` / ``has_more`` while ``total`` counts sessions
|
||||
|
|
@ -2969,8 +2969,8 @@ async def _ui_session_grouped_spend_logs(
|
|||
cursor_params: Final[tuple[object, ...]] = cursor if cursor else ()
|
||||
limit_index: Final = next_param_index + len(cursor_params)
|
||||
offset: Final = (page - 1) * page_size if cursor is None else 0
|
||||
beyond_capped_window: Final = offset >= SPEND_LOGS_PAGINATION_COUNT_CAP
|
||||
offset_params: Final[tuple[int, ...]] = (offset,) if offset and not beyond_capped_window else ()
|
||||
page_limit: Final = min(page_size, SPEND_LOGS_PAGINATION_COUNT_CAP - offset)
|
||||
offset_params: Final[tuple[int, ...]] = (offset,) if offset and page_limit > 0 else ()
|
||||
offset_clause: Final = f"OFFSET ${limit_index + 1}" if offset_params else ""
|
||||
|
||||
page_query: Final = f"""
|
||||
|
|
@ -2986,12 +2986,12 @@ async def _ui_session_grouped_spend_logs(
|
|||
"""
|
||||
page_rows: Final[Sequence[_SessionPageRow]] = (
|
||||
()
|
||||
if beyond_capped_window
|
||||
else await _query_raw(prisma_client, page_query, *sql_params, *cursor_params, page_size + 1, *offset_params)
|
||||
if page_limit <= 0
|
||||
else await _query_raw(prisma_client, page_query, *sql_params, *cursor_params, page_limit + 1, *offset_params)
|
||||
)
|
||||
|
||||
has_more: Final = len(page_rows) > page_size
|
||||
visible_rows: Final = page_rows[:page_size]
|
||||
has_more: Final = len(page_rows) > page_limit
|
||||
visible_rows: Final = page_rows[:page_limit]
|
||||
next_cursor: Final = (
|
||||
f"{visible_rows[-1]['last_activity']}|{visible_rows[-1]['api_key']}|{visible_rows[-1]['session_key']}"
|
||||
if has_more and visible_rows
|
||||
|
|
|
|||
|
|
@ -6853,6 +6853,45 @@ async def test_ui_view_spend_logs_group_by_session_page_past_count_cap_is_empty(
|
|||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_group_by_session_last_page_stops_at_the_capped_total(client, monkeypatch):
|
||||
"""A page size that does not divide the cap still ends the last page at the capped total it reports."""
|
||||
cap = spend_management_endpoints.SPEND_LOGS_PAGINATION_COUNT_CAP
|
||||
sessions = tuple((f"sess-{index:06d}", "2026-08-29 10:00:00") for index in range(cap + 50))
|
||||
mock_prisma = _session_grouped_paginating_prisma(sessions)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.spend_tracking.spend_management_endpoints._is_admin_view_safe",
|
||||
lambda user_api_key_dict: True,
|
||||
)
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin_user"
|
||||
)
|
||||
try:
|
||||
start_date, end_date = _default_date_range()
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"group_by_session": "true",
|
||||
"page": cap // 7 + 1,
|
||||
"page_size": 7,
|
||||
},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
data = response.json()
|
||||
assert data["total"] == cap
|
||||
assert [row["request_id"] for row in data["data"]] == [
|
||||
f"req-sess-{index:06d}" for index in range(cap - cap % 7, cap)
|
||||
]
|
||||
assert data["has_more"] is True
|
||||
assert data["next_session_cursor"] is not None
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_group_by_session_offset_for_non_starttime_sort(
|
||||
client, monkeypatch
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue