Skip to content

[Fix] Spend Management Tests - #21088

Merged
yuneng-jiang merged 4 commits into
mainfrom
litellm_spend_rebase
Feb 13, 2026
Merged

[Fix] Spend Management Tests#21088
yuneng-jiang merged 4 commits into
mainfrom
litellm_spend_rebase

Conversation

@yuneng-jiang

Copy link
Copy Markdown
Contributor

Relevant issues

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

✅ Test

Changes

This PR merges a contributor's PR #20720 and adjusts the tests around spend management. The previous tests only mocked the find_many but the new flow uses raw_query, so all the ui spend tests needed to be upgraded. This PR refactors the spend tests to reduce code reuse as well as ensure they all work with the new raw_query approach.

All tests passing:
image

@vercel

vercel Bot commented Feb 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 13, 2026 6:18am

Request Review

@greptile-apps

greptile-apps Bot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Overview

Greptile Summary

This PR makes two key changes: (1) Backend performance optimization — the spend log list endpoints (/spend/logs/ui, /spend/logs/v2, /spend/logs/ui/session/{session_id}) now use raw SQL queries that exclude heavy columns (messages, response, proxy_server_request) which can be hundreds of KB per row. A new DB fallback was added to the detail endpoint (/spend/logs/ui/{request_id}) so these fields are still available when no custom logger is configured. The lru_cache decorator (which was incorrectly applied to an async function) was also removed. (2) Frontend lazy-loading — the UI no longer prefetches log details for all 50 rows upfront. Instead, a new useLogDetails hook fetches messages/response on-demand when a user opens the log drawer, significantly reducing initial page load time and bandwidth.

  • Bug: sort_by/sort_order parameters are ignored — The raw SQL in ui_view_spend_logs hardcodes ORDER BY "startTime" DESC, but the endpoint validates and accepts sort parameters (spend, total_tokens, startTime, endTime). The previous find_many implementation correctly applied the sort order. This is a functional regression that should be fixed before merging.
  • Test refactoring consolidates duplicated mock classes into a shared make_ui_spend_logs_mock_prisma factory, reducing boilerplate across ~10 test functions.

Confidence Score: 3/5

  • This PR introduces a sorting regression in the spend logs endpoint that should be fixed before merging.
  • The overall performance optimization approach is sound and the UI lazy-loading is well-implemented. However, the raw SQL query hardcodes ORDER BY "startTime" DESC, completely ignoring the validated sort_by/sort_order parameters — a functional regression from the previous find_many implementation. The test refactoring is good but doesn't catch this issue because the sort test also hardcodes the expected order. Score of 3 reflects that the core logic change has a real bug that affects user-facing sorting functionality.
  • litellm/proxy/spend_tracking/spend_management_endpoints.py — the raw SQL ORDER BY clause must incorporate the validated sort_by/sort_order parameters instead of hardcoding "startTime" DESC.

Important Files Changed

Filename Overview
litellm/proxy/spend_tracking/spend_management_endpoints.py Switches list endpoint and session endpoint from Prisma find_many to raw SQL to exclude heavy columns (messages, response, proxy_server_request). Adds DB fallback in the detail endpoint. However, the raw SQL hardcodes ORDER BY "startTime" DESC, ignoring the validated sort_by/sort_order parameters — a functional regression.
tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py Refactors mock infrastructure from per-test MockDB/MockPrismaClient classes to a shared make_ui_spend_logs_mock_prisma factory, updated to mock query_raw instead of find_many. Reduces duplication significantly. Minor formatting issue with missing blank line before imports.
ui/litellm-dashboard/src/app/(dashboard)/hooks/logDetails/useLogDetails.ts New React Query hook for lazy-loading log details (messages/response) on-demand when the drawer opens, replacing the previous prefetch-all approach. Well-structured with appropriate staleTime/gcTime settings.
ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx Adds isLoadingDetails prop to show a spinner while lazy-loaded log details are being fetched, and defers the "missing data" warning until loading completes.
ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx Integrates the useLogDetails hook, builds an enriched log entry by merging lazy-loaded details with the list data, and passes loading state to LogDetailContent.
ui/litellm-dashboard/src/components/view_logs/index.tsx Removes prefetch-all logic (prefetchLogDetails call and PrefetchedLog type), passes startTime to LogDetailsDrawer for on-demand detail loading.

Sequence Diagram

sequenceDiagram
    participant UI as Dashboard UI
    participant List as /spend/logs/ui
    participant Detail as /spend/logs/ui/{id}
    participant DB as PostgreSQL
    participant Logger as Custom Logger (S3/GCS)

    UI->>List: GET (filters, pagination)
    List->>DB: COUNT(*) via Prisma ORM
    List->>DB: Raw SQL (lightweight columns only)
    DB-->>List: Rows without messages/response
    List-->>UI: Paginated log list

    Note over UI: User clicks a log row

    UI->>Detail: GET /spend/logs/ui/{request_id}
    Detail->>Logger: get_request_response_payload()
    alt Custom logger has data
        Logger-->>Detail: messages + response
    else No custom logger / no data
        Detail->>DB: Raw SQL (messages, response, proxy_server_request)
        DB-->>Detail: Heavy columns
    end
    Detail-->>UI: Log details (messages/response)
    Note over UI: Drawer shows enriched log entry
Loading

Last reviewed commit: ae44022

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread litellm/proxy/spend_tracking/spend_management_endpoints.py
Comment on lines +97 to 98
return MockPrismaClient()
from litellm.proxy._types import (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line before import

There should be a blank line between the end of the make_ui_spend_logs_mock_prisma function and the module-level import statement. This is a minor formatting issue — the from litellm.proxy._types import ... runs directly into the function's return statement.

Suggested change
return MockPrismaClient()
from litellm.proxy._types import (
from litellm.proxy._types import (

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@yuneng-jiang

Copy link
Copy Markdown
Contributor Author

@greptile

@greptile-apps

greptile-apps Bot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Overview

Greptile Summary

This PR optimizes the spend logs list endpoints (/spend/logs/ui, /spend/logs/v2, /spend/logs/ui/session) by switching from Prisma's find_many to raw SQL query_raw, excluding heavy columns (messages, response, proxy_server_request) that can be hundreds of KB per row. A new DB fallback is added to the detail endpoint (/spend/logs/ui/{request_id}) so log details can still be served when no custom logger (S3, GCS, etc.) is configured. On the frontend, prefetching all log details is replaced with on-demand lazy-loading via a new useLogDetails React Query hook. The test suite is refactored to use a shared make_ui_spend_logs_mock_prisma helper, reducing duplicated mock setup across ~10 tests.

  • The raw SQL queries use parameterized placeholders ($1, $2, etc.) throughout, avoiding SQL injection risks.
  • Sort parameters (sort_by, sort_order) are now correctly applied in the raw SQL ORDER BY clause after validation against a whitelist.
  • The lru_cache decorator was removed from the detail endpoint — this is correct since the endpoint is async and the cache would not work properly with coroutines.
  • The sort test mock pre-sorts data based on test parameters rather than verifying the actual SQL ORDER BY clause, reducing its effectiveness as a regression test.
  • prefetch.ts is no longer imported but the file still exists in the repository (dead code).

Confidence Score: 4/5

  • This PR is safe to merge — the core changes are well-structured, use parameterized SQL, and improve performance.
  • The raw SQL migration is done correctly with parameterized queries and proper sort validation. The DB fallback for the detail endpoint is a sensible addition. The test refactoring reduces code duplication effectively. Minor deductions for: (1) the sort test mock doesn't actually verify the SQL ORDER BY clause, reducing its value as a regression test, and (2) latent timezone-naive comparison bug in the test helper function.
  • tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py — the sort test mock and the _filter_logs_by_date_range helper have minor issues that could mask bugs or cause failures if reused in new contexts.

Important Files Changed

Filename Overview
litellm/proxy/spend_tracking/spend_management_endpoints.py Replaces Prisma find_many with raw SQL query_raw to exclude heavy columns (messages, response, proxy_server_request) for performance; adds DB fallback in the detail endpoint for when no custom logger is configured. Sort parameters are now correctly applied to the raw SQL query.
tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py Refactored all spend log tests to use a shared make_ui_spend_logs_mock_prisma helper that mocks both count and query_raw. Reduces code duplication but sort test mock has a testing weakness where it pre-sorts data based on test parameters rather than verifying the SQL query.
ui/litellm-dashboard/src/app/(dashboard)/hooks/logDetails/useLogDetails.ts New React Query hook for lazy-loading log details (messages/response) on demand when the drawer is opened, replacing the previous prefetch-all approach.
ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx Added isLoadingDetails prop to show a spinner while log details are being lazy-loaded and suppress the "missing data" warning during loading.
ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx Integrates useLogDetails hook to lazy-load and merge log detail data (messages/response) into the displayed log entry when the drawer is open.
ui/litellm-dashboard/src/components/view_logs/index.tsx Removed prefetch logic for log details, passing startTime to the drawer so it can lazy-load details on demand instead.

Sequence Diagram

sequenceDiagram
    participant UI as UI (SpendLogsTable)
    participant Drawer as LogDetailsDrawer
    participant ListAPI as GET /spend/logs/ui
    participant DetailAPI as GET /spend/logs/ui/{request_id}
    participant DB as PostgreSQL (LiteLLM_SpendLogs)
    participant Logger as Custom Logger (S3/GCS)

    UI->>ListAPI: Fetch logs (page, filters, sort)
    ListAPI->>DB: COUNT(*) via Prisma count()
    ListAPI->>DB: SELECT (lightweight columns) via query_raw
    DB-->>ListAPI: Paginated rows (no messages/response)
    ListAPI-->>UI: { data, total, page, total_pages }

    UI->>Drawer: User clicks row → open drawer
    Drawer->>DetailAPI: Lazy-load details (request_id, startTime)
    DetailAPI->>Logger: Check custom loggers for payload
    alt Custom logger has data
        Logger-->>DetailAPI: { messages, response, proxy_server_request }
    else No custom logger / no data
        DetailAPI->>DB: SELECT messages, response, proxy_server_request WHERE request_id=$1
        DB-->>DetailAPI: Heavy columns for single row
    end
    DetailAPI-->>Drawer: { messages, response, proxy_server_request }
    Drawer->>Drawer: Merge details into enrichedLog
    Drawer-->>UI: Display full log details
Loading

Last reviewed commit: b29c0bf

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +489 to +495
async def mock_query_raw(sql_query, *params):
# Endpoint uses raw SQL with ORDER BY startTime DESC; mock returns sorted data
order = {"startTime": "desc"} if sort_by is None else {sort_by: sort_order or "desc"}
sorted_logs = _sort_logs(base_logs, order)
page_size = params[-2] if len(params) >= 2 else 50
skip = params[-1] if len(params) >= 1 else 0
return sorted_logs[skip : skip + page_size]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort test doesn't verify SQL ordering

The mock_query_raw function reads sort_by and sort_order from the outer test scope and pre-sorts the mock data accordingly. This means the test always passes regardless of whether the endpoint actually incorporates the sort parameters into the SQL query. If the endpoint had a bug where it hardcoded ORDER BY "startTime" DESC, this test would still pass because the mock returns the expected order independently.

Consider parsing the sql_query string inside mock_query_raw to extract the actual ORDER BY clause and sort based on that, so the test validates the endpoint's SQL construction.

Comment on lines +43 to +47
filter_date = (
datetime.datetime.fromisoformat(fd.replace("Z", "+00:00"))
if "T" in fd
else datetime.datetime.strptime(fd, "%Y-%m-%d %H:%M:%S")
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naive vs aware datetime comparison

When the date filter string does not contain "T", datetime.strptime returns a naive datetime (no timezone info), while log_date (line 38-40) is always timezone-aware (from fromisoformat with +00:00). Comparing aware and naive datetimes in Python raises a TypeError.

In the current test suite, filter dates passed through where_conditions["startTime"] are ISO format strings from isoformat() (which always contain T), so this branch is not triggered today. However, the helper function's docstring and signature suggest general reuse, making this a latent bug.

Suggested change
filter_date = (
datetime.datetime.fromisoformat(fd.replace("Z", "+00:00"))
if "T" in fd
else datetime.datetime.strptime(fd, "%Y-%m-%d %H:%M:%S")
)
filter_date = (
datetime.datetime.fromisoformat(fd.replace("Z", "+00:00"))
if "T" in fd
else datetime.datetime.strptime(fd, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
)

Comment on lines +52 to +56
filter_date = (
datetime.datetime.fromisoformat(fd.replace("Z", "+00:00"))
if "T" in fd
else datetime.datetime.strptime(fd, "%Y-%m-%d %H:%M:%S")
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same naive vs aware datetime issue

Same issue as the gte branch above — datetime.strptime without .replace(tzinfo=timezone.utc) produces a naive datetime.

Suggested change
filter_date = (
datetime.datetime.fromisoformat(fd.replace("Z", "+00:00"))
if "T" in fd
else datetime.datetime.strptime(fd, "%Y-%m-%d %H:%M:%S")
)
filter_date = (
datetime.datetime.fromisoformat(fd.replace("Z", "+00:00"))
if "T" in fd
else datetime.datetime.strptime(fd, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
)

@yuneng-jiang
yuneng-jiang merged commit 5ddba48 into main Feb 13, 2026
58 of 79 checks passed
@ishaan-berri
ishaan-berri deleted the litellm_spend_rebase branch March 26, 2026 22:30
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant