mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-01 02:02:20 +00:00
fix(ui): surface x-litellm-call-id in Logs search, table and drawer (#42436)
* fix(ui): surface x-litellm-call-id in Logs search, table and drawer Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore(ui): regenerate api types for spend logs search description Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(e2e): drop redundant comments from the call id logs helpers Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style(e2e): format logs call id helper and spec Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): keep one id per Logs row, move x-litellm-call-id to hover and drawer The Request ID cell shows only request_id again. When the row's litellm_call_id differs, the cell tooltip lists it as x-litellm-call-id with its own copy button, and the drawer header labels the second line x-litellm-call-id: instead of the call id caption. Stacking two ids in every row made the column noisy for the common case where the viewer only needs the row they searched for. * test(e2e): cover the Request ID tooltip hover and copy path Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: poll the clipboard after the tooltip copy and drop a jsdom aside The e2e read navigator.clipboard right after the click, so a slow async write could fail the check even though copy works. The unit test's fireEvent choice (jsdom has no layout, so a real pointer move off the trigger closes the tooltip before the click lands) is documented here instead of inline. --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
This commit is contained in:
parent
82eb7405f5
commit
8afabe81f1
9 changed files with 238 additions and 49 deletions
|
|
@ -2444,7 +2444,7 @@ def _build_spend_log_search_condition(
|
|||
f"(request_id = {raw} OR ("
|
||||
f"\"startTime\" >= ({window_start}::timestamptz AT TIME ZONE 'UTC') "
|
||||
f"AND \"startTime\" <= ({window_end}::timestamptz AT TIME ZONE 'UTC') "
|
||||
f'AND (api_key = {raw} OR team_id = {raw} OR "user" = {raw} OR end_user = {raw} '
|
||||
f'AND (litellm_call_id = {raw} OR api_key = {raw} OR team_id = {raw} OR "user" = {raw} OR end_user = {raw} '
|
||||
f"OR session_id = {raw} OR model_id = {raw})))"
|
||||
)
|
||||
return _SpendLogSearchCondition(sql=sql, params=(search, start_date, end_date))
|
||||
|
|
@ -2557,7 +2557,7 @@ async def ui_view_spend_logs(
|
|||
search: str | None = fastapi.Query(
|
||||
default=None,
|
||||
description=(
|
||||
"Match a log whose request_id, api_key (hash), team_id, user, end_user, "
|
||||
"Match a log whose request_id, litellm_call_id, api_key (hash), team_id, user, end_user, "
|
||||
"session_id, or model_id equals this value. request_id matches across all time; the other columns "
|
||||
"match inside start_date/end_date, which stay required"
|
||||
),
|
||||
|
|
|
|||
|
|
@ -51,6 +51,21 @@ export async function sendChatCompletion(request: APIRequestContext, opts: ChatO
|
|||
return body.id as string;
|
||||
}
|
||||
|
||||
export interface ServedChat {
|
||||
requestId: string;
|
||||
callId: string;
|
||||
}
|
||||
|
||||
export async function sendChatCompletionWithCallId(request: APIRequestContext, opts: ChatOptions): Promise<ServedChat> {
|
||||
const res = await postChatCompletion(request, opts);
|
||||
expect(res.ok(), `chat completion for ${opts.model} failed (${res.status()}): ${await res.text()}`).toBe(true);
|
||||
const callId = res.headers()["x-litellm-call-id"];
|
||||
expect(callId, "proxy did not return an x-litellm-call-id header").toBeTruthy();
|
||||
const body = await res.json();
|
||||
expect(body.choices?.[0]?.message?.content).toContain(MOCK_RESPONSE_TEXT);
|
||||
return { requestId: body.id as string, callId };
|
||||
}
|
||||
|
||||
export interface ChatAttempt {
|
||||
status: number;
|
||||
body: string;
|
||||
|
|
@ -124,7 +139,7 @@ export async function waitForSpendLog(
|
|||
lastStatus = res.status();
|
||||
if (res.ok()) {
|
||||
const body = await res.json();
|
||||
const rows = Array.isArray(body) ? body : (body?.data ?? []);
|
||||
const rows = Array.isArray(body) ? body : body?.data ?? [];
|
||||
if (rows.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
CHAT_MODEL_A,
|
||||
MOCK_RESPONSE_TEXT,
|
||||
sendChatCompletion,
|
||||
sendChatCompletionWithCallId,
|
||||
waitForSpendLog,
|
||||
waitForSpendLogByPrompt,
|
||||
} from "../../helpers/traffic";
|
||||
|
|
@ -95,6 +96,50 @@ test.describe("Logs page", () => {
|
|||
await expect(drawer.getByText(MOCK_RESPONSE_TEXT, { exact: false }).first()).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
|
||||
test("a served request's Logs row and drawer show its x-litellm-call-id", async ({ page, request }) => {
|
||||
const prompt = `logs-call-id-prompt-${uniqueSuffix()}`;
|
||||
const { requestId, callId } = await sendChatCompletionWithCallId(request, {
|
||||
model: CHAT_MODEL_A,
|
||||
prompt,
|
||||
});
|
||||
expect(callId, "call id must differ from the provider response id for this check to mean anything").not.toBe(
|
||||
requestId,
|
||||
);
|
||||
await waitForSpendLog(request, requestId);
|
||||
|
||||
await navigateToPage(page, Page.Logs);
|
||||
await dismissFeedbackPopup(page);
|
||||
const search = visibleTestId(page, "datatable-search");
|
||||
await expect(search).toBeVisible({ timeout: 20_000 });
|
||||
await search.fill(callId);
|
||||
|
||||
const row = requestLogsRows(page).filter({ hasText: requestId });
|
||||
await expect(row, `no logs row for call id ${callId}`).toHaveCount(1, { timeout: 30_000 });
|
||||
await expect(row, "the row itself shows only the request id").not.toContainText(callId);
|
||||
|
||||
await row.getByText(requestId).hover();
|
||||
const tooltip = page.locator("[data-slot='tooltip-content']");
|
||||
await expect(tooltip, "hovering the Request ID cell does not list the x-litellm-call-id").toContainText(
|
||||
`x-litellm-call-id: ${callId}`,
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
await tooltip.getByRole("button", { name: "Copy x-litellm-call-id" }).click();
|
||||
if (await page.evaluate(() => window.isSecureContext)) {
|
||||
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(callId);
|
||||
}
|
||||
|
||||
await row.click();
|
||||
const drawer = page.getByRole("dialog").first();
|
||||
await expect(drawer.getByText("Request & Response")).toBeVisible({ timeout: 20_000 });
|
||||
await expect(drawer.getByText("x-litellm-call-id:"), "drawer header lacks the x-litellm-call-id line").toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(
|
||||
drawer.getByText(callId, { exact: false }).first(),
|
||||
`drawer does not show x-litellm-call-id ${callId}`,
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
// Split out because only the copy path needs a secure context; folding it in would
|
||||
// take the drawer-rendering coverage down with it.
|
||||
test("the drawer copies the request and the response to the clipboard", async ({ page, request }) => {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ def _filter_logs_by_date_range(logs, where):
|
|||
_SEARCH_CLAUSE_RE = re.compile(
|
||||
r'\(request_id = \$(\d+) OR \("startTime" >= \(\$(\d+)::timestamptz AT TIME ZONE \'UTC\'\) '
|
||||
r'AND "startTime" <= \(\$(\d+)::timestamptz AT TIME ZONE \'UTC\'\) '
|
||||
r'AND \(api_key = \$\1 OR team_id = \$\1 OR "user" = \$\1 OR end_user = \$\1 '
|
||||
r'AND \(litellm_call_id = \$\1 OR api_key = \$\1 OR team_id = \$\1 OR "user" = \$\1 OR end_user = \$\1 '
|
||||
r"OR session_id = \$\1 OR model_id = \$\1\)\)\)"
|
||||
)
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ def _matches_spend_log_search(log, search):
|
|||
return True
|
||||
if not _filter_logs_by_date_range([log], {"startTime": {"gte": search["gte"], "lte": search["lte"]}}):
|
||||
return False
|
||||
columns = ("api_key", "team_id", "user", "end_user", "session_id", "model_id")
|
||||
columns = ("litellm_call_id", "api_key", "team_id", "user", "end_user", "session_id", "model_id")
|
||||
return any(log.get(col) == search["value"] for col in columns)
|
||||
|
||||
|
||||
|
|
@ -2986,7 +2986,7 @@ def test_build_spend_log_search_condition_windows_every_branch_except_request_id
|
|||
assert condition.sql == (
|
||||
"(request_id = $3 OR (\"startTime\" >= ($4::timestamptz AT TIME ZONE 'UTC') "
|
||||
"AND \"startTime\" <= ($5::timestamptz AT TIME ZONE 'UTC') "
|
||||
'AND (api_key = $3 OR team_id = $3 OR "user" = $3 OR end_user = $3 OR session_id = $3 OR model_id = $3)))'
|
||||
'AND (litellm_call_id = $3 OR api_key = $3 OR team_id = $3 OR "user" = $3 OR end_user = $3 OR session_id = $3 OR model_id = $3)))'
|
||||
)
|
||||
assert condition.params == ("key-hash-7", start, end)
|
||||
|
||||
|
|
@ -3012,6 +3012,8 @@ def _search_fixture_logs(today):
|
|||
{**base, "request_id": "req-user", "user": "user-7", "startTime": recent},
|
||||
{**base, "request_id": "req-end-user", "end_user": "cust-7", "startTime": recent},
|
||||
{**base, "request_id": "req-model", "model_id": "mdl-7", "startTime": recent},
|
||||
{**base, "request_id": "chatcmpl-x", "litellm_call_id": "call-recent", "startTime": recent},
|
||||
{**base, "request_id": "chatcmpl-old", "litellm_call_id": "call-old", "startTime": old},
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -3046,6 +3048,8 @@ def _five_day_window(today):
|
|||
("user-7", {"req-user"}),
|
||||
("cust-7", {"req-end-user"}),
|
||||
("mdl-7", {"req-model"}),
|
||||
("call-recent", {"chatcmpl-x"}),
|
||||
("call-old", set()),
|
||||
("no-such-id", set()),
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,23 @@ describe("DrawerHeader sidebar toggle", () => {
|
|||
expect(within(row).getByText("gpt-4o")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the x-litellm-call-id with its own copy button when it differs from the request id", () => {
|
||||
renderHeader(logEntry({ request_id: "chatcmpl-h", litellm_call_id: "call-h" }), false);
|
||||
|
||||
expect(screen.getByText("chatcmpl-h")).toBeInTheDocument();
|
||||
expect(screen.getByText("call-h")).toBeInTheDocument();
|
||||
expect(screen.getByText("x-litellm-call-id:")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Copy Request ID" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Copy x-litellm-call-id" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("omits the x-litellm-call-id line and button when the ids match", () => {
|
||||
renderHeader(logEntry({ request_id: "same-h", litellm_call_id: "same-h" }), false);
|
||||
|
||||
expect(screen.queryByText("x-litellm-call-id:")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Copy x-litellm-call-id" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to the request id row when the log names no model", () => {
|
||||
renderHeader(logEntry({ model: "", custom_llm_provider: "" }), true);
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ export function DrawerHeader({
|
|||
const providerInfo = provider ? getProviderLogoAndName(provider) : null;
|
||||
const showToggleWithProvider = isSidebarCollapsed && Boolean(providerInfo || log.model);
|
||||
const showToggleWithRequestId = isSidebarCollapsed && !showToggleWithProvider;
|
||||
const callId: string | null =
|
||||
log.litellm_call_id && log.litellm_call_id !== log.request_id ? log.litellm_call_id : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -85,7 +87,20 @@ export function DrawerHeader({
|
|||
}}
|
||||
>
|
||||
{showToggleWithRequestId && <SidebarToggle isCollapsed onToggle={onToggleSidebar} />}
|
||||
<RequestIdSection requestId={log.request_id} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<CopyableId value={log.request_id} label="Request ID" fontSize={FONT_SIZE_HEADER} />
|
||||
{callId && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: FONT_SIZE_MEDIUM, fontFamily: FONT_FAMILY_MONO, whiteSpace: "nowrap" }}
|
||||
>
|
||||
x-litellm-call-id:
|
||||
</span>
|
||||
<CopyableId value={callId} label="x-litellm-call-id" fontSize={FONT_SIZE_MEDIUM} muted />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<NavigationSection onPrevious={onPrevious} onNext={onNext} onClose={onClose} />
|
||||
</div>
|
||||
|
||||
|
|
@ -140,15 +155,22 @@ function ModelProviderSection({
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request ID display with copy functionality
|
||||
*/
|
||||
function RequestIdSection({ requestId }: { requestId: string }) {
|
||||
function CopyableId({
|
||||
value,
|
||||
label,
|
||||
fontSize,
|
||||
muted,
|
||||
}: {
|
||||
value: string;
|
||||
label: string;
|
||||
fontSize: number;
|
||||
muted?: boolean;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(requestId);
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1200);
|
||||
} catch {
|
||||
|
|
@ -157,38 +179,36 @@ function RequestIdSection({ requestId }: { requestId: string }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span
|
||||
className="font-semibold"
|
||||
style={{
|
||||
fontSize: FONT_SIZE_HEADER,
|
||||
fontFamily: FONT_FAMILY_MONO,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span
|
||||
className={muted ? "text-muted-foreground" : "font-semibold"}
|
||||
style={{
|
||||
fontSize,
|
||||
fontFamily: FONT_FAMILY_MONO,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{value}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={copied ? "Copied!" : `Copy ${label}`}
|
||||
onClick={handleCopy}
|
||||
className="ml-1 align-middle text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{requestId}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={copied ? "Copied!" : "Copy Request ID"}
|
||||
onClick={handleCopy}
|
||||
className="ml-1 align-middle text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{requestId}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{value}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
|
|
@ -7,6 +7,13 @@ import { DataTable } from "@/components/shared/DataTable";
|
|||
import type { LogEntry } from "./columns";
|
||||
import { getRequestLogsTableColumns } from "./RequestLogsTableColumns";
|
||||
|
||||
const { copyToClipboardMock } = vi.hoisted(() => ({ copyToClipboardMock: vi.fn() }));
|
||||
|
||||
vi.mock("@/utils/dataUtils", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/utils/dataUtils")>()),
|
||||
copyToClipboard: copyToClipboardMock,
|
||||
}));
|
||||
|
||||
const logEntry = (overrides: Partial<LogEntry>): LogEntry => ({
|
||||
request_id: "req-1",
|
||||
api_key: "key-1",
|
||||
|
|
@ -267,13 +274,64 @@ describe("batch rows", () => {
|
|||
});
|
||||
|
||||
it("leaves ordinary request ids untouched", () => {
|
||||
renderRows([logEntry({ request_id: "chatcmpl-42" })]);
|
||||
renderRows([logEntry({ request_id: "chatcmpl-42", litellm_call_id: "chatcmpl-42" })]);
|
||||
|
||||
expect(screen.getByText("chatcmpl-42")).toBeInTheDocument();
|
||||
expect(screen.queryByText("batch cost")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Request ID column", () => {
|
||||
it("shows only the request id in the cell and the x-litellm-call-id in its tooltip when they differ", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderRows([logEntry({ request_id: "chatcmpl-9", litellm_call_id: "call-uuid-9" })]);
|
||||
|
||||
expect(screen.getByText("chatcmpl-9")).toBeInTheDocument();
|
||||
expect(screen.queryByText("call-uuid-9")).not.toBeInTheDocument();
|
||||
|
||||
await user.hover(screen.getByText("chatcmpl-9"));
|
||||
expect(await screen.findByText("x-litellm-call-id: call-uuid-9")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("copies the x-litellm-call-id from the tooltip without opening the row", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRowClick = vi.fn();
|
||||
render(
|
||||
<DataTable
|
||||
data={[logEntry({ request_id: "chatcmpl-9", litellm_call_id: "call-uuid-9" })]}
|
||||
columns={getRequestLogsTableColumns(noopDeps)}
|
||||
getRowId={(row) => row.request_id}
|
||||
size="compact"
|
||||
onRowClick={onRowClick}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.hover(screen.getByText("chatcmpl-9"));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Copy x-litellm-call-id" }));
|
||||
|
||||
expect(copyToClipboardMock).toHaveBeenCalledWith("call-uuid-9");
|
||||
expect(onRowClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the plain id tooltip when request id and call id are the same", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderRows([logEntry({ request_id: "same-id-7", litellm_call_id: "same-id-7" })]);
|
||||
|
||||
await user.hover(screen.getByText("same-id-7"));
|
||||
await waitFor(() => expect(screen.getAllByText("same-id-7")).toHaveLength(2));
|
||||
expect(screen.queryByText(/x-litellm-call-id/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the plain id tooltip when the row carries no call id", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderRows([logEntry({ request_id: "chatcmpl-no-call", litellm_call_id: null })]);
|
||||
|
||||
await user.hover(screen.getByText("chatcmpl-no-call"));
|
||||
await waitFor(() => expect(screen.getAllByText("chatcmpl-no-call")).toHaveLength(2));
|
||||
expect(screen.queryByText(/x-litellm-call-id/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Model column", () => {
|
||||
it("lists every model used across a conversation, not only the representative call's model", () => {
|
||||
const conversationCall: Partial<LogEntry> = {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Copy } from "lucide-react";
|
||||
|
||||
import { DataTableSortHeader } from "@/components/shared/DataTable";
|
||||
import { CellTooltip, DateCell, IdCell, MoneyCell, StatusBadge } from "@/components/shared/table_cells";
|
||||
import { getSpendString } from "@/utils/dataUtils";
|
||||
import { copyToClipboard, getSpendString } from "@/utils/dataUtils";
|
||||
|
||||
import { getProviderLogoAndName } from "../provider_info_helpers";
|
||||
import { getBatchIdFromRequestId, getBatchRequestCounts, isBatchCallType } from "./batchLogUtils";
|
||||
|
|
@ -30,6 +31,28 @@ const readMcpLogoUrl = (metadata: Record<string, unknown> | undefined): string |
|
|||
return typeof url === "string" && url !== "" ? url : undefined;
|
||||
};
|
||||
|
||||
function RequestIdWithCallIdTooltip({ requestId, callId }: { requestId: string; callId: string }) {
|
||||
return (
|
||||
<span className="flex flex-col gap-0.5">
|
||||
<span>{requestId}</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span>x-litellm-call-id: {callId}</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Copy x-litellm-call-id"
|
||||
className="shrink-0 cursor-pointer opacity-70 hover:opacity-100"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void copyToClipboard(callId);
|
||||
}}
|
||||
>
|
||||
<Copy className="size-3" />
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const getLogoUrl = (row: LogEntry, provider: string): string =>
|
||||
readMcpLogoUrl(row.metadata) ?? (provider ? getProviderLogoAndName(provider).logo : "");
|
||||
|
||||
|
|
@ -160,7 +183,14 @@ export const getRequestLogsTableColumns = ({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
return <IdCell value={log.request_id} variant="plain" />;
|
||||
const callId = log.litellm_call_id && log.litellm_call_id !== log.request_id ? log.litellm_call_id : null;
|
||||
return (
|
||||
<IdCell
|
||||
value={log.request_id}
|
||||
variant="plain"
|
||||
tooltip={callId ? <RequestIdWithCallIdTooltip requestId={log.request_id} callId={callId} /> : undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
4
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
4
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -66115,7 +66115,7 @@ export interface operations {
|
|||
group_by_session?: boolean;
|
||||
/** @description Keyset cursor '<last_activity>|<api_key>|<session_key>' from a previous group_by_session page. UI route only, honored when sorting by startTime */
|
||||
session_cursor?: string | null;
|
||||
/** @description Match a log whose request_id, api_key (hash), team_id, user, end_user, session_id, or model_id equals this value. request_id matches across all time; the other columns match inside start_date/end_date, which stay required */
|
||||
/** @description Match a log whose request_id, litellm_call_id, api_key (hash), team_id, user, end_user, session_id, or model_id equals this value. request_id matches across all time; the other columns match inside start_date/end_date, which stay required */
|
||||
search?: string | null;
|
||||
};
|
||||
header?: never;
|
||||
|
|
@ -66235,7 +66235,7 @@ export interface operations {
|
|||
group_by_session?: boolean;
|
||||
/** @description Keyset cursor '<last_activity>|<api_key>|<session_key>' from a previous group_by_session page. UI route only, honored when sorting by startTime */
|
||||
session_cursor?: string | null;
|
||||
/** @description Match a log whose request_id, api_key (hash), team_id, user, end_user, session_id, or model_id equals this value. request_id matches across all time; the other columns match inside start_date/end_date, which stay required */
|
||||
/** @description Match a log whose request_id, litellm_call_id, api_key (hash), team_id, user, end_user, session_id, or model_id equals this value. request_id matches across all time; the other columns match inside start_date/end_date, which stay required */
|
||||
search?: string | null;
|
||||
};
|
||||
header?: never;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue