feat(logs): support excluding an exact key alias

This commit is contained in:
zhengxinliang 2026-10-05 08:41:11 +08:00
parent 1d52985d03
commit d1ce32c4f8
11 changed files with 317 additions and 63 deletions

View file

@ -2548,6 +2548,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"),
exclude_key_alias: str | None = fastapi.Query(
default=None,
description="Exclude logs whose key alias exactly matches this value; logs without an alias remain included",
),
end_user: str | None = fastapi.Query(default=None, description="Filter logs by end user"),
error_code: str | None = fastapi.Query(default=None, description="Filter logs by error code (e.g., '404', '500')"),
error_message: str | None = fastapi.Query(
@ -2908,6 +2912,10 @@ async def ui_view_spend_logs(
sql_conditions.append(f"metadata->>'user_api_key_alias' LIKE ${p}")
sql_params.append(f"%{key_alias}%")
p += 1
if exclude_key_alias is not None:
sql_conditions.append(f"metadata->>'user_api_key_alias' IS DISTINCT FROM ${p}")
sql_params.append(exclude_key_alias)
p += 1
if error_code is not None:
sql_conditions.append(f"metadata->'error_information'->>'error_code' = ${p}")
sql_params.append(error_code)

View file

@ -120,6 +120,7 @@ def _reconstruct_ui_where_from_sql(sql_query, params):
gte = re.search(r'"startTime" >= \(\$(\d+)', cond)
lte = re.search(r'"startTime" <= \(\$(\d+)', cond)
alias = re.search(r"user_api_key_alias' LIKE \$(\d+)", cond)
excluded_alias = re.search(r"user_api_key_alias' IS DISTINCT FROM \$(\d+)", cond)
code = re.search(r"error_code' = \$(\d+)", cond)
msg = re.search(r"error_message' LIKE \$(\d+)", cond)
credential = re.fullmatch(r"metadata->>'used_client_oauth_token' = \$(\d+)", cond)
@ -166,6 +167,8 @@ def _reconstruct_ui_where_from_sql(sql_query, params):
"string_contains": str(params[int(alias.group(1)) - 1]).strip("%"),
}
)
elif excluded_alias:
where["exclude_key_alias"] = params[int(excluded_alias.group(1)) - 1]
elif code:
metadata_conds.append(
{
@ -3595,6 +3598,64 @@ async def test_ui_view_spend_logs_with_model_group(client, monkeypatch):
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
@pytest.mark.parametrize("route", ["/spend/logs/ui", "/spend/logs/v2"])
async def test_ui_view_spend_logs_excludes_exact_key_alias_before_pagination(client, monkeypatch, route):
aliases = ["noisy", "noisy-other", None, "quiet", "noisy"]
logs = [
{
"id": f"log{i}",
"request_id": f"req{i}",
"api_key": "sk-test-key",
"spend": 0.05,
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
"model": "gpt-4",
"metadata": json.dumps({"user_api_key_alias": alias}) if alias is not None else "{}",
}
for i, alias in enumerate(aliases)
]
queries = []
def filter_by_alias(where):
if where.get("exclude_key_alias") != "noisy":
return logs
return [log for log, alias in zip(logs, aliases) if alias != "noisy"]
monkeypatch.setattr(
"litellm.proxy.proxy_server.prisma_client",
make_ui_spend_logs_mock_prisma(logs, filter_by_alias, query_observer=lambda sql, params: queries.append(sql)),
)
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin_user"
)
try:
start_date, end_date = _default_date_range()
responses = [
client.get(
route,
params={
"exclude_key_alias": "noisy",
"start_date": start_date,
"end_date": end_date,
"page": page,
"page_size": 2,
},
headers={"Authorization": "Bearer sk-test"},
)
for page in (1, 2)
]
assert [response.status_code for response in responses] == [200, 200]
assert [response.json()["total"] for response in responses] == [3, 3]
assert [response.json()["total_pages"] for response in responses] == [2, 2]
assert [[row["request_id"] for row in response.json()["data"]] for response in responses] == [
["req1", "req2"],
["req3"],
]
assert all("user_api_key_alias' IS DISTINCT FROM" in query for query in queries if "WHERE" in query)
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_with_key_hash(client, monkeypatch):
mock_spend_logs = [
@ -7105,6 +7166,45 @@ async def test_ui_view_spend_logs_group_by_session_first_page(client, monkeypatc
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_grouped_exclusion_reaches_page_count_and_representatives(client, monkeypatch):
page_rows = [
_session_page_row("sess-1", "2026-08-29 10:00:00"),
_session_page_row("sess-2", "2026-08-29 09:00:00"),
]
mock_prisma = _session_grouped_mock_prisma(page_rows, 2, [_session_representative_row("req-1", "sess-1")])
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
monkeypatch.setattr(
"litellm.proxy.spend_tracking.spend_management_endpoints._is_admin_view_safe",
lambda user_api_key_dict: True,
)
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin_user"
)
try:
start_date, end_date = _default_date_range()
response = client.get(
"/spend/logs/ui",
params={
"start_date": start_date,
"end_date": end_date,
"group_by_session": "true",
"exclude_key_alias": "noisy",
"page_size": 1,
},
headers={"Authorization": "Bearer sk-test"},
)
assert response.status_code == 200, response.text
assert response.json()["total"] == 2
queries = [call.args for call in mock_prisma.db.query_raw.await_args_list]
assert len(queries) >= 3
for sql, *params in queries[:3]:
assert "metadata->>'user_api_key_alias' IS DISTINCT FROM $3" in sql
assert params[2] == "noisy"
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_group_by_session_cursor_page(client, monkeypatch):
"""A session_cursor becomes a HAVING keyset predicate instead of an OFFSET."""

View file

@ -1706,6 +1706,37 @@
"count": 1
}
},
"src/components/logs/detail/LogDetailsDrawer.tsx": {
"no-nested-ternary": {
"count": 2
},
"react-hooks/set-state-in-effect": {
"count": 2
}
},
"src/components/logs/detail/sections/EvalViewer/EvalViewer.tsx": {
"no-nested-ternary": {
"count": 1
}
},
"src/components/logs/detail/sections/GuardrailViewer/CompliancePanel.tsx": {
"no-nested-ternary": {
"count": 2
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/components/logs/detail/sections/GuardrailViewer/ContentFilterDetails.tsx": {
"no-nested-ternary": {
"count": 1
}
},
"src/components/logs/detail/sections/GuardrailViewer/GuardrailViewer.tsx": {
"no-nested-ternary": {
"count": 3
}
},
"src/components/mcp_server_management/MCPToolPermissions.tsx": {
"local/no-complex-jsx-arrow": {
"count": 1
@ -2266,57 +2297,6 @@
"count": 1
}
},
"src/components/logs/detail/sections/EvalViewer/EvalViewer.tsx": {
"no-nested-ternary": {
"count": 1
}
},
"src/components/logs/detail/sections/GuardrailViewer/CompliancePanel.tsx": {
"no-nested-ternary": {
"count": 2
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/components/logs/detail/sections/GuardrailViewer/ContentFilterDetails.tsx": {
"no-nested-ternary": {
"count": 1
}
},
"src/components/logs/detail/sections/GuardrailViewer/GuardrailViewer.tsx": {
"no-nested-ternary": {
"count": 3
}
},
"src/components/logs/detail/LogDetailsDrawer.tsx": {
"no-nested-ternary": {
"count": 2
},
"react-hooks/set-state-in-effect": {
"count": 2
}
},
"src/components/logs/detail/useKeyboardNavigation.ts": {
"react-hooks/immutability": {
"count": 2
}
},
"src/components/logs/types.ts": {
"local/filename-pascal-case": {
"count": 1
}
},
"src/components/logs/request/useLogFilterLogic.ts": {
"local/filename-pascal-case": {
"count": 1
}
},
"src/components/logs/request/timeRange.ts": {
"local/filename-pascal-case": {
"count": 1
}
},
"src/components/view_model/model_name_display.tsx": {
"local/filename-pascal-case": {
"count": 1

View file

@ -187,6 +187,32 @@ describe("RequestLogsFilters", () => {
expect(useInfiniteKeyAliases).toHaveBeenCalledWith(50, undefined, undefined);
});
it("switches Key Alias from is to is not while keeping the selected alias", async () => {
const user = userEvent.setup();
const { set } = renderFilters({ [LOG_FILTER_IDS.KEY_ALIAS]: "noisy" });
await user.click(screen.getByRole("combobox", { name: "Key Alias operator" }));
await user.click(await screen.findByRole("option", { name: "is not" }));
expect(set).toHaveBeenCalledWith(LOG_FILTER_IDS.KEY_ALIAS, undefined);
expect(set).toHaveBeenCalledWith("exclude_key_alias", "noisy");
});
it("selects an alias with the is not operator from an empty filter", async () => {
vi.mocked(useInfiniteKeyAliases).mockReturnValue({
...emptyInfiniteQuery,
data: { pages: [{ aliases: ["noisy"] }], pageParams: [1] },
} as unknown as ReturnType<typeof useInfiniteKeyAliases>);
const user = userEvent.setup();
const { set } = renderFilters();
await user.click(screen.getByRole("combobox", { name: "Key Alias operator" }));
await user.click(await screen.findByRole("option", { name: "is not" }));
await chooseSelectOption(user, screen.getByPlaceholderText("Search a key alias"), "noisy");
expect(set).toHaveBeenCalledWith(LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, "noisy");
});
it("does not leak the team scope into the Model lookup", async () => {
renderFilters({ [LOG_FILTER_IDS.TEAM_ID]: "team-42" });

View file

@ -25,6 +25,10 @@ import { CREDENTIAL_LABELS, ERROR_CODE_OPTIONS } from "../constants";
import { LOG_FILTER_IDS, type LogsWindow } from "./useLogFilterLogic";
const ALL_VALUE = "all";
const KEY_ALIAS_OPERATORS = [
{ value: "is", label: "is" },
{ value: "is not", label: "is not" },
] as const;
const STATUS_FILTER_ITEMS = [
{ value: ALL_VALUE, label: "All Statuses" },
@ -91,14 +95,32 @@ function TeamFilterField({
function KeyAliasFilterField({
value,
excludeValue,
onChange,
onExcludeChange,
teamId,
}: {
value: string;
excludeValue: string;
onChange: (value: string | undefined) => void;
onExcludeChange: (value: string | undefined) => void;
teamId: string;
}) {
const [search, setSearch] = useState("");
const [operator, setOperator] = useState<"is" | "is not">("is");
const selectedOperator = (): "is" | "is not" => {
if (excludeValue !== "") return "is not";
if (value !== "") return "is";
return operator;
};
const activeOperator = selectedOperator();
const handleOperatorChange = (next: "is" | "is not" | null) => {
if (next === null || next === activeOperator) return;
const selectedAlias = value || excludeValue || undefined;
setOperator(next);
onChange(next === "is" ? selectedAlias : undefined);
onExcludeChange(next === "is not" ? selectedAlias : undefined);
};
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteKeyAliases(
PAGE_SIZE,
emptyToUndefined(search),
@ -118,10 +140,24 @@ function KeyAliasFilterField({
return (
<DataTableFilterField label="Key Alias">
<Select items={KEY_ALIAS_OPERATORS} value={activeOperator} onValueChange={handleOperatorChange}>
<SelectTrigger className="w-full" aria-label="Key Alias operator">
<SelectValue />
</SelectTrigger>
<SelectContent>
{KEY_ALIAS_OPERATORS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<PaginatedSearchSelect
options={options}
value={value}
onValueChange={(next) => onChange(next ?? undefined)}
value={activeOperator === "is not" ? excludeValue : value}
onValueChange={(next) =>
activeOperator === "is not" ? onExcludeChange(next ?? undefined) : onChange(next ?? undefined)
}
onSearchChange={setSearch}
onLoadMore={() => void fetchNextPage()}
hasNextPage={hasNextPage}
@ -425,7 +461,9 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
<KeyAliasFilterField
value={valueOf(LOG_FILTER_IDS.KEY_ALIAS)}
excludeValue={valueOf(LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS)}
onChange={setter(LOG_FILTER_IDS.KEY_ALIAS)}
onExcludeChange={setter(LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS)}
teamId={valueOf(LOG_FILTER_IDS.TEAM_ID)}
/>

View file

@ -55,10 +55,28 @@ vi.mock("../detail", () => ({
const debounce = vi.hoisted(() => ({ settled: null as string | null }));
vi.mock("@tanstack/react-pacer/debouncer", () => ({
vi.mock("@tanstack/react-pacer/debouncer", async (importOriginal) => ({
...(await importOriginal<typeof import("@tanstack/react-pacer/debouncer")>()),
useDebouncedValue: vi.fn((value: unknown) => [debounce.settled ?? value, { cancel: vi.fn(), flush: vi.fn() }]),
}));
const emptyAliases = vi.hoisted(() => ({
data: { pages: [], pageParams: [] },
fetchNextPage: vi.fn(),
hasNextPage: false,
isFetchingNextPage: false,
isLoading: false,
}));
vi.mock("@/app/(dashboard)/hooks/keys/useKeyAliases", () => ({ useInfiniteKeyAliases: () => emptyAliases }));
vi.mock("@/app/(dashboard)/hooks/models/useModels", async (importOriginal) => ({
...(await importOriginal<typeof import("@/app/(dashboard)/hooks/models/useModels")>()),
useInfiniteModelInfo: () => emptyAliases,
}));
vi.mock("@/app/(dashboard)/hooks/spendLogs/useSpendLogUsers", () => ({ useInfiniteSpendLogUsers: () => emptyAliases }));
vi.mock("@/app/(dashboard)/hooks/spendLogs/useSpendLogEndUsers", () => ({
useInfiniteSpendLogEndUsers: () => emptyAliases,
}));
import { useDebouncedValue } from "@tanstack/react-pacer/debouncer";
import { DEBOUNCE_WAIT_MS } from "@/utils/debounceConstants";
import { uiSpendLogsCall } from "../../networking";
@ -479,6 +497,54 @@ describe("RequestLogsPanel", () => {
});
describe("time range", () => {
it("applies is not to an existing Key Alias and puts the exclusion in the URL", async () => {
const user = userEvent.setup();
renderPanel("key_alias=noisy");
await waitFor(() => expect(lastCall()?.params?.key_alias).toBe("noisy"));
await user.click(screen.getByTestId("datatable-filters-trigger"));
await user.click(screen.getByRole("combobox", { name: "Key Alias operator" }));
await user.click(await screen.findByRole("option", { name: "is not" }));
await user.click(screen.getByTestId("filter-drawer-apply"));
await waitFor(() => expect(urlParams().get("exclude_key_alias")).toBe("noisy"));
expect(urlParams().get("key_alias")).toBeNull();
await waitFor(() => expect(lastCall()?.params?.exclude_key_alias).toBe("noisy"));
expect(lastCall()?.params?.key_alias).toBeUndefined();
});
it("restores an excluded Key Alias from a bookmarked URL and clears it on reset", async () => {
const user = userEvent.setup();
renderPanel("exclude_key_alias=noisy");
await waitFor(() => expect(lastCall()?.params?.exclude_key_alias).toBe("noisy"));
expect(screen.getByTestId("filter-chip-exclude_key_alias")).toHaveTextContent("noisy");
await user.click(screen.getByRole("button", { name: "Reset Filters" }));
await waitFor(() => expect(urlParams().get("exclude_key_alias")).toBeNull());
await waitFor(() => expect(lastCall()?.params?.exclude_key_alias).toBeUndefined());
});
it("keeps an excluded Key Alias on the next page", async () => {
vi.mocked(uiSpendLogsCall).mockResolvedValue({
data: Array.from({ length: 25 }, (_, index) => logEntry({ request_id: `req-${index}` })),
total: 60,
page: 1,
page_size: 25,
total_pages: 3,
next_session_cursor: "2026-07-07 09:50:13|key-1|sess-1",
has_more: true,
});
renderPanel("exclude_key_alias=noisy");
await waitFor(() => expect(row("req-0")).not.toBeNull());
fireEvent.click(screen.getByTestId("pagination-next"));
await waitFor(() => expect(lastCall()?.page).toBe(2));
expect(lastCall()?.params?.exclude_key_alias).toBe("noisy");
});
it("requests a ~15 minute window when Last 15 Minutes is picked", async () => {
const user = userEvent.setup();
renderPanel();

View file

@ -4,6 +4,7 @@ import { useDebouncedValue } from "@tanstack/react-pacer/debouncer";
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import type { ColumnFiltersState, OnChangeFn, PaginationState, SortingState } from "@tanstack/react-table";
import moment from "moment";
import { parseAsString, useQueryStates } from "nuqs";
import { useCallback, useEffect, useMemo, useState } from "react";
import { DEFAULT_PAGE_SIZE_OPTIONS } from "@/components/shared/DataTable";
@ -17,6 +18,7 @@ import type { LogEntry } from "../types";
import {
DEFAULT_LOGS_SORTING,
formatLogsWindow,
getFilterValue,
getLogsWindowEndBound,
LOG_FILTER_IDS,
type PaginatedResponse,
@ -49,7 +51,19 @@ interface RequestLogsPanelProps {
export default function RequestLogsPanel({ accessToken, token, userRole, userID, isActive }: RequestLogsPanelProps) {
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: PAGE_SIZE });
const [sorting, setSorting] = useState<SortingState>(DEFAULT_LOGS_SORTING);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [localFilters, setLocalFilters] = useState<ColumnFiltersState>([]);
const [{ key_alias, exclude_key_alias }, setAliasParams] = useQueryStates(
{ key_alias: parseAsString, exclude_key_alias: parseAsString },
{ history: "push" },
);
const columnFilters = useMemo<ColumnFiltersState>(
() => [
...localFilters,
...(key_alias ? [{ id: LOG_FILTER_IDS.KEY_ALIAS, value: key_alias }] : []),
...(exclude_key_alias ? [{ id: LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, value: exclude_key_alias }] : []),
],
[localFilters, key_alias, exclude_key_alias],
);
const [sessionCursors, setSessionCursors] = useState<Record<number, string>>({});
const [timeRange, setTimeRange] = useState<LogsTimeRange>(defaultLogsTimeRange);
@ -179,7 +193,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
const rowCount = isLastPage ? rowsThroughThisPage : Math.max(filteredLogs.total, rowsThroughThisPage);
const handleSearchChange = useCallback((value: string) => {
setColumnFilters((previous) => {
setLocalFilters((previous) => {
const others = previous.filter((filter) => filter.id !== LOG_FILTER_IDS.SEARCH);
return value === "" ? others : [...others, { id: LOG_FILTER_IDS.SEARCH, value }];
});
@ -193,11 +207,23 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
}, []);
const handleColumnFiltersChange = useCallback<OnChangeFn<ColumnFiltersState>>((updaterOrValue) => {
setColumnFilters(updaterOrValue);
setSessionCursors({});
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
}, []);
const handleColumnFiltersChange = useCallback<OnChangeFn<ColumnFiltersState>>(
(updaterOrValue) => {
const next = typeof updaterOrValue === "function" ? updaterOrValue(columnFilters) : updaterOrValue;
setLocalFilters(
next.filter(
(filter) => filter.id !== LOG_FILTER_IDS.KEY_ALIAS && filter.id !== LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS,
),
);
void setAliasParams({
key_alias: getFilterValue(next, LOG_FILTER_IDS.KEY_ALIAS) ?? null,
exclude_key_alias: getFilterValue(next, LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS) ?? null,
});
setSessionCursors({});
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
},
[columnFilters, setAliasParams],
);
const resetToFirstPage = useCallback(() => {
setSessionCursors({});
@ -245,10 +271,11 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
);
const handleResetFilters = useCallback(() => {
setColumnFilters([]);
setLocalFilters([]);
void setAliasParams({ key_alias: null, exclude_key_alias: null });
setTimeRange(defaultLogsTimeRange());
resetToFirstPage();
}, [resetToFirstPage]);
}, [resetToFirstPage, setAliasParams]);
const handleRowClick = useCallback(
(log: LogEntry) => {

View file

@ -92,6 +92,7 @@ describe("useLogFilterLogic", () => {
{ 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" },
{ id: "exclude_key_alias", value: "alias-1", param: "exclude_key_alias" },
{ 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" },

View file

@ -26,6 +26,7 @@ export const LOG_FILTER_IDS = {
CACHE_STATUS: "cache_hit",
CREDENTIAL: "used_client_oauth_token",
KEY_ALIAS: "key_alias",
EXCLUDE_KEY_ALIAS: "exclude_key_alias",
END_USER: "end_user",
ERROR_CODE: "error_code",
ERROR_MESSAGE: "error_message",
@ -45,6 +46,7 @@ export const LOG_FILTER_LABELS: Record<string, string> = {
[LOG_FILTER_IDS.CACHE_STATUS]: "Cache",
[LOG_FILTER_IDS.CREDENTIAL]: "Credential",
[LOG_FILTER_IDS.KEY_ALIAS]: "Key Alias",
[LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS]: "Key Alias is not",
[LOG_FILTER_IDS.USER_ID]: "User ID",
[LOG_FILTER_IDS.END_USER]: "End User",
[LOG_FILTER_IDS.ERROR_CODE]: "Error Code",
@ -192,6 +194,7 @@ export function useLogFilterLogic({
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),
exclude_key_alias: getFilterValue(columnFilters, LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS),
error_code: getFilterValue(columnFilters, LOG_FILTER_IDS.ERROR_CODE),
error_message: getFilterValue(columnFilters, LOG_FILTER_IDS.ERROR_MESSAGE),
sort_by: sortBy,

View file

@ -1874,6 +1874,7 @@ interface UiSpendLogsParams {
/** Filter by model ID (litellm model deployment id) */
model_id?: string;
key_alias?: string;
exclude_key_alias?: string;
error_code?: string;
error_message?: string;
sort_by?: string;

View file

@ -72108,6 +72108,8 @@ export interface operations {
model_group?: string | null;
/** @description Filter logs by key alias */
key_alias?: string | null;
/** @description Exclude logs whose key alias exactly matches this value; logs without an alias remain included */
exclude_key_alias?: string | null;
/** @description Filter logs by end user */
end_user?: string | null;
/** @description Filter logs by error code (e.g., '404', '500') */
@ -72230,6 +72232,8 @@ export interface operations {
model_group?: string | null;
/** @description Filter logs by key alias */
key_alias?: string | null;
/** @description Exclude logs whose key alias exactly matches this value; logs without an alias remain included */
exclude_key_alias?: string | null;
/** @description Filter logs by end user */
end_user?: string | null;
/** @description Filter logs by error code (e.g., '404', '500') */