[Fix] UI - Logs: Empty Filter Results Show Stale Data#23792
[Fix] UI - Logs: Empty Filter Results Show Stale Data#23792yuneng-jiang merged 3 commits intolitellm_yj_march_16_2026from
Conversation
* 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR bundles three distinct bug fixes and associated test coverage under a v1.82.3 release:
Each fix ships with a targeted test (see Key items to note:
Confidence Score: 4/5
|
| 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
Comments Outside Diff (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:
- User applies a filter →
hasBackendFiltersbecomestrue backendFilteredLogsis stillnull/undefined(request pending)- The
elsebranch returnslogs(the unfiltered data) - 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 rawlogsso the UI can render a skeleton or empty state immediately on filter application. - User applies a filter →
Last reviewed commit: 57bba3b
| 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 |
There was a problem hiding this comment.
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|
|
||
| @pytest.mark.asyncio | ||
| @pytest.mark.flaky(retries=3, delay=1) |
There was a problem hiding this comment.
@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.
31a677e
into
litellm_yj_march_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 > 0guard in thefilteredLogsmemo inlog_filter_logic.tsx. The previous logic fell back to unfilteredlogswheneverbackendFilteredLogs.datawas an empty array, which is incorrect — an empty result is a valid filter result and should be displayed as such.Testing
Type
🐛 Bug Fix
Screenshot