mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(lens): copy supported trace and span content requests (#44902)
This commit is contained in:
parent
2cecc7e33f
commit
8f041a8f06
4 changed files with 53 additions and 6 deletions
31
ui/litellm-dashboard/src/components/lens/traces/api.test.ts
Normal file
31
ui/litellm-dashboard/src/components/lens/traces/api.test.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { agentHandoffText } from "./api";
|
||||
import { getProxyBaseUrl } from "../../networking";
|
||||
|
||||
vi.mock("../../networking", () => ({ getProxyBaseUrl: vi.fn(() => "https://proxy.test/root/") }));
|
||||
|
||||
describe("agent handoff", () => {
|
||||
it("requests bounded trace pages and explains how to retrieve captured content", () => {
|
||||
const text = agentHandoffText("trace-one", null, "source-one");
|
||||
expect(text).toContain("'https://proxy.test/root/v1/traces/trace-one?trace_ref=source-one&page_size=200'");
|
||||
expect(text).toContain("Follow next_cursor");
|
||||
expect(text).toContain("/v1/traces/trace-one/spans/{span_id}");
|
||||
expect(text).toContain("using the same trace_ref");
|
||||
});
|
||||
|
||||
it("selects one span and keeps reserved characters inside their original parameter", () => {
|
||||
const text = agentHandoffText("trace/one", "step?two", "source&cursor=other");
|
||||
expect(text).toContain(
|
||||
"'https://proxy.test/root/v1/traces/trace%2Fone/spans/step%3Ftwo?trace_ref=source%26cursor%3Dother'",
|
||||
);
|
||||
expect(text).toContain("this step's captured input, output, and attributes");
|
||||
});
|
||||
|
||||
it("quotes the proxy URL as shell data", () => {
|
||||
vi.mocked(getProxyBaseUrl).mockReturnValueOnce("https://proxy.test/a'b/$(printf injected)");
|
||||
expect(agentHandoffText("trace-one", "span-one")).toContain(
|
||||
"'https://proxy.test/a'\"'\"'b/$(printf injected)/v1/traces/trace-one/spans/span-one'",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -11,6 +11,8 @@ import {
|
|||
} from "../../networking";
|
||||
import type {
|
||||
SpanDetail,
|
||||
SpanQuery,
|
||||
TraceDetailQuery,
|
||||
SpanErrorPage,
|
||||
Trace,
|
||||
TraceListQuery,
|
||||
|
|
@ -48,9 +50,23 @@ export interface TracesApi {
|
|||
|
||||
/** A one-liner Claude Code / Codex can run to read the trace. */
|
||||
export const agentHandoffText = (traceId: string, spanId?: string | null, traceRef?: string): string => {
|
||||
const url = `${getProxyBaseUrl().replace(/\/$/, "")}/v1/traces/${traceId}?format=md${spanId ? `&span_id=${spanId}` : ""}${traceRef ? `&trace_ref=${traceRef}` : ""}`;
|
||||
const base = `${getProxyBaseUrl().replace(/\/$/, "")}/v1/traces/${encodeURIComponent(traceId)}`;
|
||||
const path = spanId ? `${base}/spans/${encodeURIComponent(spanId)}` : base;
|
||||
const query = spanId
|
||||
? ({ trace_ref: traceRef } satisfies SpanQuery)
|
||||
: ({ trace_ref: traceRef, page_size: 200 } satisfies TraceDetailQuery);
|
||||
const params = new URLSearchParams(
|
||||
Object.entries(query)
|
||||
.filter(([, value]) => value !== undefined)
|
||||
.map(([key, value]) => [key, String(value)]),
|
||||
).toString();
|
||||
const url = params ? `${path}?${params}` : path;
|
||||
const quotedUrl = "'" + url.replaceAll("'", "'\"'\"'") + "'";
|
||||
const what = spanId ? "this step of a LiteLLM agent trace" : "this LiteLLM agent trace";
|
||||
return `Read ${what}, explain what happened, and investigate any issues:\ncurl -s -H "Authorization: Bearer $LITELLM_API_KEY" "${url}"`;
|
||||
const guidance = spanId
|
||||
? "The JSON response contains this step's captured input, output, and attributes. Report missing content and capture warnings explicitly."
|
||||
: `The JSON response contains span summaries. Follow next_cursor by adding cursor to this URL until it is null, preserving trace_ref and page_size. Fetch captured content at ${base}/spans/{span_id}, using the same trace_ref. Report missing content and capture warnings explicitly.`;
|
||||
return `Read ${what}, explain what happened, and investigate any issues:\ncurl --fail-with-body -sS -H "Authorization: Bearer $LITELLM_API_KEY" ${quotedUrl}\n${guidance}`;
|
||||
};
|
||||
|
||||
export function liveTracesApi(accessToken: string): TracesApi {
|
||||
|
|
|
|||
|
|
@ -492,8 +492,8 @@ describe("RunView", () => {
|
|||
|
||||
await user.click(await screen.findByRole("button", { name: /copy for agent/i }));
|
||||
expect(copyToClipboard).toHaveBeenCalledWith(agentHandoffText(research.summary.trace_id), "Command copied");
|
||||
expect(agentHandoffText("t1")).toContain('"http://proxy.test/v1/traces/t1?format=md"');
|
||||
expect(agentHandoffText("t1", "s1")).toContain("&span_id=s1");
|
||||
expect(agentHandoffText("t1")).toContain("http://proxy.test/v1/traces/t1?page_size=200");
|
||||
expect(agentHandoffText("t1", "s1")).toContain("/v1/traces/t1/spans/s1");
|
||||
});
|
||||
|
||||
it("copies a link that reopens just this run", async () => {
|
||||
|
|
|
|||
|
|
@ -345,14 +345,14 @@ describe("DetailPane", () => {
|
|||
expect(pane).toHaveTextContent("TimeoutError('slow')");
|
||||
});
|
||||
|
||||
it("'Copy step' copies a curl for just this span as Markdown", async () => {
|
||||
it("'Copy step' copies a curl for this span's captured content", async () => {
|
||||
const user = userEvent.setup();
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true });
|
||||
renderPane(spanRow(llm));
|
||||
await user.click(screen.getByRole("button", { name: "Copy step" }));
|
||||
await waitFor(() => expect(writeText).toHaveBeenCalled());
|
||||
expect(writeText.mock.calls[0][0]).toContain("http://proxy.test/v1/traces/t1?format=md&span_id=llm1");
|
||||
expect(writeText.mock.calls[0][0]).toContain("http://proxy.test/v1/traces/t1/spans/llm1");
|
||||
});
|
||||
|
||||
it("renders the assistant tool call as a card and expands a long argument on click", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue