diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx new file mode 100644 index 00000000000..690f4ef3bbc --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useState, useEffect } from "react"; +import ChatUI from "@/components/playground/chat_ui/ChatUI"; +import CompareUI from "@/components/playground/compareUI/CompareUI"; +import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { fetchProxySettings } from "@/utils/proxyUtils"; + +interface ProxySettings { + PROXY_BASE_URL?: string; + LITELLM_UI_API_DOC_BASE_URL?: string | null; +} + +export default function PlaygroundPage() { + const { accessToken, userRole, userId, disabledPersonalKeyCreation, token } = useAuthorized(); + const [proxySettings, setProxySettings] = useState(undefined); + + useEffect(() => { + const initializeProxySettings = async () => { + if (accessToken) { + const settings = await fetchProxySettings(accessToken); + if (settings) { + setProxySettings({ + PROXY_BASE_URL: settings.PROXY_BASE_URL, + LITELLM_UI_API_DOC_BASE_URL: settings.LITELLM_UI_API_DOC_BASE_URL, + }); + } + } + }; + + initializeProxySettings(); + }, [accessToken]); + + return ( + + + Chat + Compare + + + + + + + + + + + ); +} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/test-key/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/test-key/page.tsx index bdaff3406b4..0f984686c40 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/test-key/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/test-key/page.tsx @@ -1,6 +1,6 @@ "use client"; -import ChatUI from "@/components/chat_ui/ChatUI"; +import ChatUI from "@/components/playground/chat_ui/ChatUI"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { useState, useEffect } from "react"; import { fetchProxySettings } from "@/utils/proxyUtils"; diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index b2a220d6608..8597e17438d 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -22,7 +22,7 @@ import ModelHubTable from "@/components/model_hub_table"; import PublicModelHub from "@/components/public_model_hub"; import NewUsagePage from "@/components/new_usage"; import APIReferenceView from "@/app/(dashboard)/api-reference/APIReferenceView"; -import ChatUI from "@/components/chat_ui/ChatUI"; +import PlaygroundPage from "@/app/(dashboard)/playground/page"; import Usage from "@/components/usage"; import CacheDashboard from "@/components/cache_dashboard"; import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking"; @@ -364,17 +364,7 @@ export default function CreateKeyPage() { teams={teams} /> ) : page == "llm-playground" ? ( - + ) : page == "users" ? ( { const topLevelLabels = [ "Virtual Keys", - "Test Key", + "Playground", "Models + Endpoints", "Usage", "Teams", diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 853b4be2c1a..efd9f1bb560 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -57,7 +57,7 @@ const Sidebar: React.FC = ({ accessToken, setPage, userRole, defau { key: "3", page: "llm-playground", - label: "Test Key", + label: "Playground", icon: , roles: rolesWithWriteAccess, }, @@ -148,11 +148,11 @@ const Sidebar: React.FC = ({ accessToken, setPage, userRole, defau roles: all_admin_roles, }, { - "key": "29", - "page": "agents", - "label": "Agents", - "icon": , - "roles": rolesWithWriteAccess, + key: "29", + page: "agents", + label: "Agents", + icon: , + roles: rolesWithWriteAccess, }, { key: "25", @@ -258,7 +258,7 @@ const Sidebar: React.FC = ({ accessToken, setPage, userRole, defau }); return ( - + ({ +vi.mock("../llm_calls/fetch_models", () => ({ fetchAvailableModels: vi.fn(), })); @@ -32,7 +32,7 @@ describe("ChatUI", () => { Element.prototype.scrollIntoView = vi.fn(); // Mock the fetchAvailableModels to return test models - vi.mocked(fetchModelsModule.fetchAvailableModels).mockResolvedValue([ + (fetchModelsModule.fetchAvailableModels as any).mockResolvedValue([ { model_group: "Model 1", mode: "chat" }, { model_group: "Model 2", mode: "chat" }, { model_group: "Model 3", mode: "chat" }, @@ -134,7 +134,7 @@ describe("ChatUI", () => { }); it("shows only chat-compatible models when chat endpoint is selected", async () => { - vi.mocked(fetchModelsModule.fetchAvailableModels).mockResolvedValueOnce([ + (fetchModelsModule.fetchAvailableModels as any).mockResolvedValueOnce([ { model_group: "ChatModel", mode: "chat" }, { model_group: "SpeechModel", mode: "audio_speech" }, { model_group: "ImageModel", mode: "image_generation" }, diff --git a/ui/litellm-dashboard/src/components/chat_ui/ChatUI.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx similarity index 96% rename from ui/litellm-dashboard/src/components/chat_ui/ChatUI.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx index 211382541cc..90c43df0dc4 100644 --- a/ui/litellm-dashboard/src/components/chat_ui/ChatUI.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx @@ -1,3 +1,5 @@ +"use client"; + import { ApiOutlined, ArrowUpOutlined, @@ -25,11 +27,11 @@ import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; import { v4 as uuidv4 } from "uuid"; -import { truncateString } from "../../utils/textUtils"; -import GuardrailSelector from "../guardrails/GuardrailSelector"; -import NotificationsManager from "../molecules/notifications_manager"; -import TagSelector from "../tag_management/TagSelector"; -import VectorStoreSelector from "../vector_store_management/VectorStoreSelector"; +import { truncateString } from "../../../utils/textUtils"; +import GuardrailSelector from "../../guardrails/GuardrailSelector"; +import NotificationsManager from "../../molecules/notifications_manager"; +import TagSelector from "../../tag_management/TagSelector"; +import VectorStoreSelector from "../../vector_store_management/VectorStoreSelector"; import AdditionalModelSettings from "./AdditionalModelSettings"; import AudioRenderer from "./AudioRenderer"; import { OPEN_AI_VOICE_SELECT_OPTIONS, OpenAIVoice } from "./chatConstants"; @@ -38,17 +40,17 @@ import ChatImageUpload from "./ChatImageUpload"; import { createChatDisplayMessage, createChatMultimodalMessage } from "./ChatImageUtils"; import { generateCodeSnippet } from "./CodeSnippets"; import EndpointSelector from "./EndpointSelector"; -import { makeAnthropicMessagesRequest } from "./llm_calls/anthropic_messages"; -import { makeOpenAIAudioSpeechRequest } from "./llm_calls/audio_speech"; -import { makeOpenAIAudioTranscriptionRequest } from "./llm_calls/audio_transcriptions"; -import { makeOpenAIChatCompletionRequest } from "./llm_calls/chat_completion"; -import { makeOpenAIEmbeddingsRequest } from "./llm_calls/embeddings_api"; -import type { MCPTool } from "./llm_calls/fetch_mcp_tools"; -import { fetchAvailableMCPTools } from "./llm_calls/fetch_mcp_tools"; -import { fetchAvailableModels, ModelGroup } from "./llm_calls/fetch_models"; -import { makeOpenAIImageEditsRequest } from "./llm_calls/image_edits"; -import { makeOpenAIImageGenerationRequest } from "./llm_calls/image_generation"; -import { makeOpenAIResponsesRequest } from "./llm_calls/responses_api"; +import { makeAnthropicMessagesRequest } from "../llm_calls/anthropic_messages"; +import { makeOpenAIAudioSpeechRequest } from "../llm_calls/audio_speech"; +import { makeOpenAIAudioTranscriptionRequest } from "../llm_calls/audio_transcriptions"; +import { makeOpenAIChatCompletionRequest } from "../llm_calls/chat_completion"; +import { makeOpenAIEmbeddingsRequest } from "../llm_calls/embeddings_api"; +import type { MCPTool } from "../llm_calls/fetch_mcp_tools"; +import { fetchAvailableMCPTools } from "../llm_calls/fetch_mcp_tools"; +import { fetchAvailableModels, ModelGroup } from "../llm_calls/fetch_models"; +import { makeOpenAIImageEditsRequest } from "../llm_calls/image_edits"; +import { makeOpenAIImageGenerationRequest } from "../llm_calls/image_generation"; +import { makeOpenAIResponsesRequest } from "../llm_calls/responses_api"; import MCPEventsDisplay, { MCPEvent } from "./MCPEventsDisplay"; import { EndpointType, getEndpointType } from "./mode_endpoint_mapping"; import ReasoningContent from "./ReasoningContent"; @@ -467,6 +469,24 @@ const ChatUI: React.FC = ({ }); }; + const updateTotalLatency = (totalLatency: number) => { + setChatHistory((prevHistory) => { + const lastMessage = prevHistory[prevHistory.length - 1]; + + if (lastMessage && lastMessage.role === "assistant") { + return [ + ...prevHistory.slice(0, prevHistory.length - 1), + { + ...lastMessage, + totalLatency, + }, + ]; + } + + return prevHistory; + }); + }; + const updateSearchResults = (searchResults: any[]) => { console.log("Received search results:", searchResults); setChatHistory((prevHistory) => { @@ -767,11 +787,12 @@ const ChatUI: React.FC = ({ traceId, selectedVectorStores.length > 0 ? selectedVectorStores : undefined, selectedGuardrails.length > 0 ? selectedGuardrails : undefined, - selectedMCPTools, // Pass the selected tools array - updateChatImageUI, // Pass the image callback - updateSearchResults, // Pass the search results callback - useAdvancedParams ? temperature : undefined, // Pass temperature if enabled - useAdvancedParams ? maxTokens : undefined, // Pass max_tokens if enabled + selectedMCPTools, + updateChatImageUI, + updateSearchResults, + useAdvancedParams ? temperature : undefined, + useAdvancedParams ? maxTokens : undefined, + updateTotalLatency, ); } else if (endpointType === EndpointType.IMAGE) { // For image generation @@ -973,7 +994,7 @@ const ChatUI: React.FC = ({ const antIcon = ; return ( -
+
{/* Left Sidebar with Controls */} @@ -1418,13 +1439,15 @@ const ChatUI: React.FC = ({ )} - {message.role === "assistant" && (message.timeToFirstToken || message.usage) && ( - - )} + {message.role === "assistant" && + (message.timeToFirstToken || message.totalLatency || message.usage) && ( + + )}
diff --git a/ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.test.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.test.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.test.tsx diff --git a/ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.tsx diff --git a/ui/litellm-dashboard/src/components/chat_ui/EndpointSelector.test.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/EndpointSelector.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/EndpointSelector.test.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/EndpointSelector.test.tsx diff --git a/ui/litellm-dashboard/src/components/chat_ui/EndpointSelector.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/EndpointSelector.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/EndpointSelector.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/EndpointSelector.tsx diff --git a/ui/litellm-dashboard/src/components/chat_ui/EndpointUtils.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/EndpointUtils.tsx similarity index 93% rename from ui/litellm-dashboard/src/components/chat_ui/EndpointUtils.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/EndpointUtils.tsx index c1f26e01b4a..de337e5638f 100644 --- a/ui/litellm-dashboard/src/components/chat_ui/EndpointUtils.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/EndpointUtils.tsx @@ -1,4 +1,4 @@ -import { ModelGroup } from "./llm_calls/fetch_models"; +import { ModelGroup } from "../llm_calls/fetch_models"; import { EndpointType, getEndpointType } from "./mode_endpoint_mapping"; /** diff --git a/ui/litellm-dashboard/src/components/chat_ui/MCPEventsDisplay.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/MCPEventsDisplay.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/MCPEventsDisplay.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/MCPEventsDisplay.tsx diff --git a/ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ReasoningContent.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/ReasoningContent.tsx diff --git a/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ResponseMetrics.tsx similarity index 67% rename from ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/ResponseMetrics.tsx index fc83aa06593..658d55b12d8 100644 --- a/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ResponseMetrics.tsx @@ -7,6 +7,7 @@ import { ExportOutlined, BulbOutlined, ToolOutlined, + DollarOutlined, } from "@ant-design/icons"; export interface TokenUsage { @@ -14,16 +15,18 @@ export interface TokenUsage { promptTokens?: number; totalTokens?: number; reasoningTokens?: number; + cost?: number; } interface ResponseMetricsProps { - timeToFirstToken?: number; // in milliseconds + timeToFirstToken?: number; + totalLatency?: number; usage?: TokenUsage; toolName?: string; } -const ResponseMetrics: React.FC = ({ timeToFirstToken, usage, toolName }) => { - if (!timeToFirstToken && !usage) return null; +const ResponseMetrics: React.FC = ({ timeToFirstToken, totalLatency, usage, toolName }) => { + if (!timeToFirstToken && !totalLatency && !usage) return null; return (
@@ -31,7 +34,16 @@ const ResponseMetrics: React.FC = ({ timeToFirstToken, usa
- {(timeToFirstToken / 1000).toFixed(2)}s + TTFT: {(timeToFirstToken / 1000).toFixed(2)}s +
+
+ )} + + {totalLatency !== undefined && ( + +
+ + Total Latency: {(totalLatency / 1000).toFixed(2)}s
)} @@ -72,6 +84,21 @@ const ResponseMetrics: React.FC = ({ timeToFirstToken, usa )} + {usage && ( + +
+ + {usage.cost !== undefined ? `$${usage.cost.toFixed(6)}` : "Not Tracked"} +
+
+ )} + {toolName && (
diff --git a/ui/litellm-dashboard/src/components/chat_ui/ResponsesImageRenderer.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageRenderer.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/ResponsesImageRenderer.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageRenderer.tsx diff --git a/ui/litellm-dashboard/src/components/chat_ui/ResponsesImageUpload.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUpload.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/ResponsesImageUpload.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUpload.tsx diff --git a/ui/litellm-dashboard/src/components/chat_ui/ResponsesImageUtils.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUtils.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/ResponsesImageUtils.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUtils.tsx diff --git a/ui/litellm-dashboard/src/components/chat_ui/SearchResultsDisplay.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/SearchResultsDisplay.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/SearchResultsDisplay.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/SearchResultsDisplay.tsx diff --git a/ui/litellm-dashboard/src/components/chat_ui/SessionManagement.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/SessionManagement.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/chat_ui/SessionManagement.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/SessionManagement.tsx index 3b65d974c80..e782845bfc3 100644 --- a/ui/litellm-dashboard/src/components/chat_ui/SessionManagement.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/SessionManagement.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Switch, Tooltip } from "antd"; import { InfoCircleOutlined, CopyOutlined } from "@ant-design/icons"; import { EndpointType } from "./mode_endpoint_mapping"; -import NotificationsManager from "../molecules/notifications_manager"; +import NotificationsManager from "../../molecules/notifications_manager"; interface SessionManagementProps { endpointType: string; diff --git a/ui/litellm-dashboard/src/components/chat_ui/chatConstants.ts b/ui/litellm-dashboard/src/components/playground/chat_ui/chatConstants.ts similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/chatConstants.ts rename to ui/litellm-dashboard/src/components/playground/chat_ui/chatConstants.ts diff --git a/ui/litellm-dashboard/src/components/chat_ui/mode_endpoint_mapping.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/mode_endpoint_mapping.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/chat_ui/mode_endpoint_mapping.tsx rename to ui/litellm-dashboard/src/components/playground/chat_ui/mode_endpoint_mapping.tsx diff --git a/ui/litellm-dashboard/src/components/chat_ui/types.ts b/ui/litellm-dashboard/src/components/playground/chat_ui/types.ts similarity index 96% rename from ui/litellm-dashboard/src/components/chat_ui/types.ts rename to ui/litellm-dashboard/src/components/playground/chat_ui/types.ts index e992167f59c..15c90221fc0 100644 --- a/ui/litellm-dashboard/src/components/chat_ui/types.ts +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/types.ts @@ -78,14 +78,16 @@ export interface MessageType { isAudio?: boolean; reasoningContent?: string; timeToFirstToken?: number; + totalLatency?: number; usage?: { completionTokens?: number; promptTokens?: number; totalTokens?: number; reasoningTokens?: number; + cost?: number; }; toolName?: string; - imagePreviewUrl?: string; // For storing image preview URL in chat history + imagePreviewUrl?: string; image?: { url: string; detail: string; diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.test.tsx b/ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.test.tsx new file mode 100644 index 00000000000..1941943a58b --- /dev/null +++ b/ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.test.tsx @@ -0,0 +1,91 @@ +import { render, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import CompareUI from "./CompareUI"; + +vi.mock("../llm_calls/fetch_models", () => ({ + fetchAvailableModels: vi.fn().mockResolvedValue([{ model_group: "gpt-4" }, { model_group: "gpt-3.5-turbo" }]), +})); + +vi.mock("../llm_calls/chat_completion", () => ({ + makeOpenAIChatCompletionRequest: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("./components/ComparisonPanel", () => ({ + ComparisonPanel: ({ comparison, onRemove }: { comparison: any; onRemove: () => void }) => ( +
+ +
+ ), +})); + +vi.mock("./components/MessageInput", () => ({ + MessageInput: ({ value, onChange, onSend, disabled }: any) => ( +
+