mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
feat(ui): search and plot agent runs on the server
The runs list sends its search and zoom window to /v1/traces, and the timeline plots /v1/traces/histogram, so every matching run is counted and listed instead of only the pages already loaded. The demo data answers the same calls locally An empty result from a search or a zoom keeps the runs controls. Before, the empty page looked like a proxy that never received a trace and replaced the list with onboarding
This commit is contained in:
parent
180b5ee482
commit
b99c6d40d2
15 changed files with 505 additions and 156 deletions
|
|
@ -1,7 +1,52 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createLensDemo } from "./createLensDemo";
|
||||
import { createLensDemo, demoHistogram } from "./createLensDemo";
|
||||
import { createLensDemoData } from "./fixtures";
|
||||
import { evidenceTarget } from "../../model/findings";
|
||||
import type { TraceSummary } from "../../traces/types";
|
||||
|
||||
const HOUR = 3600 * 1000;
|
||||
const START = Date.UTC(2026, 8, 30, 0, 0, 0);
|
||||
const range = { startMs: START, endMs: START + 10 * HOUR };
|
||||
|
||||
const run = (offsetMs: number, errorCount = 0, agent = "researcher"): TraceSummary =>
|
||||
({
|
||||
trace_id: `t${offsetMs}`,
|
||||
start_time: new Date(START + offsetMs).toISOString(),
|
||||
error_count: errorCount,
|
||||
agent_names: [agent],
|
||||
service: "svc",
|
||||
}) as TraceSummary;
|
||||
|
||||
describe("demoHistogram", () => {
|
||||
it("splits the window into equal buckets that tile it exactly", () => {
|
||||
const { buckets } = demoHistogram([], range, 10);
|
||||
expect(buckets).toHaveLength(10);
|
||||
expect(buckets[0].start_ms).toBe(range.startMs);
|
||||
expect(buckets.at(-1)?.end_ms).toBe(range.endMs);
|
||||
expect(buckets[3]).toMatchObject({ start_ms: START + 3 * HOUR, end_ms: START + 4 * HOUR, total: 0, failed: 0 });
|
||||
});
|
||||
|
||||
it("puts each run in the bucket covering its start time", () => {
|
||||
const { buckets } = demoHistogram([run(0), run(30 * 60 * 1000), run(2.5 * HOUR), run(10 * HOUR - 1)], range, 10);
|
||||
expect(buckets.map((b) => b.total)).toEqual([2, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
|
||||
});
|
||||
|
||||
it("counts failed runs apart and groups the rest by agent", () => {
|
||||
const { buckets } = demoHistogram(
|
||||
[run(HOUR, 3), run(HOUR + 1, 0, "writer"), run(HOUR + 2, 1), run(HOUR + 3), run(HOUR + 4, 0, "writer")],
|
||||
range,
|
||||
10,
|
||||
);
|
||||
expect(buckets[1]).toMatchObject({
|
||||
total: 5,
|
||||
failed: 2,
|
||||
agents: [
|
||||
{ agent: "researcher", runs: 1 },
|
||||
{ agent: "writer", runs: 2 },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Lens demo data", () => {
|
||||
it("links every finding to the quoted original step and assessed run", () => {
|
||||
|
|
@ -57,8 +102,8 @@ describe("Lens demo data", () => {
|
|||
const network = vi.spyOn(globalThis, "fetch");
|
||||
const now = Date.now();
|
||||
const services = createLensDemo(now);
|
||||
const all = await services.traces.list({ startMs: 0, endMs: now });
|
||||
const recent = await services.traces.list({ startMs: now - 3600_000, endMs: now });
|
||||
const all = await services.traces.list({ startMs: 0, endMs: now + 1, q: "" });
|
||||
const recent = await services.traces.list({ startMs: now - 3600_000, endMs: now + 1, q: "" });
|
||||
expect(recent.data.length).toBeGreaterThan(0);
|
||||
expect(recent.data.length).toBeLessThan(all.data.length);
|
||||
expect(recent.data.every((trace) => Date.parse(trace.start_time) >= now - 3600_000)).toBe(true);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { ApiError } from "@/lib/http/client";
|
||||
import type { TracesApi } from "@/components/lens/traces/api";
|
||||
import type { TimeRange, TracesApi } from "@/components/lens/traces/api";
|
||||
import { filterRuns, RUN_INDEX } from "@/components/lens/traces/list/runSearch/runQuery";
|
||||
import type { TraceHistogram, TraceSummary } from "@/components/lens/traces/types";
|
||||
import { traceAgentNames } from "@/components/lens/traces/utils";
|
||||
import type { LensServices } from "../LensServices";
|
||||
import type { LensApi } from "../service";
|
||||
import { createLensDemoData, type LensDemoData } from "./fixtures";
|
||||
|
|
@ -38,21 +41,62 @@ function demoLensApi(data: LensDemoData): LensApi {
|
|||
};
|
||||
}
|
||||
|
||||
const startedWithin = (run: TraceSummary, range: TimeRange): boolean => {
|
||||
const startMs = Date.parse(run.start_time);
|
||||
return startMs >= range.startMs && startMs < range.endMs;
|
||||
};
|
||||
|
||||
/** Runs per equal-width slice of the window, the shape the server's histogram returns. */
|
||||
export function demoHistogram(runs: readonly TraceSummary[], range: TimeRange, buckets: number): TraceHistogram {
|
||||
const width = (range.endMs - range.startMs) / buckets;
|
||||
const placed = runs.map((run) => ({
|
||||
index: Math.floor((Date.parse(run.start_time) - range.startMs) / width),
|
||||
failed: run.error_count > 0,
|
||||
agent: traceAgentNames(run)[0] ?? run.service,
|
||||
}));
|
||||
return {
|
||||
buckets: Array.from({ length: buckets }, (_, index) => {
|
||||
const hits = placed.filter((run) => run.index === index);
|
||||
const agents = [...new Set(hits.filter((run) => !run.failed).map((run) => run.agent))].sort();
|
||||
return {
|
||||
start_ms: range.startMs + index * width,
|
||||
end_ms: range.startMs + (index + 1) * width,
|
||||
total: hits.length,
|
||||
failed: hits.filter((run) => run.failed).length,
|
||||
agents: agents.map((agent) => ({
|
||||
agent,
|
||||
runs: hits.filter((run) => !run.failed && run.agent === agent).length,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function demoTracesApi(data: LensDemoData): TracesApi {
|
||||
const run = (traceId: string) => data.runs.find(({ trace }) => trace.summary.trace_id === traceId);
|
||||
const summaries = data.runs.map((item) => item.trace.summary);
|
||||
const matching = (range: TimeRange, q: string) =>
|
||||
filterRuns(
|
||||
summaries.filter((summary) => startedWithin(summary, range)),
|
||||
q,
|
||||
);
|
||||
return {
|
||||
scope: "demo",
|
||||
live: false,
|
||||
handoff: (traceId, spanId) => {
|
||||
const found = run(traceId);
|
||||
const step = spanId ? found?.details.find((span) => span.span_id === spanId) : found;
|
||||
return { text: JSON.stringify(step, null, 2), copied: spanId ? "Step copied" : "Trace copied" };
|
||||
},
|
||||
list: async ({ startMs, endMs }) => ({
|
||||
data: data.runs
|
||||
.map((item) => item.trace.summary)
|
||||
.filter((trace) => Date.parse(trace.start_time) >= startMs && Date.parse(trace.start_time) <= endMs),
|
||||
next_cursor: null,
|
||||
}),
|
||||
list: async ({ startMs, endMs, q }) => ({ data: matching({ startMs, endMs }, q), next_cursor: null }),
|
||||
histogram: async (range, q, buckets) => demoHistogram(matching(range, q), range, buckets),
|
||||
values: async (field, contains, range) => {
|
||||
const read = field in RUN_INDEX.read ? RUN_INDEX.read[field as keyof typeof RUN_INDEX.read] : null;
|
||||
if (!read) return [];
|
||||
const needle = contains.toLowerCase();
|
||||
const values = matching(range, "").flatMap(read);
|
||||
return [...new Set(values)].filter((value) => value && value.toLowerCase().includes(needle)).sort();
|
||||
},
|
||||
anyRecorded: async () => data.runs.length > 0,
|
||||
trace: (traceId) => found(run(traceId)?.trace),
|
||||
span: (traceId, spanId) => found(run(traceId)?.details.find((span) => span.span_id === spanId)),
|
||||
|
|
|
|||
|
|
@ -9,24 +9,36 @@ import {
|
|||
apiClient,
|
||||
getProxyBaseUrl,
|
||||
} from "../../networking";
|
||||
import type { SpanDetail, SpanErrorPage, Trace, TracePage } from "./types";
|
||||
import type { RunField, SpanDetail, SpanErrorPage, Trace, TraceHistogram, TracePage } from "./types";
|
||||
|
||||
export interface TraceWindow {
|
||||
export interface TimeRange {
|
||||
readonly startMs: number;
|
||||
readonly endMs: number;
|
||||
}
|
||||
|
||||
export interface TraceWindow extends TimeRange {
|
||||
readonly cursor?: string | null;
|
||||
}
|
||||
|
||||
/** One page of the runs list: the window, the `q` search the server applies, and where to resume. */
|
||||
export interface RunListRequest extends TraceWindow {
|
||||
readonly q: string;
|
||||
}
|
||||
|
||||
export interface TraceHandoff {
|
||||
readonly text: string;
|
||||
readonly copied: string;
|
||||
}
|
||||
|
||||
export interface TracesApi {
|
||||
/** Partitions query caches between backends (one token, or the demo). */
|
||||
readonly scope: string;
|
||||
/** False for a fixed snapshot: nothing new arrives, so live tail and tracing setup don't apply. */
|
||||
readonly live: boolean;
|
||||
handoff(traceId: string, spanId?: string | null, traceRef?: string): TraceHandoff;
|
||||
list(window: TraceWindow): Promise<TracePage>;
|
||||
list(request: RunListRequest): Promise<TracePage>;
|
||||
histogram(range: TimeRange, q: string, buckets: number): Promise<TraceHistogram>;
|
||||
values(field: RunField, contains: string, range: TimeRange): Promise<readonly string[]>;
|
||||
anyRecorded(): Promise<boolean>;
|
||||
trace(traceId: string, traceRef?: string, cursor?: string | null): Promise<Trace>;
|
||||
span(traceId: string, spanId: string, traceRef?: string): Promise<SpanDetail>;
|
||||
|
|
@ -46,12 +58,25 @@ export const agentHandoffText = (traceId: string, spanId?: string | null, traceR
|
|||
|
||||
export function liveTracesApi(accessToken: string): TracesApi {
|
||||
return {
|
||||
scope: accessToken,
|
||||
live: true,
|
||||
handoff: (traceId, spanId, traceRef) => ({
|
||||
text: agentHandoffText(traceId, spanId, traceRef),
|
||||
copied: "Command copied",
|
||||
}),
|
||||
list: (window) => agentTraceListCall({ accessToken, ...window }),
|
||||
list: (request) => agentTraceListCall({ accessToken, ...request }),
|
||||
histogram: (range, q, buckets) =>
|
||||
apiClient.get<TraceHistogram>("/v1/traces/histogram", {
|
||||
accessToken,
|
||||
query: { start_ms: range.startMs, end_ms: range.endMs, q: q || undefined, buckets },
|
||||
}),
|
||||
values: async (field, contains, range) => {
|
||||
const found = await apiClient.get<{ values: string[] }>(`/v1/traces/values/${field}`, {
|
||||
accessToken,
|
||||
query: { start_ms: range.startMs, end_ms: range.endMs, contains: contains || undefined },
|
||||
});
|
||||
return found.values;
|
||||
},
|
||||
anyRecorded: async () => {
|
||||
const page = await apiClient.get<TracePage>("/v1/traces", { accessToken, query: { start_ms: 0 } });
|
||||
return page.data.length > 0;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import traceList from "../__fixtures__/trace_list.json";
|
|||
import { LensPreviewContext } from "../../ui/LensPreviewButton";
|
||||
import AgentTracesPage from "./AgentTracesPage";
|
||||
import { filterRuns } from "./runSearch/runQuery";
|
||||
import { demoHistogram } from "../../data/demo/createLensDemo";
|
||||
import { AgentTracesSection, type TimeControls } from "./AgentTracesSection";
|
||||
import type { TracePage, TraceSummary } from "../types";
|
||||
|
||||
|
|
@ -35,6 +36,33 @@ import { agentTraceListCall, apiClient } from "../../../networking";
|
|||
|
||||
const runs = (traceList as TracePage).data as TraceSummary[];
|
||||
|
||||
const startedWithin = (run: TraceSummary, startMs: number, endMs: number) => {
|
||||
const startedMs = Date.parse(run.start_time);
|
||||
return startedMs >= startMs && startedMs < endMs;
|
||||
};
|
||||
|
||||
/** The relative views end now; pin it to the fixtures' day so their runs fall in the default window. */
|
||||
const pinNowToFixtures = () => vi.useFakeTimers({ toFake: ["Date"], now: Date.parse("2026-09-30T12:00:00Z") });
|
||||
|
||||
/** Answers the list and histogram the way the proxy does: the window and `q` are applied before paging. */
|
||||
const serve = (data: readonly TraceSummary[]) => {
|
||||
const matching = (startMs: number, endMs: number, q: string) =>
|
||||
filterRuns(
|
||||
data.filter((run) => startedWithin(run, startMs, endMs)),
|
||||
q,
|
||||
);
|
||||
vi.mocked(agentTraceListCall).mockImplementation(async ({ startMs, endMs, q }) => ({
|
||||
data: matching(startMs, endMs, q),
|
||||
next_cursor: null,
|
||||
}));
|
||||
vi.mocked(apiClient.get).mockImplementation(async (path: string, options?: { query?: Record<string, unknown> }) => {
|
||||
const query = options?.query ?? {};
|
||||
if (path !== "/v1/traces/histogram") return { data: [] };
|
||||
const range = { startMs: Number(query.start_ms), endMs: Number(query.end_ms) };
|
||||
return demoHistogram(matching(range.startMs, range.endMs, String(query.q ?? "")), range, Number(query.buckets));
|
||||
});
|
||||
};
|
||||
|
||||
const lastUrl = (onUrlUpdate: ReturnType<typeof vi.fn>) =>
|
||||
new URLSearchParams(String(onUrlUpdate.mock.lastCall?.[0].queryString ?? ""));
|
||||
|
||||
|
|
@ -109,24 +137,6 @@ describe("AgentTracesSection", () => {
|
|||
expect(agentTraceListCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("waits for the user before paging when filters hide every loaded run", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(agentTraceListCall)
|
||||
.mockResolvedValueOnce({ data: runs.slice(0, 1), next_cursor: "next" })
|
||||
.mockResolvedValueOnce({ data: runs.slice(1, 2), next_cursor: "later" });
|
||||
renderSection();
|
||||
expect(await screen.findByTestId("agent-trace-row")).toBeVisible();
|
||||
await user.type(screen.getByRole("combobox", { name: "Search runs" }), "no-such-run");
|
||||
await waitFor(() => expect(screen.queryAllByTestId("agent-trace-row")).toHaveLength(0));
|
||||
act(() => mockAllIsIntersecting(true));
|
||||
expect(screen.queryByTestId("runs-placeholder")).not.toBeInTheDocument();
|
||||
expect(agentTraceListCall).toHaveBeenCalledOnce();
|
||||
await user.click(screen.getByRole("button", { name: "Load older runs" }));
|
||||
await waitFor(() => expect(agentTraceListCall).toHaveBeenCalledTimes(2));
|
||||
act(() => mockAllIsIntersecting(true));
|
||||
expect(agentTraceListCall).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keeps the original time window and loaded rows when another page fails", async () => {
|
||||
const user = userEvent.setup();
|
||||
const now = vi.spyOn(Date, "now").mockReturnValue(Date.parse("2026-10-01T00:00Z"));
|
||||
|
|
@ -200,6 +210,30 @@ describe("AgentTracesSection", () => {
|
|||
expect(card).not.toHaveTextContent("store: clickhouse");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["a search that matches nothing", "?q=no-such-run"],
|
||||
["a zoom over an empty slice", "?from=1790726400000&to=1790727000000"],
|
||||
])("keeps the runs controls instead of onboarding for %s", async (_, searchParams) => {
|
||||
pinNowToFixtures();
|
||||
serve(runs);
|
||||
renderWithProviders(
|
||||
<AgentTracesSection
|
||||
accessToken="sk-test"
|
||||
isActive
|
||||
startTime="2026-09-30T00:00Z"
|
||||
endTime="2026-10-01T00:00Z"
|
||||
isCustomDate
|
||||
isLiveTail={false}
|
||||
/>,
|
||||
{ searchParams },
|
||||
);
|
||||
expect(await screen.findByTestId("runs-table")).toBeInTheDocument();
|
||||
await waitFor(() => expect(agentTraceListCall).toHaveBeenCalled());
|
||||
expect(screen.queryAllByTestId("agent-trace-row")).toHaveLength(0);
|
||||
expect(screen.queryByTestId("tracing-setup-card")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("combobox", { name: "Search runs" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the trace list available when traces exist outside the current time window", async () => {
|
||||
vi.mocked(agentTraceListCall).mockResolvedValue({ ...(traceList as TracePage), data: [] });
|
||||
vi.mocked(apiClient.get).mockResolvedValue(traceList);
|
||||
|
|
@ -326,7 +360,8 @@ describe("AgentTracesSection", () => {
|
|||
|
||||
it("filters by input text and by trace id", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(agentTraceListCall).mockResolvedValue(traceList as TracePage);
|
||||
pinNowToFixtures();
|
||||
serve(runs);
|
||||
renderSection();
|
||||
await screen.findAllByTestId("agent-trace-row");
|
||||
|
||||
|
|
@ -343,13 +378,11 @@ describe("AgentTracesSection", () => {
|
|||
});
|
||||
|
||||
it("uses recorded agent names for the column, suggestions and filter even when services are shared", async () => {
|
||||
vi.mocked(agentTraceListCall).mockResolvedValue({
|
||||
...(traceList as TracePage),
|
||||
data: [
|
||||
...runs.slice(1).map((run) => ({ ...run, service: "shared-app", agent_names: ["research-agent"] })),
|
||||
{ ...runs[0], service: "shared-app", agent_names: ["billing-agent", "review-agent"] },
|
||||
],
|
||||
});
|
||||
pinNowToFixtures();
|
||||
serve([
|
||||
...runs.slice(1).map((run) => ({ ...run, service: "shared-app", agent_names: ["research-agent"] })),
|
||||
{ ...runs[0], service: "shared-app", agent_names: ["billing-agent", "review-agent"] },
|
||||
]);
|
||||
const user = userEvent.setup();
|
||||
renderSection();
|
||||
await screen.findAllByTestId("agent-trace-row");
|
||||
|
|
@ -464,7 +497,7 @@ describe("AgentTracesSection", () => {
|
|||
});
|
||||
|
||||
it("narrows the list to the zoom window named in the URL and clears it on request", async () => {
|
||||
vi.mocked(agentTraceListCall).mockResolvedValue(traceList as TracePage);
|
||||
serve(runs);
|
||||
const startMs = Date.parse(runs[0].start_time);
|
||||
const inWindow = runs.filter((run) => Math.abs(Date.parse(run.start_time) - startMs) <= 1);
|
||||
const onUrlUpdate = vi.fn();
|
||||
|
|
@ -518,7 +551,8 @@ describe("AgentTracesSection", () => {
|
|||
|
||||
it("reads the query from the URL and writes typed changes back", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(agentTraceListCall).mockResolvedValue(traceList as TracePage);
|
||||
pinNowToFixtures();
|
||||
serve(runs);
|
||||
const onUrlUpdate = vi.fn();
|
||||
const failed = filterRuns(runs, "status:error");
|
||||
renderWithProviders(
|
||||
|
|
@ -545,7 +579,7 @@ describe("AgentTracesSection", () => {
|
|||
});
|
||||
|
||||
it("plots every loaded run on the timeline", async () => {
|
||||
vi.mocked(agentTraceListCall).mockResolvedValue(traceList as TracePage);
|
||||
serve(runs);
|
||||
renderWindowed();
|
||||
await screen.findAllByTestId("agent-trace-row");
|
||||
|
||||
|
|
@ -556,7 +590,7 @@ describe("AgentTracesSection", () => {
|
|||
});
|
||||
|
||||
it("zooms by dragging, resizes and pans the bracket, and clears with Esc", async () => {
|
||||
vi.mocked(agentTraceListCall).mockResolvedValue(traceList as TracePage);
|
||||
serve(runs);
|
||||
renderWindowed();
|
||||
await screen.findAllByTestId("agent-trace-row");
|
||||
const area = screen.getByTestId("timeline-area");
|
||||
|
|
@ -575,19 +609,19 @@ describe("AgentTracesSection", () => {
|
|||
|
||||
drag(area, 0, 1);
|
||||
expect(screen.getByTestId("timeline-selection")).toBeInTheDocument();
|
||||
expect(rowCount()).toBe(0);
|
||||
await waitFor(() => expect(rowCount()).toBe(0));
|
||||
|
||||
drag(screen.getByTestId("timeline-handle-hi"), 1, first);
|
||||
expect(rowCount()).toBeGreaterThan(0);
|
||||
await waitFor(() => expect(rowCount()).toBeGreaterThan(0));
|
||||
|
||||
drag(screen.getByTestId("timeline-selection"), 1, 1 - first);
|
||||
expect(rowCount()).toBeGreaterThan(0);
|
||||
await waitFor(() => expect(rowCount()).toBeGreaterThan(0));
|
||||
drag(screen.getByTestId("timeline-selection"), 0, 59);
|
||||
expect(rowCount()).toBe(0);
|
||||
await waitFor(() => expect(rowCount()).toBe(0));
|
||||
|
||||
fireEvent.keyDown(screen.getByTestId("traces-timeline"), { key: "Escape" });
|
||||
expect(screen.queryByTestId("timeline-selection")).not.toBeInTheDocument();
|
||||
expect(rowCount()).toBe(runs.length);
|
||||
await waitFor(() => expect(rowCount()).toBe(runs.length));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import moment from "moment";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { filterRuns } from "./runSearch/runQuery";
|
||||
import { RunsToolbar } from "./runSearch/RunsToolbar";
|
||||
import { Inspector } from "@/components/shared/Inspector";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -20,19 +19,14 @@ import {
|
|||
import type { TraceSummary } from "../types";
|
||||
import { RunView } from "../detail/run/RunView";
|
||||
import { TimeRangeControls } from "./TimeRangeControls";
|
||||
import { TracesTimeline, type TimeWindow } from "./TracesTimeline";
|
||||
import { TracesTimeline } from "./TracesTimeline";
|
||||
import { TracingSetupCard } from "../../onboarding/tracing/TracingSetupCard";
|
||||
import { useTracesLive } from "../api";
|
||||
import { type AgentTracesResult, traceWindowStartMs, useAgentTraces, useTraceAvailability } from "./useAgentTraces";
|
||||
import { useTraceHistogram } from "./useTraceHistogram";
|
||||
|
||||
const DRAWER_WIDTH_KEY = "litellm.agentTraces.drawerWidth";
|
||||
|
||||
const filterByWindow = (runs: TraceSummary[], range: TimeWindow): TraceSummary[] =>
|
||||
runs.filter((run) => {
|
||||
const t = moment(run.start_time).valueOf();
|
||||
return t >= range.startMs && t < range.endMs;
|
||||
});
|
||||
|
||||
export interface TimeControls {
|
||||
rangeHours: number;
|
||||
onRangeHoursChange: (hours: number) => void;
|
||||
|
|
@ -97,10 +91,11 @@ export function AgentTracesSection({
|
|||
const [showSetup, setShowSetup] = useState(false);
|
||||
const [zoom, setZoom] = useZoomRouting();
|
||||
const [rangeChanged, setRangeChanged] = useState(false);
|
||||
const traceQuery = { accessToken, startTime, endTime, isCustomDate, isLiveTail, enabled: isActive };
|
||||
const traceQuery = { accessToken, startTime, endTime, isCustomDate, isLiveTail, enabled: isActive, q: query, zoom };
|
||||
const traces = useAgentTraces(traceQuery);
|
||||
const setup = useTracingSetup(traces, isActive, rangeChanged);
|
||||
const checkHistory = setup.isEmpty && !rangeChanged;
|
||||
const narrowed = rangeChanged || zoom !== null || query.trim() !== "";
|
||||
const setup = useTracingSetup(traces, isActive, narrowed);
|
||||
const checkHistory = setup.isEmpty && !narrowed;
|
||||
const history = useTraceAvailability(accessToken, isActive && checkHistory && setup.disabledDetail == null);
|
||||
|
||||
const checkTraces = () => {
|
||||
|
|
@ -114,8 +109,8 @@ export function AgentTracesSection({
|
|||
() => ({ startMs: traceWindowStartMs(startTime, endTime, isCustomDate, endMs), endMs }),
|
||||
[startTime, endTime, isCustomDate, endMs],
|
||||
);
|
||||
const filtered = useMemo(() => filterRuns(traces.traces, query), [traces.traces, query]);
|
||||
const runs = useMemo(() => (zoom ? filterByWindow(filtered, zoom) : filtered), [filtered, zoom]);
|
||||
const buckets = useTraceHistogram(accessToken, range, query, isActive);
|
||||
const runs = traces.traces;
|
||||
const runRefs = useMemo(() => runs.map(traceRefOf), [runs]);
|
||||
|
||||
const changeRange = (hours: number, apply: (hours: number) => void) => {
|
||||
|
|
@ -196,7 +191,7 @@ export function AgentTracesSection({
|
|||
/>
|
||||
)}
|
||||
</RunsToolbar>
|
||||
<TracesTimeline runs={filtered} range={range} selection={zoom} onSelect={setZoom} />
|
||||
<TracesTimeline buckets={buckets} range={range} selection={zoom} onSelect={setZoom} />
|
||||
<AgentTracesTable
|
||||
traces={runs}
|
||||
isLoading={traces.isLoading || (checkHistory && history.isLoading)}
|
||||
|
|
|
|||
|
|
@ -205,8 +205,7 @@ export function AgentTracesTable({
|
|||
}: AgentTracesTableProps) {
|
||||
const settled = !isLoading && !error;
|
||||
const isEmpty = settled && !hasMore && traces.length === 0;
|
||||
const canContinue = settled && hasMore;
|
||||
const autoContinue = canContinue && traces.length > 0;
|
||||
const autoContinue = settled && hasMore && traces.length > 0;
|
||||
const tableOptions: TableOptions<TraceSummary> = {
|
||||
data: traces,
|
||||
columns: RUN_COLUMNS,
|
||||
|
|
@ -246,14 +245,6 @@ export function AgentTracesTable({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{canContinue && traces.length === 0 && (
|
||||
<div className="flex items-center justify-center gap-3 py-16 text-xs text-muted-foreground">
|
||||
<span>No loaded runs match these filters.</span>
|
||||
<Button size="xs" variant="outline" disabled={isFetching} onClick={onLoadMore}>
|
||||
Load older runs
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isEmpty && <EmptyRuns rangeEmpty={rangeEmpty} onSetUpTracing={onSetUpTracing} />}
|
||||
</InspectorTable.Root>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,47 +1,11 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { bandForWindow, bucketRuns, dragUpdate, formatSpan } from "./TracesTimeline";
|
||||
import type { TraceSummary } from "../types";
|
||||
import { bandForWindow, type Bucket, dragUpdate, formatSpan } from "./TracesTimeline";
|
||||
|
||||
const HOUR = 3600 * 1000;
|
||||
const START = Date.UTC(2026, 8, 30, 0, 0, 0);
|
||||
const range = { startMs: START, endMs: START + 10 * HOUR };
|
||||
|
||||
const run = (offsetMs: number, errorCount = 0): TraceSummary =>
|
||||
({
|
||||
trace_id: `t${offsetMs}`,
|
||||
start_time: new Date(START + offsetMs).toISOString(),
|
||||
error_count: errorCount,
|
||||
}) as TraceSummary;
|
||||
|
||||
describe("bucketRuns", () => {
|
||||
it("splits the window into equal buckets that tile it exactly", () => {
|
||||
const buckets = bucketRuns([], range, 10);
|
||||
expect(buckets).toHaveLength(10);
|
||||
expect(buckets[0].startMs).toBe(range.startMs);
|
||||
expect(buckets.at(-1)?.endMs).toBe(range.endMs);
|
||||
const fourthBucket = { startMs: START + 3 * HOUR, endMs: START + 4 * HOUR, total: 0, failed: 0 };
|
||||
expect(buckets[3]).toMatchObject(fourthBucket);
|
||||
});
|
||||
|
||||
it("puts each run in the bucket covering its start time", () => {
|
||||
const buckets = bucketRuns([run(0), run(30 * 60 * 1000), run(2.5 * HOUR), run(10 * HOUR - 1)], range, 10);
|
||||
expect(buckets.map((b) => b.total)).toEqual([2, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
|
||||
});
|
||||
|
||||
it("drops runs that start before or at/after the window", () => {
|
||||
const buckets = bucketRuns([run(-1), run(10 * HOUR), run(20 * HOUR), run(HOUR)], range, 10);
|
||||
expect(buckets.reduce((sum, b) => sum + b.total, 0)).toBe(1);
|
||||
expect(buckets[1].total).toBe(1);
|
||||
});
|
||||
|
||||
it("counts runs with any errors as failed", () => {
|
||||
const buckets = bucketRuns([run(HOUR, 3), run(HOUR + 1), run(HOUR + 2, 1), run(5 * HOUR)], range, 10);
|
||||
expect(buckets[1]).toMatchObject({ total: 3, failed: 2 });
|
||||
expect(buckets[5]).toMatchObject({ total: 1, failed: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSpan", () => {
|
||||
it("prints the largest two units, dropping zero parts", () => {
|
||||
expect(formatSpan(45 * 60 * 1000)).toBe("45m");
|
||||
|
|
@ -76,7 +40,13 @@ describe("dragUpdate", () => {
|
|||
|
||||
describe("bandForWindow", () => {
|
||||
it("maps a selected window back to the buckets it covers", () => {
|
||||
const buckets = bucketRuns([], range, 10);
|
||||
const buckets: Bucket[] = Array.from({ length: 10 }, (_, i) => ({
|
||||
startMs: range.startMs + i * HOUR,
|
||||
endMs: range.startMs + (i + 1) * HOUR,
|
||||
total: 0,
|
||||
failed: 0,
|
||||
series: [],
|
||||
}));
|
||||
expect(bandForWindow(buckets, { startMs: START + 2 * HOUR, endMs: START + 5 * HOUR })).toEqual({ lo: 2, hi: 4 });
|
||||
expect(bandForWindow(buckets, null)).toBeNull();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,53 +2,29 @@
|
|||
|
||||
import { X } from "lucide-react";
|
||||
import moment from "moment";
|
||||
import { useMemo, useRef, useState, type RefObject } from "react";
|
||||
import { useRef, useState, type RefObject } from "react";
|
||||
import { useResizeObserver } from "usehooks-ts";
|
||||
|
||||
import { DotFieldCanvas, DotFieldRoot } from "@/components/shared/dotField/DotField";
|
||||
import type { DotBand, DotColumn } from "@/components/shared/dotField/dots";
|
||||
import { cn } from "@/lib/cva.config";
|
||||
|
||||
import type { TraceSummary } from "../types";
|
||||
import { traceAgentNames } from "../utils";
|
||||
|
||||
const BUCKETS = 60;
|
||||
export const BUCKETS = 60;
|
||||
const TICKS = 6;
|
||||
const MINUTE_MS = 60 * 1000;
|
||||
const HOUR_MS = 60 * MINUTE_MS;
|
||||
const DAY_MS = 24 * HOUR_MS;
|
||||
const EDGE_FORMAT = "MMM DD, HH:mm";
|
||||
|
||||
export interface TimeWindow {
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
}
|
||||
import type { TimeRange as TimeWindow } from "../api";
|
||||
|
||||
export type { TimeWindow };
|
||||
|
||||
export interface Bucket extends DotColumn {
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
}
|
||||
|
||||
/** Run counts per equal-width time bucket across the window; runs outside it are dropped. */
|
||||
export function bucketRuns(runs: readonly TraceSummary[], range: TimeWindow, buckets = BUCKETS): Bucket[] {
|
||||
const width = (range.endMs - range.startMs) / buckets;
|
||||
const placed = runs.map((run) => ({
|
||||
index: Math.floor((moment(run.start_time).valueOf() - range.startMs) / width),
|
||||
failed: run.error_count > 0,
|
||||
agent: traceAgentNames(run)[0] ?? "",
|
||||
}));
|
||||
return Array.from({ length: buckets }, (_, i) => {
|
||||
const hits = placed.filter((p) => p.index === i);
|
||||
return {
|
||||
startMs: range.startMs + i * width,
|
||||
endMs: range.startMs + (i + 1) * width,
|
||||
total: hits.length,
|
||||
failed: hits.filter((p) => p.failed).length,
|
||||
series: hits.filter((p) => !p.failed).map((p) => p.agent),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Compact window length, Logfire-style: "45m", "6h 12m", "7d", "152d 23h". */
|
||||
export function formatSpan(ms: number): string {
|
||||
const totalMinutes = Math.max(0, Math.round(ms / MINUTE_MS));
|
||||
|
|
@ -72,7 +48,7 @@ const tickShift = (t: number): string => {
|
|||
};
|
||||
|
||||
interface TracesTimelineProps {
|
||||
runs: readonly TraceSummary[];
|
||||
buckets: readonly Bucket[];
|
||||
range: TimeWindow;
|
||||
selection: TimeWindow | null;
|
||||
onSelect: (selection: TimeWindow | null) => void;
|
||||
|
|
@ -232,8 +208,7 @@ function TickAxis({ range }: { range: TimeWindow }) {
|
|||
}
|
||||
|
||||
/** Histogram of runs over the window. Drag to select; drag the bracket or its edges to adjust; Esc clears. */
|
||||
export function TracesTimeline({ runs, range, selection, onSelect }: TracesTimelineProps) {
|
||||
const buckets = useMemo(() => bucketRuns(runs, range), [runs, range]);
|
||||
export function TracesTimeline({ buckets, range, selection, onSelect }: TracesTimelineProps) {
|
||||
const [hover, setHover] = useState<number | null>(null);
|
||||
const [drag, setDrag] = useState<DragState | null>(null);
|
||||
const [draft, setDraft] = useState<Band | null>(null);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { useTracesApi } from "../api";
|
||||
import { useInfiniteQuery, useQuery, type UseQueryOptions } from "@tanstack/react-query";
|
||||
import { keepPreviousData, useInfiniteQuery, useQuery, type UseQueryOptions } from "@tanstack/react-query";
|
||||
import moment from "moment";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { ApiError } from "@/lib/http/client";
|
||||
|
||||
import type { TracePage, TraceSummary } from "../types";
|
||||
import type { TraceWindow } from "../api";
|
||||
import type { TimeRange, TraceWindow } from "../api";
|
||||
|
||||
const LIVE_TAIL_INTERVAL_MS = 15000;
|
||||
|
||||
|
|
@ -41,6 +41,10 @@ interface UseAgentTracesOptions {
|
|||
isCustomDate: boolean;
|
||||
isLiveTail: boolean;
|
||||
enabled: boolean;
|
||||
/** The search the server applies before paging. */
|
||||
q: string;
|
||||
/** A window inside the range that replaces it for the list. */
|
||||
zoom: TimeRange | null;
|
||||
}
|
||||
|
||||
export interface AgentTracesResult {
|
||||
|
|
@ -60,8 +64,8 @@ export const traceWindowStartMs = (startTime: string, endTime: string, isCustomD
|
|||
isCustomDate ? moment(startTime).valueOf() : nowMs - (moment(endTime).valueOf() - moment(startTime).valueOf());
|
||||
|
||||
/**
|
||||
* GET /v1/traces for the Logs page time range, cursor-paginated as the runs list scrolls.
|
||||
* Preset ranges roll on refresh; subsequent pages keep the first page's window.
|
||||
* GET /v1/traces for the Logs page time range (or the zoom inside it) and search, cursor-paginated as the runs
|
||||
* list scrolls. Preset ranges roll on refresh; subsequent pages keep the first page's window.
|
||||
*/
|
||||
export function useAgentTraces({
|
||||
accessToken,
|
||||
|
|
@ -70,18 +74,22 @@ export function useAgentTraces({
|
|||
isCustomDate,
|
||||
isLiveTail,
|
||||
enabled,
|
||||
q,
|
||||
zoom,
|
||||
}: UseAgentTracesOptions): AgentTracesResult {
|
||||
const traces = useTracesApi(accessToken);
|
||||
const fetchPage = async (pageParam: unknown): Promise<LoadedTracePage> => {
|
||||
const nowMs = Date.now();
|
||||
const window = (pageParam as TraceWindow | null) ?? {
|
||||
startMs: traceWindowStartMs(startTime, endTime, isCustomDate, nowMs),
|
||||
endMs: isCustomDate ? moment(endTime).valueOf() : nowMs,
|
||||
};
|
||||
return { ...(await traces.list(window)), window };
|
||||
const window = (pageParam as TraceWindow | null) ??
|
||||
zoom ?? {
|
||||
startMs: traceWindowStartMs(startTime, endTime, isCustomDate, nowMs),
|
||||
endMs: isCustomDate ? moment(endTime).valueOf() : nowMs,
|
||||
};
|
||||
return { ...(await traces.list({ ...window, q })), window };
|
||||
};
|
||||
const queryOptions: Parameters<typeof useInfiniteQuery<LoadedTracePage, Error>>[0] = {
|
||||
queryKey: ["agentTraces", accessToken, startTime, endTime, isCustomDate],
|
||||
queryKey: ["agentTraces", traces.scope, startTime, endTime, isCustomDate, q, zoom],
|
||||
placeholderData: keepPreviousData,
|
||||
queryFn: ({ pageParam }) => fetchPage(pageParam),
|
||||
initialPageParam: null,
|
||||
getNextPageParam: (lastPage) =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import type { TimeRange } from "../api";
|
||||
import { useTracesApi } from "../api";
|
||||
import type { TraceHistogram } from "../types";
|
||||
import { BUCKETS, type Bucket } from "./TracesTimeline";
|
||||
|
||||
/** The dot field draws one series entry per successful run. */
|
||||
export const toBuckets = (histogram: TraceHistogram): Bucket[] =>
|
||||
histogram.buckets.map((bucket) => ({
|
||||
startMs: bucket.start_ms,
|
||||
endMs: bucket.end_ms,
|
||||
total: bucket.total,
|
||||
failed: bucket.failed,
|
||||
series: bucket.agents.flatMap(({ agent, runs }) => Array<string>(runs).fill(agent)),
|
||||
}));
|
||||
|
||||
const emptyBuckets = (range: TimeRange): Bucket[] => {
|
||||
const width = (range.endMs - range.startMs) / BUCKETS;
|
||||
return Array.from({ length: BUCKETS }, (_, i) => ({
|
||||
startMs: range.startMs + i * width,
|
||||
endMs: range.startMs + (i + 1) * width,
|
||||
total: 0,
|
||||
failed: 0,
|
||||
series: [],
|
||||
}));
|
||||
};
|
||||
|
||||
/** Matching runs per bucket across the whole range, counted by the server so every run is plotted, not just loaded ones. */
|
||||
export function useTraceHistogram(accessToken: string, range: TimeRange, q: string, enabled: boolean): Bucket[] {
|
||||
const traces = useTracesApi(accessToken);
|
||||
const histogram = useQuery({
|
||||
queryKey: ["agentTraceHistogram", traces.scope, range.startMs, range.endMs, q],
|
||||
queryFn: () => traces.histogram(range, q, BUCKETS),
|
||||
enabled,
|
||||
placeholderData: keepPreviousData,
|
||||
select: toBuckets,
|
||||
});
|
||||
return histogram.data ?? emptyBuckets(range);
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ import type { components, paths } from "@/lib/http/schema";
|
|||
|
||||
export type Trace = paths["/v1/traces/{trace_id}"]["get"]["responses"][200]["content"]["application/json"];
|
||||
export type TracePage = paths["/v1/traces"]["get"]["responses"][200]["content"]["application/json"];
|
||||
export type TraceHistogram = paths["/v1/traces/histogram"]["get"]["responses"][200]["content"]["application/json"];
|
||||
export type RunField = paths["/v1/traces/values/{field}"]["get"]["parameters"]["path"]["field"];
|
||||
export type SpanErrorPage =
|
||||
paths["/v1/traces/{trace_id}/spans/{span_id}/error"]["get"]["responses"][200]["content"]["application/json"];
|
||||
type ApiSpanDetail =
|
||||
|
|
|
|||
|
|
@ -1961,14 +1961,16 @@ export const agentTraceListCall = async ({
|
|||
accessToken,
|
||||
startMs,
|
||||
endMs,
|
||||
q,
|
||||
cursor,
|
||||
}: {
|
||||
accessToken: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
q: string;
|
||||
cursor?: string | null;
|
||||
}): Promise<TracePage> => {
|
||||
const query = { start_ms: startMs, end_ms: endMs, cursor: cursor ?? undefined };
|
||||
const query = { start_ms: startMs, end_ms: endMs, q: q || undefined, cursor: cursor ?? undefined };
|
||||
return apiClient.get<TracePage>(`/v1/traces`, { accessToken, query });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -195,4 +195,12 @@ describe("typed api client middleware", () => {
|
|||
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends Content-Type: application/json on a bodyless GET", async () => {
|
||||
const { fetch, requests } = capturingFetch(jsonResponse(200, { data: [] }));
|
||||
|
||||
await fetchClient.GET("/model_group/info", { fetch });
|
||||
|
||||
expect(requests[0].headers.get("Content-Type")).toBe("application/json");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ const middleware: Middleware = {
|
|||
if (token && !request.headers.has(getAuthHeaderName())) {
|
||||
request.headers.set(getAuthHeaderName(), `Bearer ${token}`);
|
||||
}
|
||||
// openapi-fetch omits Content-Type on a bodyless GET, but the local dev rewrite in
|
||||
// next.config.mjs (LENS_DEV_PROXY_URL) uses that header to tell an API call apart from a
|
||||
// same-path dashboard page (e.g. GET /lens collides with the Lens page route) and send it to
|
||||
// the real proxy instead of letting Next.js serve its own page HTML. Setting it unconditionally
|
||||
// keeps that signal regardless of method or body.
|
||||
request.headers.set("Content-Type", "application/json");
|
||||
},
|
||||
async onResponse({ response }) {
|
||||
if (response.ok) return response;
|
||||
|
|
|
|||
208
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
208
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -8990,6 +8990,23 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/lens/values/{field}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** List Lens Values */
|
||||
get: operations["list_lens_values_lens_values__field__get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/lens/watch-all": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -22616,6 +22633,23 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/traces/histogram": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Agent Trace Histogram */
|
||||
get: operations["agent_trace_histogram_v1_traces_histogram_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/traces/query": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -22650,6 +22684,23 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/traces/values/{field}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Agent Trace Values */
|
||||
get: operations["agent_trace_values_v1_traces_values__field__get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/traces/{trace_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -26053,6 +26104,13 @@ export interface components {
|
|||
/** Updated By */
|
||||
updated_by?: string | null;
|
||||
};
|
||||
/** AgentRuns */
|
||||
AgentRuns: {
|
||||
/** Agent */
|
||||
agent: string;
|
||||
/** Runs */
|
||||
runs: number;
|
||||
};
|
||||
/**
|
||||
* AgentSkill
|
||||
* @description Represents a distinct capability or function that an agent can perform.
|
||||
|
|
@ -33140,6 +33198,19 @@ export interface components {
|
|||
*/
|
||||
vault_token?: string | null;
|
||||
};
|
||||
/** HistogramBucket */
|
||||
HistogramBucket: {
|
||||
/** Agents */
|
||||
agents: components["schemas"]["AgentRuns"][];
|
||||
/** End Ms */
|
||||
end_ms: number;
|
||||
/** Failed */
|
||||
failed: number;
|
||||
/** Start Ms */
|
||||
start_ms: number;
|
||||
/** Total */
|
||||
total: number;
|
||||
};
|
||||
/** Hyperparameters */
|
||||
Hyperparameters: {
|
||||
/** Batch Size */
|
||||
|
|
@ -44911,6 +44982,11 @@ export interface components {
|
|||
/** Start */
|
||||
start?: string | null;
|
||||
};
|
||||
/** RunValues */
|
||||
RunValues: {
|
||||
/** Values */
|
||||
values: string[];
|
||||
};
|
||||
/** SCIMEnterpriseUser */
|
||||
SCIMEnterpriseUser: {
|
||||
/** Costcenter */
|
||||
|
|
@ -47760,6 +47836,11 @@ export interface components {
|
|||
spans: components["schemas"]["Span"][];
|
||||
summary: components["schemas"]["TraceSummary"];
|
||||
};
|
||||
/** TraceHistogram */
|
||||
TraceHistogram: {
|
||||
/** Buckets */
|
||||
buckets: components["schemas"]["HistogramBucket"][];
|
||||
};
|
||||
/** TracePage */
|
||||
TracePage: {
|
||||
/** Data */
|
||||
|
|
@ -62954,7 +63035,10 @@ export interface operations {
|
|||
};
|
||||
list_lenses_lens_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
query?: {
|
||||
/** @description Free text and key:value filters, e.g. `status:failed -schedule:paused`. Keys: name, agent, status, schedule. `*` globs and a leading `-` negates */
|
||||
q?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
|
|
@ -62970,6 +63054,15 @@ export interface operations {
|
|||
"application/json": components["schemas"]["LensList"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
create_lens_lens_post: {
|
||||
|
|
@ -63078,6 +63171,40 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
list_lens_values_lens_values__field__get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
contains?: string;
|
||||
limit?: number;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
field: "name" | "agent" | "status" | "schedule";
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": string[];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
watch_all_lens_watch_all_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -81301,11 +81428,13 @@ export interface operations {
|
|||
list_agent_traces_v1_traces_get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Free text and key:value filters, e.g. `agent:research* -status:ok "book a flight"`. Keys: name, agent, status, model, input, trace_id. `*` globs and a leading `-` negates */
|
||||
q?: string;
|
||||
cursor?: string | null;
|
||||
/** @description Window start, unix ms. Default: 24h ago */
|
||||
start_ms?: number | null;
|
||||
/** @description Window end, unix ms. Default: now */
|
||||
end_ms?: number | null;
|
||||
cursor?: string | null;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
|
|
@ -81353,6 +81482,43 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
agent_trace_histogram_v1_traces_histogram_get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Free text and key:value filters, e.g. `agent:research* -status:ok "book a flight"`. Keys: name, agent, status, model, input, trace_id. `*` globs and a leading `-` negates */
|
||||
q?: string;
|
||||
buckets?: number;
|
||||
/** @description Window start, unix ms. Default: 24h ago */
|
||||
start_ms?: number | null;
|
||||
/** @description Window end, unix ms. Default: now */
|
||||
end_ms?: number | null;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["TraceHistogram"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
query_agent_traces_v1_traces_query_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -81406,6 +81572,44 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
agent_trace_values_v1_traces_values__field__get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
contains?: string;
|
||||
limit?: number;
|
||||
/** @description Window start, unix ms. Default: 24h ago */
|
||||
start_ms?: number | null;
|
||||
/** @description Window end, unix ms. Default: now */
|
||||
end_ms?: number | null;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
field: "name" | "agent" | "status" | "model" | "input" | "trace_id";
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["RunValues"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
get_agent_trace_v1_traces__trace_id__get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue