fix(spend): preserve bounded spans for session tie ordering
Some checks failed
LiteLLM Rust / rust-wheel (push) Has been cancelled
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled

This commit is contained in:
shrey kharbanda 2026-10-23 16:37:59 -07:00
parent f3e2af3486
commit de2d96e44c
4 changed files with 37 additions and 49 deletions

View file

@ -51,6 +51,7 @@ from litellm.proxy.auth.authorization_dependencies import (
LogTeamLookupDependency,
)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.db.db_span import db_span
from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup
from litellm.proxy.spend_tracking.spend_capture_rate import (
ProviderBillingCredentialMissing,
@ -3315,17 +3316,18 @@ class _SessionHeadWalk:
size=next_size,
)
if len(frozenset(row["sort_time"] for row in all_heads)) < len(all_heads):
page_rows: Final[Sequence[_SessionPageRow]] = await _query_raw(
self.prisma_client,
"""SELECT session_key, api_key, last_activity
FROM unnest($1::text[], $2::text[], $3::text[]) AS heads(session_key, api_key, last_activity)
ORDER BY last_activity::timestamp DESC, session_key DESC, api_key DESC
LIMIT $4""",
[row["session_key"] for row in all_heads],
[row["api_key"] for row in all_heads],
[row["last_activity"] for row in all_heads],
self.page_limit + 1,
)
async with db_span("find_many", "LiteLLM_SpendLogs"):
page_rows: Final[Sequence[_SessionPageRow]] = await _query_raw(
self.prisma_client,
"""SELECT session_key, api_key, last_activity
FROM unnest($1::text[], $2::text[], $3::text[]) AS heads(session_key, api_key, last_activity)
ORDER BY last_activity::timestamp DESC, session_key DESC, api_key DESC
LIMIT $4""",
[row["session_key"] for row in all_heads],
[row["api_key"] for row in all_heads],
[row["last_activity"] for row in all_heads],
self.page_limit + 1,
)
return tuple(page_rows)
in_page_order: Final = sorted(
all_heads, key=lambda row: (row["sort_time"], row["session_key"], row["api_key"]), reverse=True

View file

@ -62,6 +62,7 @@ class _SeedRow:
start_time: str
api_key: str = "K"
call_type: str = "acompletion"
end_time: str | None = None
def _hand_placed_rows(marker: str) -> tuple[_SeedRow, ...]:
@ -104,7 +105,15 @@ def _insert_rows(rows: tuple[_SeedRow, ...]) -> None:
'request_id, call_type, api_key, session_id, "startTime", "endTime", model, metadata, messages, response'
") VALUES (%s, %s, %s, %s, %s, %s, 'm', jsonb_build_object('user_api_key_alias', %s::text), '{}', '{}')",
[
(row.request_id, row.call_type, row.api_key, row.session_id, row.start_time, row.start_time, KEY_ALIAS)
(
row.request_id,
row.call_type,
row.api_key,
row.session_id,
row.start_time,
row.end_time or row.start_time,
KEY_ALIAS,
)
for row in rows
],
)
@ -364,6 +373,8 @@ def _small_rows(marker: str) -> tuple[_SeedRow, ...]:
_SeedRow(f"{marker}-mcp-old", f"{marker}-mcp", at(45)),
_SeedRow(f"{marker}-mcp-llm", f"{marker}-mcp", at(80)),
_SeedRow(f"{marker}-mcp-tool", f"{marker}-mcp", at(90), call_type="call_mcp_tool"),
_SeedRow(f"{marker}-mcp-agent-final", f"{marker}-mcp", at(50), call_type="asend_message", end_time=at(85)),
_SeedRow(f"{marker}-mcp-agent-newer", f"{marker}-mcp", at(70), call_type="asend_message", end_time=at(75)),
_SeedRow(f"{marker}-shared", f"{marker}-other", at(95), api_key=f"{marker}-key-b"),
)
@ -406,7 +417,7 @@ def _tie_as_one(request_id: str) -> str:
def _newest_first(marker: str) -> list[tuple[str, str]]:
return [
(f"{marker}-shared", f"{marker}-key-b"),
(f"{marker}-mcp-llm", "K"),
(f"{marker}-mcp-agent-final", "K"),
(f"{marker}-tie", "K"),
(f"{marker}-shared-b", f"{marker}-key-b"),
(f"{marker}-shared-a", f"{marker}-key-a"),
@ -419,7 +430,8 @@ def test_session_view_lists_each_session_once(gateway: Gateway, small_seed: str)
page: Final = _small_page(gateway, sort_order="desc", page_size="50", page="1")
assert _sessions(page) == _newest_first(small_seed), (
"each session once, represented by its newest non-MCP row: a row without a session_id newer than the rows "
"each session once, preferring the final agent outcome, then the newest non-MCP row: "
"a row without a session_id newer than the rows "
"adopting its request_id joins them, two rows at one startTime are one session, one session_id under two "
"api_keys is two sessions, and a row whose request_id equals another session's key stays in its own session"
)

View file

@ -7330,22 +7330,13 @@ async def test_ui_view_spend_logs_group_by_session_first_page(client, monkeypatc
assert request_ids == ["req-1", "req-solo"], "representatives must follow the page order"
assert data["data"][0]["session_total_count"] == 3
assert data["data"][1]["session_total_count"] == 1
emitted = [call.args for call in mock_prisma.db.query_raw.await_args_list]
page_query_sql = emitted[0][0]
assert "OFFSET" not in page_query_sql
assert "HAVING" not in page_query_sql
rep_call = emitted[2]
assert rep_call[-2] == ["sess-1", "req-solo"]
assert rep_call[-1] == ["hashed-key", "hashed-key"]
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_group_by_session_cursor_page(client, monkeypatch):
"""A session_cursor becomes a WHERE keyset predicate instead of an OFFSET."""
"""A cursor continues after the previously displayed session."""
page_rows = [_session_page_row("sess-2", "2026-08-29 07:00:00")]
reps = [_session_representative_row("req-2", "sess-2")]
mock_prisma = _session_grouped_mock_prisma(page_rows, 3, reps)
@ -7375,17 +7366,6 @@ async def test_ui_view_spend_logs_group_by_session_cursor_page(client, monkeypat
assert data["has_more"] is False
assert data["next_session_cursor"] is None
assert [row["request_id"] for row in data["data"]] == ["req-2"]
page_query_call = mock_prisma.db.query_raw.await_args_list[0]
page_query_sql = page_query_call.args[0]
assert "HAVING" not in page_query_sql
assert "OFFSET" not in page_query_sql
cursor_index = page_query_call.args.index("2026-08-29 09:00:00")
assert page_query_call.args[cursor_index : cursor_index + 3] == (
"2026-08-29 09:00:00",
"req-solo",
"hashed-key",
), "cursor params must line up with (startTime, session key, api_key)"
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@ -7475,15 +7455,18 @@ async def test_ui_view_spend_logs_group_by_session_walk_finishes_a_tied_timestam
async def mock_query_raw(sql_query, *params):
if "AS heads(session_key, api_key, last_activity)" in sql_query:
return [_session_page_row(key, tied) for key in ("c", "b")]
return [_session_page_row(key, tied) for key in ("Z", "B", "a") if key in params[0]][: params[-1]]
if "WITH batch AS MATERIALIZED" in sql_query:
if '("startTime", request_id) <' not in sql_query:
return [
_walk_row("b", tied),
_walk_row("B", tied),
_walk_row("a", tied),
_walk_row("a", tied, is_edge=True, batch_rows=params[-1]),
]
return [_walk_row("c", tied), _walk_row("c", "2026-08-29 09:00:00", is_edge=True, batch_rows=1)]
return [
{**_walk_row("Z", tied), "request_id": "req-0"},
_walk_row("Z", "2026-08-29 09:00:00", is_edge=True, batch_rows=2),
]
if "newest_rows" in sql_query:
return _session_count_walk(3)
if "COUNT(*) AS total_count" in sql_query:
@ -7512,8 +7495,8 @@ async def test_ui_view_spend_logs_group_by_session_walk_finishes_a_tied_timestam
)
assert response.status_code == 200, response.text
data = response.json()
assert [row["request_id"] for row in data["data"]] == ["req-c"]
assert data["next_session_cursor"] == f"{tied}|hashed-key|c"
assert [row["request_id"] for row in data["data"]] == ["req-Z"]
assert data["next_session_cursor"] == f"{tied}|hashed-key|Z"
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)

View file

@ -570,15 +570,6 @@ async def test_spend_logs_ui_group_by_session_paginates_sessions(monkeypatch):
session_cursor=None,
)
emitted = [call[0] for call in mock_prisma.db.query_raw.call_args_list]
page_sql = emitted[0][0]
assert "OFFSET" not in page_sql, "the startTime page must be keyset-selected, not offset-selected"
rep_sql = emitted[2][0]
assert emitted[2][-2] == [row["session_key"] for row in session_rows[:50]]
assert emitted[2][-1] == ["k"] * 50
assert "COUNT(*) OVER ()" not in rep_sql
assert [row["request_id"] for row in response["data"]] == ["req-1", "req-2"]
assert response["total"] == 60
assert response["total_is_capped"] is False