Skip to content

[Fix] UI - Logs: Empty Filter Results Show Stale Data#23792

Merged
yuneng-jiang merged 3 commits intolitellm_yj_march_16_2026from
litellm_ui_logs_filter_2
Mar 16, 2026
Merged

[Fix] UI - Logs: Empty Filter Results Show Stale Data#23792
yuneng-jiang merged 3 commits intolitellm_yj_march_16_2026from
litellm_ui_logs_filter_2

Conversation

@yuneng-jiang
Copy link
Copy Markdown
Contributor

@yuneng-jiang yuneng-jiang commented Mar 16, 2026

Summary

Problem

When a backend filter (e.g. Key Alias) returns an empty result set ({data: [], total: 0}), the logs table continues showing stale unfiltered data instead of an empty table.

Fix

Removed the .length > 0 guard in the filteredLogs memo in log_filter_logic.tsx. The previous logic fell back to unfiltered logs whenever backendFilteredLogs.data was an empty array, which is incorrect — an empty result is a valid filter result and should be displayed as such.

Testing

  • Apply a filter (e.g. Key Alias) that matches no logs → table now correctly shows no results
  • Apply a filter that matches logs → table shows matching results as before
  • Clear filters → table returns to unfiltered view

Type

🐛 Bug Fix

Screenshot

image image

yuneng-jiang and others added 3 commits March 16, 2026 15:32
* fix(test): add missing mocks for test_streamable_http_mcp_handler_mock

The test was missing mocks for extract_mcp_auth_context and set_auth_context,
causing the handler to fail silently in the except block instead of reaching
session_manager.handle_request. This mirrors the fix already applied to the
sibling test_sse_mcp_handler_mock.

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

* fix(ci): route OpenAI models through chat completions in pass-through tests

The test_anthropic_messages_openai_model_streaming_cost_injection test fails
because the OpenAI Responses API returns 400 for requests routed through the
Anthropic Messages endpoint. Setting LITELLM_USE_CHAT_COMPLETIONS_URL_FOR_ANTHROPIC_MESSAGES=true
routes OpenAI models through the stable chat completions path instead.
Cost injection still works since it happens at the proxy level.

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

* fix(ci): fix assemblyai custom auth and router wildcard test flakiness

1. custom_auth_basic.py: Add user_role='proxy_admin' so the custom auth
   user can access management endpoints like /key/generate. The test
   test_assemblyai_transcribe_with_non_admin_key was hidden behind an
   earlier -x failure and was never reached before.

2. test_router_utils.py: Add flaky(retries=3) and increase sleep from 1s
   to 2s for test_router_get_model_group_usage_wildcard_routes. The async
   callback needs time to write usage to cache, and 1s is insufficient on
   slower CI hardware.

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

* ci: retrigger CI pipeline

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

* fix(mypy): use LitellmUserRoles enum instead of raw string in custom_auth_basic

Fixes mypy error: Argument 'user_role' has incompatible type 'str'; expected 'LitellmUserRoles | None'

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

* fix: don't close HTTP/SDK clients on LLMClientCache eviction (#22926)

* fix: don't close HTTP/SDK clients on LLMClientCache eviction

Removing the _remove_key override that eagerly called aclose()/close()
on evicted clients. Evicted clients may still be held by in-flight
streaming requests; closing them causes:

  RuntimeError: Cannot send a request, as the client has been closed.

This is a regression from commit fb72979. Clients that are no longer
referenced will be garbage-collected naturally. Explicit shutdown cleanup
happens via close_litellm_async_clients().

Fixes production crashes after the 1-hour cache TTL expires.

* test: update LLMClientCache unit tests for no-close-on-eviction behavior

Flip the assertions: evicted clients must NOT be closed. Replace
test_remove_key_closes_async_client → test_remove_key_does_not_close_async_client
and equivalents for sync/eviction paths.

Add test_remove_key_removes_plain_values for non-client cache entries.
Remove test_background_tasks_cleaned_up_after_completion (no more _background_tasks).
Remove test_remove_key_no_event_loop variant that depended on old behavior.

* test: add e2e tests for OpenAI SDK client surviving cache eviction

Add two new e2e tests using real AsyncOpenAI clients:
- test_evicted_openai_sdk_client_stays_usable: verifies size-based eviction
  doesn't close the client
- test_ttl_expired_openai_sdk_client_stays_usable: verifies TTL expiry
  eviction doesn't close the client

Both tests sleep after eviction so any create_task()-based close would
have time to run, making the regression detectable.

Also expand the module docstring to explain why the sleep is required.

* docs(AGENTS.md): add rule — never close HTTP/SDK clients on cache eviction

* docs(CLAUDE.md): add HTTP client cache safety guideline

* [Fix] Install bsdmainutils for column command in security scans

The security_scans.sh script uses `column` to format vulnerability
output, but the package wasn't installed in the CI environment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: handle string callback values in prometheus multiproc setup

When callbacks are configured as a plain string (e.g., `callbacks: "my_callback"`)
instead of a list, the proxy crashes on startup with:
  TypeError: can only concatenate str (not "list") to str

Normalize each callback setting to a list before concatenating.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* bump: version 1.82.2 → 1.82.3

* fix(test): update test_startup_fails_when_db_setup_fails for opt-in enforcement

The --enforce_prisma_migration_check flag is now required to trigger
sys.exit(1) on DB migration failure, after #23675 flipped the default
behavior to warn-and-continue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cost_calculator): use model name for per-request custom pricing when router_model_id has no pricing

When custom pricing is passed as per-request kwargs (input_cost_per_token/output_cost_per_token),
completion() registers pricing under the model name, but _select_model_name_for_cost_calc was
selecting the router deployment hash (which has no pricing data), causing response_cost to be 0.0.

Now checks whether the router_model_id entry actually has pricing before preferring it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>
Co-authored-by: Ishaan Jaff <ishaanjaffer0324@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Remove `.length > 0` check so that when a backend filter returns an
empty result set the table correctly shows no data instead of falling
back to the previous unfiltered logs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 16, 2026

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

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Mar 16, 2026 11:29pm

Request Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR bundles three distinct bug fixes and associated test coverage under a v1.82.3 release:

  1. UI – Empty filter results show stale data (log_filter_logic.tsx): The .length > 0 guard in filteredLogs was causing the table to fall back to unfiltered logs when the backend returned an empty result set. The fix removes the guard so data: [] is treated as a valid result.
  2. Cost calculator – Custom per-request pricing returns 0 (cost_calculator.py): When custom_pricing=True and a router_model_id is present in model_cost but has no actual pricing fields, the old code still selected router_model_id, yielding a zero cost. The fix adds a pricing-field existence check before preferring router_model_id.
  3. Proxy CLI – TypeError with single-string callbacks (proxy_cli.py): _maybe_setup_prometheus_multiproc_dir assumed callbacks were always lists; the fix normalises string values to single-element lists before concatenation.

Each fix ships with a targeted test (see test_cost_calculator.py, test_prometheus_cleanup.py, and the corrected test_proxy_cli.py). The PR also fixes a missing mock in test_mcp_server.py and updates an example custom-auth file to set user_role.

Key items to note:

  • The cost_calculator.py pricing guard only checks input_cost_per_token and input_cost_per_second; models priced exclusively by audio, image, or character tokens would still be misclassified.
  • test_router_utils.py acquires a @pytest.mark.flaky(retries=3) decorator — this treats the symptom of a timing race rather than fixing the underlying non-determinism.
  • During the brief window between a filter being applied and the backend response arriving, the UI still returns stale unfiltered logs (the else branch). This is a smaller form of the original issue and may be worth a follow-up.

Confidence Score: 4/5

  • PR is safe to merge — all three fixes are well-scoped, backed by tests, and do not introduce backwards-incompatible changes.
  • Each fix is narrowly targeted and fully tested. The one point deducted is for the incomplete pricing-field guard in cost_calculator.py which could silently yield wrong costs for audio/image-only priced models with custom per-request pricing, and for the @pytest.mark.flaky decorator masking a timing race in CI.
  • litellm/cost_calculator.py — the pricing-existence check should cover additional input cost field types to be robust.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx Core bug fix: removes .length > 0 guard so empty backend filter results are correctly returned instead of falling back to stale unfiltered logs.
litellm/cost_calculator.py Bug fix for custom pricing: only use router_model_id when its model_cost entry has actual pricing data; otherwise fall back to model name. Check covers input_cost_per_token and input_cost_per_second but may miss audio/image pricing fields.
litellm/proxy/proxy_cli.py Bug fix: normalizes callback config values from string to list before concatenation, preventing TypeError when callbacks are specified as a single string.
tests/test_litellm/test_cost_calculator.py New regression test covering the per-request custom pricing fix. Clearly documents the bug scenario and asserts correct model selection.
tests/local_testing/test_router_utils.py Added @pytest.mark.flaky(retries=3) and increased sleep to 2s — masks a timing-dependent flakiness rather than fixing the root cause.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User applies backend filter] --> B{hasBackendFilters?}
    B -- No --> C[Return clientDerivedFilteredLogs]
    B -- Yes --> D{backendFilteredLogs\n&& backendFilteredLogs.data?}
    D -- Yes\n'data' can be empty array --> E[Return backendFilteredLogs\n✅ including empty results]
    D -- No\nstill loading --> F[Return logs\nunfiltered fallback]

    style E fill:#90EE90
    style F fill:#FFD700

    subgraph "OLD behaviour"
        G{data.length > 0?}
        G -- No: data is empty array --> H[Return logs\n❌ stale unfiltered data]
        G -- Yes --> I[Return backendFilteredLogs]
    end
Loading

Comments Outside Diff (1)

  1. ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx, line 228-243 (link)

    Loading-state gap: stale logs still flash while the request is in-flight

    After the fix, the path is:

    1. User applies a filter → hasBackendFilters becomes true
    2. backendFilteredLogs is still null/undefined (request pending)
    3. The else branch returns logs (the unfiltered data)
    4. When the response arrives the table switches to the filtered result (or empty)

    This means there is a window where users still see the previous unfiltered logs while the backend request is pending — the original stale-data symptom in a narrower form. The fix is correct for the empty-result case, but if a loader/spinner is not already shown during step 2–3, consider returning null / a loading sentinel instead of the raw logs so the UI can render a skeleton or empty state immediately on filter application.

Last reviewed commit: 57bba3b

Comment on lines +664 to +670
if (
entry.get("input_cost_per_token") is not None
or entry.get("input_cost_per_second") is not None
):
return_model = router_model_id
else:
return_model = model
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.

Pricing check may miss non-token cost types

The guard for whether the router_model_id entry has real pricing only checks input_cost_per_token and input_cost_per_second. There are other pricing dimensions in model_prices_and_context_window.json — e.g. input_cost_per_audio_token, input_cost_per_image_token, input_cost_per_character — that would cause the condition to evaluate to False even when the entry does have meaningful custom pricing, silently falling back to the model name and potentially returning the wrong cost.

Consider broadening the guard to cover all known input cost fields:

PRICING_FIELDS = (
    "input_cost_per_token",
    "input_cost_per_second",
    "input_cost_per_audio_token",
    "input_cost_per_image_token",
    "input_cost_per_character",
)
if any(entry.get(f) is not None for f in PRICING_FIELDS):
    return_model = router_model_id
else:
    return_model = model

Comment on lines 200 to +202

@pytest.mark.asyncio
@pytest.mark.flaky(retries=3, delay=1)
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.

@pytest.mark.flaky masks a timing-dependent test

Adding retries=3, delay=1 addresses the symptom but not the root cause. The test relies on a Redis/in-memory counter being updated after router.acompletion() completes — a time-based race condition. The sleep increase from 1 s → 2 s is a better signal here, but retrying a flaky test in a shared CI environment is still fragile and can hide real regressions.

Consider instead waiting on the side-effect deterministically, e.g. polling get_model_group_usage until it reflects the expected value, or mocking the underlying cache update.

@yuneng-jiang yuneng-jiang merged commit 31a677e into litellm_yj_march_16_2026 Mar 16, 2026
43 of 63 checks passed
@ishaan-berri ishaan-berri deleted the litellm_ui_logs_filter_2 branch March 26, 2026 22:30
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