mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
fix(ui): keep untimed guardrail entries on the request lifecycle
#39050 changed RequestLifecycle from sorting every entry with (a.start_time ?? 0) to filtering on isTimed, which drops any entry whose start_time/end_time are null. That was the right call for the not_run entries the PR introduced, but it also drops entries that DID run and simply carry no timing, and those are pre-existing: add_standard_logging_guardrail_information_to_request_data defaults start_time, end_time and duration to None, and the conduct guardrail passes none of them. One such entry used to draw the whole four-row lifecycle and now draws nothing, so an admin opening that log sees an empty Request Lifecycle panel. An entry now stays on the lifecycle when it is timed OR when it ran, so not_run keeps the exclusion #39050 wanted and every other shape comes back. Offsets are number | null and render as an em dash rather than a fabricated T+0ms, which is what a null minus a null used to produce on the base. Entries without timing sort after the timed ones and the base time comes from the timed entries, so real offsets are unchanged. The two new tests fail on the base component and pass here; #39050's own not_run tests keep passing untouched, which is what makes this additive rather than a revert.
This commit is contained in:
parent
174c1ac4ed
commit
51a243e3ce
2 changed files with 60 additions and 18 deletions
|
|
@ -24,6 +24,15 @@ const skippedPreCall: Partial<GuardrailInformation> = {
|
|||
duration: null,
|
||||
};
|
||||
|
||||
const untimedPreCall: Partial<GuardrailInformation> = {
|
||||
guardrail_name: "conduct",
|
||||
guardrail_status: "success",
|
||||
guardrail_mode: "pre_call",
|
||||
start_time: null,
|
||||
end_time: null,
|
||||
duration: null,
|
||||
};
|
||||
|
||||
const ranPostCall: Partial<GuardrailInformation> = {
|
||||
guardrail_name: "ran-rail",
|
||||
guardrail_status: "success",
|
||||
|
|
@ -98,6 +107,30 @@ describe("GuardrailViewer", () => {
|
|||
expect(screen.getByText("—")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps a guardrail that ran without any timing on the lifecycle", () => {
|
||||
renderWithProviders(<GuardrailViewer data={makeGuardrailInformation(untimedPreCall)} />);
|
||||
|
||||
expect(screen.getByText("Request received")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Pre-call guardrail: conduct/)).toBeInTheDocument();
|
||||
expect(screen.getByText("LLM call")).toBeInTheDocument();
|
||||
expect(screen.getByText("Response returned")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/^T\+/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("anchors offsets on the timed entries and gives the untimed one no fabricated offset", () => {
|
||||
const untimed = makeGuardrailInformation(untimedPreCall);
|
||||
const ran = makeGuardrailInformation(ranPostCall);
|
||||
renderWithProviders(<GuardrailViewer data={[untimed, ran]} />);
|
||||
|
||||
expect(screen.getByText("Request received").parentElement).toHaveTextContent("T+0ms");
|
||||
expect(screen.getByText(/Post-call guardrail: ran-rail/).parentElement).toHaveTextContent("T+250ms");
|
||||
expect(screen.getByText("Response returned").parentElement).toHaveTextContent("T+251ms");
|
||||
|
||||
const untimedRow = screen.getByText(/Pre-call guardrail: conduct/).parentElement;
|
||||
expect(untimedRow).toHaveTextContent("—");
|
||||
expect(untimedRow).not.toHaveTextContent(/T\+/);
|
||||
});
|
||||
|
||||
it("calculates and displays masked entity totals", async () => {
|
||||
const user = userEvent.setup();
|
||||
const data = makeGuardrailInformation({
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ const GenericGuardrailResponse = ({ response }: { response: any }) => {
|
|||
interface TimelineEntry {
|
||||
type: "request" | "guardrail" | "llm" | "response";
|
||||
label: string;
|
||||
offsetMs: number;
|
||||
offsetMs: number | null;
|
||||
outcome?: EntryOutcome;
|
||||
}
|
||||
|
||||
|
|
@ -370,17 +370,26 @@ type TimedGuardrailInformation = GuardrailInformation & { start_time: number; en
|
|||
const isTimed = (e: GuardrailInformation): e is TimedGuardrailInformation =>
|
||||
typeof e.start_time === "number" && typeof e.end_time === "number";
|
||||
|
||||
const belongsOnLifecycle = (e: GuardrailInformation): boolean => isTimed(e) || getEntryOutcome(e) !== "not_run";
|
||||
|
||||
const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
|
||||
const sorted = useMemo(() => entries.filter(isTimed).sort((a, b) => a.start_time - b.start_time), [entries]);
|
||||
const sorted = useMemo(() => {
|
||||
const onLifecycle = entries.filter(belongsOnLifecycle);
|
||||
const timed = onLifecycle.filter(isTimed).sort((a, b) => a.start_time - b.start_time);
|
||||
return [...timed, ...onLifecycle.filter((e) => !isTimed(e))];
|
||||
}, [entries]);
|
||||
|
||||
const timeline = useMemo(() => {
|
||||
if (sorted.length === 0) return [];
|
||||
|
||||
const baseTime = sorted[0].start_time;
|
||||
const timed = sorted.filter(isTimed);
|
||||
const baseTime = timed.length > 0 ? timed[0].start_time : null;
|
||||
const offsetOf = (e: GuardrailInformation): number | null =>
|
||||
baseTime === null || !isTimed(e) ? null : Math.round((e.end_time - baseTime) * 1000);
|
||||
const items: TimelineEntry[] = [];
|
||||
|
||||
// Request received
|
||||
items.push({ type: "request", label: "Request received", offsetMs: 0 });
|
||||
items.push({ type: "request", label: "Request received", offsetMs: baseTime === null ? null : 0 });
|
||||
|
||||
// Pre-call guardrails — use modeMatches so array modes (e.g. ["pre_call", "post_call"])
|
||||
// place the entry in every matching bucket.
|
||||
|
|
@ -391,52 +400,50 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
|
|||
const duringCalls = sorted.filter((e) => modeMatches(e.guardrail_mode, "during_call"));
|
||||
|
||||
for (const e of preCalls) {
|
||||
const offsetMs = Math.round((e.end_time - baseTime) * 1000);
|
||||
items.push({
|
||||
type: "guardrail",
|
||||
label: `Pre-call guardrail: ${getDisplayName(e)}`,
|
||||
offsetMs,
|
||||
offsetMs: offsetOf(e),
|
||||
outcome: getEntryOutcome(e),
|
||||
});
|
||||
}
|
||||
|
||||
// LLM call — infer from gap between pre-call end and post-call start
|
||||
const lastPreEnd = preCalls.length > 0 ? Math.max(...preCalls.map((e) => e.end_time)) : baseTime;
|
||||
const firstPostStart = postCalls.length > 0 ? Math.min(...postCalls.map((e) => e.start_time)) : undefined;
|
||||
const llmEndTime = firstPostStart ?? lastPreEnd + 1;
|
||||
const llmOffsetMs = Math.round((llmEndTime - baseTime) * 1000);
|
||||
const timedPre = preCalls.filter(isTimed);
|
||||
const timedPost = postCalls.filter(isTimed);
|
||||
const lastPreEnd = timedPre.length > 0 ? Math.max(...timedPre.map((e) => e.end_time)) : baseTime;
|
||||
const firstPostStart = timedPost.length > 0 ? Math.min(...timedPost.map((e) => e.start_time)) : undefined;
|
||||
const llmEndTime = firstPostStart ?? (lastPreEnd === null ? null : lastPreEnd + 1);
|
||||
|
||||
items.push({
|
||||
type: "llm",
|
||||
label: "LLM call",
|
||||
offsetMs: llmOffsetMs,
|
||||
offsetMs: llmEndTime === null || baseTime === null ? null : Math.round((llmEndTime - baseTime) * 1000),
|
||||
});
|
||||
|
||||
// During-call guardrails (rare)
|
||||
for (const e of duringCalls) {
|
||||
const offsetMs = Math.round((e.end_time - baseTime) * 1000);
|
||||
items.push({
|
||||
type: "guardrail",
|
||||
label: `During-call guardrail: ${getDisplayName(e)}`,
|
||||
offsetMs,
|
||||
offsetMs: offsetOf(e),
|
||||
outcome: getEntryOutcome(e),
|
||||
});
|
||||
}
|
||||
|
||||
// Post-call guardrails
|
||||
for (const e of postCalls) {
|
||||
const offsetMs = Math.round((e.end_time - baseTime) * 1000);
|
||||
items.push({
|
||||
type: "guardrail",
|
||||
label: `Post-call guardrail: ${getDisplayName(e)}`,
|
||||
offsetMs,
|
||||
offsetMs: offsetOf(e),
|
||||
outcome: getEntryOutcome(e),
|
||||
});
|
||||
}
|
||||
|
||||
// Response returned
|
||||
const maxEnd = Math.max(...sorted.map((e) => e.end_time));
|
||||
const responseOffsetMs = Math.round((maxEnd - baseTime) * 1000) + 1;
|
||||
const maxEnd = timed.length > 0 ? Math.max(...timed.map((e) => e.end_time)) : null;
|
||||
const responseOffsetMs = maxEnd === null || baseTime === null ? null : Math.round((maxEnd - baseTime) * 1000) + 1;
|
||||
items.push({ type: "response", label: "Response returned", offsetMs: responseOffsetMs });
|
||||
|
||||
return items;
|
||||
|
|
@ -475,7 +482,9 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
|
|||
{OUTCOME_LABEL[item.outcome]}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground font-mono ml-auto shrink-0">T+{item.offsetMs}ms</span>
|
||||
<span className="text-xs text-muted-foreground font-mono ml-auto shrink-0">
|
||||
{item.offsetMs === null ? "—" : `T+${item.offsetMs}ms`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue