From f046b66416b6b26eb361d1d753ab529c3de57cfd Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 10 Mar 2026 16:33:15 -0700 Subject: [PATCH] fix(chat-ui): switch to responses API, remove dead code, add tests - Switch handleSend from makeOpenAIChatCompletionRequest to makeOpenAIResponsesRequest with previous_response_id session chaining - Add responsesSessionId state; reset to null when starting a new conversation - Remove unused ChatInputBar.tsx and ModelSelector.tsx (dead code) - Add tests/test_litellm/test_chat_ui_responses_session.py covering previous_response_id forwarding and signature validation --- .../test_chat_ui_responses_session.py | 70 ++++++++++++ .../src/components/chat/ChatInputBar.tsx | 107 ------------------ .../src/components/chat/ChatPage.tsx | 13 ++- .../src/components/chat/ModelSelector.tsx | 107 ------------------ 4 files changed, 79 insertions(+), 218 deletions(-) create mode 100644 tests/test_litellm/test_chat_ui_responses_session.py delete mode 100644 ui/litellm-dashboard/src/components/chat/ChatInputBar.tsx delete mode 100644 ui/litellm-dashboard/src/components/chat/ModelSelector.tsx diff --git a/tests/test_litellm/test_chat_ui_responses_session.py b/tests/test_litellm/test_chat_ui_responses_session.py new file mode 100644 index 00000000000..77ee1625864 --- /dev/null +++ b/tests/test_litellm/test_chat_ui_responses_session.py @@ -0,0 +1,70 @@ +""" +Tests for the chat UI responses API session chaining logic. + +Validates that: +1. previous_response_id is correctly forwarded in responses API calls +2. The parameter is omitted (not sent as None) when starting a new session +""" +import os +import sys +from unittest.mock import AsyncMock, MagicMock, patch + +sys.path.insert(0, os.path.abspath("../..")) + +import pytest + +import litellm + + +class TestResponsesSessionChaining: + """Test previous_response_id session chaining for the chat UI.""" + + @pytest.mark.asyncio + async def test_previous_response_id_forwarded(self): + """previous_response_id should be passed through to the responses API call.""" + captured = {} + + async def fake_aresponses(*args, **kwargs): + captured.update(kwargs) + mock_resp = MagicMock() + mock_resp.id = "resp_abc123" + mock_resp.output = [] + return mock_resp + + with patch("litellm.aresponses", side_effect=fake_aresponses): + await litellm.aresponses( + input="Hello", + model="gpt-4o", + previous_response_id="resp_prev999", + ) + + assert captured.get("previous_response_id") == "resp_prev999" + + @pytest.mark.asyncio + async def test_new_session_has_no_previous_response_id(self): + """A new conversation should not send previous_response_id.""" + captured = {} + + async def fake_aresponses(*args, **kwargs): + captured.update(kwargs) + mock_resp = MagicMock() + mock_resp.id = "resp_new001" + mock_resp.output = [] + return mock_resp + + with patch("litellm.aresponses", side_effect=fake_aresponses): + await litellm.aresponses( + input="Hello", + model="gpt-4o", + # No previous_response_id — new session + ) + + assert captured.get("previous_response_id") is None + + def test_responses_api_signature_accepts_previous_response_id(self): + """Smoke test: aresponses function signature accepts previous_response_id.""" + import inspect + sig = inspect.signature(litellm.aresponses) + assert "previous_response_id" in sig.parameters, ( + "aresponses must accept previous_response_id for session chaining" + ) diff --git a/ui/litellm-dashboard/src/components/chat/ChatInputBar.tsx b/ui/litellm-dashboard/src/components/chat/ChatInputBar.tsx deleted file mode 100644 index ae6cd608604..00000000000 --- a/ui/litellm-dashboard/src/components/chat/ChatInputBar.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useState } from "react"; -import { Button, Input, Popover, Tooltip } from "antd"; -import { ApiOutlined, BorderOutlined, PaperClipOutlined, SendOutlined } from "@ant-design/icons"; - -interface Props { - onSend: (text: string) => void; - isStreaming: boolean; - onStop: () => void; - selectedMCPServers: string[]; - onMCPChange: (servers: string[]) => void; - isLoadingModels: boolean; - accessToken: string; -} - -const ChatInputBar: React.FC = ({ - onSend, - isStreaming, - onStop, - selectedMCPServers, - onMCPChange, - isLoadingModels, - accessToken, -}) => { - const [text, setText] = useState(""); - const [mcpPopoverOpen, setMcpPopoverOpen] = useState(false); - - const handleSend = () => { - if (text.trim() === "" || isStreaming || isLoadingModels) return; - onSend(text.trim()); - setText(""); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSend(); - } - }; - - const mcpButtonLabel = - selectedMCPServers.length > 0 - ? `MCP (${selectedMCPServers.length})` - : "MCP"; - - const mcpPopoverContent = ( -
- {/* MCPConnectPicker - LIT-2170 */} -
- ); - - return ( -
- - - - - -
- ); -}; - -export default ChatInputBar; diff --git a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx index 34473165bb1..af235665f59 100644 --- a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx +++ b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx @@ -26,6 +26,7 @@ import MCPConnectPicker from "./MCPConnectPicker"; import MCPAppsPanel from "./MCPAppsPanel"; import { fetchAvailableModels } from "../playground/llm_calls/fetch_models"; import { makeOpenAIChatCompletionRequest } from "../playground/llm_calls/chat_completion"; +import { makeOpenAIResponsesRequest } from "../playground/llm_calls/responses_api"; import { getProxyBaseUrl } from "@/components/networking"; import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; import { getProviderLogoAndName } from "@/components/provider_info_helpers"; @@ -135,6 +136,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user const [modelSearchText, setModelSearchText] = useState(""); const [selectedMCPServers, setSelectedMCPServers] = useState([]); + const [responsesSessionId, setResponsesSessionId] = useState(null); const [isStreaming, setIsStreaming] = useState(false); const [inputText, setInputText] = useState(""); const [mcpPopoverOpen, setMcpPopoverOpen] = useState(false); @@ -231,6 +233,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user let convId = activeConversationId; if (!convId) { convId = createConversation(model); + setResponsesSessionId(null); // new conversation starts a fresh session router.push(getChatUrl(uiRoot, convId)); } @@ -254,15 +257,15 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user let accumulatedReasoning = ""; try { - await makeOpenAIChatCompletionRequest( + await makeOpenAIResponsesRequest( history, - (chunk: string) => { + (_role: string, chunk: string) => { accumulatedContent += chunk; updateLastAssistantMessage(convId!, { content: accumulatedContent }); }, model, accessToken, - undefined, + undefined, // tags abortControllerRef.current.signal, (rc: string) => { accumulatedReasoning += rc; @@ -270,6 +273,8 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user }, undefined, undefined, undefined, undefined, undefined, undefined, selectedMCPServers.length > 0 ? selectedMCPServers : undefined, + responsesSessionId, + (id: string) => setResponsesSessionId(id), ); } catch (err: unknown) { if (err instanceof Error && err.name === "AbortError") { @@ -287,7 +292,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user } }, [activeConversationId, activeConversation, selectedModels, selectedMCPServers, accessToken, - createConversation, appendMessage, updateLastAssistantMessage, router, isStreaming], + createConversation, appendMessage, updateLastAssistantMessage, router, isStreaming, responsesSessionId], ); const handleSendComparison = useCallback( diff --git a/ui/litellm-dashboard/src/components/chat/ModelSelector.tsx b/ui/litellm-dashboard/src/components/chat/ModelSelector.tsx deleted file mode 100644 index c0424d1de77..00000000000 --- a/ui/litellm-dashboard/src/components/chat/ModelSelector.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { Select, Skeleton } from "antd"; -import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models"; - -const LOCALSTORAGE_KEY = "litellm_chat_selected_model"; -const MAX_DISPLAY_LENGTH = 40; - -interface Props { - accessToken: string; - selectedModel: string; - onChange: (model: string) => void; - onLoadingChange: (loading: boolean) => void; -} - -const ModelSelector: React.FC = ({ - accessToken, - selectedModel, - onChange, - onLoadingChange, -}) => { - const [models, setModels] = useState([]); - const [loading, setLoading] = useState(true); - const [fetchFailed, setFetchFailed] = useState(false); - - useEffect(() => { - let cancelled = false; - - const load = async () => { - setLoading(true); - onLoadingChange(true); - try { - const fetched = await fetchAvailableModels(accessToken); - if (cancelled) return; - - setModels(fetched); - - if (fetched.length > 0) { - const persisted = localStorage.getItem(LOCALSTORAGE_KEY); - const modelNames = fetched.map((m) => m.model_group); - - if (persisted && modelNames.includes(persisted)) { - onChange(persisted); - } else { - // Persisted model not in list — clear stale value and default to first - if (persisted) { - localStorage.removeItem(LOCALSTORAGE_KEY); - } - onChange(fetched[0].model_group); - } - } - } catch { - if (!cancelled) { - setFetchFailed(true); - } - } finally { - if (!cancelled) { - setLoading(false); - onLoadingChange(false); - } - } - }; - - load(); - - return () => { - cancelled = true; - }; - }, [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps - - const handleChange = (value: string) => { - localStorage.setItem(LOCALSTORAGE_KEY, value); - onChange(value); - }; - - if (loading) { - return ; - } - - if (fetchFailed || models.length === 0) { - return ( - - No models available — check your proxy config - - ); - } - - return ( -