From 8e76aacae2e7f90c0e90f5bee77204b3a6ec737f Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Mon, 27 Jul 2026 18:32:55 -0700 Subject: [PATCH] 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 --- .../spend_management_endpoints.py | 16 ++++ .../test_spend_management_endpoints.py | 87 +++++++++++++++++++ .../_components/cache_dashboard.test.tsx | 60 ++++++++++++- .../caching/_components/cache_dashboard.tsx | 35 ++++++++ .../src/components/networking.tsx | 2 + .../view_logs/RequestLogsFilters.tsx | 26 ++++++ .../components/view_logs/RequestLogsPanel.tsx | 15 +++- .../view_logs/log_filter_logic.test.tsx | 40 +++++++++ .../components/view_logs/log_filter_logic.tsx | 30 +++++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 8 ++ 10 files changed, 313 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 0c525ee9466..3f29efb4e80 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -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}") diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py index 67945436987..c853adeb318 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py @@ -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", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.test.tsx index fd8dd011b05..cf6ee23311f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.test.tsx @@ -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( + , + ); + 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(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx index 47c266ceac0..a49b1fdc33c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx @@ -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 = ({ 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), + }), + ) + } /> diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 4abe3a6a154..69b76239787 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -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; diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.tsx index 2005b868cd6..5533654fb79 100644 --- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.tsx @@ -320,6 +320,32 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF placeholder="Enter public model or search tool…" /> + + + set(LOG_FILTER_IDS.CALL_TYPE, emptyToUndefined(event.target.value))} + placeholder="e.g. acompletion…" + /> + + + + + ); } diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx index b3b1e8c0640..460d9649f81 100644 --- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx @@ -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({ pageIndex: 0, pageSize: PAGE_SIZE }); const [sorting, setSorting] = useState(DEFAULT_LOGS_SORTING); - const [columnFilters, setColumnFilters] = useState([]); + const [columnFilters, setColumnFilters] = useState(urlSeed.columnFilters); - const [startTime, setStartTime] = useState(moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm")); - const [endTime, setEndTime] = useState(moment().format("YYYY-MM-DDTHH:mm")); - const [isCustomDate, setIsCustomDate] = useState(false); + const [startTime, setStartTime] = useState( + urlSeed.startTime ?? moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm"), + ); + const [endTime, setEndTime] = useState(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(null); @@ -56,6 +62,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID, const [selectedSessionId, setSelectedSessionId] = useState(null); const [isLiveTail, setIsLiveTail] = useState(() => { + if (seededWindow) return false; const storedValue = sessionStorage.getItem("isLiveTail"); return storedValue !== null ? JSON.parse(storedValue) : true; }); diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx index 080bf6b380a..c110e37c86c 100644 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx @@ -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([]); + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx index 8244066cadb..d0a964b1298 100644 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx @@ -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 = { @@ -42,6 +44,32 @@ export const LOG_FILTER_LABELS: Record = { [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, }, diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 80ea068cb5e..ceab5a3e9af 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -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) */