diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/useJobReviews.ts b/ui/litellm-dashboard/src/components/lens/investigations/live/useJobReviews.ts index e6c47f40119..0800b6eb882 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/useJobReviews.ts +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/useJobReviews.ts @@ -11,7 +11,8 @@ import type { Job, Review } from "../../model/types"; export function useJobReviews(api: LensApi, lensId: string, job: Pick): readonly Review[] { const [feed, setFeed] = useState(EMPTY_FEED); const live = job.status === "queued" || job.status === "running"; - const { data } = useQuery(lensQueries.reviews(api, { lensId, jobId: job.id, after: feed.cursor, live })); + const page = { lensId, jobId: job.id, after: feed.cursor, live }; + const { data } = useQuery(lensQueries.reviews(api, page)); const next = data ? appendPage(feed, data) : feed; if (next !== feed) setFeed(next); return next.reviews; diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts b/ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts index 3beb93d21d4..130b6b75e06 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts @@ -22,7 +22,8 @@ export function useStage( const [charMs] = useState(() => (prefersReducedMotion() ? 0 : CHAR_MS)); const [stage, setStage] = useState(() => startStage(reviews)); const [now, setNow] = useState(Date.now); - const next = stepStage(stage, { reading, reviews, now, slots, running, charMs }); + const input = { reading, reviews, now, slots, running, charMs }; + const next = stepStage(stage, input); if (next !== stage) setStage(next); const busy = running || next.lanes.length > 0; useEffect(() => { 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 c9e701c92c4..aa7004cd88d 100644 --- a/ui/litellm-dashboard/src/components/lens/model/live.test.ts +++ b/ui/litellm-dashboard/src/components/lens/model/live.test.ts @@ -23,7 +23,6 @@ import { outcome, providerOf, rateLabel, - reviewKey, secondsToFinishReading, tokenLabel, } from "./live"; @@ -61,7 +60,10 @@ describe("provider from model", () => { }); it("resolves a bare model name through the model catalog", () => { - const catalog = { "gpt-5.6": { litellm_provider: "openai" }, "cerebras/gpt-oss-120b": { litellm_provider: "cerebras" } }; + const catalog = { + "gpt-5.6": { litellm_provider: "openai" }, + "cerebras/gpt-oss-120b": { litellm_provider: "cerebras" }, + }; expect(providerOf("gpt-5.6", catalog)).toBe("openai"); expect(providerOf("my-alias", catalog)).toBe(""); expect(analysisModel(["", "gpt-5.6"], catalog)).toBe("gpt-5.6"); @@ -72,7 +74,6 @@ describe("provider from model", () => { expect(analysisModel(["", "analysis"])).toBe("analysis"); expect(analysisModel([])).toBe(""); }); - }); describe("review outcome", () => { @@ -155,7 +156,6 @@ describe("conclusions", () => { expect(share(5, 0)).toBe(0); expect(share(9, 4)).toBe(1); }); - }); describe("which job the live run shows", () => { @@ -189,7 +189,10 @@ describe("strip state", () => { const MODEL = "cerebras/gpt-oss-120b"; it("says what is happening before the first review instead of a silent spinner", () => { - expect(stripState(base, MODEL)).toEqual({ kind: "waiting", message: "Reading 328 traces with cerebras/gpt-oss-120b…" }); + expect(stripState(base, MODEL)).toEqual({ + kind: "waiting", + message: "Reading 328 traces with cerebras/gpt-oss-120b…", + }); expect(stripState({ ...base, stage: "Grouping observations" }, "")).toEqual({ kind: "waiting", message: "Grouping observations…", @@ -205,7 +208,10 @@ describe("strip state", () => { kind: "failed", message: "Anthropic: credit balance too low", }); - expect(stripState({ ...base, status: "failed" }, MODEL)).toEqual({ kind: "failed", message: "The investigation failed" }); + expect(stripState({ ...base, status: "failed" }, MODEL)).toEqual({ + kind: "failed", + message: "The investigation failed", + }); }); it("surfaces a model error step while still running with nothing reviewed", () => { @@ -228,16 +234,15 @@ describe("issue count", () => { it("uses the job's issue findings once the run completes", () => { const findings = [{ kind: "issue" }, { kind: "pattern" }, { kind: "issue" }] as Job["findings"]; - expect(issueCount({ status: "completed", findings, reviews: [issueReview], reviewed: 328 })).toEqual({ - count: 2, - scope: "findings", - }); + const completed = { status: "completed" as const, findings, reviews: [issueReview], reviewed: 328 }; + expect(issueCount(completed)).toEqual({ count: 2, scope: "findings" }); }); it("labels counts honestly when only the last reviews are kept", () => { const reviews = [issueReview, review("y"), issueReview]; - expect(issueCount({ status: "running", findings: null, reviews, reviewed: 3 })).toEqual({ count: 2, scope: "" }); - expect(issueCount({ status: "running", findings: null, reviews, reviewed: 120 })).toEqual({ + const running = { status: "running" as const, findings: null, reviews }; + expect(issueCount({ ...running, reviewed: 3 })).toEqual({ count: 2, scope: "" }); + expect(issueCount({ ...running, reviewed: 120 })).toEqual({ count: 2, scope: "in last 3 reviewed", }); @@ -303,12 +308,36 @@ describe("live stats", () => { 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 }, + { + 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; @@ -341,7 +370,11 @@ describe("live stats", () => { describe("honest live list", () => { it("lists completed reviews newest first in the order they finished, never re-sorted by time", () => { - const reviews = [review("a", { at: "2026-10-03T16:05:00Z" }), review("b", { at: "2026-10-03T16:01:00Z" }), review("c")]; + const reviews = [ + review("a", { at: "2026-10-03T16:05:00Z" }), + review("b", { at: "2026-10-03T16:01:00Z" }), + review("c"), + ]; expect(newestFirst(reviews, 10).map((r) => r.execution_id)).toEqual(["c", "b", "a"]); expect(newestFirst(reviews, 2).map((r) => r.execution_id)).toEqual(["c", "b"]); expect(reviews.map((r) => r.execution_id)).toEqual(["a", "b", "c"]); @@ -355,7 +388,12 @@ describe("honest live list", () => { }); 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 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"], diff --git a/ui/litellm-dashboard/src/components/lens/model/live.ts b/ui/litellm-dashboard/src/components/lens/model/live.ts index fcd18105d7f..c80f70e29d7 100644 --- a/ui/litellm-dashboard/src/components/lens/model/live.ts +++ b/ui/litellm-dashboard/src/components/lens/model/live.ts @@ -134,7 +134,7 @@ export function conclusions(reviews: readonly Review[], checks: Settings["checks return [...verdictsByCheck(review).values()].reduce((next, verdict) => { const prior = next.get(verdict.check_id); const issue = verdict.kind === "issue"; - return new Map(next).set(verdict.check_id, { + const merged: Conclusion = { key: verdict.check_id, checkId: verdict.check_id, label: checkLabel(verdict.check_id, instructions.get(verdict.check_id)), @@ -142,7 +142,8 @@ export function conclusions(reviews: readonly Review[], checks: Settings["checks count: (prior?.count ?? 0) + Number(issue), noted: (prior?.noted ?? 0) + Number(!issue), issue: (prior?.issue ?? false) || issue, - }); + }; + return new Map(next).set(verdict.check_id, merged); }, groups); }, new Map()); return [...grouped.values()].sort((a, b) => b.count - a.count || b.noted - a.noted); @@ -163,7 +164,6 @@ export function briefReasoning(reasoning: string): string { return sentences.slice(0, BRIEF_SENTENCES).join(" ").trim(); } - export function grownGroups(before: readonly Conclusion[], after: readonly Conclusion[]): Set { const prior = new Map(before.map((group) => [group.key, group.count + group.noted])); return new Set( @@ -220,7 +220,7 @@ export interface InFlight { export function inFlight(job: Job): readonly InFlight[] { const reading = (job as Job & { reading?: readonly InFlight[] }).reading; - return job.status === "running" ? (reading ?? []) : []; + return job.status === "running" ? reading ?? [] : []; } export type LiveRow = { kind: "reading"; key: string; item: InFlight } | { kind: "done"; key: string; review: Review }; @@ -242,7 +242,9 @@ export function nowLine(job: Pick, reading: number export function durationLabel(ms: number): string { if (ms < 1000) return `${Math.max(0, Math.round(ms))}ms`; - return ms < 60_000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`; + return ms < 60_000 + ? `${(ms / 1000).toFixed(1)}s` + : `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`; } export function doneLine(job: Pick): string {