Connect a worker
@@ -83,7 +97,7 @@ export function InvestigationsWelcome({
data-state={workerReady ? "active" : "inactive"}
className="grid grid-cols-[28px_minmax(0,1fr)] items-center gap-x-4 gap-y-3 py-5 text-muted-foreground data-[state=active]:text-foreground sm:grid-cols-[28px_minmax(0,1fr)_auto]"
>
-
+
Run an investigation
diff --git a/ui/litellm-dashboard/src/components/lens/route.tsx b/ui/litellm-dashboard/src/components/lens/route.tsx
index 9d07973fd09..abc5cf2c0ad 100644
--- a/ui/litellm-dashboard/src/components/lens/route.tsx
+++ b/ui/litellm-dashboard/src/components/lens/route.tsx
@@ -18,6 +18,7 @@ const LENS_PARSERS = {
tab: parseAsStringLiteral(lensTabs),
lens: parseAsString,
demo: parseAsBoolean.withDefault(false),
+ setup: parseAsStringLiteral(["lens"]),
};
const ISSUE_PARSERS = { issue: parseAsString };
@@ -56,14 +57,16 @@ export interface LensRoute {
readonly tab: LensTab | null;
readonly lensId: string | null;
readonly demo: boolean;
+ readonly settingUp: boolean;
setTab(tab: LensTab): void;
setLensId(lensId: string | null): void;
setDemo(demo: boolean): void;
+ setSetup(settingUp: boolean): void;
}
/** Lens navigation lives in the URL, sample session included, so any view is a shareable link. */
export function useLensRoute(): LensRoute {
- const [{ tab, lens, demo }, setParams] = useQueryStates(SESSION_PARSERS, { history: "push" });
+ const [{ tab, lens, demo, setup }, setParams] = useQueryStates(SESSION_PARSERS, { history: "push" });
const setTab = useCallback((next: LensTab) => void setParams({ tab: next }), [setParams]);
const setLensId = useCallback(
(next: string | null) => void setParams({ ...CLEARED_RESULTS, lens: next }),
@@ -73,7 +76,8 @@ export function useLensRoute(): LensRoute {
(next: boolean) => void setParams(next ? { ...CLEARED_SESSION, demo: true } : CLEARED_SESSION),
[setParams],
);
- return { tab, lensId: lens, demo, setTab, setLensId, setDemo };
+ const setSetup = useCallback((next: boolean) => void setParams({ setup: next ? "lens" : null }), [setParams]);
+ return { tab, lensId: lens, demo, settingUp: setup === "lens", setTab, setLensId, setDemo, setSetup };
}
export function useIssueRoute() {
diff --git a/ui/litellm-dashboard/src/components/lens/setup/GatewayFlow.tsx b/ui/litellm-dashboard/src/components/lens/setup/GatewayFlow.tsx
new file mode 100644
index 00000000000..bc61078a18d
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/setup/GatewayFlow.tsx
@@ -0,0 +1,148 @@
+import { useId } from "react";
+import styles from "./LensIntroduction.module.css";
+
+const dotColors = [
+ "fill-sky-400 dark:fill-sky-300",
+ "fill-indigo-400 dark:fill-indigo-300",
+ "fill-violet-400 dark:fill-violet-300",
+];
+
+const swarmRows = Array.from({ length: 11 }, (_, row) => ({
+ y: 4 + row * 10,
+ duration: `${[4.8, 5.2, 5.6, 5][row % 4]}s`,
+ delay: `${-row * 0.17}s`,
+ dots: Array.from({ length: 80 }, (_, column) => {
+ const seed = (column % 24) + row * 31;
+ const variation = (seed * 73 + seed * seed * 19) % 101;
+ return {
+ x: column * 10 - 235,
+ color: dotColors[variation % dotColors.length],
+ opacity: variation < 15 ? 0 : 0.5 + variation * 0.005,
+ };
+ }),
+}));
+
+const organizedColumns = Array.from({ length: 58 }, (_, column) => column - 8);
+
+export function GatewayFlow() {
+ const id = useId();
+ return (
+
+
+
+
Agent swarms
+
Every run, every recorded step
+
+
+
LiteLLM gateway
+
One place, your infrastructure
+
+
+
Lens
+
Findings to improve your agents
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {organizedColumns.map((column) => (
+
+ {[34, 44, 54, 64, 74].map((y) => (
+
+ ))}
+
+ ))}
+
+
+
+
+
+ {swarmRows.map((row, index) => (
+
+ {row.dots.map((dot, column) => (
+
+ ))}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/lens/setup/LensGettingStarted.tsx b/ui/litellm-dashboard/src/components/lens/setup/LensGettingStarted.tsx
new file mode 100644
index 00000000000..01b62c864da
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/setup/LensGettingStarted.tsx
@@ -0,0 +1,227 @@
+"use client";
+
+import { useRef, useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { Check, ShieldCheck } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import type { TraceSummary } from "@/components/view_logs/TraceView/traceTypes";
+import { lensQueries } from "../api/queries";
+import { useSaveLens } from "../api/mutations";
+import { useLensApi } from "../services";
+import { TraceSheet } from "../investigations/TraceSheet";
+import { useAnalysisKeyInfo } from "./worker/AnalysisKeyDetails";
+import { WorkerDialog } from "./worker/WorkerDialog";
+import { InvestigationSetupDialog } from "./InvestigationSetupDialog";
+import { LensIntroduction } from "./LensIntroduction";
+import type { LensSetupState } from "./useLensSetup";
+import type { Settings } from "../model/types";
+import { initialSetupStep, LensSetupSteps } from "./LensSetupSteps";
+
+export function LensGettingStarted({
+ accessToken,
+ state,
+ readOnly,
+ canInvestigate,
+ canMintTracingKey,
+ onStart,
+ onExit,
+ onCreated,
+ onDemo,
+}: {
+ accessToken: string;
+ state: LensSetupState;
+ readOnly: boolean;
+ canInvestigate: boolean;
+ canMintTracingKey: boolean;
+ onStart: () => void;
+ onExit: () => void;
+ onCreated: (id: string) => void;
+ onDemo?: () => void;
+}) {
+ const setupRef = useRef
(null);
+ const [step, setStep] = useState(() => initialSetupStep(state));
+ const [workerOpen, setWorkerOpen] = useState(false);
+ const [investigationOpen, setInvestigationOpen] = useState(false);
+ const [trace, setTrace] = useState(null);
+ const selectStep = (index: number) => {
+ onStart();
+ setStep(index);
+ setupRef.current?.querySelector(`[aria-controls="lens-setup-step-${index}"]`)?.focus();
+ };
+ const canLeave = state.tracesReady || state.requestsReady || state.hasInvestigations;
+ const start = () => {
+ onStart();
+ setupRef.current?.scrollIntoView({ block: "start" });
+ setupRef.current?.focus({ preventScroll: true });
+ };
+ const connectWorker = () => {
+ selectStep(2);
+ setWorkerOpen(true);
+ };
+ const createInvestigation = () => {
+ selectStep(3);
+ setInvestigationOpen(true);
+ };
+
+ return (
+
+
+
+
+
+
+
+ Get Lens running
+
+
+ Each step checks your connection, so you’ll see when it’s working.
+
+
+ {canLeave && (
+
+ {state.tracesReady ? "View traces" : "View investigations"}
+
+ )}
+
+
+ {state.error && (
+
+
Could not check setup. {state.error}
+
+ Retry
+
+
+ )}
+ {(readOnly || !canInvestigate) && (
+
+ A gateway administrator can connect a worker and run investigations.
+
+ )}
+
+
+
+ {workerOpen && (
+
setWorkerOpen(false)}
+ onChanged={state.refresh}
+ onReady={
+ state.ready
+ ? () => {
+ setWorkerOpen(false);
+ createInvestigation();
+ }
+ : undefined
+ }
+ />
+ )}
+ {investigationOpen && (
+ setInvestigationOpen(false)}
+ onCreated={onCreated}
+ />
+ )}
+ {trace && (
+ setTrace(null)}
+ />
+ )}
+
+ );
+}
+
+function FirstInvestigation({
+ accessToken,
+ state,
+ onClose,
+ onCreated,
+}: {
+ accessToken: string;
+ state: LensSetupState;
+ onClose: () => void;
+ onCreated: (id: string) => void;
+}) {
+ const api = useLensApi(accessToken);
+ const saveLens = useSaveLens(accessToken);
+ const models = useQuery({ ...lensQueries.models(api) });
+ const modelDetails = useQuery({ ...lensQueries.modelDetails(api) });
+ const workers = state.workers.filter((worker) => !worker.revoked);
+ const analysisAccess = useAnalysisKeyInfo(
+ accessToken,
+ workers.length === 1 ? workers[0].analysis_key_id ?? undefined : undefined,
+ );
+ const defaultModel = analysisAccess.data?.models.length === 1 ? analysisAccess.data.models[0] : undefined;
+ const save = async (settings: Settings) => {
+ if (!state.ready)
+ throw new Error("Wait for recorded activity and a connected worker before starting an investigation");
+ const saved = await saveLens.mutateAsync({ settings });
+ state.refresh();
+ onCreated(saved.id);
+ };
+
+ return (
+ model.id) ?? []}
+ modelDetails={modelDetails.data?.data ?? []}
+ modelsLoading={models.isLoading}
+ modelsError={models.error?.message}
+ onClose={onClose}
+ onSave={save}
+ />
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/lens/setup/LensIntroduction.module.css b/ui/litellm-dashboard/src/components/lens/setup/LensIntroduction.module.css
new file mode 100644
index 00000000000..b9f5fc11d88
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/setup/LensIntroduction.module.css
@@ -0,0 +1,114 @@
+.swarmRow {
+ animation: swarm-flow 5.2s linear infinite;
+}
+
+.organizedDots {
+ animation: organized-flow 1.6s linear infinite;
+}
+
+.gatewayGlow {
+ opacity: 0.7;
+ transition: opacity 300ms ease;
+}
+
+.flowGraphic:hover .gatewayGlow {
+ opacity: 1;
+}
+
+.sampleCard {
+ transition:
+ border-color 200ms ease,
+ box-shadow 200ms ease;
+}
+
+.sampleCard:hover,
+.sampleCard:focus-within {
+ border-color: color-mix(in oklab, var(--color-indigo-500) 22%, var(--border));
+ box-shadow: 0 4px 20px -16px color-mix(in oklab, var(--color-indigo-500) 40%, transparent);
+}
+
+.examples {
+ --evidence-tint: color-mix(in oklab, var(--color-rose-400) 6%, transparent);
+ --evidence-outline: color-mix(in oklab, var(--color-rose-400) 32%, transparent);
+}
+
+.evidenceLink {
+ cursor: pointer;
+ border-radius: 4px;
+ outline-offset: 4px;
+ transition: background-color 200ms ease;
+}
+
+.evidenceLink:focus-visible {
+ outline: 2px solid var(--ring);
+}
+
+.evidenceLink:hover {
+ background: var(--evidence-tint);
+}
+
+.evidenceOutput,
+.evidenceFinding,
+.evidenceBar {
+ transition:
+ background-color 200ms ease,
+ box-shadow 200ms ease;
+}
+
+.evidenceFinding {
+ border-radius: 6px;
+}
+
+.examples:is(
+ [data-evidence-active="true"],
+ :has([data-evidence-link]:hover),
+ :has([data-evidence-link]:focus-visible)
+ ) {
+ .evidenceOutput {
+ box-shadow: inset 0 0 0 1px var(--evidence-outline);
+ }
+
+ .evidenceFinding {
+ background: var(--evidence-tint);
+ box-shadow: 0 0 0 7px var(--evidence-tint);
+ }
+
+ .evidenceBar {
+ box-shadow: 0 0 10px 2px var(--evidence-outline);
+ }
+}
+
+@keyframes swarm-flow {
+ from {
+ transform: translateX(0);
+ }
+ to {
+ transform: translateX(240px);
+ }
+}
+
+@keyframes organized-flow {
+ from {
+ transform: translateX(0);
+ }
+ to {
+ transform: translateX(80px);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .swarmRow,
+ .organizedDots {
+ animation: none;
+ opacity: 0.75;
+ }
+
+ .gatewayGlow,
+ .sampleCard,
+ .evidenceLink,
+ .evidenceOutput,
+ .evidenceFinding,
+ .evidenceBar {
+ transition: none;
+ }
+}
diff --git a/ui/litellm-dashboard/src/components/lens/setup/LensIntroduction.tsx b/ui/litellm-dashboard/src/components/lens/setup/LensIntroduction.tsx
new file mode 100644
index 00000000000..c4820a95517
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/setup/LensIntroduction.tsx
@@ -0,0 +1,190 @@
+import { useState } from "react";
+import { ArrowRight, ArrowUpRight, ChevronDown, ChevronRight } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import styles from "./LensIntroduction.module.css";
+import { GatewayFlow } from "./GatewayFlow";
+
+const traceSteps = [
+ { name: "research_agent", start: 0, duration: 14.2, failed: false },
+ { name: "plan", start: 0, duration: 1.7, failed: false },
+ { name: "search_docs", start: 1.7, duration: 3.1, failed: false },
+ { name: "run_benchmark", start: 4.8, duration: 1.3, failed: true },
+ { name: "search_docs", start: 6.1, duration: 3.6, failed: false },
+ { name: "answer", start: 9.7, duration: 4.5, failed: false },
+];
+
+const sampleAnswer = "The column store is 40% faster than the row store for your workload.";
+
+type EvidenceProps = { highlighted: boolean; onHighlight: () => void };
+
+function TraceExample({ highlighted, onHighlight }: EvidenceProps) {
+ return (
+
+
+
+ Tracing
+
+
Sample trace · 14.2s
+
+
+ {traceSteps.map((item, index) => {
+ const linked = item.failed || item.name === "answer";
+ const rowClass =
+ "grid w-full grid-cols-[minmax(0,1fr)_minmax(48px,1fr)_2.5rem] items-center gap-2 text-left sm:gap-3";
+ const content = (
+ <>
+ 0 ? "pl-2" : ""}`}>
+ {index === 0 ? (
+
+ ) : (
+
+ )}
+
+ {item.name}
+
+ {item.failed && Failed }
+
+
+
+
+ {item.duration}s
+ >
+ );
+ return (
+
+ {linked ? (
+
+ {content}
+
+ ) : (
+ {content}
+ )}
+
+ );
+ })}
+
+
+
+
run_benchmark · output
+
Error: benchmark runner unavailable (503)
+
+
+
answer · output
+
“{sampleAnswer}”
+
+
+
+ );
+}
+
+function FindingExamples({ highlighted, onHighlight }: EvidenceProps) {
+ return (
+
+
+
+ Lens findings
+
+
500 sample runs reviewed
+
+
+
+
+
+
+ Claims numbers it never measured
+
+
+
+
Reports a speedup even though the benchmark failed.
+
+ “…40% faster than the row store for your workload.”
+
+
+ Next step: Report the failed benchmark instead of
+ estimating.
+
+
38 linked runs · High priority
+
+
+
+
+
+ Promises to follow up, then stops
+
+
+
After an order lookup times out, the agent promises to check and ends the run.
+
12 linked runs · Medium priority
+
+
+
+
+ );
+}
+
+export function LensIntroduction({ onStart, onDemo }: { onStart: () => void; onDemo?: () => void }) {
+ const [highlighted, setHighlighted] = useState(false);
+ const toggleEvidence = () => setHighlighted((current) => !current);
+ return (
+
+
+ The gateway that helps your agents improve
+
+
+ Turn recorded agent runs into findings linked to the exact steps, so you know what happened and what to change.
+
+
+ {onDemo && (
+
+ Explore with sample data
+
+ )}
+
+ Set up Lens
+
+
+ Docs
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/lens/setup/LensSetupSteps.tsx b/ui/litellm-dashboard/src/components/lens/setup/LensSetupSteps.tsx
new file mode 100644
index 00000000000..cdf95ef25ef
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/setup/LensSetupSteps.tsx
@@ -0,0 +1,229 @@
+import { ArrowRight, Check, ChevronDown } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { TracingSetupFields } from "@/components/view_logs/TraceView/TracingSetupCard";
+import type { TraceSummary } from "@/components/view_logs/TraceView/traceTypes";
+import type { LensSetupState } from "./useLensSetup";
+import { cn } from "@/lib/cva.config";
+
+type StepProps = {
+ accessToken: string;
+ state: LensSetupState;
+ readOnly: boolean;
+ canInvestigate: boolean;
+ canMintTracingKey: boolean;
+ onStep: (step: number) => void;
+ onTrace: (trace: TraceSummary) => void;
+ onConnect: () => void;
+ onCreate: () => void;
+};
+
+export function initialSetupStep(state: LensSetupState) {
+ if (state.tracesReady || state.requestsReady) return state.connected ? 3 : 2;
+ return state.tracingEnabled ? 1 : 0;
+}
+
+function StorageStep({ state, onStep, ...props }: StepProps) {
+ if (!state.tracingEnabled)
+ return (
+ <>
+
+
+ >
+ );
+ return (
+
+
+ Trace storage is connected
+
+
onStep(1)}>
+ Continue to your agent
+
+
+
+ );
+}
+
+function continuationLabel(state: LensSetupState) {
+ if (state.connected) return "Continue to investigation";
+ return state.tracesReady ? "Continue to worker" : "Continue with request logs";
+}
+
+function ActivityContinuation({
+ state,
+ onConnect,
+ onCreate,
+ readOnly,
+ canInvestigate,
+}: Pick) {
+ if (!state.tracesReady && !state.requestsReady) return null;
+ return (
+
+
+ {continuationLabel(state)}
+
+
+ {state.requestsReady && !state.tracesReady && (
+
+ Request logs are already available. You can investigate them now and add agent traces later.
+
+ )}
+
+ );
+}
+
+function AgentStep({ state, ...props }: StepProps) {
+ if (!state.tracingEnabled)
+ return (
+ <>
+ Connect trace storage in step 1 before sending a trace.
+
+ >
+ );
+ return (
+ <>
+
+ {
+ state.refresh();
+ props.onTrace(value);
+ }}
+ onCheck={state.refresh}
+ checking={state.checking}
+ readOnly={props.readOnly}
+ canMintTracingKey={props.canMintTracingKey}
+ />
+
+ {state.tracesReady && (
+
+ Your first trace is ready. Continue setup so Lens can investigate your agent’s behavior.
+
+ )}
+
+ >
+ );
+}
+
+function WorkerStep({ state, onConnect, onCreate, readOnly, canInvestigate }: StepProps) {
+ const activityReady = state.tracesReady || state.requestsReady;
+ return (
+
+
+ {state.connected
+ ? "Worker connected. You’re ready to create an investigation."
+ : "The worker reviews recorded activity using a model on your gateway. You choose its analysis model and spending limit."}
+
+ {!activityReady && (
+
Record activity in step 2 before connecting a worker.
+ )}
+
+ {state.connected ? "Continue to investigation" : "Connect worker"}
+
+
+
+ );
+}
+
+function InvestigationStep({ state, onCreate, readOnly, canInvestigate }: StepProps) {
+ return (
+
+
+ Choose the activity to review and describe how your agent should behave. Lens will show findings with evidence
+ and suggested changes.
+
+ {!state.ready && (
+
+ Recorded activity and a connected worker are required before you can run an investigation.
+
+ )}
+
+ New investigation
+
+
+ );
+}
+
+export function LensSetupSteps({ step, ...props }: StepProps & { step: number }) {
+ const items = [
+ {
+ title: "Enable tracing on the gateway",
+ description: "Connect ClickHouse and restart the gateway.",
+ complete: props.state.tracingEnabled,
+ content: ,
+ },
+ {
+ title: "Send your first trace",
+ description: "Capture your agent’s inputs, outputs, and tool calls.",
+ complete: props.state.tracesReady,
+ content: ,
+ },
+ {
+ title: "Connect a worker",
+ description: "Choose a model and run the worker on your infrastructure.",
+ complete: props.state.connected,
+ content: ,
+ },
+ {
+ title: "Run your first investigation",
+ description: "Describe the expected behavior and review a sample of activity.",
+ complete: props.state.hasInvestigations,
+ content: ,
+ },
+ ];
+ return (
+
+ {items.map((item, index) => (
+
+
+ props.onStep(index)}
+ className="group flex w-full items-start gap-4 p-5 text-left outline-none hover:bg-muted/30 focus-visible:bg-muted/50 sm:p-6"
+ >
+
+ {item.complete ? : index + 1}
+
+
+
+ {item.title}
+
+
+ {item.description}
+
+
+
+
+
+
+ {item.content}
+
+
+ ))}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/lens/setup/useLensSetup.ts b/ui/litellm-dashboard/src/components/lens/setup/useLensSetup.ts
new file mode 100644
index 00000000000..12903e8a15c
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/setup/useLensSetup.ts
@@ -0,0 +1,59 @@
+import { useQuery } from "@tanstack/react-query";
+import { useNow } from "@/hooks/useNow";
+import { isTracingNotEnabled, useTraceAvailability } from "@/components/view_logs/TraceView/useAgentTraces";
+import { lensQueries } from "../api/queries";
+import { workerConnected } from "../model/status";
+import { useLensApi } from "../services";
+
+function traceSetupState(traces: ReturnType, configured: boolean) {
+ const disabled = isTracingNotEnabled(traces.error);
+ return {
+ tracesReady: traces.data === true && !traces.error,
+ tracingEnabled: !disabled && (traces.isSuccess || configured),
+ missingTraces: disabled || traces.data === false,
+ error: disabled ? null : traces.error,
+ };
+}
+
+export function useLensSetup(accessToken: string, enabled: boolean, canInvestigate: boolean, settingUp: boolean) {
+ const api = useLensApi(accessToken);
+ const now = useNow(2000);
+ const traces = useTraceAvailability(accessToken, enabled);
+ const list = useQuery({ ...lensQueries.list(api, settingUp), enabled: enabled && canInvestigate });
+ const activity = useQuery(lensQueries.activity(api, enabled && canInvestigate && list.isSuccess));
+ const data = list.data ?? { lenses: [], workers: [], tracing_enabled: false };
+ const traceState = traceSetupState(traces, data.tracing_enabled);
+ const hasRequests = activity.data?.requests === true;
+ const requestsReady = hasRequests && !activity.error;
+ const connected = data.workers.some((worker) => workerConnected(worker, now));
+ const hasInvestigations = data.lenses.length > 0;
+ const activityReady = traceState.tracesReady || requestsReady;
+ const activityError = activityReady ? null : traceState.error || activity.error;
+ const error = list.error || activityError;
+ const loadingActivity = !activityReady && list.isSuccess && activity.isPending;
+ const loadingInvestigations = canInvestigate && (list.isPending || loadingActivity);
+ const refresh = () => {
+ void traces.refetch();
+ if (canInvestigate) {
+ void list.refetch();
+ void activity.refetch();
+ }
+ };
+ return {
+ tracingEnabled: traceState.tracingEnabled,
+ tracesReady: traceState.tracesReady,
+ requestsReady,
+ hasRequests,
+ connected,
+ hasInvestigations,
+ missingTraces: traceState.missingTraces,
+ loading: (!activityReady && traces.isPending) || loadingInvestigations,
+ checking: traces.isFetching || list.isFetching || activity.isFetching,
+ ready: activityReady && connected && !error,
+ workers: data.workers,
+ error: error?.message,
+ refresh,
+ };
+}
+
+export type LensSetupState = ReturnType;
diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx
index e00870e71eb..26904ef49ff 100644
--- a/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/TracingSetupCard.tsx
@@ -636,16 +636,7 @@ function ConnectAgent({
);
}
-export function TracingSetupCard({
- detail,
- accessToken,
- onOpenTrace,
- connected = false,
- onCheck,
- checking = false,
- readOnly = false,
- canMintTracingKey = false,
-}: {
+type TracingSetupProps = {
detail: string | null;
accessToken: string;
onOpenTrace: (trace: TraceSummary) => void;
@@ -654,19 +645,47 @@ export function TracingSetupCard({
checking?: boolean;
readOnly?: boolean;
canMintTracingKey?: boolean;
-}) {
+};
+
+export function TracingSetupFields({
+ detail,
+ accessToken,
+ onOpenTrace,
+ connected = false,
+ onCheck,
+ checking = false,
+ readOnly = false,
+ canMintTracingKey = false,
+}: TracingSetupProps) {
const [checked, setChecked] = useState(false);
- const enabled = detail === null;
const check = () => {
setChecked(true);
onCheck?.();
};
+ return detail === null ? (
+
+ ) : (
+
+ );
+}
+
+export function TracingSetupCard(props: TracingSetupProps) {
+ const enabled = props.detail === null;
return (
- {!connected &&
}
+ {!props.connected &&
}
-
{setupTitle(enabled, connected)}
+
{setupTitle(enabled, props.connected ?? false)}
{enabled ? (
@@ -689,20 +708,7 @@ export function TracingSetupCard({
? "Send your agent’s runs to LiteLLM to see its inputs, outputs, and tool calls."
: "Tracing needs ClickHouse and a small update to your LiteLLM proxy configuration."}
- {enabled ? (
-
- ) : (
-
- )}
+
);
}