diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json
index 107f66b8f1a..2602b8fa7ad 100644
--- a/ui/litellm-dashboard/eslint-suppressions.json
+++ b/ui/litellm-dashboard/eslint-suppressions.json
@@ -4142,11 +4142,6 @@
"count": 1
}
},
- "src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts": {
- "no-nested-ternary": {
- "count": 1
- }
- },
"src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts": {
"react-hooks/immutability": {
"count": 2
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.test.tsx
index e7295ed7a72..104c421acbf 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.test.tsx
@@ -76,6 +76,126 @@ describe("PrettyMessagesView", () => {
expect(modelElements.length).toBeGreaterThanOrEqual(1);
});
+ it("renders a Responses API log, whose body uses input/output instead of messages/choices", () => {
+ const request = {
+ model: "gpt-5.6",
+ input: [{ role: "user", content: "Reply with exactly: hello from responses api" }],
+ };
+ const response = {
+ output: [
+ {
+ id: "msg_070989277645d4ae",
+ role: "assistant",
+ type: "message",
+ status: "completed",
+ content: [{ text: "hello from responses api", type: "output_text", annotations: [] }],
+ },
+ ],
+ };
+
+ render();
+ expect(screen.getByText("Reply with exactly: hello from responses api")).toBeInTheDocument();
+ expect(screen.getByText("hello from responses api")).toBeInTheDocument();
+ expect(screen.queryByText("No response data available")).not.toBeInTheDocument();
+ });
+
+ it("renders a Responses API tool call, whose output item is a function_call", () => {
+ const request = {
+ model: "gpt-5.6",
+ input: [{ role: "user", content: "What is the weather in San Francisco? Use the tool." }],
+ };
+ const response = {
+ output: [
+ {
+ id: "fc_08edf6c2312f1485",
+ name: "get_weather",
+ type: "function_call",
+ status: "completed",
+ call_id: "call_AtO0J9eNy5jgECXzBicMJM8W",
+ arguments: '{"city":"San Francisco"}',
+ },
+ ],
+ };
+
+ render();
+ expect(screen.getByText("What is the weather in San Francisco? Use the tool.")).toBeInTheDocument();
+ expect(screen.getByText("get_weather")).toBeInTheDocument();
+ expect(screen.queryByText("No response data available")).not.toBeInTheDocument();
+ });
+
+ it("renders instructions as the system turn and a bare string input", () => {
+ const request = { model: "gpt-5.6", instructions: "You are terse.", input: "Say A" };
+ const response = {
+ output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text: "A" }] }],
+ };
+
+ render();
+ expect(screen.getByText("You are terse.")).toBeInTheDocument();
+ expect(screen.getByText("Say A")).toBeInTheDocument();
+ expect(screen.getByText("A")).toBeInTheDocument();
+ });
+
+ it("skips reasoning output items rather than rendering them as empty turns", () => {
+ const request = { input: [{ role: "user", content: "Think then answer" }] };
+ const response = {
+ output: [
+ { type: "reasoning", id: "rs_1", summary: [] },
+ { type: "message", role: "assistant", content: [{ type: "output_text", text: "answered" }] },
+ ],
+ };
+
+ render();
+ expect(screen.getByText("answered")).toBeInTheDocument();
+ expect(screen.queryByText("No response data available")).not.toBeInTheDocument();
+ });
+
+ it("renders a Responses API follow-up turn carrying a prior function_call and its output", () => {
+ const request = {
+ input: [
+ { role: "user", content: "What is the weather in San Francisco? Use the tool." },
+ {
+ type: "function_call",
+ name: "get_weather",
+ call_id: "call_AtO0J9eNy5jgECXzBicMJM8W",
+ arguments: '{"city":"San Francisco"}',
+ },
+ { type: "function_call_output", call_id: "call_AtO0J9eNy5jgECXzBicMJM8W", output: '{"temp":18}' },
+ ],
+ };
+ const response = {
+ output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text: "It is 18 degrees." }] }],
+ };
+
+ render();
+ expect(screen.getByText("It is 18 degrees.")).toBeInTheDocument();
+ expect(screen.getByText('{"temp":18}')).toBeInTheDocument();
+ expect(screen.getByText("TOOL")).toBeInTheDocument();
+ });
+
+ it("maps the developer and legacy function roles onto the roles the drawer renders", () => {
+ const request = {
+ messages: [
+ { role: "developer", content: "Stay terse." },
+ { role: "user", content: "Weather?" },
+ { role: "function", name: "get_weather", content: '{"temp":18}' },
+ ],
+ };
+ const response = { choices: [{ message: { role: "assistant", content: "18 degrees." } }] };
+
+ render();
+ expect(screen.getByText("Stay terse.")).toBeInTheDocument();
+ expect(screen.getByText("TOOL")).toBeInTheDocument();
+ expect(screen.queryByText("FUNCTION")).not.toBeInTheDocument();
+ });
+
+ it("still reports missing output when a Responses API log has an empty output array", () => {
+ const request = { input: [{ role: "user", content: "Hello" }] };
+
+ render();
+ expect(screen.getByText("Hello")).toBeInTheDocument();
+ expect(screen.getByText("No response data available")).toBeInTheDocument();
+ });
+
it("should render standard view when response has results but no realtime events", () => {
const request = {
messages: [{ role: "user", content: "Test" }],
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesTypes.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesTypes.ts
index da5c492e60f..463ba65d6ff 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesTypes.ts
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesTypes.ts
@@ -2,17 +2,29 @@
* Type definitions for pretty messages view
*/
+export type MessageRole = "system" | "user" | "assistant" | "tool";
+
export interface ParsedMessage {
- role: "system" | "user" | "assistant" | "tool";
+ role: MessageRole;
content: string;
toolCalls?: ToolCall[];
toolCallId?: string;
}
+export type RequestPayload =
+ | { kind: "chat"; messages: readonly unknown[] }
+ | { kind: "responses"; instructions: string; input: string | readonly unknown[] }
+ | { kind: "unknown" };
+
+export type ResponsePayload =
+ | { kind: "chat"; choices: readonly unknown[] }
+ | { kind: "responses"; output: readonly unknown[] }
+ | { kind: "unknown" };
+
export interface ToolCall {
id: string;
name: string;
- arguments: Record;
+ arguments: Record;
}
export interface ParsedMessages {
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts
index 09b8f551c1d..1f73da1d30e 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts
@@ -2,7 +2,15 @@
* Utility functions for parsing and formatting messages for pretty view
*/
-import { ParsedMessage, ParsedMessages, RoleStyle } from "./prettyMessagesTypes";
+import {
+ MessageRole,
+ ParsedMessage,
+ ParsedMessages,
+ RequestPayload,
+ ResponsePayload,
+ RoleStyle,
+ ToolCall,
+} from "./prettyMessagesTypes";
/**
* Role color styles for message cards - minimal, professional design
@@ -35,102 +43,188 @@ export const ROLE_STYLES: Record = {
},
};
+type UnknownRecord = Record;
+
+const isRecord = (value: unknown): value is UnknownRecord =>
+ typeof value === "object" && value !== null && !Array.isArray(value);
+
+const asString = (value: unknown): string => (typeof value === "string" ? value : "");
+
+const ROLES: readonly MessageRole[] = ["system", "user", "assistant", "tool"];
+
+const toRole = (value: unknown, fallback: MessageRole): MessageRole => {
+ if (value === "developer") return "system";
+ if (value === "function") return "tool";
+ return ROLES.includes(value as MessageRole) ? (value as MessageRole) : fallback;
+};
+
+const classifyRequest = (request: unknown): RequestPayload => {
+ if (Array.isArray(request)) return { kind: "chat", messages: request };
+ if (!isRecord(request)) return { kind: "unknown" };
+ if (Array.isArray(request.messages)) return { kind: "chat", messages: request.messages };
+ const { input } = request;
+ if (typeof input === "string" || Array.isArray(input)) {
+ return { kind: "responses", instructions: asString(request.instructions), input };
+ }
+ return { kind: "unknown" };
+};
+
+const classifyResponse = (response: unknown): ResponsePayload => {
+ if (!isRecord(response)) return { kind: "unknown" };
+ if (Array.isArray(response.choices)) return { kind: "chat", choices: response.choices };
+ if (Array.isArray(response.output)) return { kind: "responses", output: response.output };
+ return { kind: "unknown" };
+};
+
/**
* Parse request messages and response message from log data
*/
-export const parseMessages = (request: any, response: any): ParsedMessages => {
- // Parse request messages. `request` is either the raw request body
- // ({ messages: [...] }) or, when prompts come from cold storage, the bare
- // messages array itself.
- const requestMessages: ParsedMessage[] = [];
+export const parseMessages = (request: unknown, response: unknown): ParsedMessages => ({
+ requestMessages: parseRequestMessages(classifyRequest(request)),
+ responseMessage: parseResponseMessage(classifyResponse(response)),
+});
- const requestMessageList = Array.isArray(request)
- ? request
- : Array.isArray(request?.messages)
- ? request.messages
- : [];
-
- requestMessageList.forEach((msg: any) => {
- requestMessages.push({
- role: msg.role || "user",
- content: parseMessageContent(msg.content),
- toolCallId: msg.tool_call_id,
- });
- });
-
- // Parse response message
- let responseMessage: ParsedMessage | null = null;
- const responseMsg = response?.choices?.[0]?.message;
-
- if (responseMsg) {
- responseMessage = {
- role: responseMsg.role || "assistant",
- content: responseMsg.content || "",
- toolCalls: parseToolCalls(responseMsg.tool_calls),
- };
+const parseRequestMessages = (payload: RequestPayload): ParsedMessage[] => {
+ switch (payload.kind) {
+ case "chat":
+ return payload.messages.map(parseChatMessage);
+ case "responses": {
+ const instructions: ParsedMessage[] = payload.instructions
+ ? [{ role: "system", content: payload.instructions }]
+ : [];
+ const input: ParsedMessage[] =
+ typeof payload.input === "string"
+ ? [{ role: "user", content: payload.input }]
+ : payload.input.flatMap(parseResponsesInputItem);
+ return [...instructions, ...input];
+ }
+ case "unknown":
+ return [];
}
-
- return { requestMessages, responseMessage };
};
+const parseResponseMessage = (payload: ResponsePayload): ParsedMessage | null => {
+ switch (payload.kind) {
+ case "chat": {
+ const choice = payload.choices[0];
+ const message = isRecord(choice) ? choice.message : undefined;
+ if (!isRecord(message)) return null;
+ return {
+ role: toRole(message.role, "assistant"),
+ content: parseMessageContent(message.content),
+ toolCalls: parseChatToolCalls(message.tool_calls),
+ };
+ }
+ case "responses": {
+ const content = payload.output
+ .filter((item): item is UnknownRecord => isRecord(item) && item.type === "message")
+ .map((item) => parseMessageContent(item.content))
+ .filter((text) => text.length > 0)
+ .join("\n");
+ const toolCalls = payload.output.filter(isResponsesFunctionCall).map(parseResponsesFunctionCall);
+ if (content.length === 0 && toolCalls.length === 0) return null;
+ return { role: "assistant", content, toolCalls: toolCalls.length > 0 ? toolCalls : undefined };
+ }
+ case "unknown":
+ return null;
+ }
+};
+
+const parseChatMessage = (message: unknown): ParsedMessage => {
+ if (!isRecord(message)) return { role: "user", content: parseMessageContent(message) };
+ return {
+ role: toRole(message.role, "user"),
+ content: parseMessageContent(message.content),
+ toolCalls: parseChatToolCalls(message.tool_calls),
+ toolCallId: typeof message.tool_call_id === "string" ? message.tool_call_id : undefined,
+ };
+};
+
+const parseResponsesInputItem = (item: unknown): ParsedMessage[] => {
+ if (typeof item === "string") return [{ role: "user", content: item }];
+ if (!isRecord(item)) return [];
+ if (item.type === "function_call") {
+ return [{ role: "assistant", content: "", toolCalls: [parseResponsesFunctionCall(item)] }];
+ }
+ if (item.type === "function_call_output") {
+ return [{ role: "tool", content: parseMessageContent(item.output), toolCallId: asString(item.call_id) }];
+ }
+ if (item.type === "reasoning") return [];
+ if ("role" in item || "content" in item) {
+ return [{ role: toRole(item.role, "user"), content: parseMessageContent(item.content) }];
+ }
+ return [];
+};
+
+const isResponsesFunctionCall = (item: unknown): item is UnknownRecord =>
+ isRecord(item) && item.type === "function_call";
+
+const parseResponsesFunctionCall = (item: UnknownRecord): ToolCall => ({
+ id: asString(item.call_id) || asString(item.id),
+ name: asString(item.name) || "unknown",
+ arguments: parseToolArguments(item.arguments),
+});
+
/**
* Parse message content - handle strings and content arrays (for vision, etc.)
*/
-const parseMessageContent = (content: any): string => {
- if (typeof content === "string") {
- return content;
- }
-
- if (Array.isArray(content)) {
- // Handle content arrays (vision API format)
- return content
- .map((item) => {
- if (typeof item === "string") return item;
- if (item.type === "text") return item.text;
- if (item.type === "image_url") return "[Image]";
- return JSON.stringify(item);
- })
- .join("\n");
- }
-
- // Fallback to JSON string for complex content
+const parseMessageContent = (content: unknown): string => {
+ if (typeof content === "string") return content;
+ if (content === null || content === undefined) return "";
+ if (Array.isArray(content)) return content.map(parseContentPart).join("\n");
return JSON.stringify(content);
};
+const parseContentPart = (part: unknown): string => {
+ if (typeof part === "string") return part;
+ if (!isRecord(part)) return JSON.stringify(part);
+ switch (part.type) {
+ case "text":
+ case "input_text":
+ case "output_text":
+ return asString(part.text);
+ case "refusal":
+ return asString(part.refusal);
+ case "image_url":
+ case "input_image":
+ return "[Image]";
+ case "input_file":
+ return "[File]";
+ case "input_audio":
+ return "[Audio]";
+ default:
+ return JSON.stringify(part);
+ }
+};
+
/**
* Parse tool calls from response message
*/
-const parseToolCalls = (
- toolCalls: any[],
-):
- | Array<{
- id: string;
- name: string;
- arguments: Record;
- }>
- | undefined => {
- if (!toolCalls || !Array.isArray(toolCalls)) return undefined;
-
- return toolCalls.map((tc) => ({
- id: tc.id || "",
- name: tc.function?.name || "unknown",
- arguments: parseToolArguments(tc.function?.arguments),
- }));
+const parseChatToolCalls = (toolCalls: unknown): ToolCall[] | undefined => {
+ if (!Array.isArray(toolCalls)) return undefined;
+ return toolCalls.map((toolCall) => {
+ const call = isRecord(toolCall) ? toolCall : {};
+ const fn = isRecord(call.function) ? call.function : {};
+ return {
+ id: asString(call.id),
+ name: asString(fn.name) || "unknown",
+ arguments: parseToolArguments(fn.arguments),
+ };
+ });
};
/**
* Parse tool arguments - handle both string and object formats
*/
-const parseToolArguments = (args: any): Record => {
+const parseToolArguments = (args: unknown): Record => {
if (!args) return {};
-
if (typeof args === "string") {
try {
- return JSON.parse(args);
+ const parsed: unknown = JSON.parse(args);
+ return isRecord(parsed) ? parsed : { raw: args };
} catch {
return { raw: args };
}
}
-
- return args;
+ return isRecord(args) ? args : {};
};