diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx index 8fef86d47cb..d4f1fc5d099 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx @@ -5,9 +5,9 @@ import { useState, type ReactNode } from "react"; import { StatusBadge, type StatusTone } from "@/components/shared/table_cells"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"; -import { conclusions, type InFlight } from "../../model/live"; +import { conclusions } from "../../model/live"; import { releasedReviews } from "../../model/stage"; -import type { Job, Review, Settings } from "../../model/types"; +import type { InFlight, Job, Review, Settings } from "../../model/types"; import { ConclusionsPanel } from "./ConclusionsPanel"; import { ModelName } from "./LiveStrip"; import { NowReading } from "./NowReading"; diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx index b1d83a53033..8e47057274c 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx @@ -25,7 +25,6 @@ export function LiveRun({ name: string; queue?: QueueContext; }) { - const live = job.status === "queued" || job.status === "running"; const reason = useQueueReason(job, queue); const [stripOpen, setStripOpen] = useStripOpen(); const [drawerOpen, setDrawerOpen] = useState(false); diff --git a/ui/litellm-dashboard/src/components/lens/model/live.test.ts b/ui/litellm-dashboard/src/components/lens/model/live.test.ts index aa7004cd88d..b6b3bef391a 100644 --- a/ui/litellm-dashboard/src/components/lens/model/live.test.ts +++ b/ui/litellm-dashboard/src/components/lens/model/live.test.ts @@ -9,7 +9,6 @@ import { newestFirst, nowLine, inFlight, - liveRows, grownGroups, inGroup, share, @@ -19,12 +18,10 @@ import { shortVerdict, stripState, liveJob, - liveStats, outcome, providerOf, - rateLabel, secondsToFinishReading, - tokenLabel, + polling, } from "./live"; import type { Job, Review } from "./types"; @@ -273,6 +270,13 @@ describe("incremental reviews", () => { expect(feed.reviews[0].execution_id).toBe("r50"); expect(feed.cursor).toBe(250); }); + + it("keeps polling a finished run until every review it reported has arrived", () => { + const fetched = { reviews: [], cursor: 28 }; + expect(polling({ status: "running", reviewed: 28 }, fetched)).toBe(true); + expect(polling({ status: "completed", reviewed: 30 }, fetched)).toBe(true); + expect(polling({ status: "completed", reviewed: 30 }, { ...fetched, cursor: 30 })).toBe(false); + }); }); describe("time left reading", () => { @@ -301,73 +305,6 @@ describe("short verdict", () => { }); }); -describe("live stats", () => { - const job = { - reviewed: 30, - cost: 0.042, - created_at: "2026-10-03T16:00:00Z", - finished_at: null, - steps: [ - { - at: "2026-10-03T16:00:05Z", - kind: "stage", - label: "Reading executions", - model: "", - purpose: "", - cost: 0, - prompt_tokens: 0, - completion_tokens: 0, - }, - { - at: "2026-10-03T16:00:06Z", - kind: "model", - label: "extract", - model: "m", - purpose: "extract", - cost: 0.01, - prompt_tokens: 1200, - completion_tokens: 300, - }, - { - at: "2026-10-03T16:00:07Z", - kind: "model", - label: "extract", - model: "m", - purpose: "extract", - cost: 0.01, - prompt_tokens: 800, - completion_tokens: 200, - }, - ], - } as unknown as Job; - - it("measures rate from when reading started and sums model tokens", () => { - const stats = liveStats(job, Date.parse("2026-10-03T16:00:15Z")); - expect(stats.elapsedSeconds).toBe(10); - expect(stats.perSecond).toBe(3); - expect(stats.tokens).toBe(2500); - expect(stats.cost).toBe(0.042); - }); - - it("stops the clock when the job finishes", () => { - const finished = { ...job, finished_at: "2026-10-03T16:00:25Z" }; - expect(liveStats(finished, Date.parse("2026-10-03T18:00:00Z")).elapsedSeconds).toBe(20); - }); - - it("has no rate before anything is reviewed", () => { - expect(liveStats({ ...job, reviewed: 0 }, Date.parse("2026-10-03T16:00:15Z")).perSecond).toBeNull(); - }); - - it("formats rate and tokens for the footer", () => { - expect(rateLabel(null)).toBe("–"); - expect(rateLabel(2.25)).toBe("2.3 traces/s"); - expect(rateLabel(0.5)).toBe("30.0 traces/min"); - expect(tokenLabel(950)).toBe("950 tok"); - expect(tokenLabel(2500)).toBe("2.5k tok"); - expect(tokenLabel(3_400_000)).toBe("3.4M tok"); - }); -}); - describe("honest live list", () => { it("lists completed reviews newest first in the order they finished, never re-sorted by time", () => { const reviews = [ @@ -387,33 +324,11 @@ describe("honest live list", () => { expect(nowLine(job(3, 0), 1)).toBe("3 done · 1 in flight"); }); - it("keeps a trace as the same row from in flight to finished", () => { - const reading = (id: string) => ({ - execution_id: id, - trace_id: `t-${id}`, - agent: "bot", - started_at: "2026-10-03T16:00:00Z", - }); - const before = liveRows([reading("x"), reading("y")], [review("a")], 10); - expect(before.map((row) => [row.kind, row.key])).toEqual([ - ["reading", "x"], - ["reading", "y"], - ["done", "a"], - ]); - const after = liveRows([reading("x"), reading("y")], [review("a"), review("y")], 10); - expect(after.map((row) => [row.kind, row.key])).toEqual([ - ["reading", "x"], - ["done", "y"], - ["done", "a"], - ]); - }); - it("only shows in-flight traces while the job runs", () => { const item = { execution_id: "x", trace_id: "t", agent: "bot", started_at: "2026-10-03T16:00:00Z" }; const job = (status: Job["status"]) => ({ status, reading: [item] }) as unknown as Job; expect(inFlight(job("running"))).toEqual([item]); expect(inFlight(job("completed"))).toEqual([]); - expect(inFlight({ status: "running" } as Job)).toEqual([]); }); it("sums up a finished run from when reading started", () => { diff --git a/ui/litellm-dashboard/src/components/lens/model/live.ts b/ui/litellm-dashboard/src/components/lens/model/live.ts index c80f70e29d7..0c8d41f7d67 100644 --- a/ui/litellm-dashboard/src/components/lens/model/live.ts +++ b/ui/litellm-dashboard/src/components/lens/model/live.ts @@ -1,5 +1,5 @@ import { durationText } from "./format"; -import type { Job, Review, ReviewVerdict, Settings } from "./types"; +import type { InFlight, Job, Review, ReviewVerdict, Settings } from "./types"; export type Outcome = "issue" | "clear" | "unknown"; @@ -13,15 +13,8 @@ export interface Conclusion { issue: boolean; } -export interface LiveStats { - perSecond: number | null; - tokens: number; - cost: number; - elapsedSeconds: number; -} - export function liveJob(jobs: readonly Job[]): Job | undefined { - const active = jobs.find((job) => job.status === "queued" || job.status === "running"); + const active = jobs.find(isActive); if (active) return active; const latest = jobs[0]; return latest && latest.reviewed > 0 ? latest : undefined; @@ -104,6 +97,14 @@ export function appendPage(feed: ReviewFeed, page: { reviews: readonly Review[]; return { reviews: [...feed.reviews, ...added].slice(-KEPT_REVIEWS), cursor: page.reviewed }; } +export function polling(job: Pick, feed: ReviewFeed): boolean { + return isActive(job) || feed.cursor < job.reviewed; +} + +export function isActive(job: Pick): boolean { + return job.status === "queued" || job.status === "running"; +} + export function unseen(reviews: readonly Review[], seen: ReadonlySet): Review[] { return reviews.filter((review) => !seen.has(reviewKey(review))); } @@ -185,53 +186,12 @@ export function secondsToFinishReading( return Math.ceil(remaining / (job.reviewed / elapsed)); } -export function liveStats(job: Job, now: number): LiveStats { - const models = job.steps.filter((step) => step.kind === "model"); - const end = job.finished_at ? Date.parse(job.finished_at) : now; - const elapsedSeconds = Math.max(0, Math.floor((end - Date.parse(readingStart(job))) / 1000)); - return { - perSecond: elapsedSeconds > 0 && job.reviewed > 0 ? job.reviewed / elapsedSeconds : null, - tokens: models.reduce((sum, step) => sum + step.prompt_tokens + step.completion_tokens, 0), - cost: job.cost, - elapsedSeconds, - }; -} - -export function rateLabel(perSecond: number | null): string { - if (perSecond === null) return "–"; - return perSecond >= 1 ? `${perSecond.toFixed(1)} traces/s` : `${(perSecond * 60).toFixed(1)} traces/min`; -} - -export function tokenLabel(tokens: number): string { - if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M tok`; - return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k tok` : `${tokens} tok`; -} - export function newestFirst(reviews: readonly Review[], limit: number): Review[] { return [...reviews].reverse().slice(0, limit); } -export interface InFlight { - execution_id: string; - trace_id: string; - agent: string; - started_at: string; -} - -export function inFlight(job: Job): readonly InFlight[] { - const reading = (job as Job & { reading?: readonly InFlight[] }).reading; - return job.status === "running" ? reading ?? [] : []; -} - -export type LiveRow = { kind: "reading"; key: string; item: InFlight } | { kind: "done"; key: string; review: Review }; - -export function liveRows(reading: readonly InFlight[], reviews: readonly Review[], limit: number): LiveRow[] { - const finished = new Set(reviews.map((review) => review.execution_id)); - const open = reading.filter((item) => !finished.has(item.execution_id)); - return [ - ...open.map((item) => ({ kind: "reading" as const, key: item.execution_id, item })), - ...newestFirst(reviews, limit).map((review) => ({ kind: "done" as const, key: review.execution_id, review })), - ]; +export function inFlight(job: Pick): readonly InFlight[] { + return job.status === "running" ? job.reading : []; } export function nowLine(job: Pick, reading: number): string { diff --git a/ui/litellm-dashboard/src/components/lens/model/types.ts b/ui/litellm-dashboard/src/components/lens/model/types.ts index a94f894229f..497defecd33 100644 --- a/ui/litellm-dashboard/src/components/lens/model/types.ts +++ b/ui/litellm-dashboard/src/components/lens/model/types.ts @@ -35,4 +35,6 @@ export type Worker = LensList["workers"][number]; export type Review = components["schemas"]["Review"]; +export type InFlight = components["schemas"]["InFlight"]; + export type ReviewVerdict = components["schemas"]["ReviewVerdict"];