-
{setupTitle(enabled, connected)}
+
+
+
{setupTitle(enabled, connected)}
+
+ Docs
+
+
{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."}
- {enabled && }
+ {enabled && }
{enabled ? "Tracing enabled" : "Tracing is not enabled"}
-
- {!enabled ? (
- <>
-
-
- 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.
-
- config.yaml} />
-
- ClickHouse and proxy setup
-
-
- {checked && !checking && (
-
- Tracing is still unavailable. Check that the configuration was applied to this proxy and it has restarted.
-
- )}
-
- {checking && }
- {checking ? "Checking…" : "Check setup"}
-
- >
+ {enabled ? (
+
) : (
- <>
-
-
- Your agent framework
-
-
{
- if (value) {
- setFramework(value);
- setCopied(false);
- }
- }}
- >
-
- {guide.label}
-
-
- {FRAMEWORKS.map((f) => (
-
-
- {f.label}
-
- ))}
-
-
-
-
-
- Paste the setup prompt into Claude Code or Codex in your agent’s project. It uses your existing LiteLLM
- key from the environment.
-
- void copyPrompt()}>
- {copied ? (
-
- ) : (
-
- )}
- {copied ? "Prompt copied" : "Copy setup prompt"}
-
-
-
- Set up manually
-
- }
- />
-
-
- Shell} />
-
-
-
- Replace the example model with a model configured on your proxy.
-
- my_agent.py}
- />
-
- Shell} />
-
-
-
-
-
- See an example trace
- Example only. These are not your agent’s runs.
-
-
- >
+
)}
);
diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/previewTrace.json b/ui/litellm-dashboard/src/components/view_logs/TraceView/previewTrace.json
new file mode 100644
index 00000000000..7da2a5b454d
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/previewTrace.json
@@ -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
+ }
+ ]
+}
diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/sampleTrace.test.ts b/ui/litellm-dashboard/src/components/view_logs/TraceView/sampleTrace.test.ts
new file mode 100644
index 00000000000..56ac75a5ea7
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/sampleTrace.test.ts
@@ -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);
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/sampleTrace.ts b/ui/litellm-dashboard/src/components/view_logs/TraceView/sampleTrace.ts
new file mode 100644
index 00000000000..6dc6f3262b7
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/sampleTrace.ts
@@ -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
[];
+}
+
+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 },
+ })),
+ },
+ ],
+ },
+ ],
+ },
+ };
+}