mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
feat(ui): add cache hit/miss filter to Request Logs (#38432)
* feat(ui): add cache hit/miss filter to Request Logs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: guard cache_hit_filter validation for direct handler calls Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore(ui): drop redundant cache filter comment Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: yassin <yassin@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
62341e96ae
commit
a7da7928fa
8 changed files with 181 additions and 0 deletions
|
|
@ -2254,6 +2254,10 @@ async def ui_view_spend_logs(
|
|||
status_filter: str | None = fastapi.Query(
|
||||
default=None, description="Filter logs by status (e.g., success, failure)"
|
||||
),
|
||||
cache_hit_filter: str | None = fastapi.Query(
|
||||
default=None,
|
||||
description="Filter logs by cache state: 'hit' or 'miss'. Miss includes legacy rows with a null/unknown cache state",
|
||||
),
|
||||
model: str | None = fastapi.Query(default=None, description="Filter logs by model"),
|
||||
model_id: str | None = fastapi.Query(
|
||||
default=None,
|
||||
|
|
@ -2330,6 +2334,13 @@ async def ui_view_spend_logs(
|
|||
param="sort_order",
|
||||
code=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if isinstance(cache_hit_filter, str) and cache_hit_filter not in {"hit", "miss"}:
|
||||
raise ProxyException(
|
||||
message=f"Invalid cache_hit_filter: {cache_hit_filter}. Must be one of: hit, miss",
|
||||
type="bad_request",
|
||||
param="cache_hit_filter",
|
||||
code=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
try:
|
||||
is_admin_view: Final = _is_admin_view_safe(user_api_key_dict=user_api_key_dict)
|
||||
|
|
@ -2570,6 +2581,11 @@ async def ui_view_spend_logs(
|
|||
sql_params.append(status_filter)
|
||||
p += 1
|
||||
|
||||
if cache_hit_filter == "hit":
|
||||
sql_conditions.append("LOWER(cache_hit) = 'true'")
|
||||
elif cache_hit_filter == "miss":
|
||||
sql_conditions.append("(cache_hit IS NULL OR LOWER(cache_hit) != 'true')")
|
||||
|
||||
if exclude_internal_health_checks:
|
||||
sql_conditions.append(f"api_key NOT IN (${p}, ${p + 1})")
|
||||
sql_params.extend(_INTERNAL_HEALTH_CHECK_API_KEYS)
|
||||
|
|
|
|||
|
|
@ -106,6 +106,10 @@ def _reconstruct_ui_where_from_sql(sql_query, params):
|
|||
where["OR"] = where.get("OR", []) + [{"multi_team": True}]
|
||||
elif "status = 'success'" in cond:
|
||||
where["OR"] = where.get("OR", []) + [{"status": "success"}]
|
||||
elif cond == "LOWER(cache_hit) = 'true'":
|
||||
where["cache_hit"] = "hit"
|
||||
elif cond == "(cache_hit IS NULL OR LOWER(cache_hit) != 'true')":
|
||||
where["cache_hit"] = "miss"
|
||||
elif sess:
|
||||
where["session_id"] = {"contains": str(params[int(sess.group(1)) - 1]).strip("%")}
|
||||
elif status:
|
||||
|
|
@ -2444,6 +2448,96 @@ async def test_ui_view_spend_logs_with_status(client, monkeypatch):
|
|||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_with_cache_hit_filter(client, monkeypatch):
|
||||
base = {
|
||||
"api_key": "sk-test-key",
|
||||
"user": "test_user_1",
|
||||
"team_id": "team1",
|
||||
"spend": 0.05,
|
||||
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
|
||||
"model": "gpt-4",
|
||||
"status": "success",
|
||||
}
|
||||
mock_spend_logs = [
|
||||
{**base, "id": "log1", "request_id": "req-hit", "cache_hit": "True"},
|
||||
{**base, "id": "log2", "request_id": "req-miss", "cache_hit": "False"},
|
||||
{**base, "id": "log3", "request_id": "req-legacy", "cache_hit": "None"},
|
||||
{**base, "id": "log4", "request_id": "req-null", "cache_hit": None},
|
||||
]
|
||||
|
||||
def filter_by_cache(where):
|
||||
cache_filter = where.get("cache_hit")
|
||||
if cache_filter == "hit":
|
||||
return [log for log in mock_spend_logs if str(log["cache_hit"]).lower() == "true"]
|
||||
if cache_filter == "miss":
|
||||
return [log for log in mock_spend_logs if str(log["cache_hit"]).lower() != "true"]
|
||||
return mock_spend_logs
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.prisma_client",
|
||||
make_ui_spend_logs_mock_prisma(mock_spend_logs, filter_by_cache),
|
||||
)
|
||||
|
||||
start_date, end_date = _default_date_range()
|
||||
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN
|
||||
)
|
||||
try:
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"cache_hit_filter": "hit",
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 1
|
||||
assert [row["request_id"] for row in data["data"]] == ["req-hit"]
|
||||
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"cache_hit_filter": "miss",
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 3
|
||||
assert [row["request_id"] for row in data["data"]] == ["req-miss", "req-legacy", "req-null"]
|
||||
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["total"] == 4
|
||||
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"cache_hit_filter": "invalid",
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_with_model(client, monkeypatch):
|
||||
mock_spend_logs = [
|
||||
|
|
|
|||
|
|
@ -2002,6 +2002,7 @@ interface UiSpendLogsParams {
|
|||
user_id?: string;
|
||||
end_user?: string;
|
||||
status_filter?: string;
|
||||
cache_hit_filter?: string;
|
||||
/** Filter by model name (e.g. "gpt-4") */
|
||||
model?: string;
|
||||
/** Filter by model ID (litellm model deployment id) */
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ describe("RequestLogsFilters", () => {
|
|||
for (const label of [
|
||||
"Team ID",
|
||||
"Status",
|
||||
"Cache",
|
||||
"Key Alias",
|
||||
"User ID",
|
||||
"End User",
|
||||
|
|
@ -259,4 +260,37 @@ describe("RequestLogsFilters", () => {
|
|||
|
||||
expect(await screen.findByText(label)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["", "All Requests"],
|
||||
["hit", "Cache Hit"],
|
||||
["miss", "Cache Miss"],
|
||||
])("shows the human label on the Cache trigger for %s", async (cacheState, label) => {
|
||||
renderFilters(cacheState === "" ? {} : { [LOG_FILTER_IDS.CACHE_STATUS]: cacheState });
|
||||
|
||||
expect(await screen.findByText(label)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Cache Hit", "hit"],
|
||||
["Cache Miss", "miss"],
|
||||
])("selecting %s sets the cache filter to %s", async (label, expected) => {
|
||||
const user = userEvent.setup();
|
||||
const { set } = renderFilters();
|
||||
|
||||
await user.click(await screen.findByText("All Requests"));
|
||||
await user.click(await screen.findByRole("option", { name: label }));
|
||||
|
||||
expect(set).toHaveBeenCalledWith(LOG_FILTER_IDS.CACHE_STATUS, expected);
|
||||
});
|
||||
|
||||
it("selecting All Requests clears the cache filter", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { set } = renderFilters({ [LOG_FILTER_IDS.CACHE_STATUS]: "hit" });
|
||||
|
||||
await user.click(await screen.findByText("Cache Hit"));
|
||||
await user.click(await screen.findByRole("option", { name: "All Requests" }));
|
||||
|
||||
expect(set).toHaveBeenCalledWith(LOG_FILTER_IDS.CACHE_STATUS, undefined);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ const STATUS_FILTER_ITEMS = [
|
|||
{ value: "success", label: "Success" },
|
||||
{ value: "failure", label: "Failure" },
|
||||
] as const;
|
||||
|
||||
const CACHE_FILTER_ITEMS = [
|
||||
{ value: ALL_VALUE, label: "All Requests" },
|
||||
{ value: "hit", label: "Cache Hit" },
|
||||
{ value: "miss", label: "Cache Miss" },
|
||||
] as const;
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
const asString = (value: unknown): string => (typeof value === "string" ? value : "");
|
||||
|
|
@ -328,6 +334,27 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
|
|||
</Select>
|
||||
</DataTableFilterField>
|
||||
|
||||
<DataTableFilterField label="Cache">
|
||||
<Select
|
||||
items={CACHE_FILTER_ITEMS}
|
||||
value={valueOf(LOG_FILTER_IDS.CACHE_STATUS) === "" ? ALL_VALUE : valueOf(LOG_FILTER_IDS.CACHE_STATUS)}
|
||||
onValueChange={(next) =>
|
||||
set(LOG_FILTER_IDS.CACHE_STATUS, next === null || next === ALL_VALUE ? undefined : next)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="All Requests" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CACHE_FILTER_ITEMS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</DataTableFilterField>
|
||||
|
||||
<KeyAliasFilterField
|
||||
value={valueOf(LOG_FILTER_IDS.KEY_ALIAS)}
|
||||
onChange={setter(LOG_FILTER_IDS.KEY_ALIAS)}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ describe("useLogFilterLogic", () => {
|
|||
{ id: LOG_FILTER_IDS.SESSION_ID, value: "sess-1", param: "session_id" },
|
||||
{ id: LOG_FILTER_IDS.END_USER, value: "end-user-1", param: "end_user" },
|
||||
{ id: LOG_FILTER_IDS.STATUS, value: "failure", param: "status_filter" },
|
||||
{ id: LOG_FILTER_IDS.CACHE_STATUS, value: "hit", param: "cache_hit_filter" },
|
||||
{ id: LOG_FILTER_IDS.CACHE_STATUS, value: "miss", param: "cache_hit_filter" },
|
||||
{ id: LOG_FILTER_IDS.MODEL_ID, value: "model-uuid-1", param: "model_id" },
|
||||
{ id: LOG_FILTER_IDS.PUBLIC_MODEL_OR_SEARCH_TOOL, value: "gpt-4o", param: "model" },
|
||||
{ id: LOG_FILTER_IDS.KEY_ALIAS, value: "alias-1", param: "key_alias" },
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export interface PaginatedResponse {
|
|||
export const LOG_FILTER_IDS = {
|
||||
TEAM_ID: "team_id",
|
||||
STATUS: "status",
|
||||
CACHE_STATUS: "cache_hit",
|
||||
KEY_ALIAS: "key_alias",
|
||||
END_USER: "end_user",
|
||||
ERROR_CODE: "error_code",
|
||||
|
|
@ -35,6 +36,7 @@ export const LOG_FILTER_IDS = {
|
|||
export const LOG_FILTER_LABELS: Record<string, string> = {
|
||||
[LOG_FILTER_IDS.TEAM_ID]: "Team ID",
|
||||
[LOG_FILTER_IDS.STATUS]: "Status",
|
||||
[LOG_FILTER_IDS.CACHE_STATUS]: "Cache",
|
||||
[LOG_FILTER_IDS.KEY_ALIAS]: "Key Alias",
|
||||
[LOG_FILTER_IDS.USER_ID]: "User ID",
|
||||
[LOG_FILTER_IDS.END_USER]: "End User",
|
||||
|
|
@ -170,6 +172,7 @@ export function useLogFilterLogic({
|
|||
user_id: userIdFilter,
|
||||
end_user: getFilterValue(columnFilters, LOG_FILTER_IDS.END_USER),
|
||||
status_filter: getFilterValue(columnFilters, LOG_FILTER_IDS.STATUS),
|
||||
cache_hit_filter: getFilterValue(columnFilters, LOG_FILTER_IDS.CACHE_STATUS),
|
||||
model_id: getFilterValue(columnFilters, LOG_FILTER_IDS.MODEL_ID),
|
||||
model: getFilterValue(columnFilters, LOG_FILTER_IDS.PUBLIC_MODEL_OR_SEARCH_TOOL),
|
||||
key_alias: getFilterValue(columnFilters, LOG_FILTER_IDS.KEY_ALIAS),
|
||||
|
|
|
|||
4
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
4
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -55030,6 +55030,8 @@ export interface operations {
|
|||
page_size?: number;
|
||||
/** @description Filter logs by status (e.g., success, failure) */
|
||||
status_filter?: string | null;
|
||||
/** @description Filter logs by cache state: 'hit' or 'miss'. Miss includes legacy rows with a null/unknown cache state */
|
||||
cache_hit_filter?: string | null;
|
||||
/** @description Filter logs by model */
|
||||
model?: string | null;
|
||||
/** @description Filter logs by model ID (litellm model deployment id) */
|
||||
|
|
@ -55140,6 +55142,8 @@ export interface operations {
|
|||
page_size?: number;
|
||||
/** @description Filter logs by status (e.g., success, failure) */
|
||||
status_filter?: string | null;
|
||||
/** @description Filter logs by cache state: 'hit' or 'miss'. Miss includes legacy rows with a null/unknown cache state */
|
||||
cache_hit_filter?: string | null;
|
||||
/** @description Filter logs by model */
|
||||
model?: string | null;
|
||||
/** @description Filter logs by model ID (litellm model deployment id) */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue