diff --git a/ui/litellm-dashboard/src/components/lens/api/service.ts b/ui/litellm-dashboard/src/components/lens/api/service.ts index 0383efa3e20..76cb0cc4f4b 100644 --- a/ui/litellm-dashboard/src/components/lens/api/service.ts +++ b/ui/litellm-dashboard/src/components/lens/api/service.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import type { RunWindow } from "../investigations/detail/RunNowDialog"; import type { ApiClient } from "@/lib/http/client"; import type { components } from "@/lib/http/schema"; -import type { ActivitySelection, Job, Lens, LensList, Sample, Settings, WorkerCreated } from "../model/types"; +import type { ActivitySelection, Job, Lens, LensList, Review, Sample, Settings, WorkerCreated } from "../model/types"; import type { AnalysisModelInfo } from "../setup/fields/analysisModels"; export type ExecutionContent = components["schemas"]["ExecutionContent"]; @@ -25,6 +25,11 @@ export type Key = z.infer; export type KeyPage = z.infer; export type KeyInfo = z.infer["info"]; +export interface ReviewPage { + readonly reviews: readonly Review[]; + readonly reviewed: number; +} + export interface AnalysisKeyRequest { readonly model: string; readonly budget: number; @@ -37,6 +42,7 @@ export interface LensApi { activity(): Promise<{ traces: boolean; requests: boolean }>; runs(lensId: string, offset: number): Promise; run(lensId: string, jobId: string): Promise; + reviews(lensId: string, jobId: string, after: number): Promise; execution(lensId: string, executionId: string, offset: number): Promise; sample(selection: ActivitySelection, offset: number, asOf: string): Promise; agents(): Promise; @@ -64,6 +70,8 @@ export function liveLensApi(apiClient: ApiClient, accessToken: string): LensApi activity: () => apiClient.get("/lens/activity/available", { accessToken }), runs: (lensId, offset) => apiClient.get(`/lens/${lensId}/runs`, { accessToken, query: { offset } }), run: (lensId, jobId) => apiClient.get(`/lens/${lensId}/runs/${jobId}`, { accessToken }), + reviews: (lensId, jobId, after) => + apiClient.get(`/lens/${lensId}/runs/${jobId}/reviews`, { accessToken, query: { after } }), execution: (lensId, executionId, offset) => apiClient.get(`/lens/${lensId}/executions/${encode(executionId)}`, { accessToken, diff --git a/ui/litellm-dashboard/src/components/lens/demo/createLensDemo.ts b/ui/litellm-dashboard/src/components/lens/demo/createLensDemo.ts index 65c3d081601..8be824adfc9 100644 --- a/ui/litellm-dashboard/src/components/lens/demo/createLensDemo.ts +++ b/ui/litellm-dashboard/src/components/lens/demo/createLensDemo.ts @@ -354,6 +354,10 @@ function demoLensApi(data: LensDemoData): LensApi { activity: async () => ({ traces: true, requests: false }), runs: (lensId, offset) => found(jobs(lensId)?.slice(offset)), run: (lensId, jobId) => found(jobs(lensId)?.find((job) => job.id === jobId)), + reviews: async (lensId, jobId, after) => { + const job = await found(jobs(lensId)?.find((item) => item.id === jobId)); + return { reviews: job.reviews.slice(Math.max(0, after - (job.reviewed - job.reviews.length))), reviewed: job.reviewed }; + }, execution: notInDemo, sample: notInDemo, agents: notInDemo, 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 1429965e025..e9b69f91c7e 100644 --- a/ui/litellm-dashboard/src/components/lens/model/live.test.ts +++ b/ui/litellm-dashboard/src/components/lens/model/live.test.ts @@ -8,6 +8,8 @@ import { inGroup, share, issueCount, + appendPage, + EMPTY_FEED, shortVerdict, stripState, tickerLine, @@ -262,7 +264,8 @@ describe("playback queue", () => { }); describe("which job the live run shows", () => { - const job = (id: string, status: Job["status"], reviews: Review[]) => ({ id, status, reviews }) as unknown as Job; + const job = (id: string, status: Job["status"], reviews: Review[]) => + ({ id, status, reviews: [], reviewed: reviews.length }) as unknown as Job; it("shows the active job once it has reviews", () => { expect(liveJob([job("new", "running", [review("a")]), job("old", "completed", [review("b")])])?.id).toBe("new"); @@ -359,6 +362,32 @@ describe("drawer focus", () => { }); }); +describe("incremental reviews", () => { + const at = (id: string, minute: number) => review(id, { at: `2026-10-03T16:${String(minute).padStart(2, "0")}:00Z` }); + + it("appends pages in arrival order, not by time, and advances the cursor to the reviewed count", () => { + const first = appendPage(EMPTY_FEED, { reviews: [at("a", 5), at("b", 2)], reviewed: 2 }); + expect(first).toEqual({ reviews: [at("a", 5), at("b", 2)], cursor: 2 }); + const second = appendPage(first, { reviews: [at("c", 1)], reviewed: 3 }); + expect(second.reviews.map((r) => r.execution_id)).toEqual(["a", "b", "c"]); + expect(second.cursor).toBe(3); + }); + + it("keeps the same feed for an empty page and never duplicates a re-sent review", () => { + const feed = appendPage(EMPTY_FEED, { reviews: [at("a", 1)], reviewed: 1 }); + expect(appendPage(feed, { reviews: [], reviewed: 1 })).toBe(feed); + expect(appendPage(feed, { reviews: [at("a", 1)], reviewed: 1 }).reviews).toHaveLength(1); + }); + + it("caps how many reviews are kept, dropping the oldest", () => { + const many = Array.from({ length: 250 }, (_, n) => review(`r${n}`)); + const feed = appendPage(EMPTY_FEED, { reviews: many, reviewed: 250 }); + expect(feed.reviews).toHaveLength(200); + expect(feed.reviews[0].execution_id).toBe("r50"); + expect(feed.cursor).toBe(250); + }); +}); + describe("time left reading", () => { const started = { created_at: "2026-10-03T16:00:00Z", diff --git a/ui/litellm-dashboard/src/components/lens/model/live.ts b/ui/litellm-dashboard/src/components/lens/model/live.ts index c5fe5209783..69ac8722f8e 100644 --- a/ui/litellm-dashboard/src/components/lens/model/live.ts +++ b/ui/litellm-dashboard/src/components/lens/model/live.ts @@ -35,7 +35,7 @@ export function liveJob(jobs: readonly Job[]): Job | undefined { const active = jobs.find((job) => job.status === "queued" || job.status === "running"); if (active) return active; const latest = jobs[0]; - return latest?.reviews.length ? latest : undefined; + return latest && latest.reviewed > 0 ? latest : undefined; } export function reviewKey(review: Pick): string { @@ -116,6 +116,21 @@ export function verdictLine(review: Pick): return review.verdicts[0]?.summary ?? "No issue observed"; } +const KEPT_REVIEWS = 200; + +export interface ReviewFeed { + reviews: readonly Review[]; + cursor: number; +} + +export const EMPTY_FEED: ReviewFeed = { reviews: [], cursor: 0 }; + +export function appendPage(feed: ReviewFeed, page: { reviews: readonly Review[]; reviewed: number }): ReviewFeed { + if (page.reviewed === feed.cursor && !page.reviews.length) return feed; + const added = unseen(page.reviews, new Set(feed.reviews.map(reviewKey))); + return { reviews: [...feed.reviews, ...added].slice(-KEPT_REVIEWS), cursor: page.reviewed }; +} + export function unseen(reviews: readonly Review[], seen: ReadonlySet): Review[] { return reviews.filter((review) => !seen.has(reviewKey(review))); }