fix(proxy): bound client x-litellm-call-id, open log deep links by call id, prefer exact request_id rows

This commit is contained in:
mateo-berri 2026-09-02 15:45:55 -07:00
parent 808fac0d7e
commit cbeef3b98c
9 changed files with 160 additions and 7 deletions

View file

@ -104,6 +104,7 @@ DEFAULT_MCP_SEMANTIC_FILTER_SIMILARITY_THRESHOLD: Final = float(
os.getenv("DEFAULT_MCP_SEMANTIC_FILTER_SIMILARITY_THRESHOLD", 0.3)
)
MAX_MCP_SEMANTIC_FILTER_TOOLS_HEADER_LENGTH: Final = int(os.getenv("MAX_MCP_SEMANTIC_FILTER_TOOLS_HEADER_LENGTH", 150))
MAX_LITELLM_CALL_ID_LENGTH: Final = 256
DEFAULT_AUTO_ROUTER_MAX_INPUT_CHARS: Final = 2000

View file

@ -25,6 +25,7 @@ from litellm.constants import (
DEFAULT_MAX_RECURSE_DEPTH,
LITELLM_DETAILED_TIMING,
LITELLM_HTTP_STATUS_CLIENT_DISCONNECTED,
MAX_LITELLM_CALL_ID_LENGTH,
MAX_PAYLOAD_SIZE_FOR_DEBUG_LOG,
NON_INFERENCE_CALL_TYPES,
RETURN_RAW_MODEL_NAME_METADATA_KEY,
@ -210,6 +211,12 @@ def _withheld_provider_output(response: object) -> bool:
return getattr(response, "has_buffered_provider_output", False) is True
def resolve_litellm_call_id(client_call_id: str | None) -> str:
if client_call_id is not None and 0 < len(client_call_id) <= MAX_LITELLM_CALL_ID_LENGTH:
return client_call_id
return str(uuid.uuid4())
def _should_return_raw_model_name(request_data: dict[str, object]) -> bool:
return any(
isinstance(metadata, dict) and metadata.get(RETURN_RAW_MODEL_NAME_METADATA_KEY) is True
@ -1923,7 +1930,7 @@ class ProxyBaseLLMRequestProcessing:
if alias_target is not None:
self.data["model"] = alias_target
self.data["litellm_call_id"] = request.headers.get("x-litellm-call-id", str(uuid.uuid4()))
self.data["litellm_call_id"] = resolve_litellm_call_id(request.headers.get("x-litellm-call-id"))
DDSpanTagger.tag_call_id(self.data.get("litellm_call_id"))
DDSpanTagger.tag_request(
user_api_key_dict=user_api_key_dict,

View file

@ -28,6 +28,7 @@ verbose_logger.setLevel(logging.DEBUG)
ignored_keys = [
"request_id",
"litellm_call_id",
"metadata.litellm_call_id",
"session_id",
"startTime",

View file

@ -13,7 +13,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
import litellm
from litellm._uuid import uuid
from litellm.constants import RETURN_RAW_MODEL_NAME_METADATA_KEY
from litellm.constants import MAX_LITELLM_CALL_ID_LENGTH, RETURN_RAW_MODEL_NAME_METADATA_KEY
from litellm.integrations.custom_logger import CustomLogger
from litellm.integrations.opentelemetry import UserAPIKeyAuth
from litellm.proxy.common_request_processing import (
@ -30,6 +30,7 @@ from litellm.proxy.common_request_processing import (
_has_attribute_error_in_chain,
_is_azure_model_router_request,
open_sse_before_first_byte,
resolve_litellm_call_id,
ttft_keepalive_interval,
_override_openai_response_model,
_parse_event_data_for_error,
@ -7665,3 +7666,16 @@ def test_log_llm_api_exception_traceback_only_for_unexpected_errors(exc, expect_
records = [r for r in caplog.records if "_handle_llm_api_exception(): Exception occured" in r.getMessage()]
assert len(records) == 1
assert (records[0].exc_info is not None) is expect_traceback
class TestResolveLitellmCallId:
def test_client_call_id_within_the_bound_is_kept(self):
assert resolve_litellm_call_id("req-abc-123") == "req-abc-123"
at_bound: Final = "y" * MAX_LITELLM_CALL_ID_LENGTH
assert resolve_litellm_call_id(at_bound) == at_bound
@pytest.mark.parametrize("client_call_id", [None, "", "x" * (MAX_LITELLM_CALL_ID_LENGTH + 1), "z" * 3000])
def test_missing_empty_or_oversized_client_call_id_gets_a_generated_uuid(self, client_call_id):
resolved: Final = resolve_litellm_call_id(client_call_id)
assert resolved != client_call_id
assert uuid.UUID(resolved).version == 4

View file

@ -0,0 +1,97 @@
import userEvent from "@testing-library/user-event";
import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders, screen, testQueryClient, waitFor } from "../../../tests/test-utils";
import type { LogEntry as SpendLogEntry } from "@/components/view_logs/columns";
import { LogViewer } from "./LogViewer";
vi.mock("@/components/networking", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/components/networking")>();
return { ...actual, uiSpendLogsCall: vi.fn() };
});
vi.mock("@/components/view_logs/LogDetailsDrawer", () => ({
LogDetailsDrawer: function LogDetailsDrawerMock({
open,
logEntry,
}: {
open: boolean;
logEntry?: { request_id: string } | null;
}) {
return (
<div data-testid="log-details-drawer" data-log-id={logEntry?.request_id ?? ""}>
{open ? "open" : "closed"}
</div>
);
},
}));
import { uiSpendLogsCall } from "@/components/networking";
const spendLog = (overrides: Partial<SpendLogEntry>): SpendLogEntry => ({
request_id: "req-1",
api_key: "key-1",
team_id: "team-1",
model: "gpt-4o",
model_id: "model-1",
call_type: "acompletion",
spend: 0.01,
total_tokens: 10,
prompt_tokens: 5,
completion_tokens: 5,
startTime: "2026-09-02T09:50:13Z",
endTime: "2026-09-02T09:50:14Z",
cache_hit: "false",
messages: [],
response: {},
...overrides,
});
const guardrailLog = {
id: "provider-victim",
timestamp: "2026-09-02 09:50:13",
action: "passed" as const,
input_snippet: "victim prompt",
};
describe("GuardrailsMonitor LogViewer drawer", () => {
beforeEach(() => {
vi.mocked(uiSpendLogsCall).mockReset();
testQueryClient.clear();
});
it("opens the row whose request_id is the clicked log id even when a newer row carries that id as its call id", async () => {
vi.mocked(uiSpendLogsCall).mockResolvedValue({
data: [
spendLog({ request_id: "provider-attacker", litellm_call_id: "provider-victim" }),
spendLog({ request_id: "provider-victim", litellm_call_id: "call-victim" }),
],
total: 2,
});
renderWithProviders(<LogViewer logs={[guardrailLog]} accessToken="sk-test" />);
await userEvent.click(screen.getByText("victim prompt"));
await waitFor(() => {
expect(screen.getByTestId("log-details-drawer")).toHaveAttribute("data-log-id", "provider-victim");
});
expect(vi.mocked(uiSpendLogsCall)).toHaveBeenCalledWith(
expect.objectContaining({ params: { request_id: "provider-victim" } }),
);
});
it("falls back to the first returned row when none carries the clicked id as its request_id", async () => {
vi.mocked(uiSpendLogsCall).mockResolvedValue({
data: [spendLog({ request_id: "provider-other", litellm_call_id: "provider-victim" })],
total: 1,
});
renderWithProviders(<LogViewer logs={[guardrailLog]} accessToken="sk-test" />);
await userEvent.click(screen.getByText("victim prompt"));
await waitFor(() => {
expect(screen.getByTestId("log-details-drawer")).toHaveAttribute("data-log-id", "provider-other");
});
});
});

View file

@ -92,7 +92,8 @@ export function LogViewer({
enabled: Boolean(accessToken && selectedRequestId && drawerOpen),
});
const selectedLog: ViewLogsLogEntry | null = fullLogResponse?.data?.[0] ?? null;
const selectedLog: ViewLogsLogEntry | null =
fullLogResponse?.data?.find((log) => log.request_id === selectedRequestId) ?? fullLogResponse?.data?.[0] ?? null;
const handleLogClick = (log: LogEntry) => {
setSelectedRequestId(log.id);

View file

@ -298,6 +298,36 @@ describe("RequestLogsPanel", () => {
expect(byIdCall.page_size).toBe(1);
});
it("opens the drawer when ?log_id= is the log's litellm_call_id rather than its request_id", async () => {
respondWith([logEntry({ request_id: "chatcmpl-provider", litellm_call_id: "call-1" })]);
renderPanel("?log_id=call-1");
await waitFor(() => {
expect(drawer()).toHaveTextContent("open");
});
expect(drawer()).toHaveAttribute("data-log-id", "chatcmpl-provider");
});
it("fetches by litellm_call_id and opens the drawer when that log is not in the loaded page", async () => {
vi.mocked(uiSpendLogsCall).mockImplementation(async ({ params }) =>
params?.request_id === "call-old"
? {
data: [logEntry({ request_id: "chatcmpl-old", litellm_call_id: "call-old" })],
total: 1,
page: 1,
page_size: 1,
total_pages: 1,
}
: { data: [], total: 0, page: 1, page_size: 50, total_pages: 0 },
);
renderPanel("?log_id=call-old");
await waitFor(() => {
expect(drawer()).toHaveTextContent("open");
});
expect(drawer()).toHaveAttribute("data-log-id", "chatcmpl-old");
});
it("closing the drawer removes ?log_id= from the URL and closes the drawer", async () => {
const user = userEvent.setup();
respondWith([logEntry({ request_id: "req-1" })]);

View file

@ -26,6 +26,7 @@ import { RequestLogsTable } from "./RequestLogsTable";
const PAGE_SIZE = 50;
const DEFAULT_INTERVAL = { value: 24, unit: "hours" };
const matchesLogId = (log: LogEntry, logId: string) => log.request_id === logId || log.litellm_call_id === logId;
interface RequestLogsPanelProps {
accessToken: string;
@ -133,9 +134,9 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
page_size: 1,
params: { request_id: urlLogId },
});
return response.data.find((log) => log.request_id === urlLogId) ?? null;
return response.data.find((log) => matchesLogId(log, urlLogId)) ?? null;
},
enabled: urlLogId !== null && selectedLog?.request_id !== urlLogId,
enabled: urlLogId !== null && !(selectedLog !== null && matchesLogId(selectedLog, urlLogId)),
staleTime: Infinity,
};
@ -143,8 +144,8 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
const displayLog = useMemo<LogEntry | null>(() => {
if (urlLogId === null) return null;
if (selectedLog?.request_id === urlLogId) return selectedLog;
return filteredLogs.data.find((log) => log.request_id === urlLogId) ?? urlLog ?? null;
if (selectedLog !== null && matchesLogId(selectedLog, urlLogId)) return selectedLog;
return filteredLogs.data.find((log) => matchesLogId(log, urlLogId)) ?? urlLog ?? null;
}, [urlLogId, selectedLog, filteredLogs.data, urlLog]);
const displaySessionId = useMemo<string | null>(() => {

View file

@ -12,6 +12,7 @@ export type LogsSortField = keyof typeof LOGS_SORT_FIELD_MAP;
export type LogEntry = {
request_id: string;
litellm_call_id?: string | null;
api_key: string;
team_id: string;
model: string;