diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx
index 733cccdf1c9..b3e59cf507f 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx
@@ -27,6 +27,7 @@ import {
SPACING_XLARGE,
MESSAGE_REQUEST_ID_COPIED,
} from "./constants";
+import { ToolsSection } from "../ToolsSection";
const { Text } = Typography;
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/FormattedToolView.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/FormattedToolView.tsx
new file mode 100644
index 00000000000..2f036a2a34a
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/FormattedToolView.tsx
@@ -0,0 +1,124 @@
+/**
+ * Formatted view of tool definition with parameters table and call data
+ */
+
+import { Typography, Table } from "antd";
+import { ParsedTool, ParameterRow } from "./types";
+
+const { Text } = Typography;
+
+interface FormattedToolViewProps {
+ tool: ParsedTool;
+}
+
+export function FormattedToolView({ tool }: FormattedToolViewProps) {
+ // Parse parameters for table display
+ const parameterRows: ParameterRow[] = Object.entries(
+ tool.parameters?.properties || {}
+ ).map(([name, schema]: [string, any]) => ({
+ key: name,
+ name: name,
+ type: schema.type || "any",
+ description: schema.description || "-",
+ required: tool.parameters?.required?.includes(name) || false,
+ }));
+
+ const columns = [
+ {
+ title: "Parameter",
+ dataIndex: "name",
+ key: "name",
+ render: (name: string, record: ParameterRow) => (
+
+ {name}
+ {record.required && *}
+
+ ),
+ },
+ {
+ title: "Type",
+ dataIndex: "type",
+ key: "type",
+ render: (type: string) => (
+
+ {type}
+
+ ),
+ },
+ {
+ title: "Description",
+ dataIndex: "description",
+ key: "description",
+ render: (desc: string) => {desc},
+ },
+ ];
+
+ return (
+
+ {/* Description */}
+ {tool.description && (
+
+ {tool.description}
+
+ )}
+
+ {/* Parameters Table */}
+ {parameterRows.length > 0 && (
+
+ )}
+
+ {/* If tool was called, show the arguments used */}
+ {tool.called && tool.callData && (
+
+
+ Called With
+
+
+
+ {JSON.stringify(tool.callData.arguments, null, 2)}
+
+
+
+ )}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/JsonToolView.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/JsonToolView.tsx
new file mode 100644
index 00000000000..2a2ceb644dc
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/JsonToolView.tsx
@@ -0,0 +1,39 @@
+/**
+ * JSON view of tool definition
+ */
+
+import { ParsedTool } from "./types";
+
+interface JsonToolViewProps {
+ tool: ParsedTool;
+}
+
+export function JsonToolView({ tool }: JsonToolViewProps) {
+ // Reconstruct the original tool definition
+ const toolJson = {
+ type: "function",
+ function: {
+ name: tool.name,
+ description: tool.description,
+ parameters: tool.parameters,
+ },
+ };
+
+ return (
+
+ {JSON.stringify(toolJson, null, 2)}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolExpandedContent.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolExpandedContent.tsx
new file mode 100644
index 00000000000..3c06dc7f08c
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolExpandedContent.tsx
@@ -0,0 +1,52 @@
+/**
+ * Expanded content for a tool with view mode toggle
+ */
+
+import { useState } from "react";
+import { Typography, Radio } from "antd";
+import { ParsedTool } from "./types";
+import { FormattedToolView } from "./FormattedToolView";
+import { JsonToolView } from "./JsonToolView";
+
+const { Text } = Typography;
+
+type ViewMode = "formatted" | "json";
+
+interface ToolExpandedContentProps {
+ tool: ParsedTool;
+}
+
+export function ToolExpandedContent({ tool }: ToolExpandedContentProps) {
+ const [viewMode, setViewMode] = useState("formatted");
+
+ return (
+
+ {/* View Mode Toggle - Top Right */}
+
+
+ Description
+
+ setViewMode(e.target.value)}
+ >
+ Formatted
+ JSON
+
+
+
+ {viewMode === "formatted" ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolItem.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolItem.tsx
new file mode 100644
index 00000000000..a5962a387af
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolItem.tsx
@@ -0,0 +1,74 @@
+/**
+ * Individual tool item component with expandable details
+ */
+
+import { useState } from "react";
+import { Typography, Tag } from "antd";
+import { ToolOutlined, RightOutlined, DownOutlined } from "@ant-design/icons";
+import { ParsedTool } from "./types";
+import { ToolExpandedContent } from "./ToolExpandedContent";
+
+const { Text } = Typography;
+
+interface ToolItemProps {
+ tool: ParsedTool;
+}
+
+export function ToolItem({ tool }: ToolItemProps) {
+ const [expanded, setExpanded] = useState(false);
+
+ return (
+
+ {/* Header Row - Always Visible */}
+
setExpanded(!expanded)}
+ style={{
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ padding: "12px 16px",
+ cursor: "pointer",
+ background: expanded ? "#fafafa" : "#fff",
+ transition: "background 0.2s",
+ }}
+ >
+
+
+
+ {tool.index}. {tool.name}
+
+
+
+
+
+ {tool.called ? "called" : "not called"}
+
+ {expanded ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Expanded Content */}
+ {expanded && (
+
+
+
+ )}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx
new file mode 100644
index 00000000000..753a552b6db
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx
@@ -0,0 +1,117 @@
+/**
+ * Core tests for Tools section
+ */
+
+import { describe, it, expect } from "vitest";
+import { parseToolsFromLog } from "./utils";
+import { LogEntry } from "../columns";
+
+describe("ToolsSection", () => {
+ it("should parse tools from request and match with response tool calls", () => {
+ const mockLog: LogEntry = {
+ request_id: "test-123",
+ api_key: "key",
+ team_id: "team",
+ model: "gpt-4",
+ model_id: "gpt-4",
+ call_type: "completion",
+ spend: 0.01,
+ total_tokens: 100,
+ prompt_tokens: 50,
+ completion_tokens: 50,
+ startTime: "2024-01-01T00:00:00Z",
+ endTime: "2024-01-01T00:00:01Z",
+ cache_hit: "none",
+ messages: JSON.stringify({
+ model: "gpt-4",
+ messages: [{ role: "user", content: "What's the weather?" }],
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "get_weather",
+ description: "Get the current weather",
+ parameters: {
+ type: "object",
+ required: ["location"],
+ properties: {
+ location: { type: "string", description: "City name" },
+ },
+ },
+ },
+ },
+ {
+ type: "function",
+ function: {
+ name: "search_web",
+ description: "Search the web",
+ parameters: {
+ type: "object",
+ required: ["query"],
+ properties: {
+ query: { type: "string", description: "Search query" },
+ },
+ },
+ },
+ },
+ ],
+ }),
+ response: JSON.stringify({
+ choices: [
+ {
+ message: {
+ tool_calls: [
+ {
+ id: "call_123",
+ type: "function",
+ function: {
+ name: "get_weather",
+ arguments: '{"location": "San Francisco"}',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ }),
+ };
+
+ const tools = parseToolsFromLog(mockLog);
+
+ expect(tools).toHaveLength(2);
+ expect(tools[0].name).toBe("get_weather");
+ expect(tools[0].called).toBe(true);
+ expect(tools[0].callData?.arguments).toEqual({ location: "San Francisco" });
+ expect(tools[1].name).toBe("search_web");
+ expect(tools[1].called).toBe(false);
+ });
+
+ it("should return empty array when no tools in request", () => {
+ const mockLog: LogEntry = {
+ request_id: "test-456",
+ api_key: "key",
+ team_id: "team",
+ model: "gpt-4",
+ model_id: "gpt-4",
+ call_type: "completion",
+ spend: 0.01,
+ total_tokens: 100,
+ prompt_tokens: 50,
+ completion_tokens: 50,
+ startTime: "2024-01-01T00:00:00Z",
+ endTime: "2024-01-01T00:00:01Z",
+ cache_hit: "none",
+ messages: JSON.stringify({
+ model: "gpt-4",
+ messages: [{ role: "user", content: "Hello" }],
+ }),
+ response: JSON.stringify({
+ choices: [{ message: { content: "Hi there!" } }],
+ }),
+ };
+
+ const tools = parseToolsFromLog(mockLog);
+
+ expect(tools).toHaveLength(0);
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx
new file mode 100644
index 00000000000..fdeec2b249d
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx
@@ -0,0 +1,42 @@
+/**
+ * Tools section component that displays all available tools from the request
+ * and indicates which ones were actually called in the response
+ */
+
+import { Typography } from "antd";
+import { LogEntry } from "../columns";
+import { parseToolsFromLog } from "./utils";
+import { ToolItem } from "./ToolItem";
+
+const { Text } = Typography;
+
+interface ToolsSectionProps {
+ log: LogEntry;
+}
+
+export function ToolsSection({ log }: ToolsSectionProps) {
+ const tools = parseToolsFromLog(log);
+
+ // Don't render if no tools
+ if (tools.length === 0) return null;
+
+ return (
+
+
+ Tools
+
+
+ {tools.map((tool) => (
+
+ ))}
+
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/index.ts b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/index.ts
new file mode 100644
index 00000000000..e3b8600b003
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/index.ts
@@ -0,0 +1,7 @@
+/**
+ * Export main components and utilities for the Tools section
+ */
+
+export { ToolsSection } from "./ToolsSection";
+export { parseToolsFromLog, hasTools } from "./utils";
+export type { ParsedTool, ToolDefinition, ToolCall } from "./types";
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/types.ts b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/types.ts
new file mode 100644
index 00000000000..92282fd1ca3
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/types.ts
@@ -0,0 +1,42 @@
+/**
+ * Type definitions for the Tools section
+ */
+
+export interface ToolDefinition {
+ type: string;
+ function: {
+ name: string;
+ description?: string;
+ parameters?: Record;
+ };
+}
+
+export interface ToolCall {
+ id: string;
+ type: string;
+ function: {
+ name: string;
+ arguments: string;
+ };
+}
+
+export interface ParsedTool {
+ index: number;
+ name: string;
+ description: string;
+ parameters: Record;
+ called: boolean;
+ callData?: {
+ id: string;
+ name: string;
+ arguments: Record;
+ };
+}
+
+export interface ParameterRow {
+ key: string;
+ name: string;
+ type: string;
+ description: string;
+ required: boolean;
+}
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.test.ts b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.test.ts
new file mode 100644
index 00000000000..75f975e9a13
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.test.ts
@@ -0,0 +1,293 @@
+/**
+ * Tests for tool parsing utilities
+ */
+
+import { describe, it, expect } from "vitest";
+import { parseToolsFromLog, hasTools } from "./utils";
+import { LogEntry } from "../columns";
+
+describe("ToolsSection utils", () => {
+ describe("parseToolsFromLog", () => {
+ it("should return empty array when no tools in request", () => {
+ const log: Partial = {
+ request_id: "test-1",
+ messages: [],
+ response: {},
+ };
+
+ const result = parseToolsFromLog(log as LogEntry);
+
+ expect(result).toEqual([]);
+ });
+
+ it("should parse tools from proxy_server_request", () => {
+ const log: Partial = {
+ request_id: "test-2",
+ proxy_server_request: {
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "get_weather",
+ description: "Get the current weather",
+ parameters: {
+ type: "object",
+ properties: {
+ location: { type: "string" },
+ },
+ required: ["location"],
+ },
+ },
+ },
+ ],
+ },
+ response: {},
+ } as any;
+
+ const result = parseToolsFromLog(log as LogEntry);
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toMatchObject({
+ index: 1,
+ name: "get_weather",
+ description: "Get the current weather",
+ called: false,
+ });
+ });
+
+ it("should parse tools from messages object format", () => {
+ const log: Partial = {
+ request_id: "test-3",
+ messages: {
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "search_web",
+ description: "Search the web",
+ },
+ },
+ ],
+ },
+ response: {},
+ } as any;
+
+ const result = parseToolsFromLog(log as LogEntry);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe("search_web");
+ });
+
+ it("should mark tools as called when present in response", () => {
+ const log: Partial = {
+ request_id: "test-4",
+ proxy_server_request: {
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "get_weather",
+ description: "Get weather",
+ },
+ },
+ {
+ type: "function",
+ function: {
+ name: "send_email",
+ description: "Send email",
+ },
+ },
+ ],
+ },
+ response: {
+ choices: [
+ {
+ message: {
+ tool_calls: [
+ {
+ id: "call_123",
+ type: "function",
+ function: {
+ name: "get_weather",
+ arguments: '{"location": "San Francisco"}',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ } as any;
+
+ const result = parseToolsFromLog(log as LogEntry);
+
+ expect(result).toHaveLength(2);
+ expect(result[0].called).toBe(true);
+ expect(result[0].callData).toBeDefined();
+ expect(result[0].callData?.arguments).toEqual({
+ location: "San Francisco",
+ });
+ expect(result[1].called).toBe(false);
+ expect(result[1].callData).toBeUndefined();
+ });
+
+ it("should handle string format request and response", () => {
+ const log: Partial = {
+ request_id: "test-5",
+ proxy_server_request: JSON.stringify({
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "calculate",
+ },
+ },
+ ],
+ }),
+ response: JSON.stringify({
+ choices: [
+ {
+ message: {
+ tool_calls: [
+ {
+ id: "call_456",
+ type: "function",
+ function: {
+ name: "calculate",
+ arguments: '{"x": 5}',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ }),
+ } as any;
+
+ const result = parseToolsFromLog(log as LogEntry);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].called).toBe(true);
+ });
+
+ it("should handle tools with no description or parameters", () => {
+ const log: Partial = {
+ request_id: "test-6",
+ proxy_server_request: {
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "minimal_tool",
+ },
+ },
+ ],
+ },
+ response: {},
+ } as any;
+
+ const result = parseToolsFromLog(log as LogEntry);
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toMatchObject({
+ index: 1,
+ name: "minimal_tool",
+ description: "",
+ parameters: {},
+ called: false,
+ });
+ });
+
+ it("should handle invalid JSON in tool call arguments gracefully", () => {
+ const log: Partial = {
+ request_id: "test-7",
+ proxy_server_request: {
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "test_tool",
+ },
+ },
+ ],
+ },
+ response: {
+ choices: [
+ {
+ message: {
+ tool_calls: [
+ {
+ id: "call_789",
+ type: "function",
+ function: {
+ name: "test_tool",
+ arguments: "invalid json",
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ } as any;
+
+ const result = parseToolsFromLog(log as LogEntry);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].called).toBe(true);
+ expect(result[0].callData?.arguments).toEqual({});
+ });
+
+ it("should assign correct indices to multiple tools", () => {
+ const log: Partial = {
+ request_id: "test-8",
+ proxy_server_request: {
+ tools: [
+ { type: "function", function: { name: "tool1" } },
+ { type: "function", function: { name: "tool2" } },
+ { type: "function", function: { name: "tool3" } },
+ ],
+ },
+ response: {},
+ } as any;
+
+ const result = parseToolsFromLog(log as LogEntry);
+
+ expect(result).toHaveLength(3);
+ expect(result[0].index).toBe(1);
+ expect(result[1].index).toBe(2);
+ expect(result[2].index).toBe(3);
+ });
+ });
+
+ describe("hasTools", () => {
+ it("should return false when no tools in request", () => {
+ const log: Partial = {
+ request_id: "test-9",
+ messages: [],
+ response: {},
+ };
+
+ expect(hasTools(log as LogEntry)).toBe(false);
+ });
+
+ it("should return true when tools present in request", () => {
+ const log: Partial = {
+ request_id: "test-10",
+ proxy_server_request: {
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "test_tool",
+ },
+ },
+ ],
+ },
+ response: {},
+ } as any;
+
+ expect(hasTools(log as LogEntry)).toBe(true);
+ });
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.ts b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.ts
new file mode 100644
index 00000000000..33b21297c43
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.ts
@@ -0,0 +1,130 @@
+/**
+ * Utility functions for parsing and processing tool data from log entries
+ */
+
+import { LogEntry } from "../columns";
+import { ParsedTool, ToolDefinition, ToolCall } from "./types";
+
+/**
+ * Parse raw data that might be a string or object
+ */
+function parseData(input: any): any {
+ if (typeof input === "string") {
+ try {
+ return JSON.parse(input);
+ } catch {
+ return input;
+ }
+ }
+ return input;
+}
+
+/**
+ * Extract tools array from request data
+ */
+function extractToolsFromRequest(log: LogEntry): ToolDefinition[] {
+ // Check proxy_server_request first (most complete), then messages
+ const requestData = parseData(log.proxy_server_request || log.messages);
+
+ if (!requestData) return [];
+
+ // Handle array format (messages array)
+ if (Array.isArray(requestData)) {
+ // Tools are not typically in messages array, return empty
+ return [];
+ }
+
+ // Handle object format (request body)
+ if (typeof requestData === "object" && requestData.tools) {
+ return Array.isArray(requestData.tools) ? requestData.tools : [];
+ }
+
+ return [];
+}
+
+/**
+ * Extract tool calls from response data
+ */
+function extractToolCallsFromResponse(log: LogEntry): ToolCall[] {
+ const responseData = parseData(log.response);
+
+ if (!responseData || typeof responseData !== "object") return [];
+
+ // OpenAI format: response.choices[0].message.tool_calls
+ const choices = responseData.choices;
+ if (Array.isArray(choices) && choices.length > 0) {
+ const firstChoice = choices[0];
+ const message = firstChoice.message;
+ if (message && Array.isArray(message.tool_calls)) {
+ return message.tool_calls;
+ }
+ }
+
+ return [];
+}
+
+/**
+ * Parse safe JSON with fallback
+ */
+function parseSafeJson(jsonString: string): Record {
+ try {
+ return JSON.parse(jsonString);
+ } catch {
+ return {};
+ }
+}
+
+/**
+ * Main function to parse tools from a log entry
+ * Returns an array of tools with their definition and call status
+ */
+export function parseToolsFromLog(log: LogEntry): ParsedTool[] {
+ // Get tools from request
+ const requestTools = extractToolsFromRequest(log);
+
+ if (requestTools.length === 0) {
+ return [];
+ }
+
+ // Get tool calls from response
+ const toolCalls = extractToolCallsFromResponse(log);
+ const calledToolNames = new Set(
+ toolCalls.map((tc: ToolCall) => tc.function?.name).filter(Boolean)
+ );
+
+ // Map tool calls by name for quick lookup
+ const toolCallMap = new Map();
+ toolCalls.forEach((tc: ToolCall) => {
+ const name = tc.function?.name;
+ if (name) {
+ toolCallMap.set(name, {
+ id: tc.id,
+ name: name,
+ arguments: parseSafeJson(tc.function?.arguments || "{}"),
+ });
+ }
+ });
+
+ // Parse each tool definition
+ return requestTools.map((tool: ToolDefinition, index: number) => {
+ const func = tool.function || { name: `Tool ${index + 1}` };
+ const name = func.name || `Tool ${index + 1}`;
+
+ return {
+ index: index + 1,
+ name: name,
+ description: func.description || "",
+ parameters: func.parameters || {},
+ called: calledToolNames.has(name),
+ callData: toolCallMap.get(name),
+ };
+ });
+}
+
+/**
+ * Check if a log entry has any tools
+ */
+export function hasTools(log: LogEntry): boolean {
+ const requestTools = extractToolsFromRequest(log);
+ return requestTools.length > 0;
+}