feat(ui): add test trace, tracing key and otel endpoints to tracing setup (#44090)

* feat(ui): add a call that posts an OTLP export to the proxy

* feat(ui): build a sample agent run as an OTLP export

* feat(ui): add a pulsing dot for active tracing

* feat(ui): render a crisp sample run preview from real trace components

* chore(ui): remove the pixelated agent traces preview image

* feat(ui): add test trace, tracing key, otel endpoints and more frameworks to tracing setup

* test(ui): cover tracing setup test trace, key masking, endpoints and frameworks

* feat(ui): open the received test trace and mark tracing as active

* test(ui): mock the new tracing setup network calls

* fix(ui): let tracing setup use the full page width

* fix(ui): send OTLP/JSON sample trace ids as hex per the spec

* test(ui): cover the sample trace export shape and hex ids
This commit is contained in:
ishaan-berri 2026-10-01 15:45:31 -07:00 • committed by GitHub
parent 0f6a06a6b9
commit 46196339d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1061 additions and 173 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

View file

@ -37,7 +37,12 @@ export default function LensPage() {
</TabsTrigger>
</TabsList>
<TabsContent value="traces" keepMounted className="min-h-0">
<AgentTracesPage accessToken={accessToken} isActive={activeTab === "traces"} />
<AgentTracesPage
accessToken={accessToken}
isActive={activeTab === "traces"}
readOnly={isViewOnly}
canMintTracingKey={isProxyAdminRole(userRole ?? "")}
/>
</TabsContent>
<TabsContent value="investigations">
{isProxyAdminTierRole(userRole ?? "") ? (

View file

@ -2122,6 +2122,9 @@ export const agentTraceListCall = async ({
return apiClient.get<TracePage>(`/v1/traces`, { accessToken, query });
};
export const sendOtlpTraceCall = async (accessToken: string, exportRequest: object): Promise<void> =>
apiClient.post(`/v1/traces`, { accessToken, body: exportRequest });
export const agentTraceCall = async (accessToken: string, traceId: string, traceRef?: string): Promise<Trace> =>
apiClient.get<Trace>(`/v1/traces/${encodeURIComponent(traceId)}`, {
accessToken,

View file

@ -0,0 +1,8 @@
export function ActiveDot() {
return (
<span className="relative flex size-2" aria-hidden="true">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-500 opacity-60 motion-reduce:hidden" />
<span className="relative inline-flex size-2 rounded-full bg-emerald-500" />
</span>
);
}

View file

@ -8,7 +8,17 @@ import { AgentTracesSection } from "./AgentTracesSection";
const DEFAULT_RANGE_HOURS = 24;
const TIME_FORMAT = "YYYY-MM-DDTHH:mm";
export default function AgentTracesPage({ accessToken, isActive = true }: { accessToken: string; isActive?: boolean }) {
export default function AgentTracesPage({
accessToken,
isActive = true,
readOnly = false,
canMintTracingKey = false,
}: {
accessToken: string;
isActive?: boolean;
readOnly?: boolean;
canMintTracingKey?: boolean;
}) {
const [rangeHours, setRangeHours] = useState(DEFAULT_RANGE_HOURS);
const [live, setLive] = useState(true);
const [anchor, setAnchor] = useState(() => moment());
@ -34,6 +44,8 @@ export default function AgentTracesPage({ accessToken, isActive = true }: { acce
endTime={endTime}
isCustomDate={false}
isLiveTail={live}
readOnly={readOnly}
canMintTracingKey={canMintTracingKey}
timeControls={{ rangeHours, onRangeHoursChange: changeRange, onLiveChange: setLive }}
/>
</div>

View file

@ -10,8 +10,9 @@ import { AgentTracesSection, filterRuns } from "./AgentTracesSection";
import type { TracePage, TraceSummary } from "./traceTypes";
vi.mock("../../networking", () => ({
apiClient: { get: vi.fn() },
apiClient: { get: vi.fn(), post: vi.fn() },
agentTraceListCall: vi.fn(),
sendOtlpTraceCall: vi.fn(),
agentTraceCall: vi.fn(),
agentTraceSpanCall: vi.fn(),
getProxyBaseUrl: () => "http://localhost:4000",

View file

@ -1,6 +1,5 @@
"use client";
import { Plug } from "lucide-react";
import moment from "moment";
import { useMemo, useState } from "react";
@ -13,6 +12,7 @@ import type { TraceSummary } from "./traceTypes";
import { previewText } from "./traceUtils";
import { TimeRangeControls } from "./TimeRangeControls";
import { TracesTimeline, type TimeWindow } from "./TracesTimeline";
import { ActiveDot } from "./ActiveDot";
import { TracingSetupCard } from "./TracingSetupCard";
import { type AgentTracesResult, traceWindowStartMs, useAgentTraces, useTraceAvailability } from "./useAgentTraces";
@ -59,6 +59,8 @@ interface AgentTracesSectionProps {
timeControls?: TimeControls;
/** Called when a run opens / closes, so the page can hide its own header while a run fills the view. */
onRunOpenChange?: (open: boolean) => void;
readOnly?: boolean;
canMintTracingKey?: boolean;
}
function useTracingSetup(traces: AgentTracesResult, isActive: boolean, rangeChanged: boolean) {
@ -98,6 +100,8 @@ export function AgentTracesSection({
isLiveTail,
timeControls,
onRunOpenChange,
readOnly = false,
canMintTracingKey = false,
}: AgentTracesSectionProps) {
const [openTrace, setOpenTrace] = useState<TraceSummary | null>(null);
const [query, setQuery] = useState("");
@ -141,11 +145,25 @@ export function AgentTracesSection({
onRunOpenChange?.(trace !== null);
};
if (setup.disabledDetail != null)
return <TracingSetupCard detail={setup.disabledDetail} onCheck={checkTraces} checking={traces.isFetching} />;
const openSentTrace = (trace: TraceSummary) => {
setShowSetup(false);
setRangeChanged(true);
checkTraces();
openRun(trace);
};
const setupProps = {
accessToken,
readOnly,
canMintTracingKey,
onOpenTrace: openSentTrace,
onCheck: checkTraces,
checking: traces.isFetching,
};
if (setup.disabledDetail != null) return <TracingSetupCard detail={setup.disabledDetail} {...setupProps} />;
// Onboarding only on the first, default view; an empty range the user picked keeps its controls.
if (checkHistory && !history.error && history.data === false)
return <TracingSetupCard detail={null} onCheck={checkTraces} checking={traces.isFetching} />;
return <TracingSetupCard detail={null} {...setupProps} />;
if (showSetup) {
return (
<div>
@ -156,7 +174,7 @@ export function AgentTracesSection({
>
← Back to traces
</button>
<TracingSetupCard detail={null} connected onCheck={checkTraces} checking={traces.isFetching} />
<TracingSetupCard detail={null} connected {...setupProps} />
</div>
);
}
@ -183,7 +201,7 @@ export function AgentTracesSection({
onStatusChange={setStatus}
>
<Button variant="outline" size="sm" onClick={() => setShowSetup(true)} className="shrink-0 gap-1.5">
<Plug className="size-3.5" />
<ActiveDot />
Set up tracing
</Button>
{timeControls && (

View file

@ -0,0 +1,70 @@
"use client";
import { cn } from "@/lib/cva.config";
import previewTrace from "./previewTrace.json";
import { SpanIcon } from "./SpanIcon";
import type { SpanType } from "./traceTypes";
import { fmtMs } from "./traceUtils";
interface PreviewRow {
id: string;
name: string;
type: SpanType;
model: string | null;
depth: number;
start_offset_ms: number;
duration_ms: number;
error: boolean;
}
const preview = previewTrace as {
name: string;
input_preview: string;
span_count: number;
duration_ms: number;
rows: PreviewRow[];
};
export function TracePreview() {
const total = preview.duration_ms;
return (
<div
aria-hidden
data-testid="trace-preview"
className="pointer-events-none overflow-hidden rounded-lg border border-border bg-background shadow-sm select-none"
>
<div className="flex items-center gap-3 border-b border-border px-4 py-2.5">
<SpanIcon type="agent" size="md" />
<span className="text-[13px] font-medium text-foreground">{preview.name}</span>
<span className="truncate text-[12px] text-muted-foreground">{preview.input_preview}</span>
<span className="ml-auto shrink-0 font-mono text-[11px] text-muted-foreground">
{preview.span_count} steps · {fmtMs(total)}
</span>
</div>
<div className="relative">
{preview.rows.map((row) => (
<div key={row.id} className="flex h-8 items-center gap-2 border-b border-border/60 px-4 last:border-b-0">
<div className="flex min-w-0 flex-1 items-center gap-2" style={{ paddingLeft: row.depth * 16 }}>
<SpanIcon type={row.type} model={row.model} error={row.error} size="sm" />
<span className="truncate text-[12.5px] text-foreground">{row.name}</span>
</div>
<div className="relative h-1.5 w-[40%] shrink-0 rounded-full bg-muted">
<div
className={cn("absolute h-full rounded-full", row.error ? "bg-destructive" : "bg-trace-brand/70")}
style={{
left: `${(row.start_offset_ms / total) * 100}%`,
width: `${Math.max((row.duration_ms / total) * 100, 0.6)}%`,
}}
/>
</div>
<span className="w-14 shrink-0 text-right font-mono text-[11px] text-muted-foreground">
{fmtMs(row.duration_ms)}
</span>
</div>
))}
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-background to-transparent" />
</div>
</div>
);
}

View file

@ -1,39 +1,98 @@
import { render, screen } from "@testing-library/react";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { chooseSelectOption } from "@/../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { chooseSelectOption, renderWithProviders } from "@/../tests/test-utils";
import { copyToClipboard } from "@/utils/dataUtils";
import { codingAgentPrompt, tracingEnvSnippet, TracingSetupCard } from "./TracingSetupCard";
import { agentTraceCall, apiClient, sendOtlpTraceCall } from "../../networking";
import {
codingAgentCommand,
codingAgentPrompt,
maskSecret,
TRACING_KEY_REQUEST,
tracingEnvSnippet,
TracingSetupCard,
} from "./TracingSetupCard";
import type { Trace } from "./traceTypes";
vi.mock("../../networking", () => ({ getProxyBaseUrl: () => "http://proxy.test/" }));
vi.mock("../../networking", () => ({
getProxyBaseUrl: () => "http://proxy.test/",
sendOtlpTraceCall: vi.fn(),
agentTraceCall: vi.fn(),
apiClient: { post: vi.fn() },
}));
vi.mock("@/utils/dataUtils", () => ({ copyToClipboard: vi.fn().mockResolvedValue(true) }));
const SECRET = "sk-abcdefghijklmnopWXYZ";
const renderCard = (
props: {
detail?: string | null;
connected?: boolean;
onCheck?: () => void;
readOnly?: boolean;
canMintTracingKey?: boolean;
} = {},
) => {
const onOpenTrace = vi.fn();
renderWithProviders(
<TracingSetupCard
detail={props.detail ?? null}
connected={props.connected}
onCheck={props.onCheck}
readOnly={props.readOnly}
canMintTracingKey={props.canMintTracingKey ?? true}
accessToken="sk-admin"
onOpenTrace={onOpenTrace}
/>,
);
return { onOpenTrace, card: screen.getByTestId("tracing-setup-card") };
};
beforeEach(() => vi.clearAllMocks());
describe("TracingSetupCard", () => {
it("shows agent connection guidance once tracing is enabled without presenting example data as live", async () => {
it("shows agent connection guidance and labels the example run as sample data", async () => {
const user = userEvent.setup();
render(<TracingSetupCard detail={null} />);
const { card } = renderCard();
expect(screen.getByRole("heading", { name: "Connect your agent" })).toBeVisible();
expect(screen.getByText("Tracing enabled")).toBeVisible();
expect(screen.getByText("Waiting for your first trace")).toBeVisible();
expect(screen.getByRole("img", { hidden: true, name: /Example agent trace/ })).not.toBeVisible();
expect(screen.getByTestId("tracing-setup-card")).not.toHaveTextContent("store: clickhouse");
expect(screen.getByTestId("trace-preview")).toBeVisible();
expect(card).toHaveTextContent("sample data, not your runs");
expect(card).not.toHaveTextContent("store: clickhouse");
await user.click(screen.getByText("Set up manually"));
expect(screen.getByText(/OTEL_EXPORTER_OTLP_ENDPOINT=http:\/\/proxy.test/)).toBeVisible();
expect(screen.getByTestId("tracing-setup-card")).not.toHaveTextContent(/langsmith/i);
expect(screen.getByText(/^export OTEL_EXPORTER_OTLP_ENDPOINT=http:\/\/proxy.test/)).toBeVisible();
expect(card).not.toHaveTextContent(/langsmith/i);
});
it("copies instructions for the selected framework and preserves both manual installers", async () => {
it("lists the OTEL endpoints before the framework picker, each copyable", async () => {
const user = userEvent.setup();
render(<TracingSetupCard detail={null} />);
const { card } = renderCard();
const text = card.textContent ?? "";
expect(text.indexOf("OpenTelemetry (OTEL) endpoints")).toBeLessThan(text.indexOf("Your agent framework"));
await user.click(screen.getByRole("button", { name: "Copy http://proxy.test/v1/traces" }));
expect(copyToClipboard).toHaveBeenLastCalledWith("http://proxy.test/v1/traces");
});
it("hides the sample preview once traces are arriving", () => {
renderCard({ connected: true });
expect(screen.getByRole("heading", { name: "Connect another agent" })).toBeVisible();
expect(screen.queryByTestId("trace-preview")).not.toBeInTheDocument();
});
it("builds the coding agent command for the selected framework and keeps both manual installers", async () => {
const user = userEvent.setup();
renderCard();
await chooseSelectOption(user, screen.getByRole("combobox", { name: "Your agent framework" }), "CrewAI");
await user.click(screen.getByRole("button", { name: "Copy setup prompt" }));
expect(copyToClipboard).toHaveBeenLastCalledWith(
codingAgentPrompt("http://proxy.test", {
label: "CrewAI",
packages: "crewai openinference-instrumentation-crewai",
}),
);
expect(screen.getByRole("button", { name: "Prompt copied" })).toBeVisible();
const prompt = codingAgentPrompt("http://proxy.test", {
label: "CrewAI",
packages: "crewai openinference-instrumentation-crewai",
});
const commandText = () => screen.getByText(/^claude |^codex /).textContent;
expect(commandText()).toBe(codingAgentCommand("Claude Code", prompt));
await user.click(screen.getByRole("tab", { name: "Codex" }));
expect(commandText()).toBe(codingAgentCommand("Codex", prompt));
await user.click(screen.getByText("Set up manually"));
expect(screen.getByText(/pip install -U opentelemetry-distro/)).toHaveTextContent(
"crewai openinference-instrumentation-crewai",
@ -44,13 +103,88 @@ describe("TracingSetupCard", () => {
);
});
it("uses npm and a CommonJS-safe TypeScript entrypoint for the Vercel AI SDK", async () => {
const user = userEvent.setup();
const { card } = renderCard();
await chooseSelectOption(user, screen.getByRole("combobox", { name: "Your agent framework" }), "Vercel AI SDK");
await user.click(screen.getByText("Set up manually"));
expect(card).toHaveTextContent("npm install ai @ai-sdk/openai-compatible @vercel/otel");
expect(card).toHaveTextContent("my_agent.ts");
expect(card).not.toHaveTextContent("opentelemetry-instrument python");
const quickstart = screen.getByText(/registerOTel\(\{/).textContent ?? "";
expect(quickstart).toContain("async function main()");
expect(quickstart.split("\n").filter((line) => /^(const|let) .*= await /.test(line))).toEqual([]);
});
it("hides the actions a read-only viewer cannot perform", () => {
const { card } = renderCard({ readOnly: true });
expect(screen.queryByRole("button", { name: "Send a test trace" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Generate tracing key" })).not.toBeInTheDocument();
expect(card).toHaveTextContent("ask a proxy admin for one");
expect(card).toHaveTextContent("OpenTelemetry (OTEL) endpoints");
});
it("offers a scoped tracing key only to callers allowed to set key routes", () => {
const { card } = renderCard({ canMintTracingKey: false });
expect(screen.queryByRole("button", { name: "Generate tracing key" })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Send a test trace" })).toBeVisible();
expect(card).toHaveTextContent("Use any LiteLLM virtual key you already have");
});
it("generates a tracing key that stays masked on screen but copies in full", async () => {
const user = userEvent.setup();
vi.mocked(apiClient.post).mockResolvedValue({ key: SECRET });
const { card } = renderCard();
await user.click(screen.getByRole("button", { name: "Generate tracing key" }));
expect(await screen.findByText("Your tracing key")).toBeVisible();
expect(apiClient.post).toHaveBeenCalledWith("/key/generate", {
accessToken: "sk-admin",
body: TRACING_KEY_REQUEST,
});
expect(TRACING_KEY_REQUEST.allowed_routes).toEqual(["/v1/traces"]);
expect(card).not.toHaveTextContent(SECRET);
expect(card).toHaveTextContent(maskSecret(SECRET));
await user.click(screen.getAllByRole("button", { name: "Copy" })[0]);
expect(copyToClipboard).toHaveBeenLastCalledWith(SECRET);
});
it("sends a test trace, waits for it to land, then opens it", async () => {
const user = userEvent.setup();
const summary = { trace_id: "abc", name: "weather_agent" } as Trace["summary"];
vi.mocked(sendOtlpTraceCall).mockResolvedValue(undefined);
vi.mocked(agentTraceCall).mockResolvedValue({ summary, agents: [], spans: [] } as unknown as Trace);
const { onOpenTrace } = renderCard();
await user.click(screen.getByRole("button", { name: "Send a test trace" }));
await user.click(await screen.findByRole("button", { name: /View trace/ }));
expect(sendOtlpTraceCall).toHaveBeenCalledOnce();
expect(vi.mocked(agentTraceCall).mock.calls[0][1]).toMatch(/^[0-9a-f]{32}$/);
expect(onOpenTrace).toHaveBeenCalledWith(summary);
});
it("reports a failed send instead of claiming success", async () => {
const user = userEvent.setup();
vi.mocked(sendOtlpTraceCall).mockRejectedValue(new Error("boom"));
renderCard();
await user.click(screen.getByRole("button", { name: "Send a test trace" }));
expect(await screen.findByText("Could not send the test trace.")).toBeVisible();
expect(agentTraceCall).not.toHaveBeenCalled();
expect(screen.queryByRole("button", { name: /View trace/ })).not.toBeInTheDocument();
});
it("guides proxy setup before agent setup and allows checking readiness", async () => {
const user = userEvent.setup();
const onCheck = vi.fn();
render(<TracingSetupCard detail="Agent tracing is not enabled" onCheck={onCheck} />);
const { card } = renderCard({ detail: "Agent tracing is not enabled", onCheck });
expect(screen.getByRole("heading", { name: "Enable tracing" })).toBeVisible();
expect(screen.getByTestId("tracing-setup-card")).toHaveTextContent("store: clickhouse");
expect(card).toHaveTextContent("store: clickhouse");
expect(screen.queryByRole("combobox", { name: "Your agent framework" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Send a test trace" })).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Check setup" }));
expect(onCheck).toHaveBeenCalledOnce();
expect(screen.getByText(/Tracing is still unavailable/)).toBeVisible();
@ -63,7 +197,13 @@ describe("setup snippets", () => {
expect(env).toContain("OTEL_EXPORTER_OTLP_ENDPOINT=http://proxy.test\n");
expect(env).toContain("OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf");
expect(env).not.toContain("/v1/traces");
expect(env).not.toContain("export LITELLM_API_KEY=");
expect(env).toContain("Bearer $LITELLM_API_KEY");
const withKey = tracingEnvSnippet("http://proxy.test", SECRET);
expect(withKey).toContain(`export LITELLM_TRACING_KEY=${SECRET}\n`);
expect(withKey).toContain('OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $LITELLM_TRACING_KEY"');
expect(withKey).not.toContain("LITELLM_API_KEY");
const prompt = codingAgentPrompt("http://proxy.test", { label: "LangChain", packages: "langchain" });
expect(prompt).toContain("base_url=http://proxy.test/v1");
expect(prompt).toContain("opentelemetry-distro opentelemetry-exporter-otlp-proto-http langchain");
@ -71,4 +211,14 @@ describe("setup snippets", () => {
expect(prompt).toContain("Lens > Traces");
expect(prompt).not.toMatch(/langsmith/i);
});
it("builds a shell-safe command for each coding agent", () => {
expect(codingAgentCommand("Claude Code", "it's")).toBe("claude 'it'\\''s'");
expect(codingAgentCommand("Codex", "go")).toBe("codex 'go'");
});
it("masks secrets but keeps a recognisable prefix and suffix", () => {
expect(maskSecret(SECRET)).toBe(`sk-ab${"•".repeat(16)}WXYZ`);
expect(maskSecret("short")).toBe("•••••");
});
});

View file

@ -1,6 +1,6 @@
"use client";
import { ArrowUpRight, Check, Copy, Loader2 } from "lucide-react";
import { ArrowRight, ArrowUpRight, Check, Copy, KeyRound, Loader2, Send } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/cva.config";
@ -8,22 +8,42 @@ import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { copyToClipboard } from "@/utils/dataUtils";
import previewImg from "../../../../public/assets/agent-traces-preview.png";
import anthropicLogo from "../../../../public/assets/logos/anthropic.svg";
import crewaiLogo from "../../../../public/assets/logos/crewai-color.svg";
import langchainLogo from "../../../../public/assets/logos/langchain.svg";
import langgraphLogo from "../../../../public/assets/logos/langgraph-color.svg";
import llamaindexLogo from "../../../../public/assets/logos/llamaindex-color.svg";
import openaiAgentsLogo from "../../../../public/assets/logos/openai-agents.svg";
import openaiLogo from "../../../../public/assets/logos/openai_small.svg";
import otelLogo from "../../../../public/assets/logos/opentelemetry.svg";
import pydanticAiLogo from "../../../../public/assets/logos/pydantic-ai-color.svg";
import { getProxyBaseUrl } from "../../networking";
import vercelLogo from "../../../../public/assets/logos/vercel.svg";
import { agentTraceCall, apiClient, getProxyBaseUrl, sendOtlpTraceCall } from "../../networking";
import { ActiveDot } from "./ActiveDot";
import { sampleTraceExport } from "./sampleTrace";
import { TracePreview } from "./TracePreview";
import type { TraceSummary } from "./traceTypes";
const COPIED_RESET_MS = 1500;
const DOCS_URL = "https://docs.litellm.ai/docs/proxy/lens";
const OTEL_BASE_PACKAGES = "opentelemetry-distro opentelemetry-exporter-otlp-proto-http";
const RUN_SNIPPET = "opentelemetry-instrument python my_agent.py";
const PY_RUN_SNIPPET = "opentelemetry-instrument python my_agent.py";
const TS_RUN_SNIPPET = "npx tsx my_agent.ts";
const SAMPLE_TRACE_POLL_MS = 1000;
export const TRACING_KEY_REQUEST = {
key_alias: "Agent tracing",
allowed_routes: ["/v1/traces"],
metadata: { purpose: "agent_tracing" },
} as const;
const SAMPLE_TRACE_POLL_ATTEMPTS = 15;
type Installer = "pip" | "uv";
type CodingAgent = "Claude Code" | "Codex";
const PY_INSTALL: Record<Installer, (packages: string) => string> = {
pip: (packages) => `pip install -U ${packages}`,
uv: (packages) => `uv add ${packages}`,
};
interface FrameworkGuide {
id: string;
@ -31,12 +51,50 @@ interface FrameworkGuide {
logo: string;
packages: string;
quickstart: string;
typescript?: boolean;
}
const FRAMEWORKS: readonly FrameworkGuide[] = [
{
id: "deep-agents",
label: "Deep Agents",
logo: langgraphLogo.src,
packages: "deepagents langchain-openai openinference-instrumentation-langchain",
quickstart: `from deepagents import create_deep_agent
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="claude-sonnet-4-5", base_url="{PROXY}/v1", api_key=os.environ["LITELLM_API_KEY"])
agent = create_deep_agent(model=llm, tools=[], system_prompt="You are a careful researcher.")
agent.invoke({"messages": [{"role": "user", "content": "What is LiteLLM?"}]})`,
},
{
id: "vercel-ai-sdk",
label: "Vercel AI SDK",
logo: vercelLogo.src,
typescript: true,
packages: "ai @ai-sdk/openai-compatible @vercel/otel @opentelemetry/api",
quickstart: `import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { registerOTel } from "@vercel/otel";
import { generateText } from "ai";
registerOTel({ serviceName: process.env.OTEL_SERVICE_NAME ?? "my-agent" });
const litellm = createOpenAICompatible({ name: "litellm", baseURL: "{PROXY}/v1", apiKey: process.env.LITELLM_API_KEY });
async function main() {
const { text } = await generateText({
model: litellm("claude-sonnet-4-5"),
prompt: "What is LiteLLM?",
experimental_telemetry: { isEnabled: true, functionId: "my_agent" },
});
console.log(text);
}
main();`,
},
{
id: "langgraph",
label: "LangGraph / Deep Agents",
label: "LangGraph",
logo: langgraphLogo.src,
packages: "langgraph langchain-openai openinference-instrumentation-langchain",
quickstart: `from langchain.agents import create_agent
@ -119,19 +177,23 @@ with tracer.start_as_current_span("my_agent", attributes=attrs):
},
];
const installPackages = (guide: Pick<FrameworkGuide, "packages">): string =>
[OTEL_BASE_PACKAGES, guide.packages].filter(Boolean).join(" ");
const installPackages = (guide: Pick<FrameworkGuide, "packages" | "typescript">): string =>
guide.typescript ? guide.packages : [OTEL_BASE_PACKAGES, guide.packages].filter(Boolean).join(" ");
/** The endpoint is the proxy base URL: OTLP exporters append /v1/traces themselves. */
export const tracingEnvSnippet = (proxyUrl: string): string =>
export const tracingEnvSnippet = (proxyUrl: string, tracingKey: string | null = null): string =>
[
...(tracingKey ? [`export LITELLM_TRACING_KEY=${tracingKey}`] : []),
`export OTEL_EXPORTER_OTLP_ENDPOINT=${proxyUrl}`,
"export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf",
'export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $LITELLM_API_KEY"',
`export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $${tracingKey ? "LITELLM_TRACING_KEY" : "LITELLM_API_KEY"}"`,
"export OTEL_SERVICE_NAME=my-agent",
].join("\n");
export const codingAgentPrompt = (proxyUrl: string, guide: Pick<FrameworkGuide, "label" | "packages">): string =>
export const codingAgentPrompt = (
proxyUrl: string,
guide: Pick<FrameworkGuide, "label" | "packages" | "typescript">,
): string =>
[
`Send this ${guide.label} project's OpenTelemetry traces to LiteLLM.`,
"",
@ -141,7 +203,9 @@ export const codingAgentPrompt = (proxyUrl: string, guide: Pick<FrameworkGuide,
" OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf",
' OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $LITELLM_API_KEY"',
" OTEL_SERVICE_NAME=<a short name for this agent>",
"3. Start the app through OTEL auto-instrumentation: opentelemetry-instrument <existing start command>.",
guide.typescript
? "3. Call registerOTel() from @vercel/otel at startup and pass experimental_telemetry: { isEnabled: true } to every AI SDK call."
: "3. Start the app through OTEL auto-instrumentation: opentelemetry-instrument <existing start command>.",
`4. Point every LLM client at LiteLLM: base_url=${proxyUrl}/v1, api key from LITELLM_API_KEY.`,
"5. Give each agent and subagent a name so runs are easy to read.",
"6. Run the agent once and confirm the run shows up in the LiteLLM UI under Lens > Traces.",
@ -149,6 +213,21 @@ export const codingAgentPrompt = (proxyUrl: string, guide: Pick<FrameworkGuide,
"Never hardcode the key. Read it from LITELLM_API_KEY.",
].join("\n");
const shellQuote = (value: string): string => `'${value.replaceAll("'", "'\\''")}'`;
export const codingAgentCommand = (agent: CodingAgent, prompt: string): string =>
`${agent === "Claude Code" ? "claude" : "codex"} ${shellQuote(prompt)}`;
export const maskSecret = (secret: string): string =>
secret.length > 10 ? `${secret.slice(0, 5)}${"•".repeat(16)}${secret.slice(-4)}` : "•".repeat(secret.length);
export const otlpEndpoints = (proxyUrl: string): readonly (readonly [string, string, boolean])[] => [
["Traces endpoint (POST)", `${proxyUrl}/v1/traces`, true],
["OTEL_EXPORTER_OTLP_ENDPOINT", proxyUrl, true],
["Auth header", "Authorization: Bearer <LiteLLM virtual key>", true],
["Protocol", "OTLP/HTTP, protobuf or JSON (gRPC not supported)", false],
];
export const PROXY_CONFIG_SNIPPET = [
"general_settings:",
" tracing:",
@ -157,7 +236,17 @@ export const PROXY_CONFIG_SNIPPET = [
"# env: CLICKHOUSE_URL (writer) and CLICKHOUSE_READER_URL (read-only user)",
].join("\n");
function CodeBlock({ code, tabs, wrap = false }: { code: string; tabs?: React.ReactNode; wrap?: boolean }) {
function CodeBlock({
code,
display = code,
tabs,
wrap = false,
}: {
code: string;
display?: string;
tabs?: React.ReactNode;
wrap?: boolean;
}) {
const [copied, setCopied] = useState(false);
const copy = async () => {
if (await copyToClipboard(code)) {
@ -184,7 +273,7 @@ function CodeBlock({ code, tabs, wrap = false }: { code: string; tabs?: React.Re
wrap ? "whitespace-pre-wrap" : "overflow-x-auto",
)}
>
<code>{code}</code>
<code>{display}</code>
</pre>
</div>
);
@ -198,10 +287,12 @@ function LineTabs<T extends string>({
value,
options,
onChange,
logos,
}: {
value: T;
options: T[];
onChange: (v: T) => void;
logos?: Partial<Record<T, string>>;
}) {
return (
<div role="tablist" className="flex h-9 items-end gap-3">
@ -213,12 +304,13 @@ function LineTabs<T extends string>({
aria-selected={value === option}
onClick={() => onChange(option)}
className={cn(
"-mb-px h-9 border-b-2 text-[12.5px]",
"-mb-px inline-flex h-9 items-center gap-1.5 border-b-2 text-[12.5px]",
value === option
? "border-foreground text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground",
)}
>
{logos?.[option] && <img src={logos[option]} alt="" className="size-3.5" />}
{option}
</button>
))}
@ -226,7 +318,7 @@ function LineTabs<T extends string>({
);
}
function Step({ title, children }: { title: string; children: React.ReactNode }) {
function Step({ title, children }: { title: React.ReactNode; children: React.ReactNode }) {
return (
<section className="mt-6">
<h3 className="mb-2 text-[13px] font-medium text-foreground">{title}</h3>
@ -235,6 +327,86 @@ function Step({ title, children }: { title: string; children: React.ReactNode })
);
}
type SendState =
| { kind: "idle" }
| { kind: "sending" }
| { kind: "waiting" }
| { kind: "ready"; trace: TraceSummary }
| { kind: "failed"; message: string };
const SEND_LABEL: Record<Exclude<SendState["kind"], "ready">, string> = {
idle: "Send a test trace",
sending: "Sending…",
waiting: "Waiting for it to arrive…",
failed: "Send a test trace",
};
const delay = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms));
async function waitForTrace(accessToken: string, traceId: string): Promise<TraceSummary | null> {
for (let attempt = 0; attempt < SAMPLE_TRACE_POLL_ATTEMPTS; attempt++) {
try {
return (await agentTraceCall(accessToken, traceId)).summary;
} catch {
await delay(SAMPLE_TRACE_POLL_MS);
}
}
return null;
}
function SendTestTrace({
accessToken,
onOpenTrace,
}: {
accessToken: string;
onOpenTrace: (trace: TraceSummary) => void;
}) {
const [state, setState] = useState<SendState>({ kind: "idle" });
const send = async () => {
setState({ kind: "sending" });
const sample = sampleTraceExport(Date.now());
try {
await sendOtlpTraceCall(accessToken, sample.body);
} catch {
setState({ kind: "failed", message: "Could not send the test trace." });
return;
}
setState({ kind: "waiting" });
const trace = await waitForTrace(accessToken, sample.traceId);
setState(
trace
? { kind: "ready", trace }
: { kind: "failed", message: "Sent, but it has not shown up yet. Check again in a moment." },
);
};
if (state.kind === "ready") {
return (
<div className="flex flex-wrap items-center gap-3">
<span role="status" className="inline-flex items-center gap-1 text-sm text-emerald-700 dark:text-emerald-400">
<Check aria-hidden="true" className="size-4" /> Test trace received
</span>
<Button onClick={() => onOpenTrace(state.trace)}>
View trace <ArrowRight aria-hidden="true" className="size-4" />
</Button>
</div>
);
}
const busy = state.kind === "sending" || state.kind === "waiting";
return (
<div className="flex flex-wrap items-center gap-3">
<Button onClick={() => void send()} disabled={busy}>
{busy ? (
<Loader2 aria-hidden="true" className="size-4 animate-spin" />
) : (
<Send aria-hidden="true" className="size-4" />
)}
{SEND_LABEL[state.kind]}
</Button>
{state.kind === "failed" && <p className="text-sm text-destructive">{state.message}</p>}
</div>
);
}
function TraceReceipt({
connected,
checked,
@ -273,159 +445,367 @@ function TraceReceipt({
);
}
function TracingKey({
accessToken,
tracingKey,
onCreated,
}: {
accessToken: string;
tracingKey: string | null;
onCreated: (key: string) => void;
}) {
const [creating, setCreating] = useState(false);
const [error, setError] = useState("");
const create = async () => {
setCreating(true);
setError("");
try {
const result = await apiClient.post<{ key?: string }>("/key/generate", {
accessToken,
body: TRACING_KEY_REQUEST,
});
if (!result.key) throw new Error("The proxy did not return the new key");
onCreated(result.key);
} catch (cause) {
setError(cause instanceof Error ? cause.message : "Could not create a key");
} finally {
setCreating(false);
}
};
if (tracingKey) {
return (
<div className="space-y-2">
<CodeBlock code={tracingKey} display={maskSecret(tracingKey)} tabs={<FileLabel>Your tracing key</FileLabel>} />
<p className="text-sm text-muted-foreground">
Hidden for safety. Copy copies the full key, and the environment step below includes it. This key can only
send traces, so your agent still needs its own key for model calls. Manage it under Virtual Keys as
&quot;Agent tracing&quot;.
</p>
</div>
);
}
return (
<div className="flex flex-wrap items-center gap-3">
<Button variant="outline" onClick={() => void create()} disabled={creating}>
{creating ? (
<Loader2 aria-hidden="true" className="size-4 animate-spin" />
) : (
<KeyRound aria-hidden="true" className="size-4" />
)}
Generate tracing key
</Button>
<span className="text-sm text-muted-foreground">Or use any existing LiteLLM virtual key.</span>
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
);
}
function EndpointValue({ value }: { value: string }) {
const [copied, setCopied] = useState(false);
const copy = async () => {
if (await copyToClipboard(value)) {
setCopied(true);
window.setTimeout(() => setCopied(false), COPIED_RESET_MS);
}
};
return (
<button
type="button"
onClick={() => void copy()}
aria-label={`Copy ${value}`}
className="group inline-flex min-w-0 items-center gap-2 text-left font-mono text-[12.5px] text-foreground"
>
<span className="break-all">{value}</span>
{copied ? (
<Check aria-hidden="true" className="size-3.5 shrink-0" />
) : (
<Copy aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground group-hover:text-foreground" />
)}
</button>
);
}
function Endpoints({ proxyUrl }: { proxyUrl: string }) {
return (
<section className="mt-6" aria-labelledby="otel-endpoints">
<h3 id="otel-endpoints" className="inline-flex items-center gap-2 text-sm font-medium">
<img src={otelLogo.src} alt="" className="size-5" />
OpenTelemetry (OTEL) endpoints
</h3>
<p className="mt-1 text-sm text-muted-foreground">
Point any OpenTelemetry exporter here. The framework guides below set these for you.
</p>
<dl className="mt-3 grid gap-x-8 gap-y-3 rounded-md border border-l-2 border-border border-l-trace-brand bg-muted/30 px-4 py-3 md:grid-cols-2">
{otlpEndpoints(proxyUrl).map(([label, value, copyable]) => (
<div key={label} className="min-w-0">
<dt className="text-[12px] text-muted-foreground">{label}</dt>
<dd className="mt-0.5">
{copyable ? <EndpointValue value={value} /> : <span className="text-[12.5px]">{value}</span>}
</dd>
</div>
))}
</dl>
</section>
);
}
const CODING_AGENT_LOGOS: Record<CodingAgent, string> = {
"Claude Code": anthropicLogo.src,
Codex: openaiLogo.src,
};
function setupTitle(enabled: boolean, connected: boolean) {
if (!enabled) return "Enable tracing";
return connected ? "Connect another agent" : "Connect your agent";
}
export function TracingSetupCard({
detail,
connected = false,
interface ConnectAgentProps {
accessToken: string;
onOpenTrace: (trace: TraceSummary) => void;
connected: boolean;
checked: boolean;
checking: boolean;
onCheck: () => void;
readOnly: boolean;
canMintTracingKey: boolean;
}
function EnableTracing({ checked, checking, onCheck }: { checked: boolean; checking: boolean; onCheck: () => void }) {
return (
<>
<Step title="Enable tracing on the proxy">
<p className="mb-3 text-sm leading-6 text-muted-foreground">
Set your ClickHouse writer and read-only reader URLs, add this to config.yaml, then restart the proxy. Ask
your proxy administrator if you don’t manage this deployment.
</p>
<CodeBlock code={PROXY_CONFIG_SNIPPET} tabs={<FileLabel>config.yaml</FileLabel>} />
<a
className="mt-3 inline-flex items-center gap-1 text-sm underline underline-offset-4"
href={`${DOCS_URL}#configure-an-existing-proxy`}
target="_blank"
rel="noreferrer"
>
ClickHouse and proxy setup <ArrowUpRight aria-hidden="true" className="size-3.5" />
</a>
</Step>
{checked && !checking && (
<p className="mt-4 text-sm text-muted-foreground">
Tracing is still unavailable. Check that the configuration was applied to this proxy and it has restarted.
</p>
)}
<Button className="mt-6" onClick={onCheck} disabled={checking}>
{checking && <Loader2 aria-hidden="true" className="size-4 animate-spin" />}
{checking ? "Checking…" : "Check setup"}
</Button>
</>
);
}
function ConnectAgent({
accessToken,
onOpenTrace,
connected,
checked,
checking,
onCheck,
checking = false,
}: {
detail: string | null;
connected?: boolean;
onCheck?: () => void;
checking?: boolean;
}) {
readOnly,
canMintTracingKey,
}: ConnectAgentProps) {
const proxyUrl = getProxyBaseUrl().replace(/\/$/, "");
const [framework, setFramework] = useState(FRAMEWORKS[0].id);
const [installer, setInstaller] = useState<Installer>("pip");
const [copied, setCopied] = useState(false);
const [checked, setChecked] = useState(false);
const [codingAgent, setCodingAgent] = useState<CodingAgent>("Claude Code");
const [tracingKey, setTracingKey] = useState<string | null>(null);
const guide = FRAMEWORKS.find((f) => f.id === framework) ?? FRAMEWORKS[0];
const packages = installPackages(guide);
const install = installer === "pip" ? `pip install -U ${packages}` : `uv add ${packages}`;
const install = guide.typescript ? `npm install ${packages}` : PY_INSTALL[installer](packages);
const quickstart = guide.quickstart.replace("{PROXY}", proxyUrl);
return (
<>
{!readOnly && (
<Step title="See it work in one click">
<p className="mb-3 text-sm leading-6 text-muted-foreground">
Send a small sample run (an agent, an LLM call and a tool call) to confirm tracing works end to end.
</p>
<SendTestTrace accessToken={accessToken} onOpenTrace={onOpenTrace} />
</Step>
)}
<Endpoints proxyUrl={proxyUrl} />
<div className="mt-6 space-y-2">
<label id="tracing-framework" className="text-sm font-medium">
Your agent framework
</label>
<Select
value={framework}
onValueChange={(value) => {
if (value) setFramework(value);
}}
>
<SelectTrigger aria-labelledby="tracing-framework" className="w-full">
<SelectValue>
<img src={guide.logo} alt="" className="size-4" />
{guide.label}
</SelectValue>
</SelectTrigger>
<SelectContent>
{FRAMEWORKS.map((f) => (
<SelectItem key={f.id} value={f.id}>
<img src={f.logo} alt="" className="size-4" />
{f.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Step title="Get a LiteLLM key">
{canMintTracingKey && !readOnly ? (
<TracingKey accessToken={accessToken} tracingKey={tracingKey} onCreated={setTracingKey} />
) : (
<p className="text-sm text-muted-foreground">
Use any LiteLLM virtual key you already have, or ask a proxy admin for one.
</p>
)}
</Step>
<Step
title={
<span className="inline-flex items-center gap-1.5">
Let <img src={anthropicLogo.src} alt="" className="size-3.5" /> Claude Code or
<img src={openaiLogo.src} alt="" className="size-3.5" /> Codex connect it
</span>
}
>
<p className="mb-3 text-sm leading-6 text-muted-foreground">
Run this in your agent’s project. It starts your coding agent with the setup task and reads the key from
LITELLM_API_KEY.
</p>
<CodeBlock
code={codingAgentCommand(codingAgent, codingAgentPrompt(proxyUrl, guide))}
wrap
tabs={
<LineTabs
value={codingAgent}
options={["Claude Code", "Codex"]}
onChange={setCodingAgent}
logos={CODING_AGENT_LOGOS}
/>
}
/>
</Step>
<details className="mt-6 border-y py-4">
<summary className="cursor-pointer text-sm font-medium">Set up manually</summary>
<Step title="Install dependencies">
<CodeBlock
code={install}
tabs={
guide.typescript ? (
<FileLabel>npm</FileLabel>
) : (
<LineTabs value={installer} options={["pip", "uv"]} onChange={setInstaller} />
)
}
/>
</Step>
<Step title="Configure environment">
<CodeBlock
code={tracingEnvSnippet(proxyUrl, tracingKey)}
display={tracingEnvSnippet(proxyUrl, tracingKey && maskSecret(tracingKey))}
tabs={<FileLabel>Shell</FileLabel>}
/>
</Step>
<Step title="Run your agent">
<p className="mb-2 text-sm text-muted-foreground">
Replace the example model with a model configured on your proxy.
</p>
<CodeBlock
code={guide.typescript ? quickstart : `import os\n\n${quickstart}`}
tabs={<FileLabel>{guide.typescript ? "my_agent.ts" : "my_agent.py"}</FileLabel>}
/>
<div className="mt-3">
<CodeBlock code={guide.typescript ? TS_RUN_SNIPPET : PY_RUN_SNIPPET} tabs={<FileLabel>Shell</FileLabel>} />
</div>
</Step>
</details>
<TraceReceipt connected={connected} checked={checked} checking={checking} onCheck={onCheck} />
{!connected && (
<section className="mt-6">
<h3 className="mb-2 text-sm font-medium">
Example run <span className="font-normal text-muted-foreground">(sample data, not your runs)</span>
</h3>
<TracePreview />
</section>
)}
</>
);
}
export function TracingSetupCard({
detail,
accessToken,
onOpenTrace,
connected = false,
onCheck,
checking = false,
readOnly = false,
canMintTracingKey = false,
}: {
detail: string | null;
accessToken: string;
onOpenTrace: (trace: TraceSummary) => void;
connected?: boolean;
onCheck?: () => void;
checking?: boolean;
readOnly?: boolean;
canMintTracingKey?: boolean;
}) {
const [checked, setChecked] = useState(false);
const enabled = detail === null;
const copyPrompt = async () => {
if (await copyToClipboard(codingAgentPrompt(proxyUrl, guide))) {
setCopied(true);
window.setTimeout(() => setCopied(false), COPIED_RESET_MS);
}
};
const check = () => {
setChecked(true);
onCheck?.();
};
return (
<div className="mx-auto w-full max-w-2xl py-6 pb-12" data-testid="tracing-setup-card">
<h2 className="text-xl font-semibold tracking-tight">{setupTitle(enabled, connected)}</h2>
<div className="w-full py-6 pb-12" data-testid="tracing-setup-card">
<div className="flex items-start gap-4">
<h2 className="text-xl font-semibold tracking-tight">{setupTitle(enabled, connected)}</h2>
<a
className="ml-auto inline-flex shrink-0 items-center gap-1 text-sm underline underline-offset-4"
href={DOCS_URL}
target="_blank"
rel="noreferrer"
>
Docs <ArrowUpRight aria-hidden="true" className="size-3.5" />
</a>
</div>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
{enabled
? "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."}
</p>
<p role="status" className="mt-4 flex items-center gap-2 text-sm">
{enabled && <Check aria-hidden="true" className="size-4 text-emerald-600" />}
{enabled && <ActiveDot />}
{enabled ? "Tracing enabled" : "Tracing is not enabled"}
</p>
{!enabled ? (
<>
<Step title="Enable tracing on the proxy">
<p className="mb-3 text-sm leading-6 text-muted-foreground">
Set your ClickHouse writer and read-only reader URLs, add this to config.yaml, then restart the proxy. Ask
your proxy administrator if you don’t manage this deployment.
</p>
<CodeBlock code={PROXY_CONFIG_SNIPPET} tabs={<FileLabel>config.yaml</FileLabel>} />
<a
className="mt-3 inline-flex items-center gap-1 text-sm underline underline-offset-4"
href={`${DOCS_URL}#configure-an-existing-proxy`}
target="_blank"
rel="noreferrer"
>
ClickHouse and proxy setup <ArrowUpRight aria-hidden="true" className="size-3.5" />
</a>
</Step>
{checked && !checking && (
<p className="mt-4 text-sm text-muted-foreground">
Tracing is still unavailable. Check that the configuration was applied to this proxy and it has restarted.
</p>
)}
<Button className="mt-6" onClick={check} disabled={checking}>
{checking && <Loader2 aria-hidden="true" className="size-4 animate-spin" />}
{checking ? "Checking…" : "Check setup"}
</Button>
</>
{enabled ? (
<ConnectAgent
accessToken={accessToken}
onOpenTrace={onOpenTrace}
connected={connected}
checked={checked}
checking={checking}
onCheck={check}
readOnly={readOnly}
canMintTracingKey={canMintTracingKey}
/>
) : (
<>
<div className="mt-6 space-y-2">
<label id="tracing-framework" className="text-sm font-medium">
Your agent framework
</label>
<Select
value={framework}
onValueChange={(value) => {
if (value) {
setFramework(value);
setCopied(false);
}
}}
>
<SelectTrigger aria-labelledby="tracing-framework" className="w-full">
<SelectValue>{guide.label}</SelectValue>
</SelectTrigger>
<SelectContent>
{FRAMEWORKS.map((f) => (
<SelectItem key={f.id} value={f.id}>
<img src={f.logo} alt="" className="size-4" />
{f.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Step title="Let your coding agent connect it">
<p className="mb-3 text-sm leading-6 text-muted-foreground">
Paste the setup prompt into Claude Code or Codex in your agent’s project. It uses your existing LiteLLM
key from the environment.
</p>
<Button variant="outline" onClick={() => void copyPrompt()}>
{copied ? (
<Check aria-hidden="true" className="size-4" />
) : (
<Copy aria-hidden="true" className="size-4" />
)}
{copied ? "Prompt copied" : "Copy setup prompt"}
</Button>
</Step>
<details className="mt-6 border-y py-4">
<summary className="cursor-pointer text-sm font-medium">Set up manually</summary>
<Step title="Install dependencies">
<CodeBlock
code={install}
tabs={<LineTabs value={installer} options={["pip", "uv"]} onChange={setInstaller} />}
/>
</Step>
<Step title="Configure environment">
<CodeBlock code={tracingEnvSnippet(proxyUrl)} tabs={<FileLabel>Shell</FileLabel>} />
</Step>
<Step title="Run your agent">
<p className="mb-2 text-sm text-muted-foreground">
Replace the example model with a model configured on your proxy.
</p>
<CodeBlock
code={`import os\n\n${guide.quickstart.replace("{PROXY}", proxyUrl)}`}
tabs={<FileLabel>my_agent.py</FileLabel>}
/>
<div className="mt-3">
<CodeBlock code={RUN_SNIPPET} tabs={<FileLabel>Shell</FileLabel>} />
</div>
</Step>
</details>
<TraceReceipt connected={connected} checked={checked} checking={checking} onCheck={check} />
<details className="mt-6 text-sm">
<summary className="cursor-pointer text-muted-foreground">See an example trace</summary>
<p className="my-3 text-muted-foreground">Example only. These are not your agent’s runs.</p>
<img
src={previewImg.src}
alt="Example agent trace with tool calls, inputs, and outputs"
className="w-full rounded-md border"
/>
</details>
</>
<EnableTracing checked={checked} checking={checking} onCheck={check} />
)}
</div>
);

View file

@ -0,0 +1,98 @@
{
"name": "deep_research_agent",
"input_preview": "Should we store OTEL agent spans in ClickHouse or Postgres at 50k spans/sec?",
"span_count": 126,
"duration_ms": 51385,
"rows": [
{
"id": "5e79f3b5b504985e",
"name": "deep_research_agent",
"type": "agent",
"model": null,
"depth": 0,
"start_offset_ms": 0,
"duration_ms": 51385.4,
"error": false
},
{
"id": "83451f3235847f6c",
"name": "model",
"type": "chain",
"model": null,
"depth": 1,
"start_offset_ms": 1,
"duration_ms": 9518.2,
"error": false
},
{
"id": "8a6a1c31940d07af",
"name": "ChatOpenAI",
"type": "llm",
"model": "claude-sonnet-4-5",
"depth": 2,
"start_offset_ms": 6,
"duration_ms": 9510.8,
"error": false
},
{
"id": "f6fdd164d528fee5",
"name": "tools",
"type": "chain",
"model": null,
"depth": 1,
"start_offset_ms": 9520,
"duration_ms": 2.7,
"error": false
},
{
"id": "1526d46d48d29a09",
"name": "write_file",
"type": "tool",
"model": null,
"depth": 2,
"start_offset_ms": 9521,
"duration_ms": 0.6,
"error": false
},
{
"id": "2697122e295d91b6",
"name": "tools",
"type": "chain",
"model": null,
"depth": 1,
"start_offset_ms": 9523,
"duration_ms": 35177.7,
"error": false
},
{
"id": "b2fb3a8f5a2fce01",
"name": "task",
"type": "tool",
"model": null,
"depth": 2,
"start_offset_ms": 9524,
"duration_ms": 35176.1,
"error": false
},
{
"id": "81499b492fd93f85",
"name": "researcher",
"type": "agent",
"model": null,
"depth": 3,
"start_offset_ms": 9524,
"duration_ms": 35175.3,
"error": false
},
{
"id": "64a2c760897f3310",
"name": "model",
"type": "chain",
"model": null,
"depth": 4,
"start_offset_ms": 9526,
"duration_ms": 6071.3,
"error": false
}
]
}

View file

@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { SAMPLE_TRACE_SERVICE, sampleTraceExport } from "./sampleTrace";
interface ExportedSpan {
traceId: string;
spanId: string;
parentSpanId?: string;
startTimeUnixNano: string;
endTimeUnixNano: string;
attributes: { key: string; value: { stringValue: string } }[];
}
const spansOf = (body: object): ExportedSpan[] =>
(body as { resourceSpans: { scopeSpans: { spans: ExportedSpan[] }[] }[] }).resourceSpans[0].scopeSpans[0].spans;
const operation = (span: ExportedSpan) =>
span.attributes.find((a) => a.key === "gen_ai.operation.name")?.value.stringValue;
describe("sampleTraceExport", () => {
it("uses OTLP/JSON hex ids and returns the trace id the spans carry", () => {
const { traceId, body } = sampleTraceExport(Date.now());
const spans = spansOf(body);
expect(traceId).toMatch(/^[0-9a-f]{32}$/);
for (const span of spans) {
expect(span.traceId).toBe(traceId);
expect(span.spanId).toMatch(/^[0-9a-f]{16}$/);
}
});
it("nests an LLM call and a tool call under one root agent", () => {
const spans = spansOf(sampleTraceExport(Date.now()).body);
const roots = spans.filter((s) => !s.parentSpanId);
expect(roots).toHaveLength(1);
expect(operation(roots[0])).toBe("invoke_agent");
const children = spans.filter((s) => s.parentSpanId === roots[0].spanId);
expect(children.map(operation).sort()).toEqual(["chat", "execute_tool"]);
});
it("ends at the given time and tags the sample service", () => {
const now = 1_790_000_000_000;
const { body } = sampleTraceExport(now);
const root = spansOf(body).find((s) => !s.parentSpanId)!;
expect(BigInt(root.endTimeUnixNano)).toBe(BigInt(now) * BigInt(1_000_000));
expect(BigInt(root.startTimeUnixNano)).toBeLessThan(BigInt(root.endTimeUnixNano));
expect(JSON.stringify(body)).toContain(SAMPLE_TRACE_SERVICE);
});
});

View file

@ -0,0 +1,95 @@
const NS_PER_MS = BigInt(1_000_000);
export const SAMPLE_TRACE_SERVICE = "litellm-sample-agent";
const randomHexId = (bytes: number): string =>
Array.from(crypto.getRandomValues(new Uint8Array(bytes)), (b) => b.toString(16).padStart(2, "0")).join("");
const str = (key: string, value: string) => ({ key, value: { stringValue: value } });
const messages = (role: string, content: string): string => JSON.stringify([{ role, content }]);
interface SampleSpan {
name: string;
parent: string | null;
startMs: number;
endMs: number;
attributes: ReturnType<typeof str>[];
}
export function sampleTraceExport(nowMs: number): { traceId: string; body: object } {
const traceId = randomHexId(16);
const agentId = randomHexId(8);
const question = "What is the weather in San Francisco?";
const answer = "It is 18°C and sunny in San Francisco.";
const spans: readonly (SampleSpan & { id: string })[] = [
{
id: agentId,
name: "weather_agent",
parent: null,
startMs: 0,
endMs: 2400,
attributes: [
str("gen_ai.operation.name", "invoke_agent"),
str("gen_ai.agent.name", "weather_agent"),
str("gen_ai.input.messages", messages("user", question)),
str("gen_ai.output.messages", messages("assistant", answer)),
],
},
{
id: randomHexId(8),
name: "chat sample-model",
parent: agentId,
startMs: 100,
endMs: 1500,
attributes: [
str("gen_ai.operation.name", "chat"),
str("gen_ai.agent.name", "weather_agent"),
str("gen_ai.request.model", "sample-model"),
str("gen_ai.input.messages", messages("user", question)),
str("gen_ai.output.messages", messages("assistant", "Calling get_weather(city=San Francisco)")),
],
},
{
id: randomHexId(8),
name: "get_weather",
parent: agentId,
startMs: 1600,
endMs: 2300,
attributes: [
str("gen_ai.operation.name", "execute_tool"),
str("gen_ai.agent.name", "weather_agent"),
str("gen_ai.tool.call.arguments", JSON.stringify({ city: "San Francisco" })),
str("gen_ai.tool.call.result", JSON.stringify({ temp_c: 18, sky: "sunny" })),
],
},
];
const startNs = BigInt(nowMs - 2400) * NS_PER_MS;
const toNs = (ms: number): string => String(startNs + BigInt(ms) * NS_PER_MS);
return {
traceId,
body: {
resourceSpans: [
{
resource: { attributes: [str("service.name", SAMPLE_TRACE_SERVICE)] },
scopeSpans: [
{
scope: { name: "litellm-ui-sample" },
spans: spans.map((s) => ({
traceId,
spanId: s.id,
...(s.parent ? { parentSpanId: s.parent } : {}),
name: s.name,
kind: 1,
startTimeUnixNano: toNs(s.startMs),
endTimeUnixNano: toNs(s.endMs),
attributes: s.attributes,
status: { code: 1 },
})),
},
],
},
],
},
};
}