mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
feat(ui): drill down from the cache chart into pre-filtered logs
Clicking a slice of the Cache Hits vs API Requests chart now lands on the Logs page pre-filtered to exactly those requests: the failed slice maps to status=failure, the cache-hit slice to cache_hit=true, and the api-requests slice to status=success plus cache_hit=false, all scoped to the bar's call_type and the chart's date range. To support that, /spend/logs/ui and /spend/logs/v2 gain optional call_type and cache_hit query filters (additive, so no breaking change to the public v2 surface), the Logs filter bar gets matching Call Type and Cache Hit controls, and the Logs page seeds its initial filters and time window from URL query params, which also makes filtered logs views shareable by link. A seeded window disables live tail so the drill-down target stays fixed on the clicked range
This commit is contained in:
parent
4d5cab3143
commit
8e76aacae2
10 changed files with 313 additions and 6 deletions
|
|
@ -1660,6 +1660,10 @@ async def ui_view_spend_logs(
|
|||
model_group: str | None = fastapi.Query(default=None, description="Filter logs by model group"),
|
||||
key_alias: str | None = fastapi.Query(default=None, description="Filter logs by key alias"),
|
||||
end_user: str | None = fastapi.Query(default=None, description="Filter logs by end user"),
|
||||
call_type: str | None = fastapi.Query(default=None, description="Filter logs by call type (e.g., acompletion)"),
|
||||
cache_hit: bool | None = fastapi.Query(
|
||||
default=None, description="Filter logs by response-cache outcome: true for cache hits, false for misses"
|
||||
),
|
||||
error_code: str | None = fastapi.Query(default=None, description="Filter logs by error code (e.g., '404', '500')"),
|
||||
error_message: str | None = fastapi.Query(
|
||||
default=None, description="Filter logs by error message (partial string match)"
|
||||
|
|
@ -1796,6 +1800,9 @@ async def ui_view_spend_logs(
|
|||
if model_group is not None:
|
||||
where_conditions["model_group"] = model_group
|
||||
|
||||
if call_type is not None:
|
||||
where_conditions["call_type"] = call_type
|
||||
|
||||
# Build metadata filters
|
||||
metadata_filters = []
|
||||
if key_alias is not None:
|
||||
|
|
@ -1917,6 +1924,7 @@ async def ui_view_spend_logs(
|
|||
("model_id", "model_id"),
|
||||
("model_group", "model_group"),
|
||||
("end_user", "end_user"),
|
||||
("call_type", "call_type"),
|
||||
]:
|
||||
val = where_conditions.get(wc_key)
|
||||
if val is not None and isinstance(val, str):
|
||||
|
|
@ -1947,6 +1955,14 @@ async def ui_view_spend_logs(
|
|||
sql_params.append(status_filter)
|
||||
p += 1
|
||||
|
||||
# Cache hit filter - only a literal 'True' is a hit; '', 'False', and
|
||||
# NULL are all misses, so the miss side must be NULL-safe.
|
||||
if cache_hit is not None:
|
||||
if cache_hit:
|
||||
sql_conditions.append("cache_hit = 'True'")
|
||||
else:
|
||||
sql_conditions.append("cache_hit IS DISTINCT FROM 'True'")
|
||||
|
||||
# Spend range
|
||||
if min_spend is not None:
|
||||
sql_conditions.append(f"spend >= ${p}")
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ def _reconstruct_ui_where_from_sql(sql_query, params):
|
|||
"model_id": "model_id",
|
||||
"model_group": "model_group",
|
||||
"end_user": "end_user",
|
||||
"call_type": "call_type",
|
||||
}
|
||||
date_bounds: dict = {}
|
||||
metadata_conds: list = []
|
||||
|
|
@ -109,6 +110,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 == "cache_hit = 'True'":
|
||||
where["cache_hit"] = {"is_hit": True}
|
||||
elif cond == "cache_hit IS DISTINCT FROM 'True'":
|
||||
where["cache_hit"] = {"is_hit": False}
|
||||
elif sess:
|
||||
where["session_id"] = {"contains": str(params[int(sess.group(1)) - 1]).strip("%")}
|
||||
elif status:
|
||||
|
|
@ -613,6 +618,88 @@ async def test_ui_view_spend_logs_with_user_id(client, monkeypatch):
|
|||
assert data["data"][0]["user"] == "test_user_1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_with_call_type(client, monkeypatch):
|
||||
now = datetime.datetime.now(timezone.utc).isoformat()
|
||||
mock_spend_logs = [
|
||||
{"request_id": "req1", "call_type": "acompletion", "spend": 0.05, "startTime": now},
|
||||
{"request_id": "req2", "call_type": "anthropic_messages", "spend": 0.10, "startTime": now},
|
||||
]
|
||||
|
||||
def filter_by_call_type(where):
|
||||
call_type = where.get("call_type")
|
||||
if call_type is None:
|
||||
return mock_spend_logs
|
||||
return [log for log in mock_spend_logs if log["call_type"] == call_type]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.prisma_client",
|
||||
make_ui_spend_logs_mock_prisma(mock_spend_logs, filter_by_call_type),
|
||||
)
|
||||
|
||||
start_date, end_date = _default_date_range()
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"call_type": "anthropic_messages",
|
||||
"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 data["data"][0]["request_id"] == "req2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"cache_hit_query,expected_request_ids",
|
||||
[
|
||||
("true", {"req-hit"}),
|
||||
("false", {"req-false", "req-empty", "req-null"}),
|
||||
],
|
||||
)
|
||||
async def test_ui_view_spend_logs_with_cache_hit(client, monkeypatch, cache_hit_query, expected_request_ids):
|
||||
now = datetime.datetime.now(timezone.utc).isoformat()
|
||||
mock_spend_logs = [
|
||||
{"request_id": "req-hit", "cache_hit": "True", "spend": 0.0, "startTime": now},
|
||||
{"request_id": "req-false", "cache_hit": "False", "spend": 0.05, "startTime": now},
|
||||
{"request_id": "req-empty", "cache_hit": "", "spend": 0.05, "startTime": now},
|
||||
{"request_id": "req-null", "cache_hit": None, "spend": 0.05, "startTime": now},
|
||||
]
|
||||
|
||||
def filter_by_cache_hit(where):
|
||||
condition = where.get("cache_hit")
|
||||
if condition is None:
|
||||
return mock_spend_logs
|
||||
if condition["is_hit"]:
|
||||
return [log for log in mock_spend_logs if log["cache_hit"] == "True"]
|
||||
return [log for log in mock_spend_logs if log["cache_hit"] != "True"]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.prisma_client",
|
||||
make_ui_spend_logs_mock_prisma(mock_spend_logs, filter_by_cache_hit),
|
||||
)
|
||||
|
||||
start_date, end_date = _default_date_range()
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"cache_hit": cache_hit_query,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert {log["request_id"] for log in data["data"]} == expected_request_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"session_id_query,expected_request_ids",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../../../../tests/test-utils";
|
||||
import CacheDashboard from "./cache_dashboard";
|
||||
import CacheDashboard, { buildLogsDrilldownUrl } from "./cache_dashboard";
|
||||
|
||||
const { useCacheActivity, cachingHealthCheckCall } = vi.hoisted(() => ({
|
||||
useCacheActivity: vi.fn(),
|
||||
|
|
@ -11,6 +11,7 @@ const { useCacheActivity, cachingHealthCheckCall } = vi.hoisted(() => ({
|
|||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
cachingHealthCheckCall,
|
||||
serverRootPath: "/",
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/caching/useCacheActivity", () => ({
|
||||
|
|
@ -198,3 +199,58 @@ describe("CacheDashboard cache analytics charts", () => {
|
|||
expect(compactTicks(tokensCard)).toContain("60K");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cache chart drill-down into the Logs page", () => {
|
||||
const range = { startDate: "2026-07-20", endDate: "2026-07-27" };
|
||||
|
||||
it("maps a failed-requests slice to a status=failure logs link scoped to the bar's call type", () => {
|
||||
const url = buildLogsDrilldownUrl({ name: "anthropic_messages", categoryClicked: "Failed requests" }, range);
|
||||
|
||||
expect(url).toBe(
|
||||
"/ui/logs?call_type=anthropic_messages&status=failure&start_time=2026-07-20T00%3A00&end_time=2026-07-27T23%3A59",
|
||||
);
|
||||
});
|
||||
|
||||
it("maps a cache-hit slice to cache_hit=true", () => {
|
||||
const url = buildLogsDrilldownUrl({ name: "acompletion", categoryClicked: "Cache hit" }, range);
|
||||
|
||||
expect(url).toContain("cache_hit=true");
|
||||
expect(url).not.toContain("status=");
|
||||
});
|
||||
|
||||
it("maps an api-requests slice to successful cache misses", () => {
|
||||
const url = buildLogsDrilldownUrl({ name: "acompletion", categoryClicked: "LLM API requests" }, range);
|
||||
|
||||
expect(url).toContain("status=success");
|
||||
expect(url).toContain("cache_hit=false");
|
||||
});
|
||||
|
||||
it("omits call_type for the Unknown bucket since those rows have an empty call_type", () => {
|
||||
const url = buildLogsDrilldownUrl({ name: "Unknown", categoryClicked: "Failed requests" }, range);
|
||||
|
||||
expect(url).not.toContain("call_type");
|
||||
expect(url).toContain("status=failure");
|
||||
});
|
||||
|
||||
it("navigates to the logs page when a requests-chart bar is clicked", async () => {
|
||||
useCacheActivity.mockReturnValue({ data: cacheActivity, refetch: vi.fn() });
|
||||
const assign = vi.fn();
|
||||
vi.stubGlobal("location", { ...window.location, assign, search: "" });
|
||||
|
||||
renderWithProviders(
|
||||
<CacheDashboard accessToken="sk-test" token="tok" userRole="Admin" userID="u1" premiumUser={false} />,
|
||||
);
|
||||
await screen.findByText("Cache Hits vs API Requests");
|
||||
await waitFor(() => {
|
||||
expect(document.querySelectorAll("path.recharts-rectangle").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const requestsCard = screen.getByText("Cache Hits vs API Requests").closest('[data-slot="card"]') as HTMLElement;
|
||||
fireEvent.click(requestsCard.querySelector("path.recharts-rectangle")!);
|
||||
|
||||
expect(assign).toHaveBeenCalledTimes(1);
|
||||
expect(String(assign.mock.calls[0][0])).toContain("/logs?call_type=acompletion");
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||
import { RefreshCw } from "lucide-react";
|
||||
import { cachingHealthCheckCall } from "@/components/networking";
|
||||
import { useCacheActivity, type CacheActivityGroup } from "@/app/(dashboard)/hooks/caching/useCacheActivity";
|
||||
import { migratedHref } from "@/utils/migratedPages";
|
||||
|
||||
// Import the new component
|
||||
import { CacheHealthTab } from "./cache_health";
|
||||
|
|
@ -42,6 +43,31 @@ const toChartDatum = (group: CacheActivityGroup) => ({
|
|||
"Generated Completion Tokens": group.generated_completion_tokens,
|
||||
});
|
||||
|
||||
const UNKNOWN_CALL_TYPE = "Unknown";
|
||||
|
||||
export const buildLogsDrilldownUrl = (
|
||||
clicked: { name: string; categoryClicked: string },
|
||||
range: { startDate: string | undefined; endDate: string | undefined },
|
||||
): string => {
|
||||
const params = new URLSearchParams();
|
||||
if (clicked.name !== UNKNOWN_CALL_TYPE) {
|
||||
params.set("call_type", clicked.name);
|
||||
}
|
||||
if (clicked.categoryClicked === REQUEST_SERIES.failed) {
|
||||
params.set("status", "failure");
|
||||
}
|
||||
if (clicked.categoryClicked === REQUEST_SERIES.cacheHits) {
|
||||
params.set("cache_hit", "true");
|
||||
}
|
||||
if (clicked.categoryClicked === REQUEST_SERIES.apiRequests) {
|
||||
params.set("status", "success");
|
||||
params.set("cache_hit", "false");
|
||||
}
|
||||
if (range.startDate) params.set("start_time", `${range.startDate}T00:00`);
|
||||
if (range.endDate) params.set("end_time", `${range.endDate}T23:59`);
|
||||
return `${migratedHref("logs")}?${params.toString()}`;
|
||||
};
|
||||
|
||||
const formatDateWithoutTZ = (date: Date | undefined) => {
|
||||
if (!date) return undefined;
|
||||
return date.toISOString().split("T")[0];
|
||||
|
|
@ -307,6 +333,15 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
categories={[REQUEST_SERIES.apiRequests, REQUEST_SERIES.cacheHits, REQUEST_SERIES.failed]}
|
||||
colors={["sky", "teal", "red"]}
|
||||
yAxisWidth={48}
|
||||
className="cursor-pointer"
|
||||
onValueChange={(clicked) =>
|
||||
window.location.assign(
|
||||
buildLogsDrilldownUrl(clicked, {
|
||||
startDate: formatDateWithoutTZ(dateValue.from),
|
||||
endDate: formatDateWithoutTZ(dateValue.to),
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1924,6 +1924,8 @@ interface UiSpendLogsParams {
|
|||
key_alias?: string;
|
||||
error_code?: string;
|
||||
error_message?: string;
|
||||
call_type?: string;
|
||||
cache_hit?: "true" | "false";
|
||||
sort_by?: string;
|
||||
sort_order?: "asc" | "desc";
|
||||
min_spend?: number;
|
||||
|
|
|
|||
|
|
@ -320,6 +320,32 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
|
|||
placeholder="Enter public model or search tool…"
|
||||
/>
|
||||
</DataTableFilterField>
|
||||
|
||||
<DataTableFilterField label="Call Type">
|
||||
<Input
|
||||
value={valueOf(LOG_FILTER_IDS.CALL_TYPE)}
|
||||
onChange={(event) => set(LOG_FILTER_IDS.CALL_TYPE, emptyToUndefined(event.target.value))}
|
||||
placeholder="e.g. acompletion…"
|
||||
/>
|
||||
</DataTableFilterField>
|
||||
|
||||
<DataTableFilterField label="Cache Hit">
|
||||
<Select
|
||||
value={valueOf(LOG_FILTER_IDS.CACHE_HIT) === "" ? ALL_VALUE : valueOf(LOG_FILTER_IDS.CACHE_HIT)}
|
||||
onValueChange={(next) =>
|
||||
set(LOG_FILTER_IDS.CACHE_HIT, next === null || next === ALL_VALUE ? undefined : next)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="All Requests" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_VALUE}>All Requests</SelectItem>
|
||||
<SelectItem value="true">Cache Hit</SelectItem>
|
||||
<SelectItem value="false">Cache Miss</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</DataTableFilterField>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
formatLogsWindow,
|
||||
getLogsWindowEndBound,
|
||||
LOG_FILTER_IDS,
|
||||
readLogsUrlSeed,
|
||||
useLogFilterLogic,
|
||||
} from "./log_filter_logic";
|
||||
import { LogDetailsDrawer } from "./LogDetailsDrawer";
|
||||
|
|
@ -41,13 +42,18 @@ interface SessionComposition {
|
|||
}
|
||||
|
||||
export default function RequestLogsPanel({ accessToken, token, userRole, userID, isActive }: RequestLogsPanelProps) {
|
||||
const [urlSeed] = useState(() => readLogsUrlSeed(typeof window === "undefined" ? "" : window.location.search));
|
||||
const seededWindow = urlSeed.startTime !== null && urlSeed.endTime !== null;
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: PAGE_SIZE });
|
||||
const [sorting, setSorting] = useState<SortingState>(DEFAULT_LOGS_SORTING);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(urlSeed.columnFilters);
|
||||
|
||||
const [startTime, setStartTime] = useState<string>(moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm"));
|
||||
const [endTime, setEndTime] = useState<string>(moment().format("YYYY-MM-DDTHH:mm"));
|
||||
const [isCustomDate, setIsCustomDate] = useState(false);
|
||||
const [startTime, setStartTime] = useState<string>(
|
||||
urlSeed.startTime ?? moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm"),
|
||||
);
|
||||
const [endTime, setEndTime] = useState<string>(urlSeed.endTime ?? moment().format("YYYY-MM-DDTHH:mm"));
|
||||
const [isCustomDate, setIsCustomDate] = useState(seededWindow);
|
||||
const [selectedTimeInterval, setSelectedTimeInterval] = useState<{ value: number; unit: string }>(DEFAULT_INTERVAL);
|
||||
|
||||
const [selectedKeyIdInfoView, setSelectedKeyIdInfoView] = useState<string | null>(null);
|
||||
|
|
@ -56,6 +62,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null);
|
||||
|
||||
const [isLiveTail, setIsLiveTail] = useState<boolean>(() => {
|
||||
if (seededWindow) return false;
|
||||
const storedValue = sessionStorage.getItem("isLiveTail");
|
||||
return storedValue !== null ? JSON.parse(storedValue) : true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
LIVE_TAIL_INTERVAL_MS,
|
||||
LOG_FILTER_IDS,
|
||||
LOGS_WINDOW_TICK_MS,
|
||||
readLogsUrlSeed,
|
||||
useLogFilterLogic,
|
||||
type PaginatedResponse,
|
||||
} from "./log_filter_logic";
|
||||
|
|
@ -87,6 +88,8 @@ describe("useLogFilterLogic", () => {
|
|||
{ id: LOG_FILTER_IDS.ERROR_CODE, value: "429", param: "error_code" },
|
||||
{ id: LOG_FILTER_IDS.ERROR_MESSAGE, value: "rate limited", param: "error_message" },
|
||||
{ id: LOG_FILTER_IDS.USER_ID, value: "user-9", param: "user_id" },
|
||||
{ id: LOG_FILTER_IDS.CALL_TYPE, value: "anthropic_messages", param: "call_type" },
|
||||
{ id: LOG_FILTER_IDS.CACHE_HIT, value: "true", param: "cache_hit" },
|
||||
];
|
||||
|
||||
it.each(cases)("sends $id as $param", async ({ id, value, param }) => {
|
||||
|
|
@ -110,6 +113,13 @@ describe("useLogFilterLogic", () => {
|
|||
expect(params?.api_key).toBeUndefined();
|
||||
expect(params?.error_code).toBeUndefined();
|
||||
});
|
||||
|
||||
it("drops a cache_hit filter value that is not true or false", async () => {
|
||||
renderFilterHook({ columnFilters: [{ id: LOG_FILTER_IDS.CACHE_HIT, value: "maybe" }] });
|
||||
|
||||
await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalled());
|
||||
expect(lastCallParams()?.params?.cache_hit).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("paging, dates, and sort", () => {
|
||||
|
|
@ -294,3 +304,33 @@ describe("formatLogsWindow preset end bound", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readLogsUrlSeed", () => {
|
||||
it("seeds column filters from known filter params and ignores unrelated ones", () => {
|
||||
const seed = readLogsUrlSeed("?status=failure&call_type=anthropic_messages&cache_hit=true&page_size=50&foo=bar");
|
||||
|
||||
expect(seed.columnFilters).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ id: LOG_FILTER_IDS.STATUS, value: "failure" },
|
||||
{ id: LOG_FILTER_IDS.CALL_TYPE, value: "anthropic_messages" },
|
||||
{ id: LOG_FILTER_IDS.CACHE_HIT, value: "true" },
|
||||
]),
|
||||
);
|
||||
expect(seed.columnFilters).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("captures the seeded time window", () => {
|
||||
const seed = readLogsUrlSeed("?start_time=2026-07-20T00:00&end_time=2026-07-27T23:59");
|
||||
|
||||
expect(seed.startTime).toBe("2026-07-20T00:00");
|
||||
expect(seed.endTime).toBe("2026-07-27T23:59");
|
||||
});
|
||||
|
||||
it("returns an empty seed for an empty query string", () => {
|
||||
expect(readLogsUrlSeed("")).toEqual({ columnFilters: [], startTime: null, endTime: null });
|
||||
});
|
||||
|
||||
it("skips blank filter values", () => {
|
||||
expect(readLogsUrlSeed("?status=&call_type=%20%20").columnFilters).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ export const LOG_FILTER_IDS = {
|
|||
PUBLIC_MODEL_OR_SEARCH_TOOL: "model",
|
||||
REQUEST_ID: "request_id",
|
||||
USER_ID: "user_id",
|
||||
CALL_TYPE: "call_type",
|
||||
CACHE_HIT: "cache_hit",
|
||||
} as const;
|
||||
|
||||
export const LOG_FILTER_LABELS: Record<string, string> = {
|
||||
|
|
@ -42,6 +44,32 @@ export const LOG_FILTER_LABELS: Record<string, string> = {
|
|||
[LOG_FILTER_IDS.SESSION_ID]: "Session ID",
|
||||
[LOG_FILTER_IDS.MODEL_ID]: "Model",
|
||||
[LOG_FILTER_IDS.PUBLIC_MODEL_OR_SEARCH_TOOL]: "Public model / search tool",
|
||||
[LOG_FILTER_IDS.CALL_TYPE]: "Call Type",
|
||||
[LOG_FILTER_IDS.CACHE_HIT]: "Cache Hit",
|
||||
};
|
||||
|
||||
export interface LogsUrlSeed {
|
||||
columnFilters: ColumnFiltersState;
|
||||
startTime: string | null;
|
||||
endTime: string | null;
|
||||
}
|
||||
|
||||
export const readLogsUrlSeed = (search: string): LogsUrlSeed => {
|
||||
const params = new URLSearchParams(search);
|
||||
const columnFilters = Object.values(LOG_FILTER_IDS).flatMap((id) => {
|
||||
const value = params.get(id)?.trim();
|
||||
return value ? [{ id, value }] : [];
|
||||
});
|
||||
return {
|
||||
columnFilters,
|
||||
startTime: params.get("start_time"),
|
||||
endTime: params.get("end_time"),
|
||||
};
|
||||
};
|
||||
|
||||
const parseCacheHitFilter = (value: string | undefined): "true" | "false" | undefined => {
|
||||
if (value === "true" || value === "false") return value;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export interface LogsWindow {
|
||||
|
|
@ -173,6 +201,8 @@ export function useLogFilterLogic({
|
|||
key_alias: getFilterValue(columnFilters, LOG_FILTER_IDS.KEY_ALIAS),
|
||||
error_code: getFilterValue(columnFilters, LOG_FILTER_IDS.ERROR_CODE),
|
||||
error_message: getFilterValue(columnFilters, LOG_FILTER_IDS.ERROR_MESSAGE),
|
||||
call_type: getFilterValue(columnFilters, LOG_FILTER_IDS.CALL_TYPE),
|
||||
cache_hit: parseCacheHitFilter(getFilterValue(columnFilters, LOG_FILTER_IDS.CACHE_HIT)),
|
||||
sort_by: sortBy,
|
||||
sort_order: sortOrder,
|
||||
},
|
||||
|
|
|
|||
8
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
8
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -49546,6 +49546,10 @@ export interface operations {
|
|||
key_alias?: string | null;
|
||||
/** @description Filter logs by end user */
|
||||
end_user?: string | null;
|
||||
/** @description Filter logs by call type (e.g., acompletion) */
|
||||
call_type?: string | null;
|
||||
/** @description Filter logs by response-cache outcome: true for cache hits, false for misses */
|
||||
cache_hit?: boolean | null;
|
||||
/** @description Filter logs by error code (e.g., '404', '500') */
|
||||
error_code?: string | null;
|
||||
/** @description Filter logs by error message (partial string match) */
|
||||
|
|
@ -49654,6 +49658,10 @@ export interface operations {
|
|||
key_alias?: string | null;
|
||||
/** @description Filter logs by end user */
|
||||
end_user?: string | null;
|
||||
/** @description Filter logs by call type (e.g., acompletion) */
|
||||
call_type?: string | null;
|
||||
/** @description Filter logs by response-cache outcome: true for cache hits, false for misses */
|
||||
cache_hit?: boolean | null;
|
||||
/** @description Filter logs by error code (e.g., '404', '500') */
|
||||
error_code?: string | null;
|
||||
/** @description Filter logs by error message (partial string match) */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue