From 840db3fe48ac463c8e0d86c5c64c608caf27969e Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 8 Aug 2025 16:57:08 -0700 Subject: [PATCH] LiteLLM UI - Test Key Page - allow uploading images for /chat/completions and /responses (#13445) * add MultimodalContent on chat UI * add multi modal img on chat ui * utils for responses API imgs * add code snippet with imgs * chat UI add imgs * add imge upload * chat ui allow adding images * fix chat send button * fix button styles * fix clear chat --- litellm/proxy/proxy_config.yaml | 9 + .../src/components/chat_ui.tsx | 362 +++++++++++++----- .../components/chat_ui/ChatImageRenderer.tsx | 35 ++ .../components/chat_ui/ChatImageUpload.tsx | 45 +++ .../src/components/chat_ui/ChatImageUtils.tsx | 75 ++++ .../src/components/chat_ui/CodeSnippets.tsx | 77 +++- .../chat_ui/ResponsesImageRenderer.tsx | 35 ++ .../chat_ui/ResponsesImageUpload.tsx | 45 +++ .../chat_ui/ResponsesImageUtils.tsx | 66 ++++ .../chat_ui/llm_calls/chat_completion.tsx | 2 +- .../chat_ui/llm_calls/responses_api.tsx | 21 +- .../src/components/chat_ui/types.ts | 9 +- 12 files changed, 677 insertions(+), 104 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/chat_ui/ChatImageRenderer.tsx create mode 100644 ui/litellm-dashboard/src/components/chat_ui/ChatImageUpload.tsx create mode 100644 ui/litellm-dashboard/src/components/chat_ui/ChatImageUtils.tsx create mode 100644 ui/litellm-dashboard/src/components/chat_ui/ResponsesImageRenderer.tsx create mode 100644 ui/litellm-dashboard/src/components/chat_ui/ResponsesImageUpload.tsx create mode 100644 ui/litellm-dashboard/src/components/chat_ui/ResponsesImageUtils.tsx diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index 96f0870bc17..ae709c83f2f 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -3,3 +3,12 @@ model_list: litellm_params: model: gemini/* +litellm_settings: + callbacks: ["s3_v2"] + s3_callback_params: + s3_bucket_name: litellm-logs # AWS Bucket Name for S3 + s3_region_name: us-west-2 + +general_settings: + cold_storage_custom_logger: s3_v2 + store_prompts_in_cold_storage: true \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/chat_ui.tsx b/ui/litellm-dashboard/src/components/chat_ui.tsx index 71ee967af1c..170eb8251be 100644 --- a/ui/litellm-dashboard/src/components/chat_ui.tsx +++ b/ui/litellm-dashboard/src/components/chat_ui.tsx @@ -45,6 +45,12 @@ import { generateCodeSnippet } from "./chat_ui/CodeSnippets"; import { MessageType } from "./chat_ui/types"; import ReasoningContent from "./chat_ui/ReasoningContent"; import ResponseMetrics, { TokenUsage } from "./chat_ui/ResponseMetrics"; +import ResponsesImageUpload from "./chat_ui/ResponsesImageUpload"; +import ResponsesImageRenderer from "./chat_ui/ResponsesImageRenderer"; +import { convertImageToBase64, createMultimodalMessage, createDisplayMessage } from "./chat_ui/ResponsesImageUtils"; +import ChatImageUpload from "./chat_ui/ChatImageUpload"; +import ChatImageRenderer from "./chat_ui/ChatImageRenderer"; +import { createChatMultimodalMessage, createChatDisplayMessage } from "./chat_ui/ChatImageUtils"; import { SendOutlined, ApiOutlined, @@ -61,7 +67,9 @@ import { UploadOutlined, PictureOutlined, CodeOutlined, - ToolOutlined + ToolOutlined, + FilePdfOutlined, + ArrowUpOutlined } from "@ant-design/icons"; const { TextArea } = Input; @@ -157,6 +165,10 @@ const ChatUI: React.FC = ({ const [messageTraceId, setMessageTraceId] = useState(() => sessionStorage.getItem('messageTraceId') || null); const [uploadedImage, setUploadedImage] = useState(null); const [imagePreviewUrl, setImagePreviewUrl] = useState(null); + const [responsesUploadedImage, setResponsesUploadedImage] = useState(null); + const [responsesImagePreviewUrl, setResponsesImagePreviewUrl] = useState(null); + const [chatUploadedImage, setChatUploadedImage] = useState(null); + const [chatImagePreviewUrl, setChatImagePreviewUrl] = useState(null); const [isGetCodeModalVisible, setIsGetCodeModalVisible] = useState(false); const [generatedCode, setGeneratedCode] = useState(""); const [selectedSdk, setSelectedSdk] = useState<'openai' | 'azure'>('openai'); @@ -442,6 +454,36 @@ const ChatUI: React.FC = ({ setImagePreviewUrl(null); }; + const handleResponsesImageUpload = (file: File): false => { + setResponsesUploadedImage(file); + const previewUrl = URL.createObjectURL(file); + setResponsesImagePreviewUrl(previewUrl); + return false; // Prevent default upload behavior + }; + + const handleRemoveResponsesImage = () => { + if (responsesImagePreviewUrl) { + URL.revokeObjectURL(responsesImagePreviewUrl); + } + setResponsesUploadedImage(null); + setResponsesImagePreviewUrl(null); + }; + + const handleChatImageUpload = (file: File): false => { + setChatUploadedImage(file); + const previewUrl = URL.createObjectURL(file); + setChatImagePreviewUrl(previewUrl); + return false; // Prevent default upload behavior + }; + + const handleRemoveChatImage = () => { + if (chatImagePreviewUrl) { + URL.revokeObjectURL(chatImagePreviewUrl); + } + setChatUploadedImage(null); + setChatImagePreviewUrl(null); + }; + const handleSendMessage = async () => { if (inputMessage.trim() === "") return; @@ -467,7 +509,28 @@ const ChatUI: React.FC = ({ const signal = abortControllerRef.current.signal; // Create message object without model field for API call - const newUserMessage = { role: "user", content: inputMessage }; + let newUserMessage: { role: string; content: string | any[] }; + + // Handle image for responses API + if (endpointType === EndpointType.RESPONSES && responsesUploadedImage) { + try { + newUserMessage = await createMultimodalMessage(inputMessage, responsesUploadedImage); + } catch (error) { + message.error("Failed to process image. Please try again."); + return; + } + } + // Handle image for chat completions API + else if (endpointType === EndpointType.CHAT && chatUploadedImage) { + try { + newUserMessage = await createChatMultimodalMessage(inputMessage, chatUploadedImage); + } catch (error) { + message.error("Failed to process image. Please try again."); + return; + } + } else { + newUserMessage = { role: "user", content: inputMessage }; + } // Generate new trace ID for a new conversation or use existing one const traceId = messageTraceId || uuidv4(); @@ -475,8 +538,17 @@ const ChatUI: React.FC = ({ setMessageTraceId(traceId); } - // Update UI with full message object - setChatHistory([...chatHistory, newUserMessage]); + // Update UI with full message object (always display as text for UI) + let displayMessage: MessageType; + if (endpointType === EndpointType.RESPONSES && responsesUploadedImage) { + displayMessage = createDisplayMessage(inputMessage, true, responsesImagePreviewUrl || undefined, responsesUploadedImage.name); + } else if (endpointType === EndpointType.CHAT && chatUploadedImage) { + displayMessage = createChatDisplayMessage(inputMessage, true, chatImagePreviewUrl || undefined, chatUploadedImage.name); + } else { + displayMessage = createDisplayMessage(inputMessage, false); + } + + setChatHistory([...chatHistory, displayMessage]); setIsLoading(true); try { @@ -484,7 +556,11 @@ const ChatUI: React.FC = ({ if (endpointType === EndpointType.CHAT) { // Create chat history for API call - strip out model field and isImage field - const apiChatHistory = [...chatHistory.filter(msg => !msg.isImage).map(({ role, content }) => ({ role, content })), newUserMessage]; + // For chat completions, we preserve the multimodal content structure + const apiChatHistory = [...chatHistory.filter(msg => !msg.isImage).map(({ role, content }) => ({ + role, + content: typeof content === 'string' ? content : '' + })), newUserMessage]; await makeOpenAIChatCompletionRequest( apiChatHistory, @@ -577,6 +653,14 @@ const ChatUI: React.FC = ({ if (endpointType === EndpointType.IMAGE_EDITS) { handleRemoveImage(); } + // Clear image after successful request for responses API + if (endpointType === EndpointType.RESPONSES && responsesUploadedImage) { + handleRemoveResponsesImage(); + } + // Clear image after successful request for chat completions API + if (endpointType === EndpointType.CHAT && chatUploadedImage) { + handleRemoveChatImage(); + } } setInputMessage(""); @@ -585,7 +669,9 @@ const ChatUI: React.FC = ({ const clearChatHistory = () => { setChatHistory([]); setMessageTraceId(null); - handleRemoveImage(); // Clear any uploaded images + handleRemoveImage(); // Clear any uploaded images for image edits + handleRemoveResponsesImage(); // Clear any uploaded images for responses + handleRemoveChatImage(); // Clear any uploaded images for chat completions sessionStorage.removeItem('chatHistory'); sessionStorage.removeItem('messageTraceId'); message.success("Chat history cleared."); @@ -798,16 +884,6 @@ const ChatUI: React.FC = ({ accessToken={accessToken || ""} /> - -
- - Clear Chat - -
@@ -816,6 +892,13 @@ const ChatUI: React.FC = ({
Test Key
+ + Clear Chat + setIsGetCodeModalVisible(true)} className="bg-gray-100 hover:bg-gray-200 text-gray-700 border-gray-300" @@ -871,44 +954,54 @@ const ChatUI: React.FC = ({ }}> {message.isImage ? ( Generated image ) : ( - & { - inline?: boolean; - node?: any; - }) { - const match = /language-(\w+)/.exec(className || ''); - return !inline && match ? ( - - {String(children).replace(/\n$/, '')} - - ) : ( - - {children} - - ); - }, - pre: ({ node, ...props }) => ( -
-                          )
-                        }}
-                      >
-                        {message.content}
-                      
+                      <>
+                        {/* Show attached image for user messages based on current endpoint */}
+                        {endpointType === EndpointType.RESPONSES && (
+                          
+                        )}
+                        {endpointType === EndpointType.CHAT && (
+                          
+                        )}
+                        
+                         & {
+                              inline?: boolean;
+                              node?: any;
+                            }) {
+                              const match = /language-(\w+)/.exec(className || '');
+                              return !inline && match ? (
+                                
+                                  {String(children).replace(/\n$/, '')}
+                                
+                              ) : (
+                                
+                                  {children}
+                                
+                              );
+                            },
+                            pre: ({ node, ...props }) => (
+                              
+                            )
+                          }}
+                        >
+                          {typeof message.content === "string" ? message.content : ""}
+                        
+                      
                     )}
                                         
                     {message.role === "assistant" && (message.timeToFirstToken || message.usage) && (
@@ -966,53 +1059,142 @@ const ChatUI: React.FC = ({
                 )}
               
)} + {/* Show file previews above input when files are uploaded */} + {(endpointType === EndpointType.RESPONSES && responsesUploadedImage) && ( +
+
+
+ {responsesUploadedImage.name.toLowerCase().endsWith('.pdf') ? ( +
+ +
+ ) : ( + Upload preview + )} +
+
+
+ {responsesUploadedImage.name} +
+
+ {responsesUploadedImage.name.toLowerCase().endsWith('.pdf') ? 'PDF' : 'Image'} +
+
+ +
+
+ )} -
-