diff --git a/litellm-rust/crates/traces/src/normalize/messages.rs b/litellm-rust/crates/traces/src/normalize/messages.rs index 0aa5fde2754..48d8d17cb00 100644 --- a/litellm-rust/crates/traces/src/normalize/messages.rs +++ b/litellm-rust/crates/traces/src/normalize/messages.rs @@ -422,11 +422,10 @@ pub(super) fn event_payload(events: &[(bool, Value)], output: bool) -> Option String { messages .iter() - .rev() .find(|message| message.role == "user" && !message.content.is_empty()) .map_or("", |message| message.content.as_str()) .chars() @@ -434,7 +433,7 @@ pub(super) fn preview(messages: &[Message]) -> String { .collect() } -/// The latest user message when `input` is a conversation, else the input itself. +/// The first user message when `input` is a conversation, else the input itself. pub(super) fn input_preview(input: &str) -> String { match serde_json::from_str::(input) .ok() @@ -597,7 +596,7 @@ mod tests { use serde_json::Value; #[rstest] - #[case::latest_user(r#"{"messages":[{"role":"user","content":"first"},{"role":"assistant","content":"reply"},{"role":"user","content":"last"}]}"#, Some("last"))] + #[case::first_user(r#"{"messages":[{"role":"user","content":"first"},{"role":"assistant","content":"reply"},{"role":"user","content":"last"}]}"#, Some("first"))] #[case::malformed("not-json", None)] #[case::missing("{}", None)] #[case::not_messages(r#"{"messages":[{"role":"user"}]}"#, None)] diff --git a/litellm-rust/crates/traces/tests/normalization_formats.rs b/litellm-rust/crates/traces/tests/normalization_formats.rs index e8b11ece64a..30b3a81be7e 100644 --- a/litellm-rust/crates/traces/tests/normalization_formats.rs +++ b/litellm-rust/crates/traces/tests/normalization_formats.rs @@ -502,6 +502,25 @@ fn existing_formats_win_over_new_formats(span: Span, #[case] kind: &str) { assert_eq!(decoded.normalized.observation_type, ObservationType::Llm); } +#[rstest] +fn input_preview_is_the_first_user_message(span: Span) { + let conversation = json!([ + {"role": "system", "content": "sys"}, + {"role": "user", "content": "initial question"}, + {"role": "assistant", "content": "answer"}, + {"role": "user", "content": "follow up"}, + ]) + .to_string(); + let decoded = decode( + span, + "custom", + &[("gen_ai.input.messages", &conversation)], + vec![], + ) + .unwrap(); + assert_eq!(decoded.normalized.input_preview, "initial question"); +} + #[rstest] #[case::messages(&[("gen_ai.input.messages", r#"[{"role":"user","content":"modern"}]"#)], "modern")] #[case::indexed(&[("gen_ai.prompt.0.role", "user"), ("gen_ai.prompt.0.content", "indexed")], "indexed")] diff --git a/litellm-rust/crates/traces/tests/normalize.rs b/litellm-rust/crates/traces/tests/normalize.rs index bfff82fcdd7..95574b73457 100644 --- a/litellm-rust/crates/traces/tests/normalize.rs +++ b/litellm-rust/crates/traces/tests/normalize.rs @@ -82,7 +82,7 @@ fn assert_invariants(span: &DecodedSpan) { } assert!(normalized.input_preview.chars().count() <= 240); if let Ok(Value::Array(messages)) = serde_json::from_str(&normalized.input) { - let user = messages.iter().rev().find_map(|message| { + let user = messages.iter().find_map(|message| { (message.get("role")?.as_str()? == "user") .then(|| { message diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx index 5cb6e132cd2..eee9731a219 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx @@ -165,6 +165,26 @@ describe("AgentTracesTable cost cell", () => { }); }); +describe("AgentTracesTable input cell", () => { + it("shows the whole preview on one line instead of only its first line", () => { + const template = (traceList as TracePage).data[0] as TraceSummary; + renderWithProviders( + inList( + , + ), + ); + expect(screen.getByText("CURRENT USER REQUEST: - add feedback to Lens")).toBeInTheDocument(); + }); +}); + describe("AgentTracesTable column picker", () => { const runs = (traceList as TracePage).data as TraceSummary[]; const renderRuns = () => diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx index 8eeb25ec153..0494a475cdc 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx @@ -69,7 +69,7 @@ function CostCell({ run }: { run: TraceSummary }) { ); } -const firstLine = (text: string): string => text.split("\n")[0] ?? text; +const singleLine = (text: string): string => text.replace(/\s+/g, " ").trim(); const runKey = (run: TraceSummary): string => run.trace_ref || run.trace_id; const PREFETCH_MARGIN = "0px 0px 480px 0px"; @@ -96,7 +96,7 @@ function InputCell({ run }: { run: TraceSummary }) { return (
- {firstLine(previewText(run.input_preview)) || traceDisplayName(run)} + {singleLine(previewText(run.input_preview)) || traceDisplayName(run)} {run.resolution_limited && (